PHP
Article
By Christopher Pitt

Build a Superfast PHP Server in Minutes with Icicle

By Christopher Pitt

Event-based programming is a strange topic for PHP developers. In a language as procedural; events are little more than function calls. Nothing happens between events, and all meaningful code is still blocking.

Languages like JavaScript show us what PHP could be like if event loops were at the center. Some folks have taken these insights and coded them into event loops and HTTP servers. Today we’re going to create an HTTP server, in PHP. We’ll connect it to Apache to serve static files quickly. Everything else will pass through our PHP HTTP server, based on Icicle.

Icicles illustration

You can find the example code at https://github.com/sitepoint-editors/icicle-http-server

Configuring Apache

When browsers request existing files, it’s best to serve them without involving the PHP interpreter. Apache is fast and efficient at serving these files, so let’s configure it to handle all static file requests:

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*) http://%{SERVER_NAME}:9001%{REQUEST_URI} [P]

You can place this code inside a virtual host entry.

These mod_rewrite directives tell Apache to send requests to missing files to a different port. In other words: when a browser requests example.com/robots.txt, Apache will first look to see if the file exists. If so, Apache will return it without spinning up the PHP interpreter. If not, Apache will send the request to http://example.com:9001/robots.txt.

A Simple HTTP Server

Icicle ships with an event loop. We can wrap an HTTP server around that, so new requests come to us in the form of events. Much of this process is abstracted away, but let’s take a look at an example anyway. To begin, let’s download icicleio/http:

composer require icicleio/http

This installed version 0.1.0 for me. If you’re having trouble getting my examples to work, you may have a newer version. Try installing this specific version.

This will allow you to run the following code:

// server.php

require __DIR__ . "/vendor/autoload.php";

use Icicle\Http\Message\RequestInterface;
use Icicle\Http\Message\Response;
use Icicle\Http\Server\Server;
use Icicle\Loop;
use Icicle\Socket\Client\ClientInterface;

$server = new Server(
    function(RequestInterface $request, ClientInterface $client) {
        $response = new Response(200);
        $response = $response->withHeader(
            "Content-Type", "text/plain"
        );

        yield $response->getBody()->end("hello world");
        yield $response;
    }
);

$server->listen(9001);

Loop\run();
--ADVERTISEMENT--

Handling Different Routes

This is the most basic HTTP server one can create. It receives all requests and replies “hello world”. To make it more useful, we would need to incorporate some kind of router. League\Route seems like a good candidate:

composer require league/route

Now we can split up individual requests, and send more meaningful responses:

// server.php

use League\Route\Http\Exception\MethodNotAllowedException;
use League\Route\Http\Exception\NotFoundException;
use League\Route\RouteCollection;
use League\Route\Strategy\UriStrategy;

$server = new Server(
    function(RequestInterface $request, ClientInterface $client) {
        $router = new RouteCollection();
        $router->setStrategy(new UriStrategy());

        require __DIR__ . "/routes.php";

        $dispatcher = $router->getDispatcher();

        try {
            $result = $dispatcher->dispatch(
                $request->getMethod(),
                $request->getRequestTarget()
            );

            $status = 200;
            $content = $result->getContent();
        } catch (NotFoundException $exception) {
            $status = 404;
            $content = "not found";
        } catch (MethodNotAllowedException $exception) {
            $status = 405;
            $content = "method not allowed";
        }

        $response = new Response($status);
        $response = $response->withHeader(
            "Content-Type", "text/html"
        );

        yield $response->getBody()->end($content);
        yield $response;
    }
);

We’ve pulled in League\Route, and enabled the UriStrategy. It’s one of four different methods for determining which route belongs to which request. League\Route is often used alongside Symfony requests and responses. We’ll need to feed the request method and path/target to the dispatcher.

If a route is matched, we get a Symfony\HttpFoundation Response, so we get the body content with getContent. If there isn’t a matching route, or an allowed method for a matching route, then we return the appropriate errors. So what does routes.php look like?

