Customize everything with Events

We can override templates. We can override translations. We can override forms. But there's more than that. For example, after we finish registration, we're redirected to this registration confirmation page. You know what? I'd rather do something else: I'd rather redirect to the homepage and skip this page entirely. How can we do that?

Well, let's do a little bit of digging. If you hover over the route name in the web debug toolbar, you can see that this is page rendered by RegistrationController. Back in my editor I'll press Shift+Shift and look for RegistrationController in the bundle. Specifically, registerAction() is responsible for both rendering the registration page and processing the form submit.

And check this out: after the form is valid, it redirects to the confirmation page.

Events to the Rescue!

So at first, it seems like we need to override the controller itself. But not so fast! The controller - in fact every controller in FOSUserBundle is littered with events: REGISTRATION_INITIALIZE, REGISTRATION_SUCCESS, REGISTRATION_COMPLETED and REGISTRATION_FAILURE. Each of these represents a hook point where we can add custom logic.

In this case, if you look closely, you can see that after it dispatches an event called REGISTRATION_SUCCESS, below, it checks to see if the $event has a response set on it. If it does not, it redirects to the confirmation page. But if it does, it uses that response.

That's the key! If we can add a listener to REGISTRATION_SUCCESS, we can create our own RedirectResponse and set that on the event so that this controller uses it. Let's go!

Creating the Event Subscriber

Inside of AppBundle, create a new directory called EventListener. And in there, a new PHP class: how about RedirectAfterRegistrationSubscriber. Make this implement EventSubscriberInterface: the interface that all event subscribers must have. I'll use our favorite Code->Generate menu, or Command+N on a Mac, go to "Implement Methods" and select getSubscribedEvents.

12 lines src/AppBundle/EventListener/RedirectAfterRegistrationSubscriber.php
... lines 1 - 2
namespace AppBundle\EventListener;
... lines 4 - 6
class RedirectAfterRegistrationSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
}
}

We want to attach a listener to FOSUserEvents::REGISTRATION_SUCCESS, which, by the way, is just a constant that equals some string event name.

In getSubscribedEvents(), add FOSUserEvents::REGISTRATION_SUCCESS assigned to onRegistrationSuccess. This means that when the REGISTRATION_SUCCESS event is fired, the onRegistrationSuccess method should be called. Create that above: public function onRegistrationSuccess().

22 lines src/AppBundle/EventListener/RedirectAfterRegistrationSubscriber.php
<?php
... lines 2 - 8
class RedirectAfterRegistrationSubscriber implements EventSubscriberInterface
{
public function onRegistrationSuccess(FormEvent $event)
{
}
public static function getSubscribedEvents()
{
return [
FOSUserEvents::REGISTRATION_SUCCESS => 'onRegistrationSuccess'
];
}
}

Oh, and notice that when this event is dispatched, the bundle passes a FormEvent object. That will be the first argument to our listener method: FormEvent $event. That's what we need to set the response onto.

Investigating all the Events

Before we go any further, I'll hold command and click into the FOSUserEvents class... cause it's awesome! It holds a list of every event dispatched by FOSUserBundle, what its purpose is, and what event object you will receive. This is gold.

Creating the RedirectResponse

Back in onRegistrationSuccess, we need to create a RedirectResponse and set it on the event. But to redirect to the homepage, we'll need the router. At the top of the class, create public function __construct() with a RouterInterface $router argument. Next, I'll hit Option+Enter, select "Initialize Fields" and choose router.

33 lines src/AppBundle/EventListener/RedirectAfterRegistrationSubscriber.php
<?php
... lines 2 - 10
class RedirectAfterRegistrationSubscriber implements EventSubscriberInterface
{
private $router;
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
... lines 19 - 32
}

That was just a shortcut to create the private $router property and set it in the constructor: nothing fancy.

Now, in onRegistrationSuccess() we can say $url = $this->router->generate('homepage'), and $response = new RedirectResponse($url). You may or may not be familiar with RedirectResponse. In a controller, to redirect, you use $this->redirectToRoute(). In reality, that's just a shortcut for these two lines!

