[Se]   En

Event subscribers i PHP Symfony

  Annice      2020-12-27      PHP, Symfony

Applikationer som är uppbyggda utifrån MVC-mönstret (Model-View-Controller) använder ju controller-klasser som ett slags interaktionslager mellan ett view- och model-lager. Entitets/objektsklasser och deras egenskaper har då för avsikt att finnas i model-lagret med syfte att avspegla databastabeller och deras kolumner, medan controller-klasser och deras action-metoder har till syfte att avspegla en applikations olika sidor, vilka finns i view-lagret.

Med andra ord innebär detta att det är controller-metoder som dels tar emot datainput från slutanvändare via view-lagret för att passa det vidare till databas via entitetslagret, dels att dessa metoder renderar de olika webbsidorna som i slutänden presenteras för slutanvändare. När man surfar till en webbsida som använder MVC är det alltså först en controller-metod som anropas på serversidan för att förbereda den efterfrågade webbsidan med all nödvändig data, vilken då kan hämtas från en eller flera databastabeller via en eller flera entiteter.

När jag har möjligheten att välja brukar jag föredra att utgå ifrån MVC som designmönster då jag tycker det är ett väldigt strukturerat sätt att bygga och hantera applikationskod på. Inom C# ASP.NET benämns dessa lager som just model, view och controller, men i PHP Symfony-ramverket benämns de som entity (model), templates (view), samt controller.

I en större MVC-applikation är det ju inte ovanligt att använda flera olika controllers där varje controller i nästa tur har flera olika action-metoder, varvid många av dessa metoder kanske också har till uppgift att rendera olika adminsidor. För att säkersställa att obehöriga användare inte når dessa sidor behöver man ju då kontrollera detta på något sätt innan dessa sidor presenteras. Men istället för att behöva lägga samma loginkontroll i varje berörd action-metod, vilket bl.a. skulle strida mot DRY-principen (Don't Repeat Yourself), kan detta hanteras via t.ex. en middleware vilken kontrollerar detta innan controllers anropas (som jag även beskrivit i ett tidigare inlägg).

Ett liknande sätt att hantera detta på som jag upptäckt inom PHP Symfony är att använda sig av event subscribers. Principen går då ut på att man implementerar en event subscriber-klass som i sin tur implementerar ett interface vid namn "EventSubscriberInterface". EventSubscriberInterfacet är vidare tillgängligt via en EventDispatcher-komponent som kan installeras som bundle via Symfony-ramverket med pakethanteraren composer.

Vidare kan man då låta en event subscriber agera kontrollant av t.ex. en giltig admininloggning. Man kan sedan säkerställa att subscribern anropas innan önskade controllers genom att låta dessa controllers implementera ett tomt interface, vilket subscribern kontrollerar om controllern är en instans av. Jag har försökt illustrera detta kontextflöde i sekvensdiagrammet nedan:


Exempel på en implemenation av detta kan se ut som nedan där jag först valt att skapa ett separat lager (mapp) namngiven "Service". I denna mapp har jag vidare skapat ett tomt interface vid namn "AuthInterface" som jag lagt i en undermapp vid namn "Abstraction". Följande kod utgår för övrigt från PHP 7.4 utifrån Symfony 5.1.8:

<?php
namespace App\Service\Abstraction;

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


Vidare har jag skapat ytterligare en mapp vid namn "EventSubscriber" i vilken jag skapat en AuthSubscriber-klass. Under förutsättning att EventDispatcher-komponenten som sagt installerats har jag sedan kunnat låta denna AuthSubscriber-klass implementera ett EventSubscriberInterface likt nedan:

<?php
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.


Slutligen kan jag bestämma vilka controller-klasser som ska kontrolleras av denna AuthSubscriber genom att låta dessa controllers implementera AuthInterfacet likt nedanstående kod:

<?php
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.