m6web / tornado
A library for asynchronous programming.
Installs: 49 708
Dependents: 0
Suggesters: 0
Security: 0
Stars: 81
Watchers: 21
Forks: 5
Open Issues: 7
Requires
- php: ^7.3|^8.0
- psr/http-message: ^1.0
Requires (Dev)
- ext-curl: ^7.3|^8.0
- amphp/amp: ^2.0
- guzzlehttp/guzzle: ^6.3
- http-interop/http-factory-guzzle: ^1.0
- m6web/php-cs-fixer-config: ^2.0
- phpstan/phpstan: ^0.12
- phpunit/phpunit: ^9.4.4
- psr/http-factory: ^1.0
- react/event-loop: ^1.0
- react/promise: ^2.7
- symfony/http-client: ^4.3
Suggests
- ext-curl: Required to use Curl and HTTP2 features
- amphp/amp: Required to use Tornado\Adapter\Amp\EventLoop
- guzzlehttp/guzzle: Required to use Tornado\Adapter\Guzzle\HttpClient
- psr/http-factory: Required to use Tornado\Adapter\Symfony\HttpClient
- react/event-loop: Required to use Tornado\Adapter\ReactPhp\EventLoop
- react/promise: Required to use Tornado\Adapter\ReactPhp\EventLoop
- symfony/http-client: Required to use Tornado\Adapter\Symfony\HttpClient
This package is auto-updated.
Last update: 2024-10-24 11:18:34 UTC
README
A library for asynchronous programming in Php.
Tornado is composed of several interfaces to write asynchronous programs using generators. This library provides adapters for popular asynchronous frameworks (ReactPhp, Amp) and built-in adapters to understand how to write your own.
Installation
You can install it using Composer:
composer require m6web/tornado
You will also have to install additional dependencies related to the adapter you choose for your EventLoop
you may check our suggestions using Composer:
composer suggests --by-package
ℹ️ Tornado includes its own EventLoop
adapter to ease quick testing, and to show how you could write
your own EventLoop
optimized for your use case, but keep in mind that ⚠️Tornado adapters are not
yet production ready⚠️.
How to use it
You can find ready-to-use examples in examples
directory,
but here some detailed explanations about asynchronous programing, and Tornado principles.
Dealing with promises
The EventLoop
is the engine in charge of executing all asynchronous functions.
If one of those functions is waiting an asynchronous result (a Promise
)
the EventLoop
is able to pause this function and to resume an other one ready to be executed.
When you get a Promise
, the only way to retrieve its concrete value is to yield
it,
letting the EventLoop
deal internally with
Php Generators.
/** * Sends a HTTP request a returns its body as a Json array. */ function getJsonResponseAsync(Tornado\HttpClient $httpClient, RequestInterface $request): \Generator { /** @var ResponseInterface $response */ $response = yield $httpClient->sendRequest($request); return json_decode((string) $response->getBody(), true); }
⚠️ Remember that the return type can NOT be array
here,
even if we expect that json_decode
will return an array
.
Since we are creating a Generator
,
the return type is by definition \Generator
.
Asynchronous functions
As soon as your function needs to wait a Promise
, it becomes by definition an asynchronous function.
To execute it, you need to use EventLoop::async
method.
The returned Promise
will be resolved with the value returned by your function.
/** * Returns a Promise that will be resolved with a Json array. */ function requestJsonContent(Tornado\EventLoop $eventLoop, Tornado\HttpClient $httpClient): Tornado\Promise { $request = new Psr7\Request( 'GET', 'http://httpbin.org/json', ['accept' => 'application/json'] ); return $eventLoop->async(getJsonResponseAsync($httpClient, $request)); }
⚠️ Keep in mind that's a bad practice to expose publicly a Generator
.
Your asynchronous functions should return a Promise
and keep its Generator
as an implementation detail, you could choose to return a Promise
in an other manner (see dedicated examples).
Running the event loop
Now, you know that you have to create a generator to wait a Promise
,
and then call EventLoop::async
to execute the generator and obtain a new Promise
…
But how can we wait the first Promise
?
Actually, there is a second way to wait a Promise
, a synchronous one:
the EventLoop::wait
method.
It means that you should use it only once, to wait synchronously the resolution of a predefined goal.
Internally, this function will run a loop to handle all events until your goal is reached
(or an error occurred, see dedicated chapter).
function waitResponseSynchronously(Tornado\EventLoop $eventLoop, Tornado\HttpClient $httpClient) { /** @var array $jsonArray */ $jsonArray = $eventLoop->wait(requestJsonContent($eventLoop, $httpClient)); echo '>>> '.json_encode($jsonArray).PHP_EOL; }
Like with the yield
keyword,
the EventLoop::wait
method will return the resolved value of the input Promise
,
but remember that you should use it only once during execution.
Concurrency
To reveal the true power of asynchronous programming, we have to introduce concurrency in our program.
If our goal is to send only one HTTP request and wait for it,
there is no gain to deal with an asynchronous request.
However, as soon as you have at least two goals to reach,
asynchronous functions will improve your performances thanks to concurrency.
To resolve several independent Promises
,
use EventLoop::promiseAll
method to create a new Promise
that will be resolved when all others are resolved.
function waitManyResponsesSynchronously(Tornado\EventLoop $eventLoop, Tornado\HttpClient $httpClient) { $allJsonArrays = $eventLoop->wait( $eventLoop->promiseAll( requestJsonContent($eventLoop, $httpClient), requestJsonContent($eventLoop, $httpClient), requestJsonContent($eventLoop, $httpClient), requestJsonContent($eventLoop, $httpClient) ) ); foreach ($allJsonArrays as $index => $jsonArray) { echo "[$index]>>> ".json_encode($jsonArray).PHP_EOL; } }
It's important to note that
it will be more efficient to use EventLoop::promiseAll
instead of waiting each input Promise
consecutively,
because of concurrency.
Each time that you have several promises to resolve,
ask yourself if you could wait them concurrently, especially when you deal with loops
(take a look to EventLoop::promiseForeach
function
and corresponding example).
Resolving your own promises
By design, you cannot resolve a promise by yourself, you will need a Deferred
.
It allows you to create a Promise
and to resolve (or reject) it
while not exposing these advanced controls.
function promiseWaiter(Tornado\Promise $promise): \Generator { echo "I'm waiting a promise…\n"; $result = yield $promise; echo "I received [$result]!\n"; } function deferredResolver(Tornado\EventLoop $eventLoop, Tornado\Deferred $deferred): \Generator { yield $eventLoop->delay(1000); $deferred->resolve('Hello World!'); } function waitDeferredSynchronously(Tornado\EventLoop $eventLoop) { $deferred = $eventLoop->deferred(); $eventLoop->wait($eventLoop->promiseAll( $eventLoop->async(deferredResolver($eventLoop, $deferred)), $eventLoop->async(promiseWaiter($deferred->getPromise())) )); }
Error management
A Promise
is resolved in case of success,
but it will be rejected with a Throwable
in case of error.
While waiting a Promise
with yield
or EventLoop::wait
an exception may be thrown,
it's up to you to catch it or to let it propagate to the upper level.
If you throw an exception in an asynchronous function, this will reject the associated Promise
.
function failingAsynchronousFunction(Tornado\EventLoop $eventLoop): \Generator { yield $eventLoop->idle(); throw new \Exception('This is an exception!'); } function waitException(Tornado\EventLoop $eventLoop) { try { $eventLoop->wait($eventLoop->async(failingAsynchronousFunction($eventLoop))); } catch (\Throwable $throwable) { echo $throwable->getMessage().PHP_EOL; } }
When using EventLoop::async
,
all exceptions thrown inside the generator will reject the returned Promise
.
In case of a background computing you may ignore this Promise
and not yield
nor wait it,
but Tornado will still catch thrown exceptions to prevent to miss them.
By design, an ignored rejected Promise
will throw its exception during its destruction.
It means that if you really want to ignore all exceptions (really?),
you have to catch and ignore them explicitly in your code.
$ignoredPromise = $eventLoop->async((function() { try { yield from throwingGenerator(); } catch(\Throwable $throwable) { // I want to ignore all exceptions for this function } })());
FAQ
Is Tornado related to the Tornado Python library?
No, even if these two libraries deal with asynchronous programming, they are absolutely not related. The name Tornado has been chosen in reference to the horse ridden by Zorro.
I ❤️ your logo, who did it?
The Tornado logo has been designed by Cécile Moret.
Contributing
Running unit tests:
composer tests-unit
Running examples:
composer tests-examples
Running PhpStan (static analysis):
composer static-analysis
Check code style:
composer code-style-check
Fix code style:
composer code-style-fix