Finally, add $event->setResponse($response).

33 lines src/AppBundle/EventListener/RedirectAfterRegistrationSubscriber.php
<?php
... lines 2 - 10
class RedirectAfterRegistrationSubscriber implements EventSubscriberInterface
{
... lines 13 - 19
public function onRegistrationSuccess(FormEvent $event)
{
$url = $this->router->generate('homepage');
$response = new RedirectResponse($url);
$event->setResponse($response);
}
... lines 26 - 32
}

Ok, this class is perfect! To tell Symfony about the event subscriber, head to app/config/services.yml. At the bottom, add app.redirect_after_registration_subscriber, set the class, and add autowire: true. By doing that, thanks to the RouterInterface type-hint, Symfony will automatically know to pass us the router.

Finally, add a tag on the bottom: name: kernel.event_subscriber. And we are done!

28 lines app/config/services.yml
... lines 1 - 5
services:
... lines 7 - 22
app.redirect_after_registration_subscriber:
class: AppBundle\EventListener\RedirectAfterRegistrationSubscriber
autowire: true
tags:
- { name: kernel.event_subscriber }

Try it out! Go back to /register and signup as aquanaut5@gmail.com. Fill out the rest of the fields and submit!

Boom! Back to our homepage! You can customize just about anything with events. So don't override the controller. Instead, hook into an event!

