FOSUserBundle is a great way to save time, a precious resource in the coding world. It has just about everything you'd ever need to create with a login system. Registration, reset password, email confirmation, logging in, user groups, etc. It's just great. However, if anyone's worked with clients then you'd know there's a particular "something" about emails; they get lost, dissapear, or never even get there to begin with. Mysterious. The last thing you want to do to a potential user is send them an email, it goes Casper on them and they no longer can complete registeration to your site ever again, because their email is now "used" but perpetually unactivated.

This tutorial will guide you in placing the correct files into a "User Bundle" to override your FOSUserBundle core files with our new methods to allow a resend activation page.

For the people with no time, you can just download it below. You must register the bundle and add all the services and routes as usuall however. So if you're not sure how to do that, you might want to read the tutorial anyways.

Download UserBundle

Software Used:
Symfony2 - 2.7 (I'm sure this'll work with 2.3-2.9)
FOSUserBundle - 1.3.5

1 - Create The Bundle

Create the main bundle file and register it in your AppKernel.php

src/UserBundle/UserBundle.php

namespace UserBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class UserBundle extends Bundle
{
    public function getParent()
    {
        return 'FOSUserBundle';
    }    
}

Register our new UserBundle in app/AppKernel.php

$bundles = array(
    //...
    new UserBundle\UserBundle(),
    //...
);

2 - Create The Config Files

We need to create several config files for the routing, services, and validation.

src/UserBundle/Resources/config/routing.yml

user_registration_resend_confirm:
    path: /register/resend-confirm
    defaults: { _controller: UserBundle:Registration:resendConfirm }
    requirements:
        _method: GET|POST  

You'll use "user_registration_resend_confirm" to link to our resend page from here on. You can overwrite the default login error with a link going to this page for example.

src/UserBundle/Resources/config/services.yml

services:   
    user.resend_confirm.form.factory:
        class: FOS\UserBundle\Form\Factory\FormFactory
        arguments: ['@form.factory', 'user_resend_confirm', '@user.resend_confirm.form.type', ['ResendConfirm']]
            
    user.resend_confirm.form.type:
        class: UserBundle\Form\Type\ResendConfirmFormType
        arguments: [%fos_user.model.user.class%]
        tags:
            - { name: form.type, alias: user_resend_confirm }          

    user.resend_confirm:
        class: UserBundle\EventListener\EmailConfirmationListener
        arguments: [@fos_user.mailer, @fos_user.util.token_generator, @router, @session]
        tags:
            - { name: kernel.event_listener, event: user.resend_confirm, method: onResendConfirm }

In services, we're registering our new form type "ResendConfirmFormType" and using the FOSUserBundle's form factory service to create the form. We are also registering our Resend Email event.

src/UserBundle/Resources/config/validation.xml

<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://symfony.com/schema/dic/constraint-mapping
        http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">

    <class name="FOS\UserBundle\Model\User">

        <property name="email">
            <constraint name="NotBlank"><option name="message">fos_user.email.blank</option><option name="groups">
                    <value>Registration</value>
                    <value>Profile</value>
                    <value>ResendConfirm</value></option>
            </constraint>
            <constraint name="Length"><option name="min">2</option><option name="minMessage">fos_user.email.short</option><option name="max">254</option><option name="maxMessage">fos_user.email.long</option><option name="groups">
                    <value>Registration</value>
                    <value>Profile</value>
                    <value>ResendConfirm</value></option>
            </constraint>
            <constraint name="Email"><option name="message">fos_user.email.invalid</option><option name="groups">
                    <value>Registration</value>
                    <value>Profile</value>
                    <value>ResendConfirm</value></option>
            </constraint>
        </property>
    </class>

    <class name="FOS\UserBundle\Propel\User">

        <property name="email">
            <constraint name="NotBlank"><option name="message">fos_user.email.blank</option><option name="groups">
                    <value>Registration</value>
                    <value>Profile</value>
                    <value>ResendConfirm</value></option>
            </constraint>
            <constraint name="Length"><option name="min">2</option><option name="minMessage">fos_user.email.short</option><option name="max">255</option><option name="maxMessage">fos_user.email.long</option><option name="groups">
                    <value>Registration</value>
                    <value>Profile</value>
                    <value>ResendConfirm</value></option>
            </constraint>
            <constraint name="Email"><option name="message">fos_user.email.invalid</option><option name="groups">
                    <value>Registration</value>
                    <value>Profile</value>
                    <value>ResendConfirm</value></option>
            </constraint>
        </property>

    </class>

</constraint-mapping>

The validation.xml just adds and extra group onto the original FOSUserBundle validation.xml to add a new validation group named "ResendConfirm".

3 - Register The Event

We already got the service for the event in the last step, but now we must add the code for the event!

src/UserBundle/UserEvents.php

namespace UserBundle;

final class UserEvents
{
    const REGISTRATION_RESEND = 'user.resend_confirm';
}

This file references the service name for our event and places it in a constant for easy access.

src/UserBundle/EventListener/EmailConfirmationListener.php

namespace UserBundle\EventListener;

