• Stars
    star
    4,098
  • Rank 10,058 (Top 0.3 %)
  • Language
    PHP
  • License
    MIT License
  • Created over 10 years ago
  • Updated 4 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A non-blocking concurrency framework for PHP applications. 🐘

amphp/amp

AMPHP is a collection of event-driven libraries for PHP designed with fibers and concurrency in mind. amphp/amp specifically provides futures and cancellations as fundamental primitives for asynchronous programming. We're now using Revolt instead of shipping an event loop implementation with amphp/amp.

Amp makes heavy use of fibers shipped with PHP 8.1 to write asynchronous code just like synchronous, blocking code. In contrast to earlier versions, there's no need for generator based coroutines or callbacks. Similar to threads, each fiber has its own call stack, but fibers are scheduled cooperatively by the event loop. Use Amp\async() to run things concurrently.

Motivation

Traditionally, PHP follows a sequential execution model. The PHP engine executes one line after the other in sequential order. Often, however, programs consist of multiple independent sub-programs which can be executed concurrently.

If you query a database, you send the query and wait for the response from the database server in a blocking manner. Once you have the response, you can start doing the next thing. Instead of sitting there and doing nothing while waiting, we could already send the next database query, or do an HTTP call to an API. Let's make use of the time we usually spend on waiting for I/O!

Revolt allows such concurrent I/O operations. We keep the cognitive load low by avoiding callbacks. Our APIs can be used like any other library, except that things also work concurrently, because we use non-blocking I/O under the hood. Run things concurrently using Amp\async() and await the result using Future::await() where and when you need it!

There have been various techniques for implementing concurrency in PHP over the years, e.g. callbacks and generators shipped in PHP 5. These approaches suffered from the "What color is your function" problem, which we solved by shipping Fibers with PHP 8.1. They allow for concurrency with multiple independent call stacks.

Fibers are cooperatively scheduled by the event-loop, which is why they're also called coroutines. It's important to understand that only one coroutine is running at any given time, all other coroutines are suspended in the meantime.

You can compare coroutines to a computer running multiple programs using a single CPU core. Each program gets a timeslot to execute. Coroutines, however, are not preemptive. They don't get their fixed timeslot. They have to voluntarily give up control to the event loop.

Any blocking I/O function blocks the entire process while waiting for I/O. You'll want to avoid them. If you haven't read the installation guide, have a look at the Hello World example that demonstrates the effect of blocking functions. The libraries provided by AMPHP avoid blocking for I/O.

Installation

This package can be installed as a Composer dependency.

composer require amphp/amp

If you use this library, it's very likely you want to schedule events using Revolt, which you should require separately, even if it's automatically installed as a dependency.

composer require revolt/event-loop

These packages provide the basic building blocks for asynchronous / concurrent applications in PHP. We offer a lot of packages building on top of these, e.g.

Requirements

This package requires PHP 8.1 or later. No extensions required!

Extensions are only needed if your app necessitates a high numbers of concurrent socket connections, usually this limit is configured up to 1024 file descriptors.

Usage

Coroutines

Coroutines are interruptible functions. In PHP, they can be implemented using fibers.

Note Previous versions of Amp used generators for a similar purpose, but fibers can be interrupted anywhere in the call stack making previous boilerplate like Amp\call() unnecessary.

At any given time, only one fiber is running. When a coroutine suspends, execution of the coroutine is temporarily interrupted, allowing other tasks to be run. Execution is resumed once a timer expires, stream operations are possible, or any awaited Future completes.

Low-level suspension and resumption of coroutines is handled by Revolt's Suspension API.

<?php

require __DIR__ . '/vendor/autoload.php';

use Revolt\EventLoop;

$suspension = EventLoop::getSuspension();

EventLoop::delay(5, function () use ($suspension): void {
    print '++ Executing callback created by EventLoop::delay()' . PHP_EOL;

    $suspension->resume(null);
});

print '++ Suspending to event loop...' . PHP_EOL;

$suspension->suspend();

print '++ Script end' . PHP_EOL;

Callbacks registered on the Revolt event-loop are automatically run as coroutines and it's safe to suspend them. Apart from the event-loop API, Amp\async() can be used to start an independent call stack.

<?php

use function Amp\delay;

require __DIR__ . '/vendor/autoload.php';

Amp\async(function () {
    print '++ Executing callback passed to async()' . PHP_EOL;

    delay(3);

    print '++ Finished callback passed to async()' . PHP_EOL;
});

