decorative image for blog on swoole and mezzio
October 1, 2020

Combining Swoole and Mezzio for Async Programming in PHP

PHP Development
Performance

For asynchronous programming in PHP, middleware technologies like Swoole and Mezzio can combine for big performance gains. But how can developers combine them effectively to achieve the benefits of async programming?

In this blog, we continue our series on Swoole and Mezzio, with an in-depth look at how Mezzio and Swoole work together, and how to achieve async programming performance benefits with coroutines and task workers.

Recap: Why You Need Mezzio

In our last blog, we looked at how to create a basic web server with Swoole. At the end of the blog, we discussed how to work around the non-standard request/response API via abstraction.

Today, we’ll look at how to accomplish this with Mezzio, the Laminas Project PSR-15 middleware runner (formerly Expressive under the Zend Framework).

Working With Mezzio and Swoole

$server->on('request', function ($request, $response) use ($app) {
    $appResponse = $app->handle(transformRequest($request));
    transformResponse($response, $appResponse);
});

Mezzio itself provides an application runtime that gives you Dependency Injection wiring abstraction. It also allows you to choose the container you want, whether that’s Pimple, Laminas ServiceManager, PHP-DI, or another PSR-11-compliant container type.

Routing Abstraction

For routing abstraction, Mezzio uses FastRoute from Nikita Popov by default, but you can also use other routers with it if you want to.

Template Abstraction

Mezzio provides template abstraction, and works with standard solutions like Twig, Laminas View, or even templates based on Mustache. Have your own favorite? All you have to do is implement the Mezzio interfaces and have those implementations proxy to the underlying engine.

Error Handling and Default Abstraction

Mezzio also provides error handling abstraction and some defaults out of the box. The defaults can also be plugged into to add other features, such as logging.

Mezzio and Pipelines

Mezzio also provides both application and per-route pipelines. Application-level pipelines allow you to execute code for every handler in your application. For example, you might use these to include certain headers in every response returned, or to gate off requests based on certain criteria.

Per-route pipelines allow you to customize the request workflow for specific handlers. For example, if one route needs authentication and the other doesn't, you can add authentication to the pipeline of only the route that needs it. This helps save processing time, and reduce response times for your users.

Error Handling

Error handling can be done any way you want. The defaults shipped with Mezzio create an error handler that throws exceptions, and an outermost middleware layer that catches exceptions in order to return a page back to the user, while also triggering callbacks that allow you to do additional tasks with the exception, such as logging. Sometimes, however, a web page is not what you want — for instance, when you are creating an API. One of the Mezzio libraries provides an alternate error handler you can use in scenarios like this that returns responses in the Problem Details for HTTP APIs format. The point is, you can choose exactly how you want to handle errors, either using solutions you’re already familiar with, or building your own.

Mezzio as a Glue Layer

The keyword here is "abstraction". Mezzio is really a glue layer between these different contexts. Dependency injection is how we retrieve our handlers and our middleware so that we can process the request. Routing is how we map the request to a handler. Template abstraction exists so that when we want to render a template, we don't have to worry about which implementation we're using; we just provide a template name and variables to the abstraction.

Swoole Bindings and the HTTP Handler Runner

Because we have this glue already in place, Mezzio is able to provide Swoole bindings. Within the Laminas Project, we provide an additional abstractio via a package that defines an HTTP handler runner, which performs the logic and marshaling a PSR-7 request, passing it to the application, and emitting the response. The mezzio-swoole package provides a specialized runner that is designed to work with Swoole as a server API.

What's great about it is it means that it does all the work detailed in our prior post of transforming the request and transforming the response. It does all that work for you, and then it dispatches your PSR-15 application, so you can write your application just like you did before.

The primary difference in your application is that now you can start it just by using the shipped binary “mezzio-swoole”, which starts up your Swoole-based web server:

$ ./vendor/bin/mezzio-swoole start

The Mezzio skeleton is written so that it auto-detects this package. If it's there and you start the server in this way, it will use mezzio-swoole's HTTP handler runner instead of the default one. Because of this, you can write your Middleware application just like you did before using Swoole!

Server Configuration

