decorative image for blog on creating a basic php web server with swoole
September 16, 2020

Creating a Basic PHP Web Server With Swoole

PHP Development
Performance

Swoole can be a powerful framework for async programming in PHP. In this blog, we walk through how you can create a basic web server with Swoole, and talk about the next steps for your newly-created PHP web server.

Creating a Basic Web Server With Swoole

The example we're looking at today is straight out of the Swoole documentation. If you've done any NodeJS at all, this looks a lot like examples of basic Node web servers

To start, you tell it that you're creating a server on a specific address, listening on a given port. It's important to note that you actually have to listen to the "start" event, otherwise Swoole will not start the server. Usually, this is for logging purposes, but it can also be for some bootstrapping if you need to.

use Swoole\Http\Server as HttpServer;

$server = new HttpServer('127.0.0.1', 9000);
$server->on('start', function ($server) {
    echo "Server started at http://127.0.0.1:9000\n";
});
$server->on('request', function ($request, $response) {
    $response->header('Content-Type', 'text/plain');
    $response->end("Hello World\n");
});
$server->start();

After that, you listen for requests. On each request, you then handle it. In this case, we're going to handle it the same way every time, and give a content-type of text/plain,and respond with, "Hello, world."

And then we start our server, and it starts listening for incoming requests via its event loop, returning responses for each request.

To stop the web server, press Control-C.

$response->end.

The last line in the request listener, "$response->end",might look a little strange at first. That line signals to the server that this response is complete and can be sent back to the client. This is exactly the way Node works, as well.

Considerations for Async PHP Web Servers

If you're using an async server, you're probably dealing with parallel processing and/or deferment. Deferment is the typical reason for using Swoole for web applications.

$server->defer(function () {
    // work to defer
});

The easiest way to defer execution of code is to call the “defer()” method on the server with a callback. Doing so pushes the callback to the message queue managed by the event loop. At a future tick of the loop, it dequeues the deferred function and executes it.

Long-Running Calculations and Deferring Operations

Node’s big selling point is that it is “non-blocking”. Node enthusiasts argue that when you defer operations, you do not block a response from being sent or new requests from being processed, as they get deferred. However, at some point, those operations will get dequeued, and the worker will end up processing it, blocking any other operations until it completes its work — which menas it is no longer accepting requests at that point.

This is also true of Swoole when using the “defer()” method to defer execution.

One other thing to note is that the "defer()” method can also leave a worker in an indeterminate state, which means that it kind of hangs for a little while until it garbage-collects itself, and then it respawns and starts accepting messages again. Knowing that, is there a better way to defer operations than using the “defer()” method?

Using Task Workers

The answer is Task Workers. Task workers operate in a separate pool than your web workers. That means you can defer work using TaskWorkers in a way that isn't going to block your web queue.

To do this, you must first prepare the server to process tasks. This includes telling it the number of workers you want to use, registering a listener to handle the incoming tasks, and registering another listener to execute on task completion to let the server know task processing completed.

The server instance has a “set()” method which allows you to configure it. Registering the task listener and the completion listener is similar to registering listeners for the web server.

$server->set(['task_worker_num' => 4]);
$server->on('task', function ($server, $taskId, $data) {
    // Handle task

    // Finish task:
    $server->finish('');
});
$server->on('finish', function ($server, $taskId, $returnValue) {
    // Task is complete
});

In the example above, we register 4 task workers. Our task listener receives the server instance, a task ID (which it generates for itself), and then the data that you have passed to the task. When we're done, we call the "finish()” method on the server instance, which in turn triggers our task worker “finish” listener, which is listed last here. That listener also receives the server and the task ID, and whatever value we passed to the “finish()” method.

This task worker finish listener is mainly useful for logging, but must be present so that the server will release the worker to handle new tasks.

Triggering a Task

To trigger a task, we just call the “task()” method with a value, any PHP value.

$server->task($someData);

Writing a Task Worker

How would we write a Task Worker? One way to do this, so that we can actually handle multiple task types, is to write a task class that accepts a handler and some arguments. We then trigger a task with one of these instances. If it's not an instance of this, we just don't handle it.  Otherwise, we pull its handler, call it with the arguments provided, and then we call finish when we're done.

class Task {
    public callable $handler;
    public array $arguments;
}

$server->on('task', function ($server, $taskId, $task) {
    if (! $task instanceof Task) {
        $server->finish('');
        return;
    }

    ($task->handler)(...$task->arguments);
    $server->finish('');
});

The reason to do this is because you can only register one task listener. If we want to do arbitrary types of tasks, we need to do that within this listener. 

Pitfalls

Swoole has a few drawbacks that can affect developers working with it.

  • Minimal code hot-reloading
  • Coroutine is incompatible with XDebug and XHProf
  • Mocking Swoole classes is difficult
  • One listener per event
  • $response->end is problematic
  • Non-standard request/response API

By “non-standard request/response API”, we mean that it's not using an established standard, such as PSR-7 (HTTP Message Interfaces), or even framework-specific HTTP abstractions such as the Symfony HTTPKernel or laminas-http. So how can we abstract that issue away?

Abstracting With PSR-15

To solve this issue, we're going to introduce PSR-15, which is the HTTP Request Handler and Middleware specification. It defines exactly two interfaces: 

namespace Psr\Http\Server;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;

interface RequestHandlerInterface {
    public function handle(Request $request) : Response;
}

interface MiddlewareInterface {
    public function process(
        Request $request,
        RequestHandlerInterface $handler
    ) : Response;
}

The RequestHandlerInterface has a handle method that accepts a request and returns a response. The MiddlewareInterface has a process method that accepts a request and a handler, and returns a response. Essentially, your handler that is passed to the MiddlewareInterface is going to act like a queue; when you call its "handle()" method within your middleware, it's going to pull the next middleware layer off, process it, and continue until it ultimately reaches the innermost handler, which will handle the request and return a response. The response will then bubble right back out the same way it came in. It looks a lot like this onion diagram:

image blog creating a basic php web server

Now, what's interesting with this is any Middleware layer in here can decide that, "Hey, I'm done handling requests. I don't need to pass it any deeper. I can return a response." But otherwise, it can pass it on until the innermost handler is reached, processes it, and returns a response. Since the response makes its way back out each of those layers of Middleware, each also can post-process the request. This is interesting because it allows you to add headers, append the content, etc.

PSR-15 and Swoole

So, what would this look like with Swoole? Essentially, our application is a handler. It's going to have some sort of middleware pipeline as part of how it works. So we will transform our incoming Swoole request into a PSR-7 request, pass that to our application. It will handle it and get back a PSR-7 response; we transform that into a Swoole response, and we're done.

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

And that transform response part will also call the response “end()” method on that generated Swoole response for us. The great thing about this process is that there's already an easy way to do this: Mezzio.

Final Thoughts

Today we talked about how to start creating a basic PHP web server with Swoole. In the next blog, we'll talk about how to pair Swoole with Mezzio as the middleware for our newly created async PHP web server.

Additional Resources

If you want an unsegmented version of this blog series, you can watch the original webinar below.

For additional information on Swoole, async PHP, and Mezzio, these resources are worth a look:

Get Support For Your Laminas Project

Working with Laminas? Zend can help support your project! Get cost-effective, long-term support and expert guidance for Laminas with Enterprise Laminas Support from Zend.

LEARN MORE