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.
