fetzi.dev

Implicit model binding in Slim APIs

4 minutes

In my daily job I’m building resource APIs based on the Slim framework. Our apis use Eloquent for database access and I often miss the convenient functionality of Laravel (called Implicity Binding) to automatically get the corresponding model instance based on a route parameter.

For example a route /user/{user} with a controller method that has an annotated input parameter like User $user will automatically load the User model for the given identifier.

This is a really simple and convenient way to build your CRUD routes for a resource. To be able to mimic this feature in a slim application I implemented a middleware that retrieves model instances based on a defined mapping set.

Configuration

To be able to use the route object in an application middleware you need to update the slim settings with the following key. This configuration ensures that the current route object is calculated before the application middleware execution.

<?php

return [
    'settings' => [
        // ...
        'determineRouteBeforeAppMiddleware' => true,
        // ...
    ]
];

In addition to this configuration entry I need to introduce my parameter mapping to tell the middleware that a parameter named user needs to be resolved to an instance of the User class.

<?php

return [
    'settings' => [
        // ...
        'determineRouteBeforeAppMiddleware' => true,

        'parameterMapping' => [
            'user' => User::class,
        ],
        // ...
    ]
];

The middleware

Slim middlewares can be easily defined as a closure function with 3 input parameters. For details about slim middlewares please check the documentation.

In our middleware we need to retrieve the route object, get the parameter mapping defined in the settings and lookup all matching route arguments from the current route. For all matching arguments we need to call the Model::find method and override the route argument with the retrieved model instance.

To preserve the given identifier it gets stored under the name parameter plus “Id” as a route argument. This is especially helpful if no instance is found in the database.

<?php

$app->add(function (ServerRequestInterface $request, ResponseInterface $response, callable $next) {
    /** @var Route $route */
    $route = $request->getAttribute('route');

    if (!is_null($route)) {
        $parameterMapping = $this->get('settings')['parameterMapping'] ?? [];

        collect($parameterMapping)
            ->filter(function ($model, $parameter) use ($route) {
                return !is_null($route->getArgument($parameter));
            })
            ->each(function ($model, $parameter) use ($route) {
                $identifier = $route->getArgument($parameter);
                $route->setArgument(
                    $parameter,
                    $model::find($route->getArgument($parameter))
                );

                $route->setArgument(sprintf('%sId', $parameter), $identifier);
            });
    }

    return $next($request, $response);
});

Using the parameter in the route callback

The route callback function gets 3 input parameters. The request, the response and an array of arguments that contains all route parameters.

Our middleware is overriding all mapped route parameters with the corresponding model instance, so you can use the model directly. If no corresponding record to the given id was found by the middleware the returned instance is null and you can make use of the preserved identifier.

A simple callback implementation for the /user/{user} route could look something like this:

<?php

$app->get('/user/{user}', function (Request $request, Response $response, array $args) {
    /** @var User $user */
    $user = $args['user'] ?? null;

    if (!is_null($user)) {
        return $response
            ->withJson($user->getAttributes())
            ->withStatus(200);
    }

    return $response
        ->withJson(
            [
                'message' => sprintf('User with id %d not found.', $args['userId'])
            ]
        )
        ->withStatus(404);
});

Using the RequestResponse Strategy

Slim provides you with another route strategy that allows you to directly specify your route parameters in the callback signature. This functionality can be enabled by overriding the foundHandler in the IOC container.

<?php

$c['foundHandler'] = function() {
    return new \Slim\Handlers\Strategies\RequestResponseArgs();
};

With this in place you can now define your route callback like this:

<?php

$app->get('/user/{user}', function (Request $request, Response $response, User $user) {
    if (!is_null($user)) {
        return $response
            ->withJson($user->getAttributes())
            ->withStatus(200);
    }

    return $response
        ->withJson(
            [
                'message' => sprintf('User with id %d not found.', $args['userId'])
            ]
        )
        ->withStatus(404);
});

For our purpose this route strategy is more suitable because you can now use the model instance straight away without accessing the generic $args array.

Wrap-Up

This simple middleware implementation empowers your api implementation with a simple and elegant way to inject concrete model instances into your route callbacks. From now on you simply do not need to load your models manually throughout your api.

Resources

This might be also interesting

Do you enjoy reading my blog?

sponsor me at ko-fi