Build a Superfast PHP Server in Minutes with Icicle

Share this article

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

Key Takeaways

  • Icicle, a PHP library, allows for the creation of a superfast PHP server due to its asynchronous programming capabilities, providing superior performance through the handling of multiple requests concurrently.
  • The Icicle server can be connected to Apache to efficiently serve static files, while all other requests pass through the PHP HTTP server.
  • The basic Icicle HTTP server can be made more useful with the incorporation of a router, such as League\Route, which can split up individual requests and send more meaningful responses.
  • For more complex applications, a template engine like League\Plates can be used to add features like layouts and template inheritance on top of plain PHP.

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();

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!

Frequently Asked Questions about Building a Superfast PHP Server

What is the advantage of using Icicle for building a PHP server?

Icicle is a PHP library that provides superior performance for asynchronous programming. It allows you to write code that can perform multiple tasks simultaneously, without blocking the execution of other tasks. This is particularly useful for building a PHP server, as it allows the server to handle multiple requests concurrently, resulting in faster response times and improved overall performance.

How does Icicle compare to other PHP servers like RoadRunner or PHP-FPM?

While RoadRunner and PHP-FPM are popular choices for PHP servers, Icicle offers a unique advantage with its asynchronous programming capabilities. This allows for non-blocking I/O operations, which can significantly improve the performance of your server, especially when handling multiple concurrent requests. However, it’s important to note that the choice of server largely depends on your specific needs and the nature of your project.

Is Icicle suitable for building a PHP MySQL server?

Yes, Icicle can be used to build a PHP MySQL server. It provides a MySQL connector that allows for asynchronous database queries. This means that your server can continue processing other tasks while waiting for the database query to complete, resulting in improved performance and efficiency.

How does the performance of an Icicle server compare to traditional PHP servers?

An Icicle server can offer superior performance compared to traditional PHP servers due to its asynchronous programming capabilities. It allows for concurrent processing of multiple tasks, which can significantly reduce response times and improve the overall performance of your server.

Can I use Icicle with my existing PHP code?

Yes, Icicle is designed to be compatible with existing PHP code. However, to take full advantage of its asynchronous programming capabilities, you may need to refactor some parts of your code to use Icicle’s APIs.

Is Icicle suitable for large-scale projects?

Yes, Icicle is well-suited for large-scale projects. Its asynchronous programming capabilities allow it to handle a large number of concurrent requests efficiently, making it a good choice for high-traffic websites and applications.

How difficult is it to set up an Icicle server?

Setting up an Icicle server is relatively straightforward. The library provides a simple API for creating a server, and the documentation provides clear instructions and examples to help you get started.

What are the system requirements for running an Icicle server?

Icicle requires PHP 7.0 or later. It also requires the event, pcntl, and posix extensions, which are included by default in most PHP installations.

Can I use Icicle with other PHP frameworks?

Yes, Icicle can be used with other PHP frameworks. However, to take full advantage of its asynchronous programming capabilities, you may need to integrate it with the framework’s event loop.

Is there a community or support available for Icicle?

Yes, there is a community of developers who use Icicle, and you can find support on various online forums and platforms. The library’s GitHub page is also a good resource for finding information and getting help.

Christopher PittChristopher Pitt
View Author

Christopher is a writer and coder, working at Over. He usually works on application architecture, though sometimes you'll find him building compilers or robots.

Asyncasync functionsasynchronousBrunoSevent loopsicicleleagueOOPHPPHPphpleagueserver
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week