Buy

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

What if a user wants to change the behavior of our controller? Symfony does have a way to override controllers from a bundle... but not if that controller is registered as a service, like our controller. Well, ok, thanks to Symfony's incredible container, there is always a way to override a service. But let's not make our users do crazy things! If someone wants to tweak how our controller behaves, let's make it easy!

How? By dispatching a custom event. Ready for our new goal? I want to allow a user to change the data that we return from our API endpoint. Specifically, we're going to add a third key to the JSON array from our app.

Custom Event Class

The first step to dispatching an event is to create an event class. Create a new Event directory with a PHP class inside: call it FilterApiResponseEvent. I just made that up.

Make this extend a core Event class from Symfony. When you dispatch an event, you have the opportunity to pass an Event object to any listeners. To be as awesome as possible, you'll want to make sure that object contains as much useful information as you can.

... lines 1 - 6
class FilterApiResponseEvent extends Event
{
... lines 9 - 24
}

In this case, a listener might want to access the data that we're about to turn into JSON. Cool! Add public function __construct() with an array $data argument. I'll press Alt+Enter and choose "Initialize Fields" to create a data property and set it.

... lines 1 - 6
class FilterApiResponseEvent extends Event
{
private $data;
public function __construct(array $data)
{
$this->data = $data;
}
... lines 15 - 24
}

Then, we need a way for the listeners to access this. And, we also want any listeners to be able to set this. Go back to the Code -> Generate menu, or Command + N on a Mac, choose "Getter and Setters" and select data.

... lines 1 - 15
public function getData(): array
{
return $this->data;
}
public function setData(array $data)
{
$this->data = $data;
}
... lines 25 - 26

It's ready!

Dispatching the Event

Head to your controller: this is where we'll dispatch that event. First, set the data to a $data variable and then create the event object: $event = new FilterApiResponseEvent() passing it the data.

... lines 1 - 9
class IpsumApiController extends AbstractController
{
... lines 12 - 21
public function index()
{
... lines 24 - 28
$event = new FilterApiResponseEvent($data);
... lines 30 - 32
}
}

I'm not going to dispatch the event quite yet, but at the end, pass $event->getData() to the json method.

... lines 1 - 21
public function index()
{
... lines 24 - 31
return $this->json($event->getData());
}
... lines 34 - 35

To dispatch the event, we need... um... the event dispatcher! And of course, we're going to pass this in as an argument: EventDispatcherInterface $eventDispatcher. Press Alt+enter and select "Initialize Fields" to add that as a property and set it in the constructor.

... lines 1 - 13
private $eventDispatcher;
... line 15
public function __construct(KnpUIpsum $knpUIpsum, EventDispatcherInterface $eventDispatcher)
{
... line 18
$this->eventDispatcher = $eventDispatcher;
}
... lines 21 - 35

As soon as we do this, we need to also open services.xml and pass a second argument: type="service" and id="event_dispatcher".

... lines 1 - 6
<services>
... lines 8 - 13
<service id="knpu_lorem_ipsum.controller.ipsum_api_controller" class="KnpU\LoremIpsumBundle\Controller\IpsumApiController" public="true">
... line 15
<argument type="service" id="event_dispatcher" />
</service>
... lines 18 - 20
</services>
... lines 22 - 23

Back in the controller, right after you create the event, dispatch it: $this->eventDispatcher->dispatch(). The first argument is the event name and we can actually dream up whatever name we want. Let's use: knpu_lorem_ipsum.filter_api. For the second argument, pass the event.

... lines 1 - 9
class IpsumApiController extends AbstractController
{
... lines 12 - 21
public function index()
{
... lines 24 - 29
$this->eventDispatcher->dispatch('knpu_lorem_ipsum.filter_api', $event);
... lines 31 - 32
}
}

And... yea, that's it! I mean, we haven't tested it yet, but this should work: our users have a new hook point.

Being Careful with Optional Dependencies

But actually there's a small surprise. Find your terminal and re-run all the tests:

