BreadcrumbHomeResourcesBlog Creating a Basic PHP Web Server With Swoole September 16, 2020 Creating a Basic PHP Web Server With SwoolePHP DevelopmentPerformanceBy Matthew Weier O’PhinneySwoole can be a powerful framework for async programming in PHP. In this blog, we walk through how you can create a basic PHP web server with Swoole and talk about the next steps for your newly-created PHP web server.Table of ContentsCreating a Basic PHP Web Server With SwooleConsiderations for Async PHP Web ServersPitfalls in Creating PHP Web Servers With SwooleAbstracting With PSR-15PSR-15 and SwooleFinal ThoughtsAdditional ResourcesTable of Contents1 - Creating a Basic PHP Web Server With Swoole2 - Considerations for Async PHP Web Servers3 - Pitfalls in Creating PHP Web Servers With Swoole4 - Abstracting With PSR-155 - PSR-15 and Swoole6 - Final Thoughts7 - Additional ResourcesBack to topCreating a Basic PHP Web Server With SwooleThe 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 serversTo start, you tell it that you're creating a PHP web 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.Back to topConsiderations for Async PHP Web ServersIf you're using an async PHP web 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 OperationsNode’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 means 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 WorkersThe 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 TaskTo trigger a task, we just call the “task()” method with a value, any PHP value.$server->task($someData);Writing a Task WorkerHow 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. Back to topPitfalls in Creating PHP Web Servers With SwooleSwoole has a few drawbacks that can affect developers working with it.Minimal code hot-reloadingCoroutine is incompatible with XDebug and XHProfMocking Swoole classes is difficultOne listener per event$response->end is problematicNon-standard request/response APIBy “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?Back to topAbstracting With PSR-15To 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: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.Back to topPSR-15 and SwooleSo, 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.Back to topFinal ThoughtsToday 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.Get Support For Your Laminas ProjectWorking 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 ABOUT OUR LAMINAS SUPPORTBack to topAdditional ResourcesIf 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:Blog - PHP Basics: What Is Swoole?Blog – Why You Should Use Asynchronous PHPBlog – The Correlation Between Performance and User ExperienceVideo – What is Mezzio? Why would I use it?Video – What is Laminas Enterprise Support?Webinar – Migrating from Zend Framework to LaminasBack to top
Matthew Weier O’Phinney Senior Product Manager, OpenLogic and Zend by Perforce Matthew began developing on Zend Framework (ZF) before its first public release, and led the project for Zend from 2009 through 2019. He is a founding member of the PHP Framework Interop Group (PHP-FIG), which creates and promotes standards for the PHP ecosystem — and is serving his second elected term on the PHP-FIG Core Committee.