Leave a comment!

  • 2017-08-16 weaverryan

    Yo mattxtlm!

    I would use an event subscriber for that :). Specifically, create a subscriber on the same event that we use in this chapter - FOSUserEvents::REGISTRATION_SUCCESS. The FormEvent object that you're passed allows you to say $user = $event->getForm()->getData(). You can then update the User object, and it will be saved by FOSUserBundle right after.

    Try it out and let me know how it goes :).

    Cheers!

  • 2017-08-16 weaverryan

    Ah, I'm so happy it worked! Thanks for following up and good luck! :)

  • 2017-08-15 mattxtlm

    Hey,

    I need to store the users registration IP, so I added an extra DB field. But in the registration process (i.e. the RegistrationController), I can't find a way to store its IP.

    I think, it should be somewhere before


    public function registerAction(Request $request)
    {
    /* ... */
    $userManager->updateUser($user);
    /* ... */

    What would you suggest?

    Thanks,
    Matt

  • 2017-08-15 Jürgen Schatral

    Thank you that`s it! It works with redirection to the homepage.

    After that we looked again in you documention file controller_envents.rst - "Registration success listener with enabled confirmation at the same time". We have read your documention before but we have read it so badly. The solution of this facts stood very near by us, but we don`t have seen it. ...To change the priority is not so unusual. Now we are shure that we have with this bundle a great range of flexibity. We have learned a lot. Thank you for that.
    Greeting from Germany - J.Schatral

  • 2017-08-15 weaverryan

    Yo Jürgen Schatral!

    Well, this is very interesting! So first, I don't think the issue is Symfony 3.3 - I can't think of a reason why this would cause an issue, and you (very smartly) tried commenting out all the new Symfony 3.3 services.yml code, which didn't change anything. Also, I just tried it :). I downloaded a fresh Symfony 3.3 project, installed FOSUserBundle, copy and pasted your subscriber and... it works! I'm redirected to the homepage as expected. You can see the code here: https://github.com/weaverry...

    The issue is something different - and you discovered it in your last message: *if* you activate the email confirmation step, then that overrides the response that you're setting. The fix is a bit odd: you need to set the priority of your subscriber to be lower than the EmailConfirmationListener, so that your listener is called *after*. The change should look like this in your subscriber


    public static function getSubscribedEvents()
    {
    return [
    // use a -5 priority - the other listener uses a 0 priority
    FOSUserEvents::REGISTRATION_SUCCESS => ['onRegistrationSuccess', -5]
    ];
    }

    I haven't tested this, so let me know if it works. And great issue - very tricky!

    Cheers!

  • 2017-08-14 Jürgen Schatral

    First the Response status:


    HTTP status 200 OK
    Controller RegistrationController :: registerAction
    Controller class FOS\UserBundle\Controller\RegistrationController
    Route name fos_user_registration_register
    Has session yes

    Second the url property:


    $url = $this->getTargetPath($event->getRequest()->getSession(), 'main');
    if (!$url) {
    $url = $this->router->generate('homepage');
    #Debugger shows /ISTFramework2/web/app_dev.php/
    }

    Yes you are right the whole session will be used to reference to the $url at first but the $url will be empty and the access to the property which contain the name followed in the if block in the next code line.
    After the execution of the overriden method we landed in C:/xampp/htdocs/ISTFramework2/vendor/friendsofsymfony/user-bundle/EventListener/EmailConfirmationListener.php.FOS\UserBundle\EventListener\EmailConfirmationListener->onRegistrationSuccess().
    After that a e-mail confirmation will be send and the FOSUserEvents::REGISTRATION_COMPLETED event will dispatched and the homepage event will be ignored again.

    ...But this code is identical to your download version and nothing has changed.
    If it is posible for you to generate a simple new Symfony 3.3 application with friendsofsymfony/user-bundle with a copy of the eventlisner from your download code you can simply debug it with phpstorm or eclipse.
    It is not much work. I think if you debug it on your own you see something more what I don`t see... :-) After that you can upload a actual version Symfony 3.3 application without errors - if possible
    Thank you! Greetings from Germany.

  • 2017-08-10 Diego Aguiar

    Hmm, interesting, what does it do after ? could you open your browser's debugger and check the response status ?

    Also I just noticed this line: $url = $this->getTargetPath($event->getRequest()->getSession(), 'main');
    You are passing the whole session, I believe you need to access to the property which contains the name or the URL

  • 2017-08-10 Jürgen Schatral

    Yes! At that point It look`s like, but it doesn`t :-)

  • 2017-08-10 Diego Aguiar

    > Why not "Dev" ?
    Because Dev branches might contain unknown bugs or backward incompatibilities

    Hmm, look's like it is trying to redirect you to the homepage, exactly what you want, isn't it ?

  • 2017-08-10 Jürgen Schatral

    Okay, if changed the version and run an composer update, clean and warm up the project...
    If have set a breakpoint in the event onRegistrationSuccess() it halted definitly at the line with $url = $this->router->generate('homepage');
    $response = new RedirectResponse($url); Debug information shows me that:
    HTTP/1.0 302 Found
    Cache-Control: no-cache, private
    Date: Thu, 10 Aug 2017 10:33:32 GMT
    Location: /ISTFramework2/web/app_dev.php/

    <html>
    <head>
    <meta charset="UTF-8"/>
    <meta http-equiv="refresh" content="1;url=/ISTFramework2/web/app_dev.php/"/> <title>Redirecting to /ISTFramework2/web/app_dev.php/</title>
    </head>
    <body>
    Redirecting to /ISTFramework2/web/app_dev.php/.
    </body>
    </html>
    The next line
    $event->setResponse($response); brings me back zu my old problem :-)
    Why not "Dev" instead of "^2.0" - Thank you!
    Greeting from Germany

  • 2017-08-09 Diego Aguiar

    Oh, you installed the "Dev" branch of FOSUserBundle, just change it to: "friendsofsymfony/user-bundle" : "^2.0"
    as we do (composer.json), and run: composer update friendsofsymfony/user-bundle

    If it doesn't fix it, I believe your listener is not been fired, what it means is, the listener is not registered, try adding a `die()` statement so we can tell if it's been executed or not, so we can keep debugging :)

    Cheers!

  • 2017-08-09 Jürgen Schatral

    Sorry I have made a mistake here is the EventListerner again...
    Mfg. J.Schatral


    class RedirectAfterRegistrationSubscriber implements EventSubscriberInterface
    {
    use TargetPathTrait;

    private $router;

    public function __construct(RouterInterface $router)
    {
    $this->router = $router;
    }

    public function onRegistrationSuccess(FormEvent $event)
    {
    // main is your firewall's name
    $url = $this->getTargetPath($event->getRequest()->getSession(), 'main');

    if (!$url) {
    $url = $this->router->generate('homepage');
    }

    $response = new RedirectResponse($url);
    $event->setResponse($response);
    }

    public static function getSubscribedEvents()
    {
    return [
    FOSUserEvents::REGISTRATION_SUCCESS => 'onRegistrationSuccess'
    ];
    }
    }
  • 2017-08-08 Diego Aguiar

    Hey Jürgen Schatral

    Can you show me the snippet of your listener ? I believe you forgot to attach the RedirectResponse object to the event, but I might be wrong

    Cheers!

  • 2017-08-08 Jürgen Schatral

    Hello together,
    First, thankyou for your good work. I`ve tested your sample code - no problems. FOSUserEvents::REGISTRATION_SUCCESS is overriden and the "homepage" is displayed. But if I generate a new Symfony 3.3 application framework with friendsofsymfony/user-bundle the landing page with "The user has been created successfully.. " will be still dispayed but no homepage. If I debug onRegistrationSuccess in step over mode, I see how the line $this->router->generate('homepage') will be executed, but the result no homepage. Other FOSUserEvents do have the same problem. To avoid this problem I comment out all of the new code in services.yml but the result will be the same no homepage.
    What is going wrong? Is there something more to do? Thank you... Greetings from Germany

  • 2017-07-11 Victor Bocharsky

    Hey kaizoku ,

    Uh oh, sounds like a big piece of work ;) Yea, events rock! But sometimes you are not enough of them.

    Cheers!

  • 2017-07-10 kaizoku

    Wow ! Thank you this is cool !
    I wish I knew this before I override all my controllers haha :D

  • 2017-05-25 Michael

    Dear Diego
    And now I unterstand how it works and it works! ;-)
    Thanks a lot for your help!
    Michael

  • 2017-05-24 Diego Aguiar

    I got you now! (Sorry, I misunderstood you)

    Indeed you are missing your validations, and the reason is because FOSUserBundle add's them via "Validation Groups", you have to specify which group you want to use in your FormType's (actually there are more ways of doing it), in this case you want the "Registration" group, you can do it like this:


    //Registration Form Type

    public function configureOptions(OptionsResolver $resolver)
    {
    $resolver->setDefaults(array(
    'validation_groups' => array('registration'),
    ));
    }

    You can read more about validation groups here http://symfony.com/doc/curr...
    Or even better, we have a tutorial where Ryan explains it in a cool way https://knpuniversity.com/s...

    Cheers!

  • 2017-05-24 Michael

    Hi Diego
    Thanks a lot for the fast replay!
    I don't think it's a translation problem. For me, it seems to be a validation problem. Because with my own fields it works perfect. But with the standard fields like email, only the error-screen of symfony is displayed.

  • 2017-05-24 Diego Aguiar

    Hey Michael

    We are glad to hear you are liking our tutorials :)

    Have you tried using translations ? you can customize any message from the system, if you don't know how, you can check the docs here http://symfony.com/doc/curr...
    or even better, check our video about it: https://knpuniversity.com/s...

    Have a nice day!

  • 2017-05-23 Michael

    Dear KnpUniversitiy-Team
    First, thanks a lot for the really great tutorial!!
    I added some other fields like name or firstname to the user entity. Now I Have the problem, that the validation of the form works for this new fields fine and when a field with @Assert\NotBlank() is missing, a nice note is displayed in the form. But when the email field is empty, no note is displayed, but the error "Column 'email' cannot be null" is displayed. How can I fix this? I tried to find a solution in the web, but I haven't' found a solutions which I understand.
    Best regards!
    Michael