$router->addRoute("GET", "/home", function() {
    return "hello world";
});

Rendering Complex Views

Strings are fine for simple pages. But when we start to build more complex applications, we may need a better tool. How about we use League\Plates? It’s a template engine that adds things like layouts and template inheritance on top of plain PHP.

composer require league/plates

Then we’ll create a layout template, for all the views in our site to inherit from:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>
            <?php print $this->e($title); ?>
        </title>
    </head>
    <body>
        <?php print $this->section("content"); ?>
    </body>
</html>

This is from templates/layout.php.

The e method escapes HTML entities. The section method will be where the page content gets rendered:

<?php $this->layout("layout", ["title" => "Home"]); ?>
<p>
    Hello, <?php print $this->e($name); ?>
</p>

The above is from templates/home.php.

Finally, we change our /home route to return a rendered template instead of a simple string:

$router->addRoute("GET", "/home", function() {
    $engine = new League\Plates\Engine(
        __DIR__ . "/templates"
    );

    return $engine->render("home", [
        "name" => "Chris"
    ]);
});

The above is from routes.php.

Of course, we could create a shortcut function, to save us having to create the engine each time:

function view($name, array $data = []) {
    static $engine = null;

    if ($engine === null) {
        $engine = new League\Plates\Engine(
            __DIR__ . "/templates"
        );
    }

    return $engine->render($name, $data);
}

The above is from helpers.php.

… and if we include that (or add it to the Composer autoload definition), then our /home route becomes:

$router->addRoute("GET", "/home", function() {
    return view("home", [
        "name" => "Chris"
    ]);
});

Conclusion

We’ve managed to cobble together a reasonable application framework, using Icicle\Http and a couple of League libraries. Hopefully this has shown you that life outside of Apache (or Nginx) is possible. And that’s just the beginning…

I was able to get the following stats (while running Chrome and iTunes, on a 13” Macbook Pro Retina 2014):

Concurrency Level:    100
Time taken for tests: 60.003 seconds
Complete requests:    11108
Failed requests:      0
Total transferred:    3810044 bytes
HTML transferred:     2243816 bytes
Requests per second:  185.12 [#/sec] (mean)
Time per request:     540.182 [ms] (mean)
Time per request:     5.402 [ms] (mean, across all concurrent requests)
Transfer rate:        62.01 [Kbytes/sec] received

I imagine those figures will fluctuate as you add more complexity, and they don’t mean anything when compared to popular frameworks. The point is that this little event-based HTTP server can serve 11.1k requests in a minute, without failures. If you’re careful to avoid memory leaks, you can create a stable server out of this!

That’s exciting, isn’t it?

What are your thoughts about this setup? Have you played with Icicle yet? Let us know!


Edit: Aaron Piotrowski, the author of Icicle chimed in with some extra info on why the benchmark above may have been flawed (also discussed in the comments). Here are his (much more impressive) results. He says:

“I was able to get the following stats (while running iTunes, Chrome, and several other programs on a 3.4 GHz i7 iMac) using the command ab -n 10000 -c 100 http://127.0.0.1:9001/home:

Concurrency Level:      100
Time taken for tests:   5.662 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      2650000 bytes
HTML transferred:       2020000 bytes
Requests per second:    1766.04 [#/sec] (mean)
Time per request:       56.624 [ms] (mean)
Time per request:       0.566 [ms] (mean, across all concurrent requests)
Transfer rate:          457.03 [Kbytes/sec] received

I imagine those figures will fluctuate as you add more complexity, and they don’t mean anything when compared to popular frameworks. The point is that this little event-based HTTP server could potentially serve over 100,000 requests in a minute, without failures. If you’re careful to avoid memory leaks, you can create a stable server out of this!

Thanks for chiming in, Aaron!

Recommended
Sponsors
The most important and interesting stories in tech. Straight to your inbox, daily. Get Versioning.
Login or Create Account to Comment
Login Create Account