./vendor/bin/simple-phpunit

They fail! Check this out: it says that our controller service has a dependency on a non-existent service event_dispatcher. But, the service id is event_dispatcher - that's not a typo! The problem is that the event_dispatcher service - like many services - comes from FrameworkBundle.

Open up the test that's failing: FunctionalTest. Inside, we're testing with a kernel that does not include FrameworkBundle! We did this on purpose: FrameworkBundle is an optional dependency.

Let me say it a different way: one of our services depends on another service that may or may not exist. Since we want our bundle to work without FrameworkBundle, we need to make the event_dispatcher service optional. To do that, add an on-invalid attribute set to null.

... lines 1 - 13
<service id="knpu_lorem_ipsum.controller.ipsum_api_controller" class="KnpU\LoremIpsumBundle\Controller\IpsumApiController" public="true">
... line 15
<argument type="service" id="event_dispatcher" on-invalid="null" />
</service>
... lines 18 - 23

Thanks to this, if the event_dispatcher service doesn't exist, instead of an error, it'll just pass null. That means, we need to make that argument optional, with = null, or by adding a ? before the type-hint.

... lines 1 - 9
class IpsumApiController extends AbstractController
{
... lines 12 - 15
public function __construct(KnpUIpsum $knpUIpsum, EventDispatcherInterface $eventDispatcher = null)
{
... lines 18 - 19
}
... lines 21 - 35
}

Inside the action, be sure to code defensively: if there is an event dispatcher, do our magic.

... lines 1 - 21
public function index()
{
... lines 24 - 29
if ($this->eventDispatcher) {
$this->eventDispatcher->dispatch('knpu_lorem_ipsum.filter_api', $event);
}
... lines 33 - 34
}
... lines 36 - 37

Try the tests again:

./vendor/bin/simple-phpunit

Aw yea! Next, let's make our event easier to use by documenting it with an event constants class. Then... let's make sure it works!

