Swoole & Zend Expressive

Adventures in making fast, much faster

So I like to build websites and apps that are as fast as possible and I've generally focussed my attention on writing performant code (As far as is possible) and using tools and frameworks with decent out-of-the-box performance. This is one of the reasons I love Zend Expressive - It's really well built, easy to understand and work with, flexible and fast.

With CMS based sites (Using Prismic for example) built on top of Zend Expressive, I have TTFB at between 50ms - 100ms on a commodity VPS - that's with nginx, a bit of micro-caching maybe. That's a pretty reasonable amount of time for server side processing but there's a lot of room for improvement.

You’ll likely know that using PHP on the server results in bootstrapping and tear down of your app for every single request, so I decided to explore Swoole after reading around the web the amazing performance benefits it can yield…

Installing Swoole

Swoole is a PHP extension. In development I happen to run a mac with brew-installed PHP, so installation goes something like this for me:

$ php -v
PHP 7.2.11 (cli) (built: Oct 11 2018 16:24:11) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.11, Copyright (c) 1999-2018, by Zend Technologies
$ wget https://github.com/swoole/swoole-src/archive/v4.2.3.tar.gz
$ tar xzf v4.2.3.tar.gz
$ cd swoole-src-4.2.3/
$ phpize
$ ./configure
$ make
$ mkdir -p /usr/local/opt/php72-swoole
$ cp modules/swoole.so /usr/local/opt/php72-swoole/
$ echo "extension=/usr/local/opt/php72-swoole/swoole.so" > /usr/local/etc/php/7.2/conf.d/ext-swoole.ini

With the extension available, the next thing to do is get zend-expressive-swoole installed in the website. This is a straight-forward composer install and a configuration alteration:

$ composer require zendframework/zend-expressive-swoole
<?php
// config/autoload/swoole.local.php
declare(strict_types=1);
use Zend\Expressive\Swoole\ConfigProvider;

return array_merge((new ConfigProvider())(), [
    'zend-expressive-swoole' => [
        'swoole-http-server' => [
            'host' => 'whatever.com',
            'port' => 8124,
        ],
    ],
]);

Amazingly, after issuing a `php public/index.php start` - everything looked fine. No errors, no problems! Speed? 6ms TTFB. Yes 6 thousandths of a second - this is compared to the same site, on nginx with php-fpm yielding a TTFB of around 50ms.

My first snag was the session - the site was setup using `zend-expressive-session-ext` for storing CSRF tokens - I'd already seen mention of zend-expressive-session-cache that uses PSR-6 cache pools to read and write sessions, so I composer installed that and configured a factory to return a Redis cache instance and the site was fixed, but…

Zend View Gotcha's

I like Zend View… Mostly I like it because because of View Helpers and how easy it is to throw together useful, project specific, testable and re-usable helpers that can hook into application services and do stuff that you really shouldn't do directly in the View. Anyhow, the shipped placeholder helper and it's derivatives such as headLink, headTitle, inlineScript etc all maintain state so that you can easily build up meta data for example that's output as markup in your layout (Zend View is 2-step generally speaking). The problem was that data set in view helpers was persisting between requests, so the placeholder containers had to be manually deleted or cleaned after rendering.

Cookies

Previewing unpublished work in Prismic relies on reading and writing cookies, and, very unfortunately, the Prismic SDK for PHP reads directly from the $_COOKIE super global. Initially I thought that I'd just populate the super global with whatever was present in the request headers but those were empty. I later found out that Swoole strips the `Cookie` header from the incoming request, so you must retrieve cookie values from $request->getCookieParams() - it’s the only place you'll find them. I've released a new version of my Prismic SDK fork that allows you to manually supply the request cookies, so it was just a case of adding middleware to the pipeline that injects the cookie values for each incoming request.

More stuff to read

I'm feeling very positive about using Swoole and moving away from the traditional nginx/php-fpm setup so I'll be getting something into production as soon as possible - likely I'll load balance it behind nginx reverse proxies, which I'm yet to read up on, even though it's running well in dev.