print '++ Suspending to event loop...' . PHP_EOL;
delay(5);

print '++ Script end' . PHP_EOL;

Future

A Future is an object representing the eventual result of an asynchronous operation. There are three states:

  • Completed successfully: The future has been completed successfully.
  • Errored: The future failed with an exception.
  • Pending: The future is still pending.

A successfully completed future is analog to a return value, while an errored future is analog to throwing an exception.

One way to approach asynchronous APIs is using callbacks that are passed when the operation is started and called once it completes:

doSomething(function ($error, $value) {
    if ($error) {
        /* ... */
    } else {
        /* ... */
    }
});

The callback approach has several drawbacks.

  • Passing callbacks and doing further actions in them that depend on the result of the first action gets messy really quickly.
  • An explicit callback is required as input parameter to the function, and the return value is simply unused. There's no way to use this API without involving a callback.

That's where futures come into play. They're placeholders for the result that are returned like any other return value. The caller has the choice of awaiting the result using Future::await() or registering one or several callbacks.

try {
    $value = doSomething()->await();
} catch (...) {
    /* ... */
}

Combinators

In concurrent applications, there will be multiple futures, where you might want to await them all or just the first one.

await

Amp\Future\await($iterable, $cancellation) awaits all Future objects of an iterable. If one of the Future instances errors, the operation will be aborted with that exception. Otherwise, the result is an array matching keys from the input iterable to their completion values.

The await() combinator is extremely powerful because it allows you to concurrently execute many asynchronous operations at the same time. Let's look at an example using amphp/http-client to retrieve multiple HTTP resources concurrently:

<?php

use Amp\Future;
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;

$httpClient = HttpClientBuilder::buildDefault();
$uris = [
    "google" => "https://www.google.com",
    "news"   => "https://news.google.com",
    "bing"   => "https://www.bing.com",
    "yahoo"  => "https://www.yahoo.com",
];

try {
    $responses = Future\await(array_map(function ($uri) use ($httpClient) {
        return Amp\async(fn () => $httpClient->request(new Request($uri, 'HEAD')));
    }, $uris));

    foreach ($responses as $key => $response) {
        printf(
            "%s | HTTP/%s %d %s\n",
            $key,
            $response->getProtocolVersion(),
            $response->getStatus(),
            $response->getReason()
        );
    }
} catch (Exception $e) {
    // If any one of the requests fails the combo will fail
    echo $e->getMessage(), "\n";
}
awaitAnyN

Amp\Future\awaitAnyN($count, $iterable, $cancellation) is the same as await() except that it tolerates individual errors. A result is returned once exactly $count instances in the iterable complete successfully. The return value is an array of values. The individual keys in the component array are preserved from the iterable passed to the function for evaluation.

awaitAll

Amp\Promise\awaitAll($iterable, $cancellation) awaits all futures and returns their results as [$errors, $values] array.

awaitFirst

Amp\Promise\awaitFirst($iterable, $cancellation) unwraps the first completed Future, whether successfully completed or errored.

awaitAny

Amp\Promise\awaitAny($iterable, $cancellation) unwraps the first successfully completed Future.

Future Creation

Futures can be created in several ways. Most code will use Amp\async() which takes a function and runs it as coroutine in another Fiber.

Sometimes an interface mandates a Future to be returned, but results are immediately available, e.g. because they're cached. In these cases Future::complete(mixed) and Future::error(Throwable) can be used to construct an immediately completed Future.

DeferredFuture

Note The DeferredFuture API described below is an advanced API that many applications probably don't need. Use Amp\async() or combinators instead where possible.

Amp\DeferredFuture is responsible for completing a pending Future. You create a Amp\DeferredFuture and uses its getFuture method to return an Amp\Future to the caller. Once result is ready, you complete the Future held by the caller using complete or error on the linked DeferredFuture.

final class DeferredFuture
{
    public function getFuture(): Future;
    public function complete(mixed $value = null);
    public function error(Throwable $throwable);
}

Warning If you're passing DeferredFuture objects around, you're probably doing something wrong. They're supposed to be internal state of your operation.

Warning You can't complete a future with another future; Use Future::await() before calling DeferredFuture::complete() in such cases.

Here's a simple example of an asynchronous value producer asyncMultiply() creating a DeferredFuture and returning the associated Future to its caller.