Leave a comment!

  • 2018-08-06 weaverryan

    Yep, just an update: you should NOT need the new CreateUserCommand() line in your test: you do NOT need to instantiate your command - it is already registered inside the container/kernel. I just removed that line and the test still works just fine. Updating the docs now :) https://github.com/symfony/...

  • 2018-08-06 weaverryan

    Hey Juan Miguel Ardissone!

    Sorry for the slow reply - just got back from vacation :). And yes, this stuff can be very confusing. So, I'll do my best to explain.

    When the user uses your bundle, they will instantiate your *Bundle class into their Kernel class. The MOST important result of this is that, when *their* container is being built, *your* DependencyInjection/<BundleName>Extension class will be called (I mean, the process function in that class will be called). This is your opportunity to add new services to *their* container. If you register a service and give it the console.command tag, then it will become a Command in the user's container (and so, it will be available in the user's bin/console script). If, in the services.yml (or services.xml, whatever you decide to call it) of your bundle, you specify that your command has an

    <argument type="service" id="translator" />

    , then the user's container will know to pass *their* translator service into your command service. Why/how do we know that they have a translator service available? Well, we don't. The Translator classes obviously come from the symfony/translation component. But, the *services* are actually provided by FrameworkBundle. Like most components, FrameworkBundle automatically enables the translator service IF it detects that the symfony/translation component is installed. So, IF your bundle put symfony/translation into its "require" key, then you can effectively assume that the user will have the translator component. If it is an optional dependency, then it should go into require-dev. In that case, you *cannot* assume that the translator component will be there, and you should make it an optional dependency on your service and code accordingly.

    But, I have 1 more important point. Your bundle should not (though, it's not a HUGE deal) require FrameworkBundle (as you already know). But... in my note above, I am talk about how the FrameworkBundle is responsible for supplying the translator service. So, this may seem confusing. Basically, you should write your bundle *assuming* that the user IS using FrameworkBundle - i.e. that all the normal services provided by that bundle are available. But, you simply should not *actually* require it in composer.json. This helps one VERY edge case: the small group of advanced users who choose not to use FrameworkBundle, can still use your bundle. Those users are very aware of the things that the FrameworkBundle does (like provide the translator) and so will be able to figure out what to do to make their app work with your code. So, do not *actually* require FrameworkBundle (it's ok to put it in require-dev of course), but DO assume that the user has it.

    Ok, FINALLY, let's talk about testing your command! When your bundle is used by an application, the application creates the container and your bundle adds services to it. But, when you're testing your bundle, there is no container: you just have a bundle. IF you want to test your bundle, you basically need to create a "fake" container and instantiate your bundle (and usually FrameworkBundle). As you already know, we do this EXACT thing in this tutorial - we use it in our integration & functional tests. This is how your should test your bin/console commands - via a PHPUnit script where you boot a container. You should not actually need to create your own bin/console script. In the link you posted - https://symfony.com/doc/cur... - there is no bin/console script here either.

    I think what is confusing is that the test requires you to say new CreateUserCommand(). That IS confusing - even for me. You should not need to do this... in theory... because your command is already registered as a service in the container. So, that is puzzling for me - the docs may even be out of date (this line may not be needed). However, if it IS needed, then it's really is a workaround for some shortcoming of testing commands. In other words: DO instantiate it directly, and pass in the translator manually. It isn't ideal to need to do this, but it's fine - something like this:


    $application->add(new CreateUserCommand($kernel->getContainer()->get('translator'));

    I'm going to look into the docs - they may be wrong, and they are at least confusing ;).

    I hope this helps! Cheers!

  • 2018-07-30 Juan Miguel Ardissone

    Hi Ryan. Yes, of course, you are right. I now the final project will have a bin/console and yes, this bundle is an open source project to be used in several projects.

    I create a bin/console script because I want to test the commands in a test environment (https://symfony.com/doc/cur...

    Let me try to explain my situation with another example. This articule talks about dispaching an event and you let this functionality as optional. Let's pretend you want the event_dispacher service as required dependency. You will require symfony/event-dispatcher without --dev option. But event dispatcher is not a bundle (I think) and and I don't have a configuration key to configure it (https://github.com/symfony/.... So, I don't understand how event_dispatcher injects into the container to be used. Maybe with FrameworkBundle?

    I have this problem with translation component. I want to configure message commands in multiple languages and of course I only need this works in the final project (that will be requiring my bundle), but, I want to test this functionality in dev and test environment, so I don't know what is the correct way to use this service in these environments.

    As you say, the main objective of a bundle is to inject services into your project but, some symfony components like translation are not really bundle (again, I think) so I don't now how get those from the container. Thats because I think to require framework-bundle into my dev dependencies and I think the framework bundle injects the translation into the container but I don't really know if this is true.

    I try to avoid the framework bundle like you say in your articles, thats because I try to understand how it works and how to use this services and require it just in --dev dependecies

    Thanks very much for your patience

  • 2018-07-29 weaverryan

    Hey Juan!

    Yea, the formatting is annoying in Disqus - you need to wrap everything in pre and code tags. I've updated your previous message with these :)

    You did a good job with your solution :). But, there is a lot to talk about! Or really, I still have some questions. So, I understand that you are creating a bundle - MicayaelCommandsBundle. And, I assume this is a bundle that you will share between multiple projects, or even as an open source bundle? Is that correct? If so, then your *bundle* doesn't really need a bin/console command. The applications that *use* your bundle will have their own bin/console commands. And you will design your bundle so that your command is usable from *their* bin/console.

    So, in your situation, are you creating a bin/console file that lives *inside* the bundle? And if so, why? I won't say anything more for now, because I don't want to say anything wrong if I'm misunderstanding what you're trying to do :). But, I *will* say that you did a good job booting your own Kernel class and creating your own bin/console file. But, I'm not convinced yet that you should really need/want to do this. Let me know!

    Cheers!

  • 2018-07-24 Juan Miguel Ardissone

    Maybe I found the solution but I would like your opinion. I have to require symfony/framework-bundle --dev

    Into my Kernel class I use MicroKernelTrait and I registered "FrameworkBundle" like this:


    public function registerBundles()
    {
    return [
    new MicayaelCommandsBundle(),
    new FrameworkBundle(),
    ];
    }

    And that's it!! My bin/console script looks like this:


    boot();

    $application = new Application();

    $application->add(new SearchInCodeCommand(
    $kernel->getContainer()->get('translator'), <------------- inject the translator service.
    [
    'default_option' => 'php',
    'app' => [
    'php' => [
    'php' => ['src']
    ]
    ],
    'vendors' => [],
    ]
    ));

    $application->run($input);

    Could you explain why the container did not have the service before and now it has registered the service? and what other services would behave this way?

    Thanks for your help

    PS: I don't know how use well format code here. Could you tell me how to write it correctly?

  • 2018-07-24 Juan Miguel Ardissone

    Yes, I want to be able to run the commands to do some tests on dev environment.

    1. About using "data collector" service: I run bin/console debug:autowiring and I did not see a "translator" service just these:

    Symfony\Component\Translation\Extractor\ExtractorInterface
    alias to translation.extractor
    Symfony\Component\Translation\Reader\TranslationReaderInterface
    alias to translation.reader
    Symfony\Component\Translation\TranslatorInterface
    alias to translator.data_collector
    Symfony\Component\Translation\Writer\TranslationWriterInterface
    alias to translation.writer
    Symfony\Component\Validator\Validator\ValidatorInterface

    But actually "translator" works. I don't know why but works jaja

    2. I am not using symfony skeleton for my bundle. I create my bin/console and Kernel class based in http://symfony.com/doc/curr... and your tutorial, and note that Symfony\Component\Console\Application dosen't have a constructor with kernel argument.

    I don't know how to get the translator service because I think I don't have a container full of services into this console script right now. make sense? Do you know how to get the service here?

  • 2018-07-23 weaverryan

    Hey Juan Miguel Ardissone!

    Ah, ok! I think this makes sense now! Well, at least mostly - I may have a few other questions ;).

    Your extension looks good :). Though, the translator service id should be translator (unless you actually do what the "data collector" for some reason). Also, if you've defined your search_in_code key in your Configuration class, then you don't need to use isset - the key will *always* be there. You can just use if ($config['search_in_code']). But those are minor details.

    It sounds to me like your real question is around *testing* your bundle. Is that correct? Do you want to write some automated tests for the bundle itself? Or do you simply want to be able to "play" with your bundle by writing some "dummy" code?

    Usually, if you're in a Symfony application (e.g. with a Kernel class), then you already have a bin/console file. And this file *already* creates your Kernel correctly... which takes care of creating the container... which handles instantiating your command class directly (with the correct arguments). So yes, you should not need to do all the work of creating the SearchInCodeCommand & Translator objects directly: we have already configured the container to be able to do that.

    So, tell me a bit more about exactly what you are trying to do with this "testing in the development environment". We can definitely figure it out :).

    Cheers!

  • 2018-07-20 Juan Miguel Ardissone

    I have a bundle in which the services (commands) are registered dynamically depending on the configurations of the bundle

    If you set command 1 then that command is created. I do this in my extension in this way


    $configuration = $this->getConfiguration($configs, $container);
    $config = $this->processConfiguration($configuration, $configs);

    if (isset($config['search_in_code'])) {
    $searchInCodeCommand = $container->register('micayael_commands.command.search_in_code', SearchInCodeCommand::class);

    $searchInCodeCommand->setArguments([
    new Reference('translator.data_collector'), <-- Here I inject de translator
    $config['search_in_code'],
    ]);

    $searchInCodeCommand->addTag('console.command');
    }

    To be able to perform tests in the development environment I create an application assigning the default settings and building the translator

    -- bin/console


    #!/usr/bin/env php
    boot();

    $translator = new Translator('en'); <-- Translator
    $translator->addLoader('yaml', new YamlFileLoader());
    $translator->addResource('yaml', __DIR__.'/../src/Resources/translations/messages.es.yaml', 'es');
    $translator->addResource('yaml', __DIR__.'/../src/Resources/translations/messages.en.yaml', 'en');
    $translator->setFallbackLocales(['en']);

    $application = new Application();

    $application->add(new SearchInCodeCommand(
    $translator,
    [
    'default_option' => 'php',
    'app' => [
    'php' => [
    'php' => ['src']
    ]
    ],
    'vendors' => [],
    ]
    ));

    $application->run($input);

    Would there be any way to inject the translator without having to create it here? Maybe through the Kernel


    use Symfony\Component\Config\Loader\LoaderInterface;
    use Symfony\Component\DependencyInjection\ContainerBuilder;
    use Symfony\Component\HttpKernel\Kernel as BaseKernel;
    use Symfony\Component\Translation\Loader\YamlFileLoader;
    use Symfony\Component\Translation\Translator;

    class Kernel extends BaseKernel
    {
    public function __construct(string $environment)
    {
    parent::__construct($environment, true);
    }

    public function registerBundles()
    {
    return [
    new MicayaelCommandsBundle(),
    ];
    }

    public function registerContainerConfiguration(LoaderInterface $loader)
    {
    }
    }
  • 2018-07-18 Juan Miguel Ardissone

    I don't know how to put well format code here but I will paste it anyway :'(

    In this bundle I have to register services (commands) depending on the configurations. If the service 1 configs are made then I have to register the corresponding service, so I am using something like this in the extension

    if (isset($config['search_in_code'])) {
    $searchInCodeCommand = $container->register('micayael_commands.command.search_in_code', SearchInCodeCommand::class);

    $searchInCodeCommand->setArguments([
    new Reference('translator.data_collector'), <-- here is the translator
    $config['search_in_code'],
    ]);

    $searchInCodeCommand->addTag('console.command');
    }

    and I require the symfony/translation within the require key in composer file.

    As I want to be able to test the command within the bundle in the dev environment I create a console script by injecting the default configs and instantiating the translator manually.

    My question: can I inject the translator here like I did in extension?

    bin/console file:

    #!/usr/bin/env php
    boot();

    // I have to create the translator manually
    $translator = new Translator('en');
    $translator->addLoader('yaml', new YamlFileLoader());
    $translator->addResource('yaml', __DIR__.'/../src/Resources/translations/messages.es.yaml', 'es');
    $translator->addResource('yaml', __DIR__.'/../src/Resources/translations/messages.en.yaml', 'en');
    $translator->setFallbackLocales(['en']);

    $application = new Application();

    $application->add(new SearchInCodeCommand(
    $translator,
    [
    'default_option' => 'php',
    'app' => [
    'php' => [
    'php' => ['src']
    ]
    ],
    'vendors' => [],
    ]
    ));

    $application->run($input);

    Maybe I can use my Kernel class for this?

    class Kernel extends BaseKernel
    {
    public function __construct(string $environment)
    {
    parent::__construct($environment, true);
    }

    public function registerBundles()
    {
    return [
    new MicayaelCommandsBundle(),
    ];
    }

    public function registerContainerConfiguration(LoaderInterface $loader)
    {
    }
    }

  • 2018-07-18 Victor Bocharsky

    Hey Juan,

    Thank you! :)

    Not sure I understand your first question, what is your problem exactly? Since Translator is already a declared service.

    You can make your service arguments optional, see this docs: https://symfony.com/doc/cur...

    I hope this helps.

    Cheers!

  • 2018-07-16 Juan Miguel Ardissone

    Great course. I've been waiting for it for a long time :-)

    Hi. What if I want to use a service like Translations. Where I should use the constructor and how because the constructor have parameters and I have to load the catalogs (https://symfony.com/doc/cur...

    Another question. Can I use this service as optional? this make sense?