Blog

SlimPHP and Symfony Dependency Injection

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:

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:

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:

$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:

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:

# 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.

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:

$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:

$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.

Jira Cloud + ScriptRunner

When working with Jira, we wanted the fixVersion of any ticket to be automatically set based on the fixVersion of it’s associated epic. To do this, create a script listener with ScriptRunner that listens to both the Issue Created and Issue Updated events with the following contents:

def epicKey = get('/rest/api/2/issue/' + issue.key).asObject(Map).body['fields']['customfield_10003']

if (!epicKey) {
    return false;
}

def epicFixVersions = get('/rest/api/2/issue/' + epicKey).asObject(Map).body['fields']['fixVersions']

if (epicFixVersions.size() == 0) {
    return false;
}

def epicName = epicFixVersions[0]['name']

def result = put('/rest/api/2/issue/' + issue.key)
        .header('Content-Type', 'application/json')
        .body([
        fields:[
                fixVersions: [[name: epicName]]
        ]
]).asString()

if (result.status == 204) {
    return 'Success'
} else {
    return "${result.status}: ${result.body}"
}

Bootstrap a new Laravel project with user authentication (in 5 minutes)

We’re starting from scratch here, so the first thing we need to do is to create a basic Laravel installation for our project. To to this, run the following command:

composer create-project --prefer-dist laravel/laravel demo

This will create a new folder called demo with a Laravel application inside it. We can change to the demo directory and run php artisan serve before visiting http://localhost:8000 in a browser to make sure that it worked correctly. You should see the word “Laravel” and links to the documentation page, Laracasts, Forge and Github.

As we’re going to be working with user authentication we’ll need to configure a few more things too, such as our database. Edit the .env file and change the values for the DB_* entries so that they connect to your database. For me, they look something like this:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=demo
DB_USERNAME=mydemouser
DB_PASSWORD=supersecretpassword

You’ll also need to create the database for Laravel to use with the following command (you may need to edit it to provide the root password):

mysql -u root -e "CREATE DATABASE demo;"

Once we have our database set up, it’s time to start thinking about authentication. As it’s such a common part of most applications, Laravel makes it really easy to get started – in fact it’s as easy as running a single command:

php artisan make:auth

The Laravel documentation explains what this does perfectly:

This command should be used on fresh applications and will install a layout view, registration and login views, as well as routes for all authentication end-points. A HomeController will also be generated to handle post-login requests to your application’s dashboard.

As well as generating all of those files, Laravel will create a database migration for us (in the database/migrations folder), so we need to run php artisan migrate to make sure that the relevant database tables are created too.

Once this is done, you’ll notice some new Login and Register links in the top right corner when you visit http://localhost:8000/ in your browser. Click on Register and fill in your details to create a new account with our application. Once your registration is complete, you’ll be redirected to the home controller which will show you a message, telling you that you’re logged in!

That’s really all there is to it – five steps:

  • Use composer to install Laravel
  • Edit the .env file
  • Create a database for our application
  • Run php artisan make:auth
  • Run php artisan migrate

Laravel makes it super easy to get a multi-user application up and running. The documentation is great and they’ve really distilled it down to the minimum number of steps. I’ve used lots of applications in the past, but Laravel is definitely the easiest to get to this point with.