Abstract controller for Silex

Abstract controller for Silex
http://bit.ly/2yXGlKr

The recommended way (well, at least from the examples in their docs) of working with the controllers in Silex is by passing the container object in the controller method itself. So you can either work with a closure:

$app->get('/', function () use ($app) {
    return $app->redirect('/hello');
});

or with a class method if you happen to organize your controllers in classes:

public function redirectToHelloAction(Application $app)
{
    return $app->redirect('/hello');
}

You might find this container injection on every method useful, because it is mostly scoped.

On the other hand, you might be one of those people that don't like that and would rather like to have the Request object as the only method dependency. Or maybe you would like to extract some common controller functionality into reusable methods. The way that Silex extracts common functionality is via implementing traits and using them in the Application (Container) class.

Another approach you can take instead of implementing a trait is to create an abstract controller. If you are in a situation in which you're thinking of implementing an abstract controller, here's a possible solution to the problem.

Let's create the abstract controller class as a first step:

namespace Vendor\Controller;

use Silex\Application as Container;

abstract class AbstractController
{
    /**
     * @var Container
     */
    protected $container;

    /**
     * @param Container $container
     */
    public function __construct(Container $container)
    {
        $this->container = $container;
    }
}

Next, we need to create a ControllerResolver which will set the container on our AbstractController.

If you check out the Silex\ControllerResolver class, it might look to you as something we can reuse, but the class is being deprecated because the only method in it doGetArguments will be dropped along with the class itself when Symfony 3.0 goes out of support. So we need to create a ControllerResolver of our own.

namespace Vendor\Controller;

use Silex\Application as Container;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolver as BaseControllerResolver;

class ControllerResolver extends BaseControllerResolver
{
    /**
     * @var Container
     */
    private $container;

    /**
     * @param Container $container
     * @param LoggerInterface|null $logger
     */
    public function __construct(Container $container, LoggerInterface $logger = null)
    {
        parent::__construct($logger);

        $this->container = $container;
    }

    /**
     * {@inheritdoc}
     */
    protected function instantiateController($class)
    {
        return new $class($this->container);
    }
}

As a last step you will need to register your ControllerResolver through a Provider in order to replace the currently used one. Don't forget to register the provider in your configuration.

namespace Vendor\Provider;

use Vendor\Controller\ControllerResolver;
use Pimple\Container;
use Pimple\ServiceProviderInterface;

class HttpKernelServiceProvider implements ServiceProviderInterface
{
    /**
     * {@inheritdoc}
     */
    public function register(Container $container)
    {
        $container['resolver'] = function ($container) {
            return new ControllerResolver($container, $container['logger']);
        };
    }
}

This is pretty much it. You now have an abstract controller that you can work with in your controllers. But looking at how the abstract controller is implemented in Symfony, we can improve our approach by adding some more abstraction.

The best way would be to get the DependencyInjection component and work with the Container related functionality such as the ContainerAwareInterface, ContainerAwareTrait and the ContainerInterface. Unfortunately the "best way" is at the same time not achievable, because in the ContainerInterface there's a get method that we will need to implement and the Silex application class already contains a get method with a completely different meaning (it's a routing method). This kind of highlights the mix of concerns in the application class, which is the main reason for why Silex suggests using traits to extend the "application" functionality.

Nevertheless, we can still improve our code, but this time by mimicking the Symfony approach. So create your ContainerAwareInterface:

namespace Vendor\DependencyInjection;

interface ContainerAwareInterface
{
    /**
     * @param ContainerInterface $container
     */
    public function setContainer(ContainerInterface $container);
}

and the ContainerInterface:

namespace Vendor\DependencyInjection;

interface ContainerInterface
{
}

Extend the Silex application class to be able to implement your new interface:

namespace Vendor\DependencyInjection;

use Silex\Application;

class Container extends Application implements ContainerInterface
{
}

We can now modify our AbstractController like the following:

namespace Vendor\Controller;

use Silex\Application;

abstract class AbstractController implements ContainerAwareInterface
{
    /**
     * @var ContainerInterface
     */
    protected $container;

    /**
     * @param ContainerInterface $container
     */
    public function setContainer(ContainerInterface $container)
    {
        $this->container = $container;
    }
}

And this is how our improved ControllerResolver is going to look like now:

namespace Vendor\Controller;

use Vendor\DependencyInjection\ContainerAwareInterface;
use Vendor\DependencyInjection\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolver as BaseControllerResolver;

class ControllerResolver extends BaseControllerResolver
{
    /**
     * @var ContainerInterface
     */
    private $container;

    /**
     * @param ContainerInterface $container
     * @param LoggerInterface|null $logger
     */
    public function __construct(ContainerInterface $container, LoggerInterface $logger = null)
    {
        parent::__construct($logger);

        $this->container = $container;
    }

    /**
     * {@inheritdoc}
     */
    protected function instantiateController($class)
    {
        $controller = parent::instantiateController($class);

        if ($controller instanceof ContainerAwareInterface) {
            $controller->setContainer($this->container);
        }

        return $controller;
    }
}

And there you have it. You have your amazingly awesome Silex abstract controller now. But there's always the question of do you need one? Hmmm… I don't know, I'll leave that up to you.

Previous article

Welcome on board

Next article

IBM Watson for a day