I'll start by saying this is rarely the right thing to do with Silex. It's meant to be a micro framework, so if you're going to be building something that requires organising controllers in separate files or modules, maybe the full stack symfony framework would be a better choice. However, if you're like me and have gradually evolved a legacy application to be a Silex application, you may be in a situation where you want to try and keep things organised.
There's currently a pull request in the queue for Silex that adds a cookbook entry for [using controller classes][Cookbook], but I wanted to take it a step further and have my controllers as services, much like what's possible with the full symfony framework (See Richard Miller's post for further reading).
Things are best explained with an example, so let's start with this simple silex app:
<?php
use Silex\Application;
use Demo\Repository\PostRepository;
$app = new Application;
$app['posts.repository'] = $app->share(function() {
return new PostRepository;
});
$app->get('/posts.json', function() use ($app) {
return $app->json($app['posts.repository']->findAll());
});
First we need to tell Silex how we're going to map routes to our controllers. We
do this by overriding the controller resolver with our own custom resolver. We
can take the existing controller resolver and override the createController
method. If we encounter a single colon in the controller name, we look to see if the
string before the colon is a service, if it is, then we return a simple PHP
callback array.
<?php
namespace Demo\Controller;
use Silex\ControllerResolver as BaseControllerResolver;
class ControllerResolver extends BaseControllerResolver
{
protected function createController($controller)
{
if (false !== strpos($controller, '::')) {
return parent::createController($controller);
}
if (false === strpos($controller, ':')) {
throw new \LogicException(sprintf('Unable to parse the controller name "%s".', $controller));
}
list($service, $method) = explode(':', $controller, 2);
if (!isset($this->app[$service])) {
throw new \InvalidArgumentException(sprintf('Service "%s" does not exist.', $controller));
}
return array($this->app[$service], $method);
}
}
We simply then override Silex's default resolver.
<?php
$app['resolver'] = $app->share(function () use ($app) {
return new Demo\Controller\ControllerResolver($app, $app['logger']);
});
We can now convert our controller to be a class, add it to Silex as a service ready to be instantiated, complete with dependencies, when needed:
<?php
namespace Demo\Controller;
use Demo\Repository\PostRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
class PostController
{
protected $repo;
public function __construct(PostRepository $repo)
{
$this->repo = $repo;
}
public function indexJson()
{
return new JsonResponse($this->repo->findAll());
}
}
<?php
$app['posts.controller'] = $app->share(function() use ($app) {
return new PostController($app['posts.repository']);
});
$app->get('/posts.json', "posts.controller:indexJson");
If I'm honest, the jury is still out on this technique for me. The benefits are that my controller code is a little more organised and I can actually practice spec level BDD with my controllers, rather than just functional tests, but sometimes I really just feel like throwing a closure in rather than knocking up a class. Maybe it might work for you, maybe not. Thanks for reading, full code example on github.