Working with Slim Middleware
Slim is a microframework that offers routing capabilities for easily creating small PHP applications. But an interesting, and powerful, feature is its concept of Middleware. Slim implements the Rack protocol, a pipelining architecture common to many Ruby frameworks. Thus, middleware can be written that wraps the application, and has access to and can affect the app’s environment and request and response objects in a chained manner.
I’ve found middleware to be an eloquent solution for implementing various filter-like services in a Slim app, such as authentication and caching. In this article I’ll explain how middleware works and share with you a simple cache example that highlights how you can implement your own custom middleware.
Understanding Slim Middleware
The Slim documentation compares a Slim application to an onion, where each layer of the onion is middleware. This is an appropriate metaphor. To better understand it though, let’s assume we’re writing an application which makes use of authentication and caching. Our architecture might look like the following illustration:
The code that is responsible for generating the page’s content is wrapped in several layers of middleware, most importantly the authentication logic and caching logic.
The flow of execution passes through each layer and is either allowed to flow through to the next or is diverted. First a check is made to ensure the user is authenticated. If not, flow is interrupted and an HTTP 401 status is returned. Then a check is made to see if a cached copy of the content is available. If it is, flow is interrupted with a cached copy of the page being returned. Other layers of middleware might exist until the flow finally reaches the logic responsible for generating the page.
As our middleware methods return, the execution flow bubbles back up through them. The remaining logic of the caching middleware, for instance, would cache the page’s content for later look up.
Implementing Middleware
To see how one can goes about implementing custom middleware, let’s look at code that could serve as the caching middleware referenced above.
The requirements for implementing any basic Slim middleware component is actually quite minimal. We only need to write a class that extends SlimMiddleware and override the call()
method. The middleware’s entry point is this call()
method, which we can either return from (thus interrupting the flow of execution) or call the next layer.
<?php
namespace MyMiddleware;
class Cache extends SlimMiddleware
{
protected $db;
public function __construct(PDO $db)
{
$this->db = $db;
}
public function call()
{
$key = $this->app->request()->getResourceUri();
$rsp = $this->app->response();
$data = $this->fetch($key);
if ($data) {
// cache hit... return the cached content
$rsp["Content-Type"] = $data["content_type"];
$rsp->body($data["body"]);
return;
}
// cache miss... continue on to generate the page
$this->next->call();
if ($rsp->status() == 200) {
// cache result for future look up
$this->save($key, $rsp["Content-Type"], $rsp->body());
}
}
protected function fetch($key)
{
$query = "SELECT content_type, body FROM cache
WHERE key = " . $this->db->quote($key);
$result = $this->db->query($query);
$row = $result->fetch(PDO::FETCH_ASSOC);
$result->closeCursor();
return $row;
}
protected function save($key, $contentType, $body)
{
$query = sprintf("INSERT INTO cache (key, content_type, body)
VALUES (%s, %s, %s)",
$this->db->quote($key),
$this->db->quote($contentType),
$this->db->quote($body)
);
$this->db->query($query);
}
}
The call()
method first checks whether the content is available in the cache. If it is, it sets the response’s Content-Type header and body and then returns, short-circuiting the pipeline. If there’s a cache miss, then $this->next->call()
is called to invoke the next middleware layer. When flow returns back to this point from the other middleware calls, a quick check is made on the request status and the relevant data is cached for future look ups.
Because the class extends Slim’s Middleware
class, it has access to the Slim application’s instance through $this->app
, and thus indirectly access to the response and request objects. We can affect the response’s headers by treating it as an array, and the response’s body through it’s body()
method.
The fetch()
and save()
methods are protected helpers which simply wrap the database queries to look up and persist the content. I’ve included them here just to complete the example. It assumes a table cache
exists with the columns key
, content_type
, and body
. Your persistence mechanism may be different depending on your needs. Also, not shown here (for simplicity’s sake) is cache expiration, though it’s trivial to incorporate on your own.
Registering and Configuring Middleware
Registering the middleware is done with Slim’s add()
method.
<?php
require_once "../vendor/autoload.php";
$app = new SlimSlim();
$app->add(new MyMiddlewareCache($db));
Of course, more than one middleware can be registered by subsequent calls to add()
. Because new middleware surrounds any previously added middleware, this means they must be added in the reverse order that they’ll be called.
<?php
$app = new SlimSlim();
$app->add(new MyMiddlewareCache($db));
$app->add(new MyMiddlewareAuth($db));
// ...
In the example above, the Cache middleware wraps the Slim app, and then Auth middleware wraps Cache. When $app->run()
is called, the flow of execution will resemble that in the illustration above, first entering the authentication middleware and working its way down to the route.
Configuring middleware is generally done through the service’s constructor. In our example I’ve simply passed an active database connection so it can access the cache table, but you can write your class to accept whatever information you may need to customize its behavior. For example, the component could be re-written to accept a handler object that exposes a fetch()
and save()
method; this would allow us to remove the example methods (or use them as a default fallback), and the end-user developer would provide the functionality per their requirements as part of the component’s configuration.
Conclusion
I’ve found middleware to be an eloquent solution for implementing various aspects of a Slim application. In this article I explained how the middleware architecture works and what’s necessary to implement your own middleware. There’s a small repository of extras with some basic middleware examples, such as CSRF protection and HTTP Authentication. I’ve refactored the example here and submitted a pull request, so if you write a useful middleware service, why not consider submitting it to the project so others can benefit as well?
Image via Fotolia