<?php // Example async producer using DeferredFuture

use Revolt\EventLoop;

function asyncMultiply(int $x, int $y): Future
{
    $deferred = new Amp\DeferredFuture;

    // Complete the async result one second from now
    EventLoop::delay(1, function () use ($deferred, $x, $y) {
        $deferred->complete($x * $y);
    });

    return $deferred->getFuture();
}

$future = asyncMultiply(6, 7);
$result = $future->await();

var_dump($result); // int(42)

Cancellation

Every operation that supports cancellation accepts an instance of Cancellation as argument. Cancellations are objects that allow registering handlers to subscribe to cancellation requests. These objects are passed down to sub-operations or have to be handled by the operation itself.

$cancellation->throwIfRequested() can be used to fail the current operation with a CancelledException once cancellation has been requested. While throwIfRequested() works well, some operations might want to subscribe with a callback instead. They can do so using Cancellation::subscribe() to subscribe any cancellation requests that might happen.

The caller creates a Cancellation by using one of the implementations below.

Note Cancellations are advisory only. A DNS resolver might ignore cancellation requests after the query has been sent as the response has to be processed anyway and can still be cached. An HTTP client might continue a nearly finished HTTP request to reuse the connection, but might abort a chunked encoding response as it cannot know whether continuing is actually cheaper than aborting.

TimeoutCancellation

A TimeoutCancellations automatically cancels itself after the specified number of seconds.

request("...", new Amp\TimeoutCancellation(30));

SignalCancellation

A SignalCancellation automatically cancels itself after a specified signal has been received by the current process.

request("...", new Amp\SignalCancellation(SIGINT));

DeferredCancellation

A DeferredCancellation allows manual cancellation with the call of a method. This is the preferred way if you need to register some custom callback somewhere instead of shipping your own implementation. Only the caller has access to the DeferredCancellation and can cancel the operation using DeferredCancellation::cancel().

$deferredCancellation = new Amp\DeferredCancellation();

// Register some custom callback somewhere
onSomeEvent(fn () => $deferredCancellation->cancel());

request("...", $deferredCancellation->getCancellation());

NullCancellation

A NullCancellation will never be cancelled. Cancellation is often optional, which is usually implemented by making the parameter nullable. To avoid guards like if ($cancellation), a NullCancellation can be used instead.

$cancellation ??= new NullCancellationToken();

CompositeCancellation

A CompositeCancellation combines multiple independent cancellation objects. If any of these cancellations is cancelled, the CompositeCancellation itself will be cancelled.

Versioning

amphp/amp follows the semver semantic versioning specification like all other amphp packages.

Compatible Packages

Compatible packages should use the amphp topic on GitHub.

Security

If you discover any security related issues, please email [email protected] instead of using the issue tracker.

License

The MIT License (MIT). Please see LICENSE for more information.

More Repositories

1

http-server

An advanced async HTTP server library for PHP, perfect for real-time apps and APIs with high concurrency demands.
PHP
1,269
star
2

parallel

An advanced parallelization library for PHP, enabling efficient multitasking, optimizing resource use, and application responsiveness through multiple CPU threads.
PHP
720
star
3

http-client

An advanced async HTTP client library for PHP, enabling efficient, non-blocking, and concurrent requests and responses.
PHP
682
star
4

byte-stream

A non-blocking stream abstraction for PHP based on Amp.
PHP
351
star
5

mysql

An async MySQL client for PHP, optimizing database interactions with efficient non-blocking capabilities. Perfect for responsive, high-performance applications.
PHP
335
star
6

thread

Unmaintained. Use https://github.com/amphp/parallel.
PHP
297
star
7

parallel-functions

Simplified parallel processing for PHP based on Amp.
PHP
263
star
8

ext-fiber

PHP Fiber extension
Assembly
238
star
9

process

An async process dispatcher for Amp.
PHP
222
star
10

socket

Non-blocking socket and TLS functionality for PHP based on Amp.
PHP
210
star
11

ext-uv

C
185
star
12

sync

Non-blocking synchronization primitives for PHP based on Amp and Revolt.
PHP
151
star
13

dns

Async DNS resolution for PHP based on Amp.
PHP
146
star
14

redis

Efficient asynchronous communication with Redis servers, enabling scalable and responsive data storage and retrieval.
PHP
145
star
15

websocket-client

Async WebSocket client for PHP based on Amp.
PHP
130
star
16

