PHP runs per-request in a web server, but you will likely have to build event-driven applications at some point, which involves non-blocking code.
In that case, all tasks must be designed to be executed asynchronously, so they don't have to wait for the completion of other tasks.
However, PHP does not have built-in async/await keywords.
The Event Loop
The Event Loop is a critical concept.
This mechanism constantly monitors and processes events in a loop.
When event happen, it dispatches them to the matching event handlers, then the event handlers executes, which may trigger or schedule other events.
In other words, the Event Loop processes events sequentially, one at a time.
The Event Loop is not built-in. You either need to implement it or use a framework, such as ReactPHP.
Simple as that:
$loop = React\EventLoop\Loop::get();
DIY
If you want to do it yourself, be aware you'll have to monitor possible leaks, ensure everything is in sync, manage lifecycle, probably build your own event loop, and watch many other elements, like deadlocks and memory accesses, we won't cover here.
It's not impossible but quite challenging.
You may start with Fibers (introduced in PHP 8.1) that are the building blocks of various third-party libraries, like Swoole.
I/O: async vs. parallel programming
Most developers write sequential PHP (synchronous).
However, the following input/output (I/O) operations may benefit from asynchronous programming:
- multiple HTTP requests
- multiple database queries
The word "non-blocking" refer to operations that take time, and thus, cannot occur at the same time.
It is not parallel programming, as such approach would deal with true simultaneous execution.
Instead, we start a task and continue other work while waiting for the task to complete.
Coroutines
You can use Swoole to run concurrent database operations without blocking:
co::set(['hook_flags' => OpenSwoole\Runtime::HOOK_TCP]);
co::run(function() {
go(function () use ($pdo, $limit, $offset) {
$statement = $pdo->prepare("SELECT * FROM users LIMIT :limit, :offset");
$statement->execute(['limit' => $limit, 'offset' => $offset]);
$data = $statement->fetchAll();
// Process $data...
});
});
Here, blocking PDO run in a non-blocking coroutine context.
N.B.: Swoole is third-party library that provides useful APIs, and its syntax that is very similar to Go-lang
Design Patterns vs. asynchronous
How do you structure your code for event-driven architecture?
The Observer pattern can help.
At its core, the pattern simply consists of having a list of observers and a mechanism that notifies them when events happen.
You could use a private array of observer callables. On change, the code would loop through and calls a specific method on each observer.
You may also want to pass some payload.
Implementing the following methods can do the job.
attach/detach
Observers would be added or removed from the list at runtime when they subscribe/unsubscribe
notify
Would push the notification to all observers:
public function notify() {
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
dispatch
A bit more complex, as observers would not be notified of all changes but only those they care about:
public function dispatch(string $eventName, $eventPayload) {
if (isset($this->observers[$eventName])) {
foreach ($this->observers[$eventName] as $observer) {
$observer->handle($eventPayload);
}
}
}
You could name it "listener" instead of "observer".
So, what to do?
notify() looks basic, whereas dispatch() involves registration by event name, but it's more granular.
More concrete examples can be found here.
Alternatively, popular frameworks such as Symfony or Laravel provide event/observer ecosystems.
Queues vs. asynchronous PHP
Queues/jobs run tasks asynchronously with tasks stored (e.g., in a database or any other storage) until a worker pick them up.
If you need to setup resource-intensive background work, like sending bulk emails or processing videos, then queues/jobs are best suited for that.
I would add that queues/jobs allow you to implement some retry mechanism quite conveniently, whereas async runs in-memory within a single process.
If the async process crashes or restarts, retry state is usually lost.
Wrap this up
- use simpler implementation of the Observer pattern when you need immediate notifications with minimal infrastructure overhead
- use queues/jobs when you need to decouple task initiation from execution, and handle longer or resource-intensive operations
- use async programming when you want to improve concurrency within the same request (e.g., many HTTP requests in one script)
There are existing tools to ease the implementation, but the use cases can be tricky.
Top comments (0)