The mezzio-swoole pacakge also provides us the ability to configure how the server works: enabling coroutine support, specifying the address and port it listens on, and more; we can even configure Swoole’s hot-code reloading features. Since Swoole is now also your web server, you might have static assets, such as JSON schema files or images; you can configure whether those are served, and how to map files to media types as well.

Achieving Async Programming Benefits With Swoole and Mezzio

What is the point of using Swoole as your web server, exactly? As discussed in the previous article, it’s primarily to gain performance, which is accomplished by eliminating bootstrapping operations, and deferring execution of operations when possible. With what we’ve covered so far, we’ve eliminated bootstrap operations simply by firing up our server using Swoole. Let’s now turn to how we defer operations.

Coroutines

The easiest way to achieve async benefits with Swoole is with Coroutines. Anything that Swoole provides coroutine support for can be consumed like normal, synchronous code. Out of the box, this includes most PDO drivers, Redis, and TCP/UDP operations (generally for maing HTTP requests). When using coroutines, your code doesn’t change at all, but you immediately gain performance benefits.

$result = $mysqli->query($sql);
while ($data = $result->fetch_assoc()) {
    // ...
}

Getting Started With Task Workers

Sometimes, however, you will want to return a response without making the user wait for you to complete processing of some data. Swoole offers the ability to start a set of task workers that operate outside the web worker pool, and send tasks to them. Those tasks are executed independently of the web request entirely. A good use case would be for handling webhooks, where a response is often required immediately indicating a payload was accepted, but you still need to process the data at some point.

To trigger a task, you will call the Swoole HTTP server’s “task()” method with the data you wish to process. Because the server itself is a service within the application, you can pass it as a dependency to your handlers and middleware, and then later trigger the task:

$server->task($someData);

If you recall from the previous article, we recommended against coding your application specifically to Swoole, however. So we need a way to abstract away from this.

One package providing such abstraction is phly/phly-swoole-taskworker (developed by the author of this article). It provides a Task class to compose a callback and the data to call it with, and a “task” listener for the server that then executes the callback from the task with that data.

use Phly\Swoole\TaskWorker\Task;

$server->task(new Task(function ($event) {
    // handle the event
}, $event));

While you can pass an arbitrary number of arguments, the suggestion is to pass an object of some kind, such as an event class. The problem with this approach, though, is that it still requires access to the Swoole HTTP server instance.

As such, the package helps alleviate this by providing a decorator, “Phly\Swoole\TaskWorker\DeferredListener”, for PSR-14 event listeners. The decorator composes the HTTP server instance and the listener, and, internally, creates the Task instance and triggers a “task()” on the server:

$listener = new DeferredListener($server, $listener);

// Where DeferredListener is equivalent to:
function (object $event) use ($server, $listener) : void {
    $server->task(new Task($listener, $event));
}

With a listener decorated in this way, in your own code, all you need to do is call on a PSR-14 event dispatcher’s “dispatch()” method to dispatch an event; the code dispatching the event does not need to know at all that this will trigger a task!

$this->dispatcher->dispatch($someEvent);

To automate decoration of the event listener, the package provides a delegator factory, allowing you to selectively opt-in to decorating your event listeners as they are pulled from the DI container. As such, making listeners defer tasks becomes a matter of configuration, fully insulating you from the specifics of using Swoole within your own code.

If you decide later to get rid of Swoole, you can just remove the delegator factory configuration on those listeners, and those listeners just execute sequentially like normal.

The mezzio-swoole package, and supporting packages in the Mezzio ecosystem, give you all the abilities to use Swoole to its best advantages, while allowing you to write your applications as if they were targetting a standard web server.

Final Thoughts

Async is a wonderful way to write your applications. It gives you huge performance benefits, it allows you to do normally difficult and complicated things in PHP, and it allows you to streamline and minimize the amount of architectures you need to use.

But it's also not a silver bullet. While you get to eliminate the bootstrap operations, and increase performance, there are requisite practices for using it safely and effectively. In our next blog, we'll look at a few pitfalls that can in the way of a successful Swoole and Mezzio integration.

Additional Resources

The webinar presents much of the same content featured in this, and our previous two blogs in this series on Swoole and Mezzio (linked below the video). 

If you're working with Laminas, Zend can provide long-term support and expert guidance for your team.

LEARN MORE