There is an example in the official documentation about how to write custom provider, but it doesn't work.
My question is: what is the best way to write custom provider, especially how to write and register provider as a new service?
When I try to use this code from documentation, I get errors about type of arguments.
What does mean empty argument?
Thank you.
After some investigation, the following code works:
Register provider as a service:
// src/Application/Sonata/MediaBundle/Resources/config/services.yml
parameters:
application_sonata_media.custom_class: Application\Sonata\MediaBundle\Provider\CustomProvider
services:
sonata.media.provider.custom:
class: %application_sonata_media.custom_class%
tags:
- { name: sonata.media.provider }
arguments:
- sonata.media.provider.custom
- #sonata.media.filesystem.local
- #sonata.media.cdn.server
- #sonata.media.generator.default
- #sonata.media.thumbnail.format
Custom Provider code:
// src/Application/Sonata/MediaBundle/Provider/CustomProvider.php
<?php
namespace Application\Sonata\MediaBundle\Provider;
use Sonata\MediaBundle\Model\MediaInterface;
use Sonata\MediaBundle\Provider\FileProvider;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\HttpFoundation\File\File;
/**
* Class CustomProvider
* #package Application\Sonata\MediaBundle\Provider
*/
class CustomProvider extends FileProvider
{
/**
* #param MediaInterface $media
*/
protected function doTransform(MediaInterface $media)
{
// ...
}
/**
* {#inheritdoc}
*/
public function generatePublicUrl(MediaInterface $media, $format)
{
// new logic
}
/**
* {#inheritdoc}
*/
public function postPersist(MediaInterface $media)
{
}
/**
* {#inheritdoc}
*/
public function postUpdate(MediaInterface $media)
{
}
}
Updated sonata configuration:
// app/config/sonata/sonata_media.yml
sonata_media:
...
product:
providers:
- sonata.media.provider.image
- sonata.media.provider.custom
formats:
small: { width: 40 , quality: 100}
...
And I've also setup DI extension to autoload services.yml
I made a PR to update outdated documentation.
I couldn’t get this to work until i named the service exactly as the one i was overriding (sonata.media.provider.image)
See https://stackoverflow.com/a/20118256/4239642
Related
I just made the migration from symfony 4.1 to 4.4
I have this error:
Argument 1 passed to App\EventListener\KernelRequestListener::__construct() must be an instance of Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage, instance of Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage given, called in C:\xampp\htdocs\chat-project-symfony\var\cache\dev\Container06Mjwya\srcApp_KernelDevDebugContainer.php on line 1130
While if you look at my KernelRequestListener :
<?php
namespace App\EventListener;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
//..
class KernelRequestListener
{
private $tokenStorage;
/**
* KernelRequestListener constructor.
* #param TokenStorage $tokenStorage
* ...
*/
public function __construct(TokenStorage $tokenStorage/*...*/)
{
$this->tokenStorage = $tokenStorage;
//..
}
}
Here is my config/services.yaml file:
#...
services:
#..
App\EventListener\KernelRequestListener:
arguments: [ '#security.token_storage' ]
tags:
- { name: kernel.event_listener, event: kernel.request }
- { name: kernel.event_listener, event: kernel.response }
I don't know why symfony tell me that I'm using Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage while it's clearing written Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage
I already tried to clear the cache folder and also delete the cache folder and it didn't change.
How can I fix this ?
Thank you
I don't know why symfony tell me that I'm using Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage while it's clearing written Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage
It's not symfony but PHP's type checking feature. You are stating that your Listener wants a TokenStorage but symfony is passing to it different class, thus the error.
So, as #JaredFarrish pointed, you should be using TokenStorageInterface in your constructor, like this:
namespace App\EventListener;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
//..
class KernelRequestListener
{
private $tokenStorage;
/**
* KernelRequestListener constructor.
* #param TokenStorageInterface $tokenStorage
* ...
*/
public function __construct(TokenStorageInterface $tokenStorage/*...*/)
{
$this->tokenStorage = $tokenStorage;
//..
}
}
It's a common practice to use interfaces where they exists, because this way you will loose coupling with other classes and provide a way to unit test your classes.
Take a look: https://github.com/symfony/security-bundle/blob/master/Resources/config/security.xml#L22 they switched class for #security.token_storage service, because of deprecation. But when you use an interface you don't care of anything underlying, you just know that you will have your methods because of interface contract.
I fixed it changing this line:
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
With this one:
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface as TokenStorage;
I would like to show on EasyAdmin a custom property, here is an example :
class Book
{
/**
* #ORM\Id()
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
public $id;
/**
* #ORM\Column(type="string")
*/
public $name;
/**
* #ORM\Column(type="float")
*/
public $price;
public function getBenefit(): float
{
// Here the method to retrieve the benefits
}
}
In this example, the custom parameter is benefit it's not a parameter of our Entity and if we configure EasyAdmin like that, it works !
easy_admin:
entities:
Book:
class: App\Entity\Book
list:
fields:
- { property: 'title', label: 'Title' }
- { property: 'benefit', label: 'Benefits' }
The problem is if the function is a bit complexe and need for example an EntityRepository, it becomes impossible to respect Controller > Repository > Entities.
Does anyone have a workaround, maybe by using the AdminController to show custom properties properly in EasyAdmin ?
You shouldn't put the logic to retrieve the benefits inside the Book entity, especially if it involves external dependencies like entityManager.
You could probably use the Doctrine events to achieve that. Retrieve the benefits after a Book entity has been loaded from the DB. Save the benefits before or after saving the Book entity in the DB.
You can find out more about it here https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html
class Book
{
...
public $benefits;
}
// src/EventListener/RetrieveBenefitListener.php
namespace App\EventListener;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use App\Entity\Book;
class RetrieveBenefitListener
{
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getObject();
// only act on some "Book" entity
if (!$entity instanceof Book) {
return;
}
// Your logic to retrieve the benefits
$entity->benefits = methodToGetTheBenefits();
}
}
// src/EventListener/SaveBenefitListener.php
namespace App\EventListener;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use App\Entity\Book;
class SaveBenefitListener
{
public function postUpdate(LifecycleEventArgs $args)
{
$entity = $args->getObject();
// only act on some "Book" entity
if (!$entity instanceof Book) {
return;
}
// Your logic to save the benefits
methodToSaveTheBenefits($entity->benefits);
}
}
// services.yml
services:
App\EventListener\RetrieveBenefitListener:
tags:
- { name: doctrine.event_listener, event: postLoad }
App\EventListener\SaveBenefitListener:
tags:
- { name: doctrine.event_listener, event: postUpdate }
This is just an example, I haven't tested the code. You will probably have to add the logic for the postPersist event if you create new Book objects.
Depending on the logic to retrieve the benefits (another DB call? loading from an external API?), you might want to to approach the problem differently (caching, loading them in your DB via a cron job, ...)
so i've got a custom Drupal 8 migration, where we're importing nodes from XML - everything's great. Now i want to add a pre-import function, so that before the migration. In Drupal 7 Migrate there was preImport() - what's the Drupal 8 method? I've found this article about Events added to migration process, but it's still not clear to me how to proceed... thanks for any tips!
You need to create your own event subscriber, here a short guide: https://www.chapterthree.com/blog/how-to-register-event-subscriber-drupal8
Here a specific example of an EventSubscriber (my_migration/src/EventSubscriber/PreImportEvent.php):
<?php
namespace Drupal\my_migration\EventSubscriber;
use Drupal\migrate\Event\MigrateEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Class PreImportEvent
*
* #package Drupal\my_migration\EventSubscriber
*/
class PreImportEvent implements EventSubscriberInterface {
/**
* #return mixed
*/
public static function getSubscribedEvents() {
$events[MigrateEvents::PRE_IMPORT][] = [
'preImport',
0,
];
return $events;
}
/**
* #param $event
*/
public function preImport($event) {
// Do whatever you want with $event
}
}
Now you need to register the service for your EventSubscriber (my_migration/my_migration.services.yml):
services:
my_migration.subscriber.pre_import:
class: Drupal\my_migration\EventSubscriber\PreImportEvent
tags:
- { name: event_subscriber }
Note: If you need to alter your migration per field base you should better use a process plugin (https://www.drupal.org/docs/8/api/migrate-api/migrate-process-plugins).
I am using Gedmo Loggable to keep track of changes that users make to entities. The username is not stored by default for each change, but it is necessary to provide it to the listener.
An example of this, is found in the documentation of Loggable:
$loggableListener = new Gedmo\Loggable\LoggableListener;
$loggableListener->setAnnotationReader($cachedAnnotationReader);
$loggableListener->setUsername('admin');
$evm->addEventSubscriber($loggableListener);
This does not work for me for two reasons:
I am registering the listener in services.yml, not in a controller
I do not wish to store a pre-known username like in the example but the username of the user that is logged in
The method setUsername of the loggableListener either expects a string or an object with a method getUsername that provides a string.
How can I pass either one to the listener? I found a way to pass the security_token but this is not sufficient. What I currently have is this:
(...)
gedmo.listener.loggable:
class: Gedmo\Loggable\LoggableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "#annotation_reader" ] ]
#- [ setUsername, [ "A fixed value works" ] ]
- [ setUserValue, [ #security.??? ] ]
I am also using Blameable and found a nice workaround for a similar problem (the entire listener is overriden). I tried to do the same for Loggable, but this appears to be a bit more complex.
Main question: how can I pass the security user object (or its username) to a listener in services.yml?
Update
Matteo showed me how to pass the result of a function as a parameter to a listener. This almost solves the problem. There is another service, that provides the username when given the token_storage. But this means that I need to pass a parameter, to a service, that is given as a parameter to another service. This example will explain:
- [ setUsername, [ "#=service('gedmo.listener.blameable').getUsername( #security.token_storage )" ] ]
The problem now is, that #security.token_storage is not accepted in this context. How can I pass a parameter to the method getUsername() ?
You can use the Symfony Service Expressions, as example you can try the following:
gedmo.listener.loggable:
class: Gedmo\Loggable\LoggableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "#annotation_reader" ] ]
- [ setUserValue, ["#=service('security.token_storage').getToken()->getUser()->getUsername()"] ]
But in some case the user can be null, so you can a condition with ? (see the doc).
Hope this help
I think it's not possible to inject the authed user in a service. But you can inject the token storage : #security.token_storage (before symfony 2.6, you will have to use #security.context instead of #security.token_storage)
In your service (LoggableListener), you'll be able to get the authed username like this :
$tokenStorage->getToken()->getUser()->getUsername()
I encountered the same problem and I solved it by creating a logger listener like in stofDoctrineExtensionsBundle.
To do that, I created this file:
//src/AppBundle/Listener/LoggerListener
<?php
namespace AppBundle\Listener;
use Gedmo\Loggable\LoggableListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
/**
* LoggerListener
*
* #author Alexandre Tranchant <alexandre.tranchant#gmail.com>
* #author Christophe Coevoet <stof#notk.org>
*/
class LoggerListener implements EventSubscriberInterface
{
/**
* #var AuthorizationCheckerInterface
*/
private $authorizationChecker;
/**
* #var TokenStorageInterface
*/
private $tokenStorage;
/**
* #var LoggableListener
*/
private $loggableListener;
/**
* LoggerListener constructor.
*
* #param LoggableListener $loggableListener
* #param TokenStorageInterface|null $tokenStorage
* #param AuthorizationCheckerInterface|null $authorizationChecker
*/
public function __construct(LoggableListener $loggableListener, TokenStorageInterface $tokenStorage = null, AuthorizationCheckerInterface $authorizationChecker = null)
{
$this->loggableListener = $loggableListener;
$this->tokenStorage = $tokenStorage;
$this->authorizationChecker = $authorizationChecker;
}
/**
* Set the username from the security context by listening on core.request
*
* #param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
if (null === $this->tokenStorage || null === $this->authorizationChecker) {
return;
}
$token = $this->tokenStorage->getToken();
if (null !== $token && $this->authorizationChecker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
$this->loggableListener->setUsername($token);
}
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => 'onKernelRequest',
);
}
}
Then I declared this listener in my services.yml file.
services:
#Loggable
gedmo.listener.loggable:
class: Gedmo\Loggable\LoggableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "#annotation_reader" ] ]
# KernelRequest listener
app.listener.loggable:
class: AppBundle\Listener\LoggerListener
tags:
# loggable hooks user username if one is in token_storage or authorization_checker
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest}
arguments:
- '#gedmo.listener.loggable'
- '#security.token_storage'
- '#security.authorization_checker'
As you can see in this dump screenshot, it works fine:
I want to make
behat.yml -
default:
extensions:
Behat\MinkExtension\Extension:
base_url: 'my-url'
a parameter pulled from parameters.yml... Is this possible? I made a mink_base_url parameter in parameters.yml and then added
imports:
- { resource: parameters.yml }
to behat.yml. No matter what I do, I get this
[Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException]
The service "behat.mink.context.initializer" has a dependency on a non-existent parameter "mink_base_url"
Behat configuration is in no way related to Symfony's. It's true that Behat uses Symfony's DI container, but it's a separate instance.
If wanted to implement it, you'd probably need to create your own Behat extension to support the imports section.
This worked for me with Symfony 3. Just omit base_url from behat.yml, and set it from the container parameters. Thanks to #DanielM for providing the hint.
<?php
use Behat\MinkExtension\Context\MinkContext;
use Symfony\Component\DependencyInjection\ContainerInterface;
class FeatureContext extends MinkContext {
/**
* FeatureContext constructor.
* #param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* #BeforeScenario
*/
public function setUpTestEnvironment()
{
$this->setMinkParameter('base_url', $this->container->getParameter('my_url'));
}
}
It is possible to access the symfony parameters within behat yaml as using
- '%%name_of_the_parameter%%'
Double percentage sign (%%) does the trick.
If you just want to access base_url, you can get it once mink has been started.
$this->getMinkParameter('base_url');
Here's an example :
class AbstractBehatContext extends MinkContext {
/**
* The base url as set behat.yml
* #var bool
*/
protected $baseUrl;
/**
* #BeforeScenario
*/
public function getBaseUrl() {
$this->baseUrl = $this->getMinkParameter('base_url');
}
}
Note, this needs to be able to access Mink, so it won't work in __construct or in #BeforeSuite. Additionally #BeforeScenario will be called at the start of every scenario which is going to set it pointlessly a lot.