SlimPHP and Symfony Dependency Injection

13 Mar 2017 in Tech

I've been building up a SlimPHP application and learning how it all fits together whilst working with my mentee. This week, the topic of dependency injection came up and I wanted to see if I could fit the Symfony dependency injection component in to Slim. It took a little while, but I finally managed it in this commit. Here's how I did it:

First, install all of the Symfony components along with the slim-symfony-di-container:

bash
composer require flexsounds/slim-symfony-di-container symfony/config symfony/yaml symfony/dependency-injection

Once these are installed, we can start to create our DI config. Create a folder named config at the same level as public, and create a file in there named services.yml with the following contents:

yaml
parameters:
slim.config.httpVersion: "1.1"
slim.config.outputBuffering: append
slim.config.determineRouteBeforeAppMiddleware: 0
slim.config.displayErrorDetails: 0
slim.config.response.defaultContentType: text/html; charset=UTF-8
slim.config.response.defaultStatus: 200
slim.config.response.chunkSize: 4096
# You may want to add/amend these response headers for your application
slim.config.response.defaultheaders:
Content-Type: %slim.config.response.defaultContentType%
# These are default classes as used by Slim. Change at your own risk
slim.config.className.settings: Slim\Collection
slim.config.classname.environment: Slim\Http\Environment
slim.config.className.request: Slim\Http\Request
slim.config.className.response: Slim\Http\Response
slim.config.className.headers: Slim\Http\Headers
slim.config.className.router: Slim\Router
slim.config.className.foundHandler: Slim\Handlers\Strategies\RequestResponse
slim.config.className.notFoundHandler: Slim\Handlers\NotFound
slim.config.className.errorHandler: Slim\Handlers\Error
slim.config.className.notAllowedHandler: Slim\Handlers\NotAllowed
slim.config.className.callableResolver: Slim\CallableResolver

This configures all of the default settings that Slim expects in it's container (which is usually an instance of Slim\Container).

We'll also want to register Twig and our own controllers in this configuration file. Let's start with Twig. Previously, our config for adding Twig to the container looked like the following:

php
$container['view'] = function ($container) {
$view = new \Slim\Views\Twig(__DIR__.'/../views', [
'cache' => false
]);
// Instantiate and add Slim specific extension
$basePath = rtrim(str_ireplace('index.php', '', $container['request']->getUri()->getBasePath()), '/');
$view->addExtension(new Slim\Views\TwigExtension($container['router'], $basePath));
return $view;
};

I copied this from the documentation, and when I looked at what it actually does I realised that I didn't need the majority of it. The basePath computation wasn't required, which left me with two class instantiations and one call to addExtension. This can be replicated by creating a services section in config/services.yml and adding the view service to it like so:

yaml
services:
# Core Services
TwigExtension:
class: \Slim\Views\TwigExtension
arguments: ["@router", "/"]
View:
class: \Slim\Views\Twig
arguments: ["../views"]
calls:
- [addExtension, ["@TwigExtension"]]

First, we create an instance of TwigExtension, then we register Twig itself, telling Symfony DI to call the addExtension method, passing in our instance of TwigExtension.

Next, we define our controllers. They're nice and simple - they only depend on the View service. Add these to config/services.yml too under the services key:

yaml
# Controllers
HomeController:
class: \Demo\Controller\HomeController
arguments: ["@view"]
WeatherController:
class: \Demo\Controller\WeatherController
arguments: ["@view"]

This completes our service configuration, but we're not actually using it yet. The last thing we need to do is edit public/index.php and replace our existing container with the new one. Open up public/index.php and remove anything that refers to $container (except the line that contains new \Slim\App($container);).

At the top of the file, we'll need to import the new classes that we use.

php
use \Flexsounds\Component\SymfonyContainerSlimBridge\ContainerBuilder;
use \Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\Config\FileLocator;

Once they're imported, it's time to use them. Here, we create a container builder and tell the components to find a file named services.yml in __DIR__./../config:

php
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
$loader->load('services.yml');

Finally, we have to update our routes. Whilst previously the Home controller was stored in our container as controller.home, it's no longer called that. If we look at config/services.yml we can see that it's called HomeController. Replace your routes with the following to make things work again:

php
$app->get('/', 'HomeController:hello');
$app->get('/weather', "WeatherController:index");

That should be all that it takes! By integrating Symfony DI with Slim PHP, we can reduce the amount of PHP that we have to write when working with controllers. Instead, we replace it with configuration that can be much easier to manage.