Event subscribers in PHP Symfony

  Annice      2020-12-27      PHP, Symfony

Applications based on the MVC pattern (Model-View-Controller) use controller classes as an interaction layer between a view and a model layer. Moreover, the purpose with entity/object classes and their properties in the model layer is to reflect the database tables and their columns, while the controller classes and their action methods reflect the different application pages in the view layer.

In other words, this means that the controller methods receive the data inputs from end users via the view layer to pass it to the database via the model/entity layer. Also, these methods render the different web pages to be presented to the end users. When you navigate to a web page based on MVC, a controller method is first called on the server side to prepare the requested page with all necessary data, which is then fetched from one or many database tables via one or many entities.

When I have the opportunity to choose, I usually prefer to use the MVC pattern as I think it is a very structured way of building and managing the application code. Within C# ASP.NET, these layers are simply called model, view, and controller, but within the PHP Symfony framework they are called entity (model), templates (view), and controller.

In larger MVC applications, it is common to use several controllers which in turn consist of several different action methods. Many of these methods might also have the task to render different admin pages. To prevent unauthorized users from accessing these pages, you need to some how control this before the pages are presented. But instead of having to control this with the same code in every concerned action method, which would violate the DRY principle (Don't Repeat Yourself), this can rather be handled via e.g. a middleware to control this before controllers are called (which I have also described in another entry).

However, a similar way of handling this, which I have discovered within PHP Symfony, is to use event subscribers. The principle with these subscribers is to implement an event subscriber class, which in turn implements an interface named "EventSubscriberInterface". Furthermore, the EventSubscriberInterface is available via an EventDispatcher component which can be installed as a bundle via the Symfony framework using the package manager composer. Moreover, this enables you to let an event subscriber control e.g. a valid user login.

Furthermore, you can ensure that this subscriber is called before wanted controllers by letting these controllers implement an empty interface, in which the subscriber checks if the controller is an instance of. I have tried to illustrate this context flow in the sequence diagram below:


An example of an implementation of this can look like below, where I first chose to create a separate layer (folder) named "Service". Furthermore, I created an empty interface called "AuthInterface" put in a sub folder named "Abstraction". The following code is also based on PHP 7.4 with Symfony 5.1.8:

namespace App\Service\Abstraction;

interface AuthInterface
{
    // No need for method declarations in this interface.
}


Additionally, I created another folder named "EventSubscriber" in which I created an AuthSubscriber class. With the EventDispatcher component installed, I was further able to let this AuthSubscriber class implement the EventSubscriberInterface as below:

namespace App\EventSubscriber;

use App\Service\Abstraction\AuthInterface;
use App\Service\AuthManager;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * The AuthSubscriber is called before the controllers to check valid admin login
 * for password protected controllers.
 */
class AuthSubscriber implements EventSubscriberInterface
{
    private $authManager;
    private $router;

    public function __construct(AuthManager $auth, UrlGeneratorInterface $router)
    {
        $this->authManager = $auth;
        $this->router = $router;
    }

    public function onKernelController(ControllerEvent $event)
    {
        $controller = $event->getController();

        // When a controller class defines multiple action methods, the controller
        // is returned as [$controllerInstance, 'methodName']:
        if (is_array($controller)) {
            $controller = $controller[0];
        }

        // Check if the controller implements our AuthInterface:
        if ($controller instanceof AuthInterface) {

           // Call a separately implemented authManager class in which we check if an admin session is set.
           // If the session is not set, just redirect the user to a public start page.
            if (!$this->authManager->isLoggedIn()) {

                $redirectUrl = $this->router->generate('start');

                $event->setController(function () use ($redirectUrl) {
                    return new RedirectResponse($redirectUrl);
                });
            }
        }
    }

    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::CONTROLLER => 'onKernelController',
        ];
    }

} // End class.


Finally, I can choose which controllers I want to be checked by this AuthSubscriber by letting these controllers implement the AuthInterface as the following code:

namespace App\Controller;

use App\Service\Abstraction\AuthInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

/**
 * Controller class to handle the admin pages.
 */
class AdminController extends AbstractController implements AuthInterface
{
    /**
     * @Route("/admin", name="admin_page")
     * @Method({"GET"})
     */
    public function adminPageAction()
    {
        // Prepare data etc here for the admin page...

        return $this->render("admin/admin-page.html.twig");
    }

} // End class.