use FOS\UserBundle\EventListener\EmailConfirmationListener as FOSEmailConfirmationListener;
use FOS\UserBundle\Event\GetResponseUserEvent;
use FOS\UserBundle\Mailer\MailerInterface;
use FOS\UserBundle\Util\TokenGeneratorInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class EmailConfirmationListener extends FOSEmailConfirmationListener
{
    private $mailer;
    private $tokenGenerator;
    private $router;
    private $session;

    public function __construct(MailerInterface $mailer, TokenGeneratorInterface $tokenGenerator, UrlGeneratorInterface $router, SessionInterface $session)
    {
        $this->mailer = $mailer;
        $this->tokenGenerator = $tokenGenerator;
        $this->router = $router;
        $this->session = $session;
    }
    
    public function onResendConfirm(GetResponseUserEvent $event)
    {

        /** @var $user \FOS\UserBundle\Model\UserInterface */
        $user = $event->getUser();

        $user->setEnabled(false);
        if (null === $user->getConfirmationToken()) {
            $user->setConfirmationToken($this->tokenGenerator->generateToken());
        }

        $this->mailer->sendConfirmationEmailMessage($user);
        $this->session->set('fos_user_send_confirmation_email/email', $user->getEmail());

        $url = $this->router->generate('fos_user_registration_check_email');
        $event->setResponse(new RedirectResponse($url));        
        
    }    
}

Here's the meat, we are hooking into FOSUserBundle's EmailConfirmationListener. We use the method onResendConfirm in our service. This is what calls the Mailer service that resends the email.

4 - The Controller

Let's tie it all together now with our controller and our display form.

src/UserBundle/Controller/RegistrationController.php


namespace UserBundle\Controller;

use FOS\UserBundle\Controller\RegistrationController as FOSRegistrationController;
use FOS\UserBundle\Event\GetResponseUserEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use UserBundle\UserEvents;

class RegistrationController extends FOSRegistrationController
{

    public function resendConfirmAction(Request $request)
    {
        /** @var $formFactory \FOS\UserBundle\Form\Factory\FactoryInterface */
        $formFactory = $this->container->get('user.resend_confirm.form.factory');
        /** @var $userManager \FOS\UserBundle\Model\UserManagerInterface */
        $userManager = $this->container->get('fos_user.user_manager');
        /** @var $dispatcher \Symfony\Component\EventDispatcher\EventDispatcherInterface */
        $dispatcher = $this->get('event_dispatcher');

        $user = $userManager->createUser();

        $form = $formFactory->createForm();
        $form->setData($user);

        if ('POST' === $request->getMethod()) {
            $form->bind($request);

            if ($form->isValid()) {
                $email = $user->getEmail();
                $user = $userManager->findUserByEmail($email);

                if (null === $user) {
                    throw new NotFoundHttpException(sprintf('The user with email "%s" does not exist', $email));
                }

                $event = new GetResponseUserEvent($user, $request);
                $dispatcher->dispatch(UserEvents::REGISTRATION_RESEND, $event);
                $userManager->updateUser($user);

                if (null === $response = $event->getResponse()) {
                    $url = $this->container->get('router')->generate('fos_user_registration_confirmed');
                    $response = new RedirectResponse($url);
                }

                return $response;
            }
        }

        return $this->render('UserBundle:Registration:resend_confirm.html.twig', array(
            'form' => $form->createView(),
        ));        
    }
    
}

This checks if the form is valid and calls our event we just created above. This again extends FOSUserBundle and just adds a new method. If you update FOSUserBundle this should not be affected unless it's a major version upgrade.

src/UserBundle/Form/Type/ResendConfirmFormType.php

namespace UserBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class ResendConfirmFormType extends AbstractType
{
    private $class;

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

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('email', 'email', array('label' => 'Email'));
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => $this->class,
            'intention'  => 'resend_confirm',
        ));
    }

    public function getName()
    {
        return 'user_resend_confirm';
    }
}

Here's our form, nice n' shiney.

5 - The Views

/src/UserBundle/Resources/views/Registration/resend_confirm.html.twig

{% extends "FOSUserBundle::layout.html.twig" %}

{% block fos_user_content %}
{% include "UserBundle:Registration:resend_confirm_content.html.twig" %}
{% endblock fos_user_content %}

/src/UserBundle/Resources/views/Registration/resend_confirm_content.html.twig

<form action="{{ path('user_registration_resend_confirm') }}" {{ form_enctype(form) }} method="POST" class="user_registration_resend_confirm">
    {{ form_widget(form) }}
    <div>
        <input type="submit" value="Submit" />
    </div>
</form>  

6 - Register Your Bundle Services & Routing with App

To peice your bundle together with the main application, be sure to include your services.yml and routing.yml in your main config files.

app/config/config.yml

imports:
    - { resource: @UserBundle/Resources/config/services.yml }

app/config/routing.yml

user: 
    resource: "@UserBundle/Resources/config/routing.yml"

That's it!

Clear your cache out, then navigate to /registration/resend-confirm to try it out.

Questions? Comments? Please post below, I'll try to answer any questions.