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.
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,
],
// ...
]
];
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);
});
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);
});
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.
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.