Thursday, September 27, 2012

Calling a Method Before Every Controller Action in Symfony2


Calling a Method Before Every Controller Action in Symfony2

There are times when you need to execute a method on a controller before every action and sometimes on multiple controllers. In my case I had to check that a user was associated with a particular company and if so, fetch the company and some related data from the database. This had to happen before every action in the controller. It also had to happen in several controllers. Rather than extending a common controller class and calling a method from every action I decided to imitate the old symfony1 behavior of the preExecute method.

First, I created an interface. I wanted to be able to implement this interface on any controller that needed to be initialized without adding any extra code or configuration. In my case I always need access to the request and the security context so I specified that here.
namespace SOTB\CoreBundle\Model;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\SecurityContextInterface;
/**
* @author Matt Drollette
*/
interface InitializableControllerInterface
{
    public function initialize(Request $request, SecurityContextInterface $security_context);
}
Next I implemented this interface on one of my controllers. In it, I get the user and assign to a property. I also check that the user is assigned to a company and throw an error otherwise.
php
namespace SOTB\CustomerBundle\Controller;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\SecurityContextInterface;
use SOTB\CoreBundle\Model\InitializableControllerInterface;
/**
* @author Matt Drollette
*/
class CustomerController implements InitializableControllerInterface
{
    private $user;
    private $company;
    public function initialize(Request $request, SecurityContextInterface $security_context)
    {
        $this->user = $security_context->getToken()->getUser();
        $this->company = $this->user->getCompany();
        if (!$this->company) {
            throw new NotFoundHttpException('You are not assigned to a company.');
        }
    }
    // ... rest of controller actions
}
The next step is to create an event listener that checks the controller being called to see if it implements our initializable interface and if so to execute the initialize method.
php
namespace SOTB\CoreBundle\EventListener;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\Security\Core\SecurityContextInterface;
use SOTB\CoreBundle\Model\InitializableControllerInterface;
/**
* @author Matt Drollette
*/
class BeforeControllerListener
{
    private $security_context;
    public function __construct(SecurityContextInterface $security_context)
    {
        $this->security_context = $security_context;
    }
    public function onKernelController(FilterControllerEvent $event)
    {
        $controller = $event->getController();
        if (!is_array($controller)) {
            // not a object but a different kind of callable. Do nothing
            return;
        }
        $controllerObject = $controller[0];
        // skip initializing for exceptions
        if ($controllerObject instanceof ExceptionController) {
            return;
        }
        if ($controllerObject instanceof InitializableControllerInterface) {
            // this method is the one that is part of the interface.
            $controllerObject->initialize($event->getRequest(), $this->security_context);
        }
    }
}
We now have to attach that event listener to the kernel.controller event.
xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
         id="sotb_core.listener.before_controller" class="SOTB\CoreBundle\EventListener\BeforeControllerListener" scope="request">
             name="kernel.event_listener" event="kernel.controller" method="onKernelController"/>
             type="service" id="security.context"/>
        
    


Now any time we need to execute a method before every action in a controller we simply implement the InitializableControllerInterface.


PS:

this post actually is the implementation of preExecute of symfony1 in symfony2 

1,
the config of listener in yml is:


  sotb_core.listener.before_controller:
    class: SOTB\CoreBundle\EventListener\BeforeControllerListener
    arguments:    [@security.context]
    tags:
      - { name: kernel.event_listener, event: kernel.controller, method: onKernelController }  

2,  
if you get

atal error: Declaration of blablaController::initialize() must be compatible with that of blablaBundle\Model\InitializableControllerInterface::initialize() in blablaController.php on line 17

make sure you have


use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\SecurityContextInterface;

in your controller



3,
if you get

Fatal error: Call to undefined method blablaBundle\Controller\blablaController::getDoctrine() in blablaController.php on line 42

you need your controller extends Controller

class yourController extends Controller implements InitializableControllerInterface

More than 3 requests, I'll translate this to Chinese.
超过3个请求,我就会把这篇文章翻译成中文。

2 comments:

prabhu said...

Hi its very good post that u have given. Actually i need to check whether the session exists for a user who have logged in. If there is no session exists the user must be redirected to login page. I need to check this on every action. I changed a lot but still no go. Can u please help me in working out with that.

ALGO said...

seems you are building a user management system with login/logout

with symfony2, there's already build-in security component, please read the doc.