UX Improvements - Part 1 - Redirect /login when Logged In


In this video we are going to look at the first of two possible ways in which you can stop a User from being able to visit the /login route / login form page when they have already logged in.

There are likely many more ways of achieving this goal.

In this video we will cover how to override a FOSUserBundle controller to achieve this goal. In the next video we will do the same, but use Symfony events instead.

This way illustrates the point in an easier to understand manner than looking at events, if you have never done so. Once you see the outcome of this video, the next will show you a better way (in my opinion) of doing this.

This whole process is really a usability enhancement rather than a necessity. If you feel your Users may experience confusion if they accidentally find their way to the login form whilst already logged in, then consider doing this. Likewise, if your Users are technically savvy, they may try this to see what happens out of sheer curiosity.

Looking back to a video earlier in this series where we added in a Bootstrap 3 topnav / navbar, you may remember the following snippet:

    <div>
        {% if is_granted("IS_AUTHENTICATED_REMEMBERED") %}
            {{ 'layout.logged_in_as'|trans({'%username%': app.user.username}, 'FOSUserBundle') }} |
            <a href="{{ path('fos_user_security_logout') }}">
                {{ 'layout.logout'|trans({}, 'FOSUserBundle') }}
            </a>
        {% else %}
            <a href="{{ path('fos_user_security_login') }}">{{ 'layout.login'|trans({}, 'FOSUserBundle') }}</a>
        {% endif %}
    </div>

This stopped our Users from seeing the 'log in' link once they had logged in. As such, that may be good enough for you.

Overriding FOSUserBundle Controllers

Whilst this implementation follows the guidelines in the FOSUserBundle documentation on Overriding Default Controllers, we are not going to alter the controller logic, but rather, wrap our own method around the default LoginAction inside FOSUserBundle's SecurityController.

This sounds more complicated than it really is.

All we are doing here is saying hey, Symfony, first take a look at our custom SecurityController, and then once we've done our little custom bit, pass on the real work to FOSUserBundle's SecurityController.

The logic we want to implement is pretty simple:

When a User tries to access the /login route (fos_user_security_login), if they are not yet logged in, then show them the /login page.

If they are already logged in, don't show them the login page, instead redirect them to the homepage (/).

In order to make Symfony look at our SecurityController over the default one supplied by FOSUserBundle we must either configure our AppBundle to be a child of FOSUserBundle, or, create a brand new Bundle and make that a child of FOSUserBundle.

I'm going to opt for creating a brand new Bundle as it is easier for me to reason about. Feel free to make the decision that best suits your needs.

To create a new Bundle is as easy as creating a new folder under your src/ directory called YourBundleName, and adding a single .php file to that folder, matching the name of your new folder name e.g.:

// src/ChrisUserBundle/ChrisUserBundle.php

namespace ChrisUserBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

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

As with any new Bundle, we must enable it in our app/AppKernel.php file:

<?php

// app/AppKernel.php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
            new Symfony\Bundle\SecurityBundle\SecurityBundle(),
            // etc

            new ChrisUserBundle\ChrisUserBundle(),

And that's pretty much it, our new Bundle is enabled and ready to go.

Now, Symfony knows our ChrisUserBundle is a child of FOSUserBundle, which is great.

However, we haven't added any files to our ChrisUserBundle so Symfony will fall back to looking at the parent bundle for anything it can't find. In other words, right now it will always fall back to the files inside FOSUserBundle (vendor/friendsofsymfony/user-bundle/{whatever}).

To alter this, as long the file names we create match up to a file that exists in FOSUserBundle already, then our files will get read first.

As part of the installation process you will remember that we had to import all of FOSUserBundle's routes to tell Symfony that these routes were now available:

# app/config/routing.yml
fos_user:
    resource: "@FOSUserBundle/Resources/config/routing/all.xml"

One of those routes was the fos_user_security_login route, or /login:

<!-- vendor/friendsofsymfony/user-bundle/Resources/config/routing/security.xml -->

    <route id="fos_user_security_login" path="/login" methods="GET POST">
        <default key="_controller">FOSUserBundle:Security:login</default>
    </route>

The part we are interested in is the _controller definition.

Essentially, as long as our definition matches, our controller will be read first.

So, if we create a new Controller:

<?php

// src/ChrisUserBundle/Controller/SecurityController.php

namespace ChrisUserBundle\Controller;

use FOS\UserBundle\Controller\SecurityController as BaseController;
use Symfony\Component\HttpFoundation\Request;

class SecurityController extends BaseController
{
    public function loginAction(Request $request)
    {
        // exit('we got here');

        return parent::loginAction($request);
    }
}

Remember, the default route definition is: FOSUserBundle:Security:login.

We have created a child bundle of FOSUserBundle, so ChrisUserBundle is for all intents and purposes, equal to FOSUserBundle. That matches the first part.

The remaining part of the route is again, standard Symfony notation: :Security:login.

Our new controller is SecurityController, and our action is loginAction.

Standard Symfony naming would remove the Controller and Action suffixes, so in short, our definition is the exact same as the one FOSUserBundle needs to match. Ours wins.

We can prove this by uncommenting the exit() statement, accessing the /login route and seeing our application blow up.

How To Get the Current User in Symfony 2

In Symfony 2.6 there was a change to the way we access the currently logged in User:

// Symfony 2.5
$user = $this->get('security.context')->getToken()->getUser();
// Symfony 2.6
$user = $this->get('security.token_storage')->getToken()->getUser();

Read more about this here.

If you are using an older version of Symfony, be sure to use the older method detailed above.

Once we have the made the call above, we can check to see if the $user variable is an instance of FOS\UserBundle\Model\User.

Why do we check this?

Well, way back in the first video when we added FOSUserBundle to our project, we created our own implementation of User, which looked something like this:

<?php
// src/AppBundle/Entity/User.php

namespace AppBundle\Entity;

use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="fos_user")
 */
class User extends BaseUser
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    public function __construct()
    {
        parent::__construct();
        // your own logic
    }
}

As you can see, here our User extends FOS\UserBundle\Model\User.

The downside to this is that we are tightly tying ourselves to FOSUserBundle. In my opinion, this is a trade off that I am willing to make at this stage. The likelyhood of me switching out from using FOSUserBundle is slim - it hasn't happened yet, and I've been using this Bundle in pretty much every project I have ever done.

The logic at this point is nice and easy:

// other use statements here
use FOS\UserBundle\Model\User;
use Symfony\Component\HttpFoundation\RedirectResponse;

class SecurityController extends BaseController
{
    public function loginAction(Request $request)
    {
        $user = $this->get('security.token_storage')->getToken()->getUser();

        if ($user instanceof User) {

            return new RedirectResponse(
                $this->generateUrl('some_route_on_your_site')
            );

        }

        return parent::loginAction($request);
    }

This is one way of handling this situation. In my opinion it is not the nicest way of handling this situation, but it does work.

In the next video we will repeat the same process but use Symfony events to remove the need for overriding FOSUserBundle at all.

Of course, if doing this in your real project then be sure to write the appropriate tests to cover this flow.

Code For This Course

Get the code for this course.

Episodes