Take PHP to the next level with Coroutines, Event Loop or Golang

Take PHP to the next level with Coroutines, Event Loop or Golang

Overview

PHP in recent years with each version has been improving both in syntax and performance, when talking about frameworks the first thing that comes to mind is Symfony or Laravel, however new frameworks have quietly emerged that lead PHP to reach a Considerably high performance by adding non-blocking event loop, concurrency, async/await functions or even interoperability with Golang.

The frameworks that allow working with PHP with high performance are:

  • ReactPHP
  • Amp
  • Framework X
  • Spiral Framework

ReactPHP

https://reactphp.org/

It is a low-level library for event-driven programming in PHP. At its core is an event loop, making it ideal for servers handling hundreds or thousands of simultaneous connections, long-running applications, and multitasking with non-blocking I/O.

The event loop is based on the reactor pattern and is heavily inspired by libraries like Node.js V8 so it is possible to use something similar to promises.

ReactPHP uses the concept of streams throughout its ecosystem to provide a consistent high-level abstraction for processing content streams but with an interface better suited for non-blocking, asynchronous I/O.

Features

  • Handling data by streams and promises
  • Does not require additional PHP extensions
  • Supports from PHP 5.3 onwards
  • Diverse ecosystem of bookstores around
  • Production ready
  • Has a DNS resolver

Example of a server with React PHP

<?php

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

$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
    return React\Http\Message\Response::plaintext(
        "Hello World!\n"
    );
});

$socket = new React\Socket\SocketServer('127.0.0.1:8080');
$http->listen($socket);

echo "Server running at http://127.0.0.1:8080" . PHP_EOL;

Example of Promises with React PHP

$deferred = new React\Promise\Deferred();

$deferred->promise()
    ->then(function ($x) {
        return $x + 1;
    })
    ->then(function ($x) {
        return $x + 1;
    })
    ->then(function ($x) {
        return $x + 1;
    })
    ->then(function ($x) {
        echo 'Resolve ' . $x;
    });

$deferred->resolve(1);

Amp

https://amphp.org/

It is an event-based concurrency framework for managing cooperative multitasking building on top of a loop of events and promises which allows you to build complete applications using non-blocking I/O and taking advantage of long-running processes.

The event loop is the one that dispatches the associated controllers once the events registered by means of coroutines are interruptible functions that can be paused and resumed.

Amp can be used wherever you have to wait for multiple I/O activities to occur without them having to occur in any specific order.

Features

  • It relies on the use of iterators in PHP (yield)
  • Allows work with promises and coroutines
  • Wide number of supported packages
  • Can be integrated into legacy systems
  • Parallel processing support

Example of a server with Amp PHP

<?php

require \dirname(__DIR__) . "/vendor/autoload.php";

use Amp\ByteStream\ResourceOutputStream;
use Amp\Http\Server\HttpServer;
use Amp\Http\Server\RequestHandler\CallableRequestHandler;
use Amp\Http\Server\Response;
use Amp\Http\Status;
use Amp\Log\ConsoleFormatter;
use Amp\Log\StreamHandler;
use Amp\Socket;
use Monolog\Logger;

Amp\Loop::run(static function () {
    $cert = new Socket\Certificate(__DIR__ . '/../test/server.pem');

    $context = (new Socket\BindContext)
        ->withTlsContext((new Socket\ServerTlsContext)->withDefaultCertificate($cert));

    $servers = [
        Socket\Server::listen("0.0.0.0:1337"),
        Socket\Server::listen("[::]:1337"),
        Socket\Server::listen("0.0.0.0:1338", $context),
        Socket\Server::listen("[::]:1338", $context),
    ];

    $logHandler = new StreamHandler(new ResourceOutputStream(STDOUT));
    $logHandler->setFormatter(new ConsoleFormatter);
    $logger = new Logger('server');
    $logger->pushHandler($logHandler);

    $server = new HttpServer($servers, new CallableRequestHandler(static function () {
        return new Response(Status::OK, [
            "content-type" => "text/plain; charset=utf-8"
        ], "Hello, World!");
    }), $logger);

    yield $server->start();

    // Stop the server when SIGINT is received (this is technically optional, but it is best to call Server::stop()).
    Amp\Loop::onSignal(\SIGINT, static function (string $watcherId) use ($server) {
        Amp\Loop::cancel($watcherId);
        yield $server->stop();
    });
});

Example with Coroutines with Amp PHP

$promise = Amp\call(function () use ($http) {
    try {
        // Yield control until the generator resolves
        // and return its eventual result.
        $response = yield $http->request("https://example.com/");

        $body = yield $response->getBody();

        return $body;
    } catch (HttpException $e) {
        // If promise resolution fails the exception is
        // thrown back to us and we handle it as needed.
    }
});

Framework X

https://framework-x.org/

Simple and fast micro framework for building reactive web applications, supports non-blocking and asynchronous execution for maximum performance, has the advantage of being able to run on shared hosting or own VPN server.

