Asynchronous PHP

Florian Engelhardt

  • proud dad of four kids
  • Husband
  • Linux and Vim user
  • works with PHP since 2k
  • Software Architect
  • works @ Bergfreunde
  • https://dotbox.org

The Problem

Your customers are waiting too long at the checkout

Stuff that happens during checkout

  • send order confirmation email
  • send tracking data to services
  • capture money from payment gateway
  • register customer to newsletter
  • ...

Summary

Your customers are wating for things they ...

  • ... don't know about
  • ... don't care about

The Big Picture

Start with

Separation of concerns!

Add to your Stack

How RabbitMQ works

This is very simplified 😉

Asynchronous Consumer

  • consume the messages from RabbitMQ
  • send tracking data to service via HTTP
  • non blocking / async
  • written in PHP

Why PHP?

Why don't you just use Node.js?
— RandomUserAtStackoverflow

But really, why PHP?

  • has the native functionality to write async code
  • there are cool libraries doing the heavy lifting for you
  • these libs work on core php w/o extensions
  • you already understand it anyway

Event Loop

  • 💖 of ReactPHP
  • implements the reactor pattern
  • passes stream data around
  • executes timers when they are due
  • ...
composer require react/event-loop
$loop = \React\EventLoop\Factory::create();
// do something
$loop->run();

Timers

$loop = \React\EventLoop\Factory::create();
$i = 0;
$loop->addPeriodicTimer(1, function($timer) use (&$i, $loop) {
    echo ++$i, PHP_EOL;
    if ($i >= 4) {
        // cancle timer after five iterations
        $loop->cancelTimer($timer);
    }
});
$loop->run();

Streams

composer require react/stream
$loop = \React\EventLoop\Factory::create();
$stream = new \React\Stream\ReadableResourceStream(
    fopen(
        'file.txt', 'r'
    ),
    $loop
);

$stream->on('data', function($data) {
    echo 'Line: ' . $data . PHP_EOL;
});

$stream->on('end', function(){
    echo 'finished' . PHP_EOL;
});

$loop->run();

Deffered / Promises

  • a Deferred represents a computation or unit of work
    that may not have completed yet
  • a Promise represents the result of that computation
composer require react/promise
$deferred = new \React\Promise\Deferred();

$promise = $deferred->promise();
$promise->done(function($data) {
    echo 'Done: ' . $data . PHP_EOL;
});

$deferred->resolve('hello world');

HTTP Client

Async PSR-7 HTTP client

$ composer require clue/buzz-react
$loop = \React\EventLoop\Factory::create();
$browser = new \Clue\React\Buzz\Browser($loop);

$browser->get('https://foo.com/')->then(
    function ($response) {
        var_dump(
            $response->getHeaders(),
            (string)$response->getBody()
        );
    }
);

$loop->run();

RabbitMQ?

Pure-PHP AMQP sync/async library

$ composer require bunny/bunny
$loop = \React\EventLoop\Factory::create();
$client = new \Bunny\Async\Client($loop);

$client->connect()->then(function ($client) {
    return $client->channel();
})->then(function ($channel) {
    // waiting for messages
    $channel->consume(
        function ($message, $channel, $client) {
            echo 'Received: ', $message->content, "\n";
            $channel->ack($message);
        },
        'hello'
    );
});

$loop->run();

Putting it all together

The Message

{
    "@context": "http://schema.org/",
    "@type": "Order",
    "customer": {
        "@type": "Person",
        "name": "Jane Doe",
        "email": "test@home.com"
    },
    "orderedItem": [{
        "@type": "OrderItem",
        "orderItemNumber": "abc123",
        "orderQuantity": 2,
        "orderedItem": {
            "@type": "Product",
            "name": "Widget accessories",
            "offers": {
                "@type": "Offer",
                "price": "55.00"
            }
        }
    }]
}

The connection

$loop = \React\EventLoop\Factory::create();
$client = new \Bunny\Async\Client($loop);
$browser = new \Clue\React\Buzz\Browser($loop);

$app = ....

$client->connect()->then(function ($client) {
    return $client->channel();
})->then(function ($channel) use ($app) {
    // waiting for messages
    $channel->consume(
        $app,
        'hello'
    );
});

$loop->run();

The App

$app = function ($message, $channel, $client) use ($browser) {
    $body = json_decode($message->content);
    $promises = [];
    foreach ($body->orderedItem as $orderedItem) {
        $promises[] = $browser->post(
            'https://httpbin.org/post',
            [],
            http_build_query([
                'sku' => $orderedItem->orderItemNumber,
                'amount' => $orderedItem->orderQuantity
            ])
        );
    }
    \React\Promise\all($promises)->done(
        function() use ($channel, $message) {
            $channel->ack($message);
        }
    );
};

More Examples

  • Order-Confirmation E-Mail
  • Server-Side Tracking
  • CSV Import/Export
  • validating sitemap.xml
  • ...

Golden rules

  • asynchronous ≟ faster
  • async code does not guarantee execution order
  • no multi process (no fork)
  • no multi threading

→ still single threaded PHP

Further readings

  • https://reactphp.org/
  • http://sergeyzhuk.me/reactphp-series/
  • https://dotbox.org/