PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

ReactPHP – event loop, parallel HTTP requests, HTTP server, Fibers bridge

by Henryk Tews / Tuesday, 05 March 2024 / Published in PHP

ReactPHP is a library for event-driven, non-blocking I/O in PHP. It brings the same model that made Node.js famous to the PHP world – a single-threaded event loop that handles multiple concurrent connections without threads. I show how to build a simple HTTP server, make parallel HTTP requests, and bridge between ReactPHP’s promise-based async and PHP 8.1’s Fibers.

The event loop – the mental model

PHP’s default execution model is synchronous and blocking: one request, one thread, sequential operations. ReactPHP changes this with an event loop – a single thread that continuously monitors sockets, timers, and streams, and fires callbacks when they are ready. No blocking, no waiting.

composer require react/http react/async

HTTP server with ReactPHP

<?php

declare(strict_types=1);

require 'vendor/autoload.php';

use React\Http\HttpServer;
use React\Http\Message\Response;
use React\Socket\SocketServer;
use Psr\Http\Message\ServerRequestInterface;

// A non-blocking HTTP server in 20 lines
$server = new HttpServer(function (ServerRequestInterface $request): Response {
    $path = $request->getUri()->getPath();

    return match(true) {
        $path === '/'        => Response::json(['status' => 'ok', 'path' => '/']),
        $path === '/health'  => new Response(200, [], 'healthy'),
        str_starts_with($path, '/products/') => handleProduct($request),
        default => new Response(404, [], 'Not found'),
    };
});

function handleProduct(ServerRequestInterface $request): Response
{
    $id = basename($request->getUri()->getPath());
    // This is non-blocking - we do not block the event loop
    return Response::json(['id' => (int)$id, 'name' => 'Product ' . $id]);
}

$socket = new SocketServer('0.0.0.0:8080');
$server->listen($socket);

echo "Server listening on http://localhost:8080\n";
\React\EventLoop\Loop::run(); // start the event loop

Parallel HTTP requests – the real use case

<?php

declare(strict_types=1);

require 'vendor/autoload.php';

use React\Http\Browser;
use React\Async\await;
use function React\Async\async;
use function React\Async\parallel;

// Making N HTTP requests in parallel instead of sequentially
async(function(): void {
    $client = new Browser();

    $productIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // Sequential: 10 requests × 200ms = 2000ms
    // Parallel: all fire at once, wait for the slowest = ~200ms

    $requests = array_map(fn($id) => async(function() use ($client, $id) {
        $response = await($client->get("https://api.example.com/products/{$id}"));
        return json_decode($response->getBody(), true);
    }), $productIds);

    $results = await(parallel($requests));

    foreach ($results as $idx => $product) {
        echo "Product {$productIds[$idx]}: {$product['name']}\n";
    }
})();

\React\EventLoop\Loop::run();

Fibers bridge – async/await with Fibers

<?php

declare(strict_types=1);

// React\Async\await() uses PHP 8.1 Fibers under the hood
// This means you can write async code that looks synchronous

require 'vendor/autoload.php';

use React\Http\Browser;
use function React\Async\async;
use function React\Async\await;

// This looks like synchronous PHP but runs concurrently
function fetchProductWithInventory(int $productId): array
{
    $client = new Browser();

    // These two requests fire concurrently
    $productPromise   = $client->get("https://catalog.api/products/{$productId}");
    $inventoryPromise = $client->get("https://inventory.api/stock/{$productId}");

    // await() suspends the current Fiber and resumes when the promise resolves
    $productResponse   = await($productPromise);
    $inventoryResponse = await($inventoryPromise);

    $product   = json_decode($productResponse->getBody(), true);
    $inventory = json_decode($inventoryResponse->getBody(), true);

    return array_merge($product, ['in_stock' => $inventory['qty'] > 0]);
}

// Wrap in async() to enable Fiber-based suspension
async(function() {
    $result = fetchProductWithInventory(42);
    echo "Product: {$result['name']}, In stock: " . ($result['in_stock'] ? 'yes' : 'no') . "\n";
})();

\React\EventLoop\Loop::run();

Practical application – parallel Magento API calls

<?php

declare(strict_types=1);

// CLI script: sync 1000 products from external API to Magento
// Without ReactPHP: 1000 requests × 100ms each = 100 seconds
// With ReactPHP: batches of 20 concurrent requests = ~5 seconds

use React\Http\Browser;
use function React\Async\async;
use function React\Async\await;
use function React\Async\parallel;

function syncProducts(array $skus, int $concurrency = 20): void
{
    $client = new Browser();
    $batches = array_chunk($skus, $concurrency);

    foreach ($batches as $batchIndex => $batch) {
        $requests = array_map(fn($sku) => async(function() use ($client, $sku) {
            $response = await($client->get("https://supplier.api/products/{$sku}"));
            $data     = json_decode($response->getBody(), true);
            return ['sku' => $sku, 'data' => $data];
        }), $batch);

        $results = await(parallel($requests));

        foreach ($results as $result) {
            saveToMagento($result['sku'], $result['data']);
        }

        echo "Batch " . ($batchIndex + 1) . "/" . count($batches) . " done\n";
    }
}

async(fn() => syncProducts(range(1, 1000)))();
\React\EventLoop\Loop::run();

When to use ReactPHP

  • Yes: CLI scripts making many external API calls (product sync, price import)
  • Yes: Long-running processes (webhook listeners, queue consumers)
  • Yes: WebSocket servers or real-time features
  • No: Standard Magento request handling – PHP-FPM is still the right tool
  • No: When the bottleneck is CPU (computation), not I/O

Summary

ReactPHP + Fibers is the most practical async PHP solution today. The await/async pattern from React\Async makes code readable and the Fibers integration removes the callback hell of older ReactPHP versions. For Magento developers the sweet spot is CLI import/export scripts where parallel API calls turn a 10-minute job into a 30-second one.

About Henryk Tews

What you can read next

PHP 7.2 – object type hint, sodium instead of mcrypt, deprecations

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}