11: Async in PHP - Promises vs Fibers
Async in PHP: Promises vs Fibers
Section titled “Async in PHP: Promises vs Fibers”Overview
Section titled “Overview”Coming from JavaScript’s async/await, you might wonder: “How does PHP handle asynchronous operations?” The answer: PHP traditionally doesn’t—it’s synchronous by design. However, modern PHP offers Fibers (PHP 8.1+), event loops (ReactPHP, Amp), and async frameworks that bring async capabilities to PHP.
Learning Objectives
Section titled “Learning Objectives”By the end of this chapter, you’ll be able to:
- ✅ Understand PHP’s synchronous execution model
- ✅ Use Fibers for cooperative multitasking
- ✅ Work with ReactPHP for event-driven programming
- ✅ Implement promise-like patterns
- ✅ Handle concurrent HTTP requests
- ✅ Know when async PHP makes sense
- ✅ Use async libraries effectively
Code Examples
Section titled “Code Examples”📁 View Code Examples on GitHub
This chapter includes comprehensive async examples:
01-basic-fibers.php- Introduction to PHP Fibers02-fiber-tasks.php- Practical task management with Fibers03-reactphp-server.php- Async HTTP server with ReactPHP04-promises.php- Promise patterns in PHP05-guzzle-async.php- Concurrent HTTP requests with Guzzle06-websocket-server.php- Real-time WebSocket server
Run the examples:
cd code/php-typescript-developers/chapter-11composer installphp 01-basic-fibers.phpThe Fundamental Difference
Section titled “The Fundamental Difference”JavaScript: Async by Default
Section titled “JavaScript: Async by Default”// JavaScript is single-threaded with event loopconsole.log('1: Start');
setTimeout(() => { console.log('2: Async operation');}, 0);
console.log('3: End');
// Output: 1, 3, 2 (async runs later)JavaScript event loop allows non-blocking I/O operations.
PHP: Synchronous by Default
Section titled “PHP: Synchronous by Default”<?phpecho "1: Start\n";
sleep(1); // Blocks execution
echo "2: After sleep\n";echo "3: End\n";
// Output: 1, 2, 3 (sequential, blocking)PHP blocks on I/O operations by default. No event loop.
Why PHP is Synchronous
Section titled “Why PHP is Synchronous”Request/Response Model
Section titled “Request/Response Model”PHP was designed for web requests:
1. Request comes in2. PHP executes script (blocking is fine)3. Response sent back4. Process endsEach request is independent. Blocking doesn’t matter because the process dies after response.
Node.js Long-Running Process
Section titled “Node.js Long-Running Process”1. Server starts2. Event loop runs indefinitely3. Handles multiple requests concurrently4. Process stays aliveNode.js must be non-blocking because one blocked operation would freeze all requests.
When You Need Async in PHP
Section titled “When You Need Async in PHP”✅ Use Cases for Async PHP
Section titled “✅ Use Cases for Async PHP”- Long-running processes (workers, daemons)
- Concurrent HTTP requests (calling multiple APIs)
- WebSocket servers (real-time communication)
- Queue workers (processing background jobs)
- Scraping/crawling (many concurrent requests)
❌ You DON’T Need Async PHP For
Section titled “❌ You DON’T Need Async PHP For”- Regular web requests (Laravel, Symfony handle this fine)
- Database queries (connection pooling handles this)
- File operations (usually fast enough)
- Most CRUD applications
Reality: 95% of PHP applications don’t need async programming.
Fibers (PHP 8.1+)
Section titled “Fibers (PHP 8.1+)”Fibers provide cooperative multitasking (like coroutines):
JavaScript Async/Await
Section titled “JavaScript Async/Await”async function fetchUser(id) { const response = await fetch(`/api/users/${id}`); return response.json();}
async function main() { const user = await fetchUser(1); console.log(user);}PHP Fibers
Section titled “PHP Fibers”<?php// Fibers allow pausing/resuming execution$fiber = new Fiber(function(): void { echo "1: Fiber starts\n"; Fiber::suspend(); // Pause here echo "3: Fiber resumes\n";});
echo "0: Before fiber\n";$fiber->start();echo "2: Fiber suspended\n";$fiber->resume();echo "4: After fiber\n";
// Output: 0, 1, 2, 3, 4Fibers are low-level - you typically don’t use them directly. Libraries build on top of them.
Practical Fiber Example
Section titled “Practical Fiber Example”<?phpclass AsyncTask { private Fiber $fiber;
public function __construct(callable $task) { $this->fiber = new Fiber($task); }
public function run(): mixed { if (!$this->fiber->isStarted()) { return $this->fiber->start(); } return $this->fiber->resume(); }
public function isFinished(): bool { return $this->fiber->isTerminated(); }}
// Simulate async operation$task1 = new AsyncTask(function() { echo "Task 1: Starting\n"; Fiber::suspend(); echo "Task 1: Finishing\n"; return "Result 1";});
$task2 = new AsyncTask(function() { echo "Task 2: Starting\n"; Fiber::suspend(); echo "Task 2: Finishing\n"; return "Result 2";});
// Interleave execution$task1->run();$task2->run();$task1->run();$task2->run();
// Output:// Task 1: Starting// Task 2: Starting// Task 1: Finishing// Task 2: FinishingReactPHP: Event Loop for PHP
Section titled “ReactPHP: Event Loop for PHP”ReactPHP brings Node.js-style async to PHP:
Installation
Section titled “Installation”composer require react/http react/promiseJavaScript HTTP Server
Section titled “JavaScript HTTP Server”const http = require('http');
const server = http.createServer((req, res) => { res.writeHead(200); res.end('Hello World');});
server.listen(3000);console.log('Server running on port 3000');ReactPHP HTTP Server
Section titled “ReactPHP HTTP Server”<?phprequire 'vendor/autoload.php';
use React\Http\HttpServer;use React\Http\Message\Response;use Psr\Http\Message\ServerRequestInterface;
$server = new HttpServer(function (ServerRequestInterface $request) { return new Response(200, ['Content-Type' => 'text/plain'], 'Hello World');});
$socket = new React\Socket\SocketServer('127.0.0.1:8080');$server->listen($socket);
echo "Server running on http://127.0.0.1:8080\n";Run with:
php server.php# Server stays running (like Node.js)Promises in PHP
Section titled “Promises in PHP”JavaScript Promises
Section titled “JavaScript Promises”function fetchUser(id) { return new Promise((resolve, reject) => { fetch(`/api/users/${id}`) .then(response => response.json()) .then(user => resolve(user)) .catch(error => reject(error)); });}
fetchUser(1) .then(user => console.log(user)) .catch(error => console.error(error));ReactPHP Promises
Section titled “ReactPHP Promises”<?phpuse React\Promise\Promise;
function fetchUser(int $id): Promise { return new Promise(function ($resolve, $reject) use ($id) { // Simulate async operation $user = ['id' => $id, 'name' => 'Alice']; $resolve($user); });}
fetchUser(1) ->then(function ($user) { echo "User: " . $user['name'] . "\n"; }) ->catch(function ($error) { echo "Error: {$error}\n"; });Promise Chaining
Section titled “Promise Chaining”JavaScript:
fetchUser(1) .then(user => fetchPosts(user.id)) .then(posts => console.log(posts)) .catch(error => console.error(error));ReactPHP:
<?phpfetchUser(1) ->then(function ($user) { return fetchPosts($user['id']); }) ->then(function ($posts) { echo "Posts: " . count($posts) . "\n"; }) ->catch(function ($error) { echo "Error: {$error}\n"; });Concurrent HTTP Requests
Section titled “Concurrent HTTP Requests”JavaScript (Promise.all)
Section titled “JavaScript (Promise.all)”async function fetchMultipleUsers() { const promises = [ fetch('/api/users/1').then(r => r.json()), fetch('/api/users/2').then(r => r.json()), fetch('/api/users/3').then(r => r.json()), ];
const users = await Promise.all(promises); console.log(users);}PHP with Guzzle (Concurrent Requests)
Section titled “PHP with Guzzle (Concurrent Requests)”<?phpuse GuzzleHttp\Client;use GuzzleHttp\Promise;
$client = new Client();
// Create promises$promises = [ 'user1' => $client->getAsync('https://api.example.com/users/1'), 'user2' => $client->getAsync('https://api.example.com/users/2'), 'user3' => $client->getAsync('https://api.example.com/users/3'),];
// Wait for all to complete$results = Promise\Utils::unwrap($promises);
foreach ($results as $key => $response) { echo "{$key}: " . $response->getBody() . "\n";}Performance:
- Sequential: 3 × 1s = 3s total
- Concurrent: ~1s total (all at once)
Amp: Alternative Async Library
Section titled “Amp: Alternative Async Library”Amp is another async library (competitor to ReactPHP):
Installation
Section titled “Installation”composer require amphp/http-client amphp/parallelAmp HTTP Requests
Section titled “Amp HTTP Requests”<?phpuse Amp\Http\Client\HttpClientBuilder;use Amp\Http\Client\Request;
require 'vendor/autoload.php';
// Amp uses coroutines (async functions)$client = HttpClientBuilder::buildDefault();
$request = new Request('https://api.example.com/users/1');$response = $client->request($request);
$body = $response->getBody()->buffer();echo $body;Concurrent Requests with Amp
Section titled “Concurrent Requests with Amp”<?phpuse Amp\Http\Client\HttpClientBuilder;use Amp\Http\Client\Request;use function Amp\Promise\all;
$client = HttpClientBuilder::buildDefault();
$promises = [ $client->request(new Request('https://api.example.com/users/1')), $client->request(new Request('https://api.example.com/users/2')), $client->request(new Request('https://api.example.com/users/3')),];
$responses = all($promises);
foreach ($responses as $response) { echo $response->getBody()->buffer() . "\n";}Async Frameworks
Section titled “Async Frameworks”Swoole (High-Performance)
Section titled “Swoole (High-Performance)”Swoole is a PHP extension that provides async capabilities:
<?php$server = new Swoole\HTTP\Server("127.0.0.1", 9501);
$server->on("start", function ($server) { echo "Swoole HTTP server started at http://127.0.0.1:9501\n";});
$server->on("request", function ($request, $response) { $response->header("Content-Type", "text/plain"); $response->end("Hello World\n");});
$server->start();Performance: 10-100x faster than traditional PHP-FPM.
RoadRunner (Go-based PHP Server)
Section titled “RoadRunner (Go-based PHP Server)”server: command: "php worker.php"
http: address: 0.0.0.0:8080 workers: num_workers: 4<?phpuse Spiral\RoadRunner;
require 'vendor/autoload.php';
$worker = RoadRunner\Worker::create();$psr7 = new RoadRunner\Http\PSR7Worker($worker);
while ($req = $psr7->waitRequest()) { $psr7->respond(new Response(200, [], 'Hello World'));}Practical Async Patterns
Section titled “Practical Async Patterns”Pattern 1: Concurrent API Calls
Section titled “Pattern 1: Concurrent API Calls”Problem: Fetch data from 3 APIs sequentially (slow).
Solution: Use Guzzle async requests:
<?phpuse GuzzleHttp\Client;use GuzzleHttp\Promise;
function fetchMultipleAPIs(): array { $client = new Client();
$promises = [ 'weather' => $client->getAsync('https://api.weather.com/current'), 'news' => $client->getAsync('https://api.news.com/latest'), 'stocks' => $client->getAsync('https://api.stocks.com/prices'), ];
$results = Promise\Utils::settle($promises)->wait();
$data = []; foreach ($results as $key => $result) { if ($result['state'] === 'fulfilled') { $data[$key] = json_decode($result['value']->getBody(), true); } else { $data[$key] = null; // Handle error } }
return $data;}
$data = fetchMultipleAPIs();// All API calls made concurrently!Pattern 2: Background Job Processing
Section titled “Pattern 2: Background Job Processing”<?php// Traditional (blocking)function processOrder($order) { sendEmail($order); // Blocks for 2s updateInventory($order); // Blocks for 1s notifyShipping($order); // Blocks for 1s // Total: 4s}
// Async (non-blocking)use React\Promise;
function processOrderAsync($order) { return Promise\all([ sendEmailAsync($order), updateInventoryAsync($order), notifyShippingAsync($order), ]); // Total: ~2s (longest operation)}Pattern 3: WebSocket Server
Section titled “Pattern 3: WebSocket Server”JavaScript (ws library):
const WebSocket = require('ws');const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', ws => { ws.on('message', message => { ws.send(`Echo: ${message}`); });});PHP (Ratchet):
<?phpuse Ratchet\Server\IoServer;use Ratchet\Http\HttpServer;use Ratchet\WebSocket\WsServer;use Ratchet\MessageComponentInterface;use Ratchet\ConnectionInterface;
class Chat implements MessageComponentInterface { public function onMessage(ConnectionInterface $from, $msg) { $from->send("Echo: {$msg}"); }
// Other interface methods...}
$server = IoServer::factory( new HttpServer( new WsServer( new Chat() ) ), 8080);
$server->run();When to Use Async PHP
Section titled “When to Use Async PHP”✅ Good Use Cases
Section titled “✅ Good Use Cases”1. Microservices / API Gateway
// Aggregate data from multiple services$data = Promise\all([ $userService->getUser($id), $orderService->getOrders($id), $paymentService->getPayments($id),]);2. Real-Time Applications
- Chat servers
- Live dashboards
- WebSocket connections
3. Background Workers
- Queue processing
- Scheduled tasks
- Data synchronization
4. High-Concurrency Scenarios
- Web scraping
- Bulk API calls
- Load testing tools
❌ Bad Use Cases
Section titled “❌ Bad Use Cases”1. Traditional Web Apps - Use Laravel/Symfony (they handle concurrency at the process level)
2. Simple CRUD - Async adds complexity without benefit
3. Database-Heavy Apps - Connection pooling is sufficient
Laravel Queue (Alternative to Async)
Section titled “Laravel Queue (Alternative to Async)”Instead of async in-process, Laravel uses job queues:
<?php// Dispatch job to queue (non-blocking)dispatch(new SendEmailJob($user));
// Job runs asynchronously in background workerclass SendEmailJob implements ShouldQueue { public function handle() { Mail::to($this->user)->send(new WelcomeEmail()); }}Start worker:
php artisan queue:workAdvantages:
- Simpler than async code
- Failed jobs can retry
- Easy to scale (add more workers)
- Works with traditional PHP
Comparison Summary
Section titled “Comparison Summary”| Feature | JavaScript | PHP Traditional | PHP Async (ReactPHP/Amp) |
|---|---|---|---|
| Event Loop | Built-in | None | Library-provided |
| Async/Await | Native | N/A | Fibers + libraries |
| Promises | Native | Library | ReactPHP/Amp |
| Non-blocking I/O | Default | No | With libraries |
| Long-running | Yes | No (per-request) | Yes (with async libs) |
| Complexity | Medium | Low | High |
| Use Cases | All apps | Web requests | Specific scenarios |
Key Takeaways
Section titled “Key Takeaways”- PHP is synchronous by default - This is fine for 95% of web applications
- Fibers (PHP 8.1+) enable cooperative multitasking (low-level building block)
- ReactPHP/Amp bring Node.js-style async event loops to PHP
- Guzzle async handles concurrent HTTP requests easily with promises
- Most PHP apps don’t need async - Laravel queues are often sufficient alternative
- Async PHP is complex - Only use for WebSockets, high-concurrency, or microservices
- Swoole/RoadRunner offer high-performance alternatives (10-100x faster than PHP-FPM)
- No automatic async like JavaScript - must explicitly use async libraries
- Use Laravel queues for background jobs instead of in-process async
- ReactPHP promises work like JavaScript promises but with PHP syntax
- Async PHP best for: WebSocket servers, scraping, concurrent API calls, long-running daemons
- Traditional PHP-FPM handles concurrency at process level, not async level
Practical Advice
Section titled “Practical Advice”For TypeScript Developers
Section titled “For TypeScript Developers”Coming from Node.js:
- Don’t expect everything to be async
- Traditional PHP-FPM is fine for web apps
- Use queues instead of async for background work
- ReactPHP feels like Node, but ecosystem is smaller
When to Choose Async PHP
Section titled “When to Choose Async PHP”- Building WebSocket server? → Use ReactPHP/Ratchet
- Need high concurrency? → Consider Swoole
- Making many HTTP requests? → Use Guzzle async
- Building traditional web app? → Stick with Laravel/Symfony
Next Steps
Section titled “Next Steps”Now that you understand async patterns, let’s build REST APIs with PHP.
Next Chapter: 12: REST APIs: Express.js vs PHP Native
Resources
Section titled “Resources”Questions or feedback? Open an issue on GitHub