This framework is ideal for rapid development since it comes with built-in stacks such as testing, database access, routes, controllers, authentication, etc.

Framework X is designed to be cloud-native and supports running on your own servers or on a cloud provider.

One big difference between a traditional PHP web application is that Framework X comes with its own server, which allows it to process millions of requests in conjunction with asynchronous programming based on Fibers, coroutines or promises.

Features:

  • Support shared hosting
  • Support for WebSockets
  • Handling Events sent by the server
  • Compatible with PHP 8.1
  • Includes your own server
  • Offer dedicated professional support
  • Production ready

Example of a server in Framework X

<?php

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

$app = new FrameworkX\App();

$app->get('/', function () {
    return React\Http\Message\Response::plaintext(
        "Hello wörld!\n"
    );
});

$app->get('/users/{name}', function (Psr\Http\Message\ServerRequestInterface $request) {
    return React\Http\Message\Response::plaintext(
        "Hello " . $request->getAttribute('name') . "!\n"
    );
});

$app->run();

Example of promises with the await function in Framework X

$app->get('/book', function () use ($db) {
    $result = await($db->query(
        'SELECT COUNT(*) AS count FROM book'
    ));

    $data = "Found " . $result->resultRows[0]['count'] . " books\n";
    return React\Http\Message\Response::plaintext(
        $data
    );
});

Spiral Framework

https://spiral.dev/

It is a framework designed for high performance environments available for the PHP and Go languages, the execution model is based on a hybrid execution time where some services (GRPC, Queue, WebSockets, etc.)

To work optimally, Spiral Framework relies on its own application server written in the Go language called RoadRunner.

It is a framework too with quite a few stack functionalities included like native queuing support (AMQP, SQS, Beanstalk), GRPC, Pub/Sub, event streaming, ORM, templating engine, encryption, etc.

One of its star features is that when RoadRunner relies on a server in Go, certain functionalities can be written in Go and be called from PHP and vice versa.

Features:

  • High-performance framework to serve millions of users.
  • Ready to scale out via Queue, GRPC, event streaming
  • Extendable via Golang
  • production ready
  • Support from PHP 8.0 onwards
  • Compatible with Prometheus metrics
  • ORM with support for MySQL, MariaDB, SQLite, PostgreSQL and SQLServer
  • Allows dependency injection

Example of a controller with Spiral Framework

namespace App\Controller;

use App\Interceptor\CustomInterceptor;
use Spiral\Core\CoreInterface;
use Spiral\Core\InterceptableCore;

class HomeController
{
    public function index(CoreInterface $core)
    {
        $customCore = new InterceptableCore($core);
        $customCore->addInterceptor(new CustomInterceptor());

        // intercepted: Hello, Antony
        return $customCore->callAction(HomeController::class, "other", ["name" => "Antony"]);
    }

    public function other(string $name)
    {
        return sprintf("Hello, %s", $name);
    }
}

Example of compatibility between Go and PHP

Middleware in Go

package bdetect

import (
    "encoding/json"
    "fmt"
    "github.com/avct/uasurfer"
    rhttp "github.com/spiral/roadrunner/service/http"
    "github.com/spiral/roadrunner/service/http/attributes"
    "net/http"
)

const ID = "bdetect"

type Service struct{}

func (s *Service) Init(rhttp *rhttp.Service) (bool, error) {
    rhttp.AddMiddleware(s.middleware)

    return true, nil
}

func (s *Service) middleware(f http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ua := uasurfer.Parse(r.Header.Get("User-Agent"))
        data, _ := json.Marshal(struct {
            Browser        string `json:"browser"`
            BrowserVersion string `json:"browserVersion"`
            IsBot          bool   `json:"isBot"`
        }{
            Browser: ua.Browser.Name.StringTrimPrefix(),
            BrowserVersion: fmt.Sprintf(
                "%v.%v.%v",
                ua.Browser.Version.Major,
                ua.Browser.Version.Minor,
                ua.Browser.Version.Patch,
            ),
            IsBot: ua.IsBot(),
        })

        attributes.Set(r, "bdetect", string(data))

        f(w, r)
    }
}

Using the middleware from PHP

namespace App\Middleware;

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Spiral\Prototype\Traits\PrototypeTrait;

class BotDetect implements MiddlewareInterface
{
    use PrototypeTrait;

    public function process(Request $request, RequestHandlerInterface $handler): Response
    {
        $ua = json_decode($request->getAttribute('bdetect'));

        if ($ua->isBot) {
            return $this->response->html('Not bots allowed', 401);
        }

        return $handler->handle($request);
    }

Comparative table between frameworks

FrameworkPromises/CoroutinesLegacy PHPOwn serverSupport Go
ReactPHPokokok-
Ampokokok-
Framework Xokokok-
Spiral Framework--okok

As we can see, the PHP ecosystem has improved a lot and it is quite worth taking a look at one of these frameworks and why not until you get to use one 😉