parser

A generator parser to make streaming parsers simple.
PHP
120
star
17

websocket-server

WebSocket component for PHP based on the Amp HTTP server.
PHP
109
star
18

serialization

Serialization tools for IPC and data storage in PHP.
PHP
104
star
19

cache

A fiber-aware cache API based on Amp and Revolt.
PHP
95
star
20

file

An abstraction layer and non-blocking file access solution that keeps your application responsive.
PHP
89
star
21

windows-registry

Windows Registry Reader.
PHP
89
star
22

hpack

HPack - HTTP/2 header compression implementation in PHP.
PHP
89
star
23

http

HTTP primitives which can be shared by servers and clients.
PHP
86
star
24

postgres

Async Postgres client for PHP based on Amp.
PHP
86
star
25

beanstalk

Asynchronous Beanstalk Client for PHP.
PHP
63
star
26

cluster

Building multi-core network applications with PHP.
PHP
54
star
27

aerys

A non-blocking HTTP application, WebSocket and file server for PHP based on Amp.
PHP
52
star
28

pipeline

Concurrent iterators and pipeline operations.
PHP
39
star
29

http-server-router

A router for Amp's HTTP Server.
PHP
37
star
30

green-thread

PHP
37
star
31

getting-started

A getting started guide for Amp.
PHP
36
star
32

ssh

Async SSH client for PHP based on Amp.
PHP
33
star
33

websocket

Shared code for websocket servers and clients.
PHP
32
star
34

injector

A recursive dependency injector used to bootstrap and wire together S.O.L.I.D., object-oriented PHP applications.
PHP
32
star
35

log

Non-blocking logging for PHP based on Amp and Monolog.
PHP
31
star
36

uri

Uri Parser and Resolver.
PHP
24
star
37

amphp.github.io

Main website repository.
HTML
23
star
38

react-adapter

Makes any ReactPHP library compatible with Amp.
PHP
23
star
39

artax

An async HTTP/1.1 client for PHP based on Amp.
PHP
21
star
40

http-server-static-content

An HTTP server plugin to serve static files like HTML, CSS, JavaScript, and images effortlessly.
PHP
21
star
41

phpunit-util

Helper package to ease testing with PHPUnit.
PHP
20
star
42

http-server-session

An HTTP server plugin that simplifies session management for your applications. Effortlessly handle user sessions, securely managing data across requests.
PHP
17
star
43

mysql-dbal

PHP
15
star
44

stomp

A non-blocking STOMP client built on the amp concurrency framework
PHP
15
star
45

aerys-reverse

Reverse HTTP proxy handler for Aerys
PHP
15
star
46

http-server-form-parser

An HTTP server plugin that simplifies form data handling. Effortlessly parse incoming form submissions and extracting its data.
HTML
15
star
47

sql

Common interfaces for Amp based SQL drivers.
PHP
14
star
48

loop

Discontinued. Merged into https://github.com/amphp/amp.
PHP
12
star
49

http-client-cookies

Automatic cookie handling for Amp's HTTP client.
PHP
10
star
50

http-tunnel

This package provides an HTTP CONNECT tunnel for PHP based on Amp.
PHP
10
star
51

http-client-cache

An async HTTP cache for Amp's HTTP client.
PHP
7
star
52

sql-common

Implementations shared by amphp/postgres and amphp/mysql
PHP
7
star
53

http-client-psr7

PSR-7 adapter for amphp/http-client.
PHP
7
star
54

rpc

Remote procedure calls for PHP based on Amp.
PHP
6
star
55

php-cs-fixer-config

Common code style configuration for all @amphp projects.
PHP
6
star
56

react-stream-adapter

Adapters to make React's and Amp's streams compatible.
PHP
6
star
57

windows-process-wrapper

Child process wrapper to support non-blocking process pipes on Windows.
C
5
star
58

amphp.org

Documentation for AMPHP v3 based libraries.
HTML
5
star
59

quic

PHP
4
star
60

logo

Repository to store the logo and other assets.
3
star
61

dbus

A non-blocking DBus Connector with message serialization based on Amp.
PHP
2
star
62

website-tools

Website administration tools for amphp.org.
PHP
1
star
63

template

This repository serves as template for new amphp projects.
1
star
64

website-shared

Unmaintained. Has been merged into https://github.com/amphp/amphp.github.io.
1
star
65

.github

1
star