Extreme caching with PSR-7

PSR-7 brought some interesting patterns that can be applied to PHP application regardless of what framework it uses. It is particularly interesting when it comes to performance - no matter what technology your project uses, you can apply the same techniques to make it faster.

Here I will show how PSR-7 middleware can be used to cache application's output. I call it "extreme caching", because I want to trigger it as early as possible, in order to reduce amount of code to be executed on each request.

I will present this pattern on Zend Expressive-based application. It will work for any PSR-7 framework that uses middleware with following signature (which has become de facto standard):

function ($request, $response, $next) { }

File-based caching

Before starting, let's decide when caching should be enabled - you don't want to cache any page that is dynamic, but everything static (blog post, "about us" page, ...) is fine. This decision should be based on what comes in from the user (Request) and on what comes out from your application (Response).

In case of this blog, I cache all GET requests that return HTTP 200 status. Here's a middleware class that saves output into file if this conditions are met:

class CachingMiddleware
{
    public function __invoke(
        ServerRequestInterface $request,
        ResponseInterface $response,
        callable $next
    ) {
        // check if request can be cached
        if ($request->getMethod() != 'GET') {
            return $next($request, $response);
        }

        // return early if page is cached
        if ($html = $this->getCachedHtml($request)) {
            return new HtmlResponse($html);
        }

        //
        $response = $next($request, $response);

        // check if response can be cached
        if ($response->getStatusCode() == 200) {
            $this->cacheResponse($request, $response);
        }

        return $response;
    }
}

Note: example above is missing two methods (getCachedHtml and cacheResponse). Full version can be found in this gist.

Enabling this middleware in Expressive is super easy. Can be done programmatically:

$app = AppFactory::create();
$app->pipe(new CachingMiddleware();

Or, it can be injected using configuration file (when using ApplicationFactory):

return [
    'middleware_pipeline' => [
        'pre_routing' => [
            ['middleware' => CachingMiddleware::class],
            ['middleware' => FooBarMiddleware::class],
            ['middleware' => AnotherMiddleware::class],
        ],
        'post_routing' => [
            ['middleware' => ErrorHandlerMiddleware::class, 'error' => true],
        ],
    ],
];

Because of what it does, CachingMiddleware should be added to the stack as early as possible - ideally as a first element.

Early caching

While bringing good speed increase, it is not everything that we can get out of Expressive. With middleware added to Application class, Expressive will still execute a lot of code to setup everything. Fortunately, Application is a middleware itself, so it can be wrapped around "inside" our caching system.

This requires to change how application is ran. You cannot use run() method anymore, you have to invoke Application directly and manually send response via emitter. It's still pretty simple - just wrap code that creates application in a closure, and pass it to CachingMiddleware:

// index.php
use Zend\Diactoros\ServerRequestFactory;
use Zend\Diactoros\Response;
use Zend\Expressive\AppFactory;

include 'vendor/autoload.php';

$cache = new CachingMiddleware();
$response = $cache(
    ServerRequestFactory::fromGlobals(),
    new Response(),
    function ($req, $res, $next = null) {
        // create Expressive Application
        $app = AppFactory::create();
        $app->get('/', function ($req, $res) {
            $res->getBody()->write('Hello, World!');
        });
        return $app($req, $res, $next);
    }
);
$emitter = new Response\SapiEmitter();
$emitter->emit($response);

Apply this and you should see incredible increase of page loading time.

HTTP caching

File-based caching works really well when your infrastructure is simple and you don't have additional caching layer available. If your app is behind Varnish, CloudFlare, or any similar service, caching is much easier - usually it is enough to feed them with correct response headers and they will do all the caching for you. Exact headers to be set may vary from one solution to another. Here's what will work with Amazon's CloudFront:

class HttpCacheMiddleware
{
    public function __invoke(
        ServerRequestInterface $request,
        ResponseInterface $response,
        callable $next
    ) {
        $response = $next($request, $response);

        if ($request->getMethod() != 'GET' || $response->getStatusCode() != 200) {
            return $response;
        }

        $maxLifetime = 3600; // cache for 1 hour
        $response = $response->withHeader(
            'Expires',
            gmdate("D, d M Y H:i:s", time() + $maxLifetime) . " GMT"
        );

        return $response;
    }
}

If you have a choice, always use HTTP headers instead of files. Dedicated caching layer will be better optimized and more flexible. Plus, this middleware doesn't need to be invoked early, so it is easier to use it.

Existing caching middleware

oscarotero/psr7-middlewares comes with SaveResponse middleware, very similar to what I described here.

Read more 

Speeding up PHP application bootstrap with Class Dumper

One reasons for PHP still being considered slow is a consequence of how it works under web server environment: every time a client sends request, application is initialized from scratch - it runs all the bootstrap code. Bootstrapping is repeated over and over again, for every connecting client.

While this is an obvious waste of resources, it is also very difficult to avoid without rewriting an application under different architecture. Is there anything that could be done to at least reduce impact of application bootstrap, without making any changes to actual application? As it turns out, there is.

Read more 

PSR-7: HTTP Messages Today

PSR-7 is here and is big. Now, more than one month after it was voted, a lot of work has been put into projects supporting this standard. Even though we’re still at the beginning of this great journey, it is exciting to see what is already available, thanks to great work of PHP community.

Watching related projects since the inception of that standard, I will present packages that can serve as foundation for actual applications: HTTP message implementations, dispatchers and micro frameworks.

Read more 

ZF-Console: PHP microframework for console applications

As you may know, folks from ZF2/Apigility projects keep bringing interesting utility modules, enriching ZF2 ecosystem. Today I discovered small framework/helper for writing console applications: ZF-Console. And, to my surprise, it was based on my own pull request from last year! I was really proud to see that I brought something useful to the community.

Read more 

Testing ZF2 module services

There's an important question often rising when working on Zend Framework 2 module: should I test service factories? After all, they are usually trivial, they create some object and inject it with dependencies from ServiceManager. Having one test per factory seems to be an overkill.

Better to go one step back, and ask yourself a question: what exactly do you want to test?

Read more 

MtMail: e-mail module for ZF2

I'm happy to present a ZF2 module that handles composing and sending e-mail messages.

Why another module? There are a few of them already available on ZF modules website. However, when I was looking for solution to use in my application, I quickly realized that most of them are either outdated, or they miss features I needed. That's why I decided to write my own.

My intention was to create something powerful, but still simple to use. You an customize e-mail headers, add layout, automatically generate plaintext version of HTML e-mail, and so on. But you can also start composing and sending e-emails from your controllers with just a few lines of code.

Read more 

About me
Mateusz Tymek

After compiling my very first lines of code at the age of 12, I became passionate about computer science and technology. Now I'm a PHP developer, enjoying my work as a member of Cleeng team.
Doing some sports in my spare time.

Github activity