Pragmatism in the real world

Dependency injection in Serverless PHP with Bref

When writing PHP for AWS Lambda, Bref is the way to do it. One thing I like about Bref is that you can use PSR-15 Request Handlers to respond to API Gateway HTTP events as documented in the API Gateway HTTP events section of the Bref docs.

The request handler is the same as used in PSR-7 micro-frameworks like Slim or Mezzio and can be thought of as a controller action. As such, it’s really common to use dependency injection in a controller class and I vaguely remembered Matthieu tweeting about Bref having support so I poked around.

Turns out that its really easy.

Bref supports any PSR-11 container with the Bref::setContainer() static function.

This takes a closure that must return a ContainerInterface, so using PHP-DI, we can set it up like this:

bootstrap.php:

<?php

declare(strict_types=1);

use Bref\Bref;
use DI\ContainerBuilder;

Bref::setContainer(function () {
    // Create and build the container
    $containerBuilder = new ContainerBuilder;
    $containerBuilder->addDefinitions(
        [
            'debug' => (bool)(getenv('APP_DEBUG') ?: false),

            Connection::class => function (Container $c) {
                // create and return a DBAL Connection instance here
            }
        ]
    );

    return $containerBuilder->build();
});

Now we can create constructors in our Handler classes and pass in what we need:

src/Handler/UserHandler.php:

<?php

declare(strict_types=1);

namespace App\Handler;

use Doctrine\DBAL\Connection;
use Nyholm\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class UserHandler implements RequestHandlerInterface
{
    public function __construct(private Connection $db) {
    }

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        // get user data using $this->db and return a Response
    }
}

Fantastic!

(Note the use of PHP 8’s constructor property promotion too!)

Registering the Handler

To register the handler so that the DI container will be used to instantiate it, we use the class name, not the file name in serverless.yml like this:

functions:
    users:
        handler: App\Handler\UserHandler
        events:
          - httpApi: 'GET /users'

We use an httpApi event as nothing else makes sense for a handler that returns a Response.

Automatically registering our container

The final piece of the puzzle is how do we automatically call Bref::setContainer()?

The answer to this is Composer‘s ability to autoload files! Any file we register in the files section of autoload will be automatically loaded by Composer’s autoloader, which rather handily is called by Bref itself.

composer.json:

    "autoload": {
        "psr-4": {
            "App\\": "src/"
        },
        "files": [
            "bootstrap.php"
        ]
    },

To sum up

Dependency injection is a very valuable tool in our toolbox for writing flexible code that’s easy to reason about and test. This is just as easy in the serverless environment with Bref as it is in a fast-cgi environment.