tipo: modificación del cpanel y gitignore de la carpeta vendor
This commit is contained in:
Vendored
+96
@@ -0,0 +1,96 @@
|
||||
# Changelog
|
||||
|
||||
## 1.2.0 (2022-11-30)
|
||||
|
||||
* Feature: Support PHP 8.1 and PHP 8.2.
|
||||
(#47 by @SimonFrings and #52 by @WyriHaximus)
|
||||
|
||||
* Minor documentation improvements.
|
||||
(#48 by @SimonFrings and #51 by @nhedger)
|
||||
|
||||
* Update test suite and use GitHub actions for continuous integration (CI).
|
||||
(#45 and #49 by @SimonFrings and #54 by @clue)
|
||||
|
||||
## 1.1.0 (2020-09-18)
|
||||
|
||||
* Feature: Forward compatibility with react/promise 3.
|
||||
(#39 by @WyriHaximus)
|
||||
|
||||
* Add `.gitattributes` to exclude dev files from exports.
|
||||
(#40 by @reedy)
|
||||
|
||||
* Improve test suite, update to support PHP 8 and PHPUnit 9.3.
|
||||
(#41 and #43 by @SimonFrings and #42 by @WyriHaximus)
|
||||
|
||||
## 1.0.0 (2019-07-11)
|
||||
|
||||
* First stable LTS release, now following [SemVer](https://semver.org/).
|
||||
We'd like to emphasize that this component is production ready and battle-tested.
|
||||
We plan to support all long-term support (LTS) releases for at least 24 months,
|
||||
so you have a rock-solid foundation to build on top of.
|
||||
|
||||
> Contains no other changes, so it's actually fully compatible with the v0.6.0 release.
|
||||
|
||||
## 0.6.0 (2019-07-04)
|
||||
|
||||
* Feature / BC break: Add support for `getMultiple()`, `setMultiple()`, `deleteMultiple()`, `clear()` and `has()`
|
||||
supporting multiple cache items (inspired by PSR-16).
|
||||
(#32 by @krlv and #37 by @clue)
|
||||
|
||||
* Documentation for TTL precision with millisecond accuracy or below and
|
||||
use high-resolution timer for cache TTL on PHP 7.3+.
|
||||
(#35 and #38 by @clue)
|
||||
|
||||
* Improve API documentation and allow legacy HHVM to fail in Travis CI config.
|
||||
(#34 and #36 by @clue)
|
||||
|
||||
* Prefix all global functions calls with \ to skip the look up and resolve process and go straight to the global function.
|
||||
(#31 by @WyriHaximus)
|
||||
|
||||
## 0.5.0 (2018-06-25)
|
||||
|
||||
* Improve documentation by describing what is expected of a class implementing `CacheInterface`.
|
||||
(#21, #22, #23, #27 by @WyriHaximus)
|
||||
|
||||
* Implemented (optional) Least Recently Used (LRU) cache algorithm for `ArrayCache`.
|
||||
(#26 by @clue)
|
||||
|
||||
* Added support for cache expiration (TTL).
|
||||
(#29 by @clue and @WyriHaximus)
|
||||
|
||||
* Renamed `remove` to `delete` making it more in line with `PSR-16`.
|
||||
(#30 by @clue)
|
||||
|
||||
## 0.4.2 (2017-12-20)
|
||||
|
||||
* Improve documentation with usage and installation instructions
|
||||
(#10 by @clue)
|
||||
|
||||
* Improve test suite by adding PHPUnit to `require-dev` and
|
||||
add forward compatibility with PHPUnit 5 and PHPUnit 6 and
|
||||
sanitize Composer autoload paths
|
||||
(#14 by @shaunbramley and #12 and #18 by @clue)
|
||||
|
||||
## 0.4.1 (2016-02-25)
|
||||
|
||||
* Repository maintenance, split off from main repo, improve test suite and documentation
|
||||
* First class support for PHP7 and HHVM (#9 by @clue)
|
||||
* Adjust compatibility to 5.3 (#7 by @clue)
|
||||
|
||||
## 0.4.0 (2014-02-02)
|
||||
|
||||
* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks
|
||||
* BC break: Update to React/Promise 2.0
|
||||
* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0
|
||||
|
||||
## 0.3.2 (2013-05-10)
|
||||
|
||||
* Version bump
|
||||
|
||||
## 0.3.0 (2013-04-14)
|
||||
|
||||
* Version bump
|
||||
|
||||
## 0.2.6 (2012-12-26)
|
||||
|
||||
* Feature: New cache component, used by DNS
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Vendored
+367
@@ -0,0 +1,367 @@
|
||||
# Cache
|
||||
|
||||
[](https://github.com/reactphp/cache/actions)
|
||||
[](https://packagist.org/packages/react/cache)
|
||||
|
||||
Async, [Promise](https://github.com/reactphp/promise)-based cache interface
|
||||
for [ReactPHP](https://reactphp.org/).
|
||||
|
||||
The cache component provides a
|
||||
[Promise](https://github.com/reactphp/promise)-based
|
||||
[`CacheInterface`](#cacheinterface) and an in-memory [`ArrayCache`](#arraycache)
|
||||
implementation of that.
|
||||
This allows consumers to type hint against the interface and third parties to
|
||||
provide alternate implementations.
|
||||
This project is heavily inspired by
|
||||
[PSR-16: Common Interface for Caching Libraries](https://www.php-fig.org/psr/psr-16/),
|
||||
but uses an interface more suited for async, non-blocking applications.
|
||||
|
||||
**Table of Contents**
|
||||
|
||||
* [Usage](#usage)
|
||||
* [CacheInterface](#cacheinterface)
|
||||
* [get()](#get)
|
||||
* [set()](#set)
|
||||
* [delete()](#delete)
|
||||
* [getMultiple()](#getmultiple)
|
||||
* [setMultiple()](#setmultiple)
|
||||
* [deleteMultiple()](#deletemultiple)
|
||||
* [clear()](#clear)
|
||||
* [has()](#has)
|
||||
* [ArrayCache](#arraycache)
|
||||
* [Common usage](#common-usage)
|
||||
* [Fallback get](#fallback-get)
|
||||
* [Fallback-get-and-set](#fallback-get-and-set)
|
||||
* [Install](#install)
|
||||
* [Tests](#tests)
|
||||
* [License](#license)
|
||||
|
||||
## Usage
|
||||
|
||||
### CacheInterface
|
||||
|
||||
The `CacheInterface` describes the main interface of this component.
|
||||
This allows consumers to type hint against the interface and third parties to
|
||||
provide alternate implementations.
|
||||
|
||||
#### get()
|
||||
|
||||
The `get(string $key, mixed $default = null): PromiseInterface<mixed>` method can be used to
|
||||
retrieve an item from the cache.
|
||||
|
||||
This method will resolve with the cached value on success or with the
|
||||
given `$default` value when no item can be found or when an error occurs.
|
||||
Similarly, an expired cache item (once the time-to-live is expired) is
|
||||
considered a cache miss.
|
||||
|
||||
```php
|
||||
$cache
|
||||
->get('foo')
|
||||
->then('var_dump');
|
||||
```
|
||||
|
||||
This example fetches the value of the key `foo` and passes it to the
|
||||
`var_dump` function. You can use any of the composition provided by
|
||||
[promises](https://github.com/reactphp/promise).
|
||||
|
||||
#### set()
|
||||
|
||||
The `set(string $key, mixed $value, ?float $ttl = null): PromiseInterface<bool>` method can be used to
|
||||
store an item in the cache.
|
||||
|
||||
This method will resolve with `true` on success or `false` when an error
|
||||
occurs. If the cache implementation has to go over the network to store
|
||||
it, it may take a while.
|
||||
|
||||
The optional `$ttl` parameter sets the maximum time-to-live in seconds
|
||||
for this cache item. If this parameter is omitted (or `null`), the item
|
||||
will stay in the cache for as long as the underlying implementation
|
||||
supports. Trying to access an expired cache item results in a cache miss,
|
||||
see also [`get()`](#get).
|
||||
|
||||
```php
|
||||
$cache->set('foo', 'bar', 60);
|
||||
```
|
||||
|
||||
This example eventually sets the value of the key `foo` to `bar`. If it
|
||||
already exists, it is overridden.
|
||||
|
||||
This interface does not enforce any particular TTL resolution, so special
|
||||
care may have to be taken if you rely on very high precision with
|
||||
millisecond accuracy or below. Cache implementations SHOULD work on a
|
||||
best effort basis and SHOULD provide at least second accuracy unless
|
||||
otherwise noted. Many existing cache implementations are known to provide
|
||||
microsecond or millisecond accuracy, but it's generally not recommended
|
||||
to rely on this high precision.
|
||||
|
||||
This interface suggests that cache implementations SHOULD use a monotonic
|
||||
time source if available. Given that a monotonic time source is only
|
||||
available as of PHP 7.3 by default, cache implementations MAY fall back
|
||||
to using wall-clock time.
|
||||
While this does not affect many common use cases, this is an important
|
||||
distinction for programs that rely on a high time precision or on systems
|
||||
that are subject to discontinuous time adjustments (time jumps).
|
||||
This means that if you store a cache item with a TTL of 30s and then
|
||||
adjust your system time forward by 20s, the cache item SHOULD still
|
||||
expire in 30s.
|
||||
|
||||
#### delete()
|
||||
|
||||
The `delete(string $key): PromiseInterface<bool>` method can be used to
|
||||
delete an item from the cache.
|
||||
|
||||
This method will resolve with `true` on success or `false` when an error
|
||||
occurs. When no item for `$key` is found in the cache, it also resolves
|
||||
to `true`. If the cache implementation has to go over the network to
|
||||
delete it, it may take a while.
|
||||
|
||||
```php
|
||||
$cache->delete('foo');
|
||||
```
|
||||
|
||||
This example eventually deletes the key `foo` from the cache. As with
|
||||
`set()`, this may not happen instantly and a promise is returned to
|
||||
provide guarantees whether or not the item has been removed from cache.
|
||||
|
||||
#### getMultiple()
|
||||
|
||||
The `getMultiple(string[] $keys, mixed $default = null): PromiseInterface<array>` method can be used to
|
||||
retrieve multiple cache items by their unique keys.
|
||||
|
||||
This method will resolve with an array of cached values on success or with the
|
||||
given `$default` value when an item can not be found or when an error occurs.
|
||||
Similarly, an expired cache item (once the time-to-live is expired) is
|
||||
considered a cache miss.
|
||||
|
||||
```php
|
||||
$cache->getMultiple(array('name', 'age'))->then(function (array $values) {
|
||||
$name = $values['name'] ?? 'User';
|
||||
$age = $values['age'] ?? 'n/a';
|
||||
|
||||
echo $name . ' is ' . $age . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
This example fetches the cache items for the `name` and `age` keys and
|
||||
prints some example output. You can use any of the composition provided
|
||||
by [promises](https://github.com/reactphp/promise).
|
||||
|
||||
#### setMultiple()
|
||||
|
||||
The `setMultiple(array $values, ?float $ttl = null): PromiseInterface<bool>` method can be used to
|
||||
persist a set of key => value pairs in the cache, with an optional TTL.
|
||||
|
||||
This method will resolve with `true` on success or `false` when an error
|
||||
occurs. If the cache implementation has to go over the network to store
|
||||
it, it may take a while.
|
||||
|
||||
The optional `$ttl` parameter sets the maximum time-to-live in seconds
|
||||
for these cache items. If this parameter is omitted (or `null`), these items
|
||||
will stay in the cache for as long as the underlying implementation
|
||||
supports. Trying to access an expired cache items results in a cache miss,
|
||||
see also [`getMultiple()`](#getmultiple).
|
||||
|
||||
```php
|
||||
$cache->setMultiple(array('foo' => 1, 'bar' => 2), 60);
|
||||
```
|
||||
|
||||
This example eventually sets the list of values - the key `foo` to `1` value
|
||||
and the key `bar` to `2`. If some of the keys already exist, they are overridden.
|
||||
|
||||
#### deleteMultiple()
|
||||
|
||||
The `setMultiple(string[] $keys): PromiseInterface<bool>` method can be used to
|
||||
delete multiple cache items in a single operation.
|
||||
|
||||
This method will resolve with `true` on success or `false` when an error
|
||||
occurs. When no items for `$keys` are found in the cache, it also resolves
|
||||
to `true`. If the cache implementation has to go over the network to
|
||||
delete it, it may take a while.
|
||||
|
||||
```php
|
||||
$cache->deleteMultiple(array('foo', 'bar, 'baz'));
|
||||
```
|
||||
|
||||
This example eventually deletes keys `foo`, `bar` and `baz` from the cache.
|
||||
As with `setMultiple()`, this may not happen instantly and a promise is returned to
|
||||
provide guarantees whether or not the item has been removed from cache.
|
||||
|
||||
#### clear()
|
||||
|
||||
The `clear(): PromiseInterface<bool>` method can be used to
|
||||
wipe clean the entire cache.
|
||||
|
||||
This method will resolve with `true` on success or `false` when an error
|
||||
occurs. If the cache implementation has to go over the network to
|
||||
delete it, it may take a while.
|
||||
|
||||
```php
|
||||
$cache->clear();
|
||||
```
|
||||
|
||||
This example eventually deletes all keys from the cache. As with `deleteMultiple()`,
|
||||
this may not happen instantly and a promise is returned to provide guarantees
|
||||
whether or not all the items have been removed from cache.
|
||||
|
||||
#### has()
|
||||
|
||||
The `has(string $key): PromiseInterface<bool>` method can be used to
|
||||
determine whether an item is present in the cache.
|
||||
|
||||
This method will resolve with `true` on success or `false` when no item can be found
|
||||
or when an error occurs. Similarly, an expired cache item (once the time-to-live
|
||||
is expired) is considered a cache miss.
|
||||
|
||||
```php
|
||||
$cache
|
||||
->has('foo')
|
||||
->then('var_dump');
|
||||
```
|
||||
|
||||
This example checks if the value of the key `foo` is set in the cache and passes
|
||||
the result to the `var_dump` function. You can use any of the composition provided by
|
||||
[promises](https://github.com/reactphp/promise).
|
||||
|
||||
NOTE: It is recommended that has() is only to be used for cache warming type purposes
|
||||
and not to be used within your live applications operations for get/set, as this method
|
||||
is subject to a race condition where your has() will return true and immediately after,
|
||||
another script can remove it making the state of your app out of date.
|
||||
|
||||
### ArrayCache
|
||||
|
||||
The `ArrayCache` provides an in-memory implementation of the [`CacheInterface`](#cacheinterface).
|
||||
|
||||
```php
|
||||
$cache = new ArrayCache();
|
||||
|
||||
$cache->set('foo', 'bar');
|
||||
```
|
||||
|
||||
Its constructor accepts an optional `?int $limit` parameter to limit the
|
||||
maximum number of entries to store in the LRU cache. If you add more
|
||||
entries to this instance, it will automatically take care of removing
|
||||
the one that was least recently used (LRU).
|
||||
|
||||
For example, this snippet will overwrite the first value and only store
|
||||
the last two entries:
|
||||
|
||||
```php
|
||||
$cache = new ArrayCache(2);
|
||||
|
||||
$cache->set('foo', '1');
|
||||
$cache->set('bar', '2');
|
||||
$cache->set('baz', '3');
|
||||
```
|
||||
|
||||
This cache implementation is known to rely on wall-clock time to schedule
|
||||
future cache expiration times when using any version before PHP 7.3,
|
||||
because a monotonic time source is only available as of PHP 7.3 (`hrtime()`).
|
||||
While this does not affect many common use cases, this is an important
|
||||
distinction for programs that rely on a high time precision or on systems
|
||||
that are subject to discontinuous time adjustments (time jumps).
|
||||
This means that if you store a cache item with a TTL of 30s on PHP < 7.3
|
||||
and then adjust your system time forward by 20s, the cache item may
|
||||
expire in 10s. See also [`set()`](#set) for more details.
|
||||
|
||||
## Common usage
|
||||
|
||||
### Fallback get
|
||||
|
||||
A common use case of caches is to attempt fetching a cached value and as a
|
||||
fallback retrieve it from the original data source if not found. Here is an
|
||||
example of that:
|
||||
|
||||
```php
|
||||
$cache
|
||||
->get('foo')
|
||||
->then(function ($result) {
|
||||
if ($result === null) {
|
||||
return getFooFromDb();
|
||||
}
|
||||
|
||||
return $result;
|
||||
})
|
||||
->then('var_dump');
|
||||
```
|
||||
|
||||
First an attempt is made to retrieve the value of `foo`. A callback function is
|
||||
registered that will call `getFooFromDb` when the resulting value is null.
|
||||
`getFooFromDb` is a function (can be any PHP callable) that will be called if the
|
||||
key does not exist in the cache.
|
||||
|
||||
`getFooFromDb` can handle the missing key by returning a promise for the
|
||||
actual value from the database (or any other data source). As a result, this
|
||||
chain will correctly fall back, and provide the value in both cases.
|
||||
|
||||
### Fallback get and set
|
||||
|
||||
To expand on the fallback get example, often you want to set the value on the
|
||||
cache after fetching it from the data source.
|
||||
|
||||
```php
|
||||
$cache
|
||||
->get('foo')
|
||||
->then(function ($result) {
|
||||
if ($result === null) {
|
||||
return $this->getAndCacheFooFromDb();
|
||||
}
|
||||
|
||||
return $result;
|
||||
})
|
||||
->then('var_dump');
|
||||
|
||||
public function getAndCacheFooFromDb()
|
||||
{
|
||||
return $this->db
|
||||
->get('foo')
|
||||
->then(array($this, 'cacheFooFromDb'));
|
||||
}
|
||||
|
||||
public function cacheFooFromDb($foo)
|
||||
{
|
||||
$this->cache->set('foo', $foo);
|
||||
|
||||
return $foo;
|
||||
}
|
||||
```
|
||||
|
||||
By using chaining you can easily conditionally cache the value if it is
|
||||
fetched from the database.
|
||||
|
||||
## Install
|
||||
|
||||
The recommended way to install this library is [through Composer](https://getcomposer.org).
|
||||
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
|
||||
|
||||
This project follows [SemVer](https://semver.org/).
|
||||
This will install the latest supported version:
|
||||
|
||||
```bash
|
||||
composer require react/cache:^1.2
|
||||
```
|
||||
|
||||
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
|
||||
|
||||
This project aims to run on any platform and thus does not require any PHP
|
||||
extensions and supports running on legacy PHP 5.3 through current PHP 8+ and
|
||||
HHVM.
|
||||
It's *highly recommended to use PHP 7+* for this project.
|
||||
|
||||
## Tests
|
||||
|
||||
To run the test suite, you first need to clone this repo and then install all
|
||||
dependencies [through Composer](https://getcomposer.org):
|
||||
|
||||
```bash
|
||||
composer install
|
||||
```
|
||||
|
||||
To run the test suite, go to the project root and run:
|
||||
|
||||
```bash
|
||||
vendor/bin/phpunit
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT, see [LICENSE file](LICENSE).
|
||||
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "react/cache",
|
||||
"description": "Async, Promise-based cache interface for ReactPHP",
|
||||
"keywords": ["cache", "caching", "promise", "ReactPHP"],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"homepage": "https://clue.engineering/",
|
||||
"email": "christian@clue.engineering"
|
||||
},
|
||||
{
|
||||
"name": "Cees-Jan Kiewiet",
|
||||
"homepage": "https://wyrihaximus.net/",
|
||||
"email": "reactphp@ceesjankiewiet.nl"
|
||||
},
|
||||
{
|
||||
"name": "Jan Sorgalla",
|
||||
"homepage": "https://sorgalla.com/",
|
||||
"email": "jsorgalla@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Chris Boden",
|
||||
"homepage": "https://cboden.dev/",
|
||||
"email": "cboden@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3.0",
|
||||
"react/promise": "^3.0 || ^2.0 || ^1.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"React\\Cache\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"React\\Tests\\Cache\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace React\Cache;
|
||||
|
||||
use React\Promise;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class ArrayCache implements CacheInterface
|
||||
{
|
||||
private $limit;
|
||||
private $data = array();
|
||||
private $expires = array();
|
||||
private $supportsHighResolution;
|
||||
|
||||
/**
|
||||
* The `ArrayCache` provides an in-memory implementation of the [`CacheInterface`](#cacheinterface).
|
||||
*
|
||||
* ```php
|
||||
* $cache = new ArrayCache();
|
||||
*
|
||||
* $cache->set('foo', 'bar');
|
||||
* ```
|
||||
*
|
||||
* Its constructor accepts an optional `?int $limit` parameter to limit the
|
||||
* maximum number of entries to store in the LRU cache. If you add more
|
||||
* entries to this instance, it will automatically take care of removing
|
||||
* the one that was least recently used (LRU).
|
||||
*
|
||||
* For example, this snippet will overwrite the first value and only store
|
||||
* the last two entries:
|
||||
*
|
||||
* ```php
|
||||
* $cache = new ArrayCache(2);
|
||||
*
|
||||
* $cache->set('foo', '1');
|
||||
* $cache->set('bar', '2');
|
||||
* $cache->set('baz', '3');
|
||||
* ```
|
||||
*
|
||||
* This cache implementation is known to rely on wall-clock time to schedule
|
||||
* future cache expiration times when using any version before PHP 7.3,
|
||||
* because a monotonic time source is only available as of PHP 7.3 (`hrtime()`).
|
||||
* While this does not affect many common use cases, this is an important
|
||||
* distinction for programs that rely on a high time precision or on systems
|
||||
* that are subject to discontinuous time adjustments (time jumps).
|
||||
* This means that if you store a cache item with a TTL of 30s on PHP < 7.3
|
||||
* and then adjust your system time forward by 20s, the cache item may
|
||||
* expire in 10s. See also [`set()`](#set) for more details.
|
||||
*
|
||||
* @param int|null $limit maximum number of entries to store in the LRU cache
|
||||
*/
|
||||
public function __construct($limit = null)
|
||||
{
|
||||
$this->limit = $limit;
|
||||
|
||||
// prefer high-resolution timer, available as of PHP 7.3+
|
||||
$this->supportsHighResolution = \function_exists('hrtime');
|
||||
}
|
||||
|
||||
public function get($key, $default = null)
|
||||
{
|
||||
// delete key if it is already expired => below will detect this as a cache miss
|
||||
if (isset($this->expires[$key]) && $this->now() - $this->expires[$key] > 0) {
|
||||
unset($this->data[$key], $this->expires[$key]);
|
||||
}
|
||||
|
||||
if (!\array_key_exists($key, $this->data)) {
|
||||
return Promise\resolve($default);
|
||||
}
|
||||
|
||||
// remove and append to end of array to keep track of LRU info
|
||||
$value = $this->data[$key];
|
||||
unset($this->data[$key]);
|
||||
$this->data[$key] = $value;
|
||||
|
||||
return Promise\resolve($value);
|
||||
}
|
||||
|
||||
public function set($key, $value, $ttl = null)
|
||||
{
|
||||
// unset before setting to ensure this entry will be added to end of array (LRU info)
|
||||
unset($this->data[$key]);
|
||||
$this->data[$key] = $value;
|
||||
|
||||
// sort expiration times if TTL is given (first will expire first)
|
||||
unset($this->expires[$key]);
|
||||
if ($ttl !== null) {
|
||||
$this->expires[$key] = $this->now() + $ttl;
|
||||
\asort($this->expires);
|
||||
}
|
||||
|
||||
// ensure size limit is not exceeded or remove first entry from array
|
||||
if ($this->limit !== null && \count($this->data) > $this->limit) {
|
||||
// first try to check if there's any expired entry
|
||||
// expiration times are sorted, so we can simply look at the first one
|
||||
\reset($this->expires);
|
||||
$key = \key($this->expires);
|
||||
|
||||
// check to see if the first in the list of expiring keys is already expired
|
||||
// if the first key is not expired, we have to overwrite by using LRU info
|
||||
if ($key === null || $this->now() - $this->expires[$key] < 0) {
|
||||
\reset($this->data);
|
||||
$key = \key($this->data);
|
||||
}
|
||||
unset($this->data[$key], $this->expires[$key]);
|
||||
}
|
||||
|
||||
return Promise\resolve(true);
|
||||
}
|
||||
|
||||
public function delete($key)
|
||||
{
|
||||
unset($this->data[$key], $this->expires[$key]);
|
||||
|
||||
return Promise\resolve(true);
|
||||
}
|
||||
|
||||
public function getMultiple(array $keys, $default = null)
|
||||
{
|
||||
$values = array();
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$values[$key] = $this->get($key, $default);
|
||||
}
|
||||
|
||||
return Promise\all($values);
|
||||
}
|
||||
|
||||
public function setMultiple(array $values, $ttl = null)
|
||||
{
|
||||
foreach ($values as $key => $value) {
|
||||
$this->set($key, $value, $ttl);
|
||||
}
|
||||
|
||||
return Promise\resolve(true);
|
||||
}
|
||||
|
||||
public function deleteMultiple(array $keys)
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
unset($this->data[$key], $this->expires[$key]);
|
||||
}
|
||||
|
||||
return Promise\resolve(true);
|
||||
}
|
||||
|
||||
public function clear()
|
||||
{
|
||||
$this->data = array();
|
||||
$this->expires = array();
|
||||
|
||||
return Promise\resolve(true);
|
||||
}
|
||||
|
||||
public function has($key)
|
||||
{
|
||||
// delete key if it is already expired
|
||||
if (isset($this->expires[$key]) && $this->now() - $this->expires[$key] > 0) {
|
||||
unset($this->data[$key], $this->expires[$key]);
|
||||
}
|
||||
|
||||
if (!\array_key_exists($key, $this->data)) {
|
||||
return Promise\resolve(false);
|
||||
}
|
||||
|
||||
// remove and append to end of array to keep track of LRU info
|
||||
$value = $this->data[$key];
|
||||
unset($this->data[$key]);
|
||||
$this->data[$key] = $value;
|
||||
|
||||
return Promise\resolve(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
private function now()
|
||||
{
|
||||
return $this->supportsHighResolution ? \hrtime(true) * 1e-9 : \microtime(true);
|
||||
}
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
namespace React\Cache;
|
||||
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
interface CacheInterface
|
||||
{
|
||||
/**
|
||||
* Retrieves an item from the cache.
|
||||
*
|
||||
* This method will resolve with the cached value on success or with the
|
||||
* given `$default` value when no item can be found or when an error occurs.
|
||||
* Similarly, an expired cache item (once the time-to-live is expired) is
|
||||
* considered a cache miss.
|
||||
*
|
||||
* ```php
|
||||
* $cache
|
||||
* ->get('foo')
|
||||
* ->then('var_dump');
|
||||
* ```
|
||||
*
|
||||
* This example fetches the value of the key `foo` and passes it to the
|
||||
* `var_dump` function. You can use any of the composition provided by
|
||||
* [promises](https://github.com/reactphp/promise).
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $default Default value to return for cache miss or null if not given.
|
||||
* @return PromiseInterface<mixed>
|
||||
*/
|
||||
public function get($key, $default = null);
|
||||
|
||||
/**
|
||||
* Stores an item in the cache.
|
||||
*
|
||||
* This method will resolve with `true` on success or `false` when an error
|
||||
* occurs. If the cache implementation has to go over the network to store
|
||||
* it, it may take a while.
|
||||
*
|
||||
* The optional `$ttl` parameter sets the maximum time-to-live in seconds
|
||||
* for this cache item. If this parameter is omitted (or `null`), the item
|
||||
* will stay in the cache for as long as the underlying implementation
|
||||
* supports. Trying to access an expired cache item results in a cache miss,
|
||||
* see also [`get()`](#get).
|
||||
*
|
||||
* ```php
|
||||
* $cache->set('foo', 'bar', 60);
|
||||
* ```
|
||||
*
|
||||
* This example eventually sets the value of the key `foo` to `bar`. If it
|
||||
* already exists, it is overridden.
|
||||
*
|
||||
* This interface does not enforce any particular TTL resolution, so special
|
||||
* care may have to be taken if you rely on very high precision with
|
||||
* millisecond accuracy or below. Cache implementations SHOULD work on a
|
||||
* best effort basis and SHOULD provide at least second accuracy unless
|
||||
* otherwise noted. Many existing cache implementations are known to provide
|
||||
* microsecond or millisecond accuracy, but it's generally not recommended
|
||||
* to rely on this high precision.
|
||||
*
|
||||
* This interface suggests that cache implementations SHOULD use a monotonic
|
||||
* time source if available. Given that a monotonic time source is only
|
||||
* available as of PHP 7.3 by default, cache implementations MAY fall back
|
||||
* to using wall-clock time.
|
||||
* While this does not affect many common use cases, this is an important
|
||||
* distinction for programs that rely on a high time precision or on systems
|
||||
* that are subject to discontinuous time adjustments (time jumps).
|
||||
* This means that if you store a cache item with a TTL of 30s and then
|
||||
* adjust your system time forward by 20s, the cache item SHOULD still
|
||||
* expire in 30s.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param ?float $ttl
|
||||
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
|
||||
*/
|
||||
public function set($key, $value, $ttl = null);
|
||||
|
||||
/**
|
||||
* Deletes an item from the cache.
|
||||
*
|
||||
* This method will resolve with `true` on success or `false` when an error
|
||||
* occurs. When no item for `$key` is found in the cache, it also resolves
|
||||
* to `true`. If the cache implementation has to go over the network to
|
||||
* delete it, it may take a while.
|
||||
*
|
||||
* ```php
|
||||
* $cache->delete('foo');
|
||||
* ```
|
||||
*
|
||||
* This example eventually deletes the key `foo` from the cache. As with
|
||||
* `set()`, this may not happen instantly and a promise is returned to
|
||||
* provide guarantees whether or not the item has been removed from cache.
|
||||
*
|
||||
* @param string $key
|
||||
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
|
||||
*/
|
||||
public function delete($key);
|
||||
|
||||
/**
|
||||
* Retrieves multiple cache items by their unique keys.
|
||||
*
|
||||
* This method will resolve with an array of cached values on success or with the
|
||||
* given `$default` value when an item can not be found or when an error occurs.
|
||||
* Similarly, an expired cache item (once the time-to-live is expired) is
|
||||
* considered a cache miss.
|
||||
*
|
||||
* ```php
|
||||
* $cache->getMultiple(array('name', 'age'))->then(function (array $values) {
|
||||
* $name = $values['name'] ?? 'User';
|
||||
* $age = $values['age'] ?? 'n/a';
|
||||
*
|
||||
* echo $name . ' is ' . $age . PHP_EOL;
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* This example fetches the cache items for the `name` and `age` keys and
|
||||
* prints some example output. You can use any of the composition provided
|
||||
* by [promises](https://github.com/reactphp/promise).
|
||||
*
|
||||
* @param string[] $keys A list of keys that can obtained in a single operation.
|
||||
* @param mixed $default Default value to return for keys that do not exist.
|
||||
* @return PromiseInterface<array> Returns a promise which resolves to an `array` of cached values
|
||||
*/
|
||||
public function getMultiple(array $keys, $default = null);
|
||||
|
||||
/**
|
||||
* Persists a set of key => value pairs in the cache, with an optional TTL.
|
||||
*
|
||||
* This method will resolve with `true` on success or `false` when an error
|
||||
* occurs. If the cache implementation has to go over the network to store
|
||||
* it, it may take a while.
|
||||
*
|
||||
* The optional `$ttl` parameter sets the maximum time-to-live in seconds
|
||||
* for these cache items. If this parameter is omitted (or `null`), these items
|
||||
* will stay in the cache for as long as the underlying implementation
|
||||
* supports. Trying to access an expired cache items results in a cache miss,
|
||||
* see also [`get()`](#get).
|
||||
*
|
||||
* ```php
|
||||
* $cache->setMultiple(array('foo' => 1, 'bar' => 2), 60);
|
||||
* ```
|
||||
*
|
||||
* This example eventually sets the list of values - the key `foo` to 1 value
|
||||
* and the key `bar` to 2. If some of the keys already exist, they are overridden.
|
||||
*
|
||||
* @param array $values A list of key => value pairs for a multiple-set operation.
|
||||
* @param ?float $ttl Optional. The TTL value of this item.
|
||||
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
|
||||
*/
|
||||
public function setMultiple(array $values, $ttl = null);
|
||||
|
||||
/**
|
||||
* Deletes multiple cache items in a single operation.
|
||||
*
|
||||
* @param string[] $keys A list of string-based keys to be deleted.
|
||||
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
|
||||
*/
|
||||
public function deleteMultiple(array $keys);
|
||||
|
||||
/**
|
||||
* Wipes clean the entire cache.
|
||||
*
|
||||
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
|
||||
*/
|
||||
public function clear();
|
||||
|
||||
/**
|
||||
* Determines whether an item is present in the cache.
|
||||
*
|
||||
* This method will resolve with `true` on success or `false` when no item can be found
|
||||
* or when an error occurs. Similarly, an expired cache item (once the time-to-live
|
||||
* is expired) is considered a cache miss.
|
||||
*
|
||||
* ```php
|
||||
* $cache
|
||||
* ->has('foo')
|
||||
* ->then('var_dump');
|
||||
* ```
|
||||
*
|
||||
* This example checks if the value of the key `foo` is set in the cache and passes
|
||||
* the result to the `var_dump` function. You can use any of the composition provided by
|
||||
* [promises](https://github.com/reactphp/promise).
|
||||
*
|
||||
* NOTE: It is recommended that has() is only to be used for cache warming type purposes
|
||||
* and not to be used within your live applications operations for get/set, as this method
|
||||
* is subject to a race condition where your has() will return true and immediately after,
|
||||
* another script can remove it making the state of your app out of date.
|
||||
*
|
||||
* @param string $key The cache item key.
|
||||
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
|
||||
*/
|
||||
public function has($key);
|
||||
}
|
||||
Vendored
+460
@@ -0,0 +1,460 @@
|
||||
# Changelog
|
||||
|
||||
## 1.14.0 (2025-11-18)
|
||||
|
||||
* Feature: Improve PHP 8.5+ support by avoiding deprecated `setAccessible()` calls.
|
||||
(#238 by @W0rma and #243 by @WyriHaximus)
|
||||
|
||||
* Improve test suite, update test environment and increase query count in excessive TCP query tests.
|
||||
(#239 and #240 by @WyriHaximus)
|
||||
|
||||
## 1.13.0 (2024-06-13)
|
||||
|
||||
* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable type declarations.
|
||||
(#224 by @WyriHaximus)
|
||||
|
||||
## 1.12.0 (2023-11-29)
|
||||
|
||||
* Feature: Full PHP 8.3 compatibility.
|
||||
(#217 by @sergiy-petrov)
|
||||
|
||||
* Update test environment and avoid unhandled promise rejections.
|
||||
(#215, #216 and #218 by @clue)
|
||||
|
||||
## 1.11.0 (2023-06-02)
|
||||
|
||||
* Feature: Include timeout logic to avoid dependency on reactphp/promise-timer.
|
||||
(#213 by @clue)
|
||||
|
||||
* Improve test suite and project setup and report failed assertions.
|
||||
(#210 by @clue, #212 by @WyriHaximus and #209 and #211 by @SimonFrings)
|
||||
|
||||
## 1.10.0 (2022-09-08)
|
||||
|
||||
* Feature: Full support for PHP 8.2 release.
|
||||
(#201 by @clue and #207 by @WyriHaximus)
|
||||
|
||||
* Feature: Optimize forward compatibility with Promise v3, avoid hitting autoloader.
|
||||
(#202 by @clue)
|
||||
|
||||
* Feature / Fix: Improve error reporting when custom error handler is used.
|
||||
(#197 by @clue)
|
||||
|
||||
* Fix: Fix invalid references in exception stack trace.
|
||||
(#191 by @clue)
|
||||
|
||||
* Minor documentation improvements.
|
||||
(#195 by @SimonFrings and #203 by @nhedger)
|
||||
|
||||
* Improve test suite, update to use default loop and new reactphp/async package.
|
||||
(#204, #205 and #206 by @clue and #196 by @SimonFrings)
|
||||
|
||||
## 1.9.0 (2021-12-20)
|
||||
|
||||
* Feature: Full support for PHP 8.1 release and prepare PHP 8.2 compatibility
|
||||
by refactoring `Parser` to avoid assigning dynamic properties.
|
||||
(#188 and #186 by @clue and #184 by @SimonFrings)
|
||||
|
||||
* Feature: Avoid dependency on `ext-filter`.
|
||||
(#185 by @clue)
|
||||
|
||||
* Feature / Fix: Skip invalid nameserver entries from `resolv.conf` and ignore IPv6 zone IDs.
|
||||
(#187 by @clue)
|
||||
|
||||
* Feature / Fix: Reduce socket read chunk size for queries over TCP/IP.
|
||||
(#189 by @clue)
|
||||
|
||||
## 1.8.0 (2021-07-11)
|
||||
|
||||
A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop).
|
||||
|
||||
* Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop).
|
||||
(#182 by @clue)
|
||||
|
||||
```php
|
||||
// old (still supported)
|
||||
$factory = new React\Dns\Resolver\Factory();
|
||||
$resolver = $factory->create($config, $loop);
|
||||
|
||||
// new (using default loop)
|
||||
$factory = new React\Dns\Resolver\Factory();
|
||||
$resolver = $factory->create($config);
|
||||
```
|
||||
|
||||
## 1.7.0 (2021-06-25)
|
||||
|
||||
* Feature: Update DNS `Factory` to accept complete `Config` object.
|
||||
Add new `FallbackExecutor` and use fallback DNS servers when `Config` lists multiple servers.
|
||||
(#179 and #180 by @clue)
|
||||
|
||||
```php
|
||||
// old (still supported)
|
||||
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
|
||||
$server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
|
||||
$resolver = $factory->create($server, $loop);
|
||||
|
||||
// new
|
||||
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
|
||||
if (!$config->nameservers) {
|
||||
$config->nameservers[] = '8.8.8.8';
|
||||
}
|
||||
$resolver = $factory->create($config, $loop);
|
||||
```
|
||||
|
||||
## 1.6.0 (2021-06-21)
|
||||
|
||||
* Feature: Add support for legacy `SPF` record type.
|
||||
(#178 by @akondas and @clue)
|
||||
|
||||
* Fix: Fix integer overflow for TCP/IP chunk size on 32 bit platforms.
|
||||
(#177 by @clue)
|
||||
|
||||
## 1.5.0 (2021-03-05)
|
||||
|
||||
* Feature: Improve error reporting when query fails, include domain and query type and DNS server address where applicable.
|
||||
(#174 by @clue)
|
||||
|
||||
* Feature: Improve error handling when sending data to DNS server fails (macOS).
|
||||
(#171 and #172 by @clue)
|
||||
|
||||
* Fix: Improve DNS response parser to limit recursion for compressed labels.
|
||||
(#169 by @clue)
|
||||
|
||||
* Improve test suite, use GitHub actions for continuous integration (CI).
|
||||
(#170 by @SimonFrings)
|
||||
|
||||
## 1.4.0 (2020-09-18)
|
||||
|
||||
* Feature: Support upcoming PHP 8.
|
||||
(#168 by @clue)
|
||||
|
||||
* Improve test suite and update to PHPUnit 9.3.
|
||||
(#164 by @clue, #165 and #166 by @SimonFrings and #167 by @WyriHaximus)
|
||||
|
||||
## 1.3.0 (2020-07-10)
|
||||
|
||||
* Feature: Forward compatibility with react/promise v3.
|
||||
(#153 by @WyriHaximus)
|
||||
|
||||
* Feature: Support parsing `OPT` records (EDNS0).
|
||||
(#157 by @clue)
|
||||
|
||||
* Fix: Avoid PHP warnings due to lack of args in exception trace on PHP 7.4.
|
||||
(#160 by @clue)
|
||||
|
||||
* Improve test suite and add `.gitattributes` to exclude dev files from exports.
|
||||
Run tests on PHPUnit 9 and PHP 7.4 and clean up test suite.
|
||||
(#154 by @reedy, #156 by @clue and #163 by @SimonFrings)
|
||||
|
||||
## 1.2.0 (2019-08-15)
|
||||
|
||||
* Feature: Add `TcpTransportExecutor` to send DNS queries over TCP/IP connection,
|
||||
add `SelectiveTransportExecutor` to retry with TCP if UDP is truncated and
|
||||
automatically select transport protocol when no explicit `udp://` or `tcp://` scheme is given in `Factory`.
|
||||
(#145, #146, #147 and #148 by @clue)
|
||||
|
||||
* Feature: Support escaping literal dots and special characters in domain names.
|
||||
(#144 by @clue)
|
||||
|
||||
## 1.1.0 (2019-07-18)
|
||||
|
||||
* Feature: Support parsing `CAA` and `SSHFP` records.
|
||||
(#141 and #142 by @clue)
|
||||
|
||||
* Feature: Add `ResolverInterface` as common interface for `Resolver` class.
|
||||
(#139 by @clue)
|
||||
|
||||
* Fix: Add missing private property definitions and
|
||||
remove unneeded dependency on `react/stream`.
|
||||
(#140 and #143 by @clue)
|
||||
|
||||
## 1.0.0 (2019-07-11)
|
||||
|
||||
* First stable LTS release, now following [SemVer](https://semver.org/).
|
||||
We'd like to emphasize that this component is production ready and battle-tested.
|
||||
We plan to support all long-term support (LTS) releases for at least 24 months,
|
||||
so you have a rock-solid foundation to build on top of.
|
||||
|
||||
This update involves a number of BC breaks due to dropped support for
|
||||
deprecated functionality and some internal API cleanup. We've tried hard to
|
||||
avoid BC breaks where possible and minimize impact otherwise. We expect that
|
||||
most consumers of this package will actually not be affected by any BC
|
||||
breaks, see below for more details:
|
||||
|
||||
* BC break: Delete all deprecated APIs, use `Query` objects for `Message` questions
|
||||
instead of nested arrays and increase code coverage to 100%.
|
||||
(#130 by @clue)
|
||||
|
||||
* BC break: Move `$nameserver` from `ExecutorInterface` to `UdpTransportExecutor`,
|
||||
remove advanced/internal `UdpTransportExecutor` args for `Parser`/`BinaryDumper` and
|
||||
add API documentation for `ExecutorInterface`.
|
||||
(#135, #137 and #138 by @clue)
|
||||
|
||||
* BC break: Replace `HeaderBag` attributes with simple `Message` properties.
|
||||
(#132 by @clue)
|
||||
|
||||
* BC break: Mark all `Record` attributes as required, add documentation vs `Query`.
|
||||
(#136 by @clue)
|
||||
|
||||
* BC break: Mark all classes as final to discourage inheritance
|
||||
(#134 by @WyriHaximus)
|
||||
|
||||
## 0.4.19 (2019-07-10)
|
||||
|
||||
* Feature: Avoid garbage references when DNS resolution rejects on legacy PHP <= 5.6.
|
||||
(#133 by @clue)
|
||||
|
||||
## 0.4.18 (2019-09-07)
|
||||
|
||||
* Feature / Fix: Implement `CachingExecutor` using cache TTL, deprecate old `CachedExecutor`,
|
||||
respect TTL from response records when caching and do not cache truncated responses.
|
||||
(#129 by @clue)
|
||||
|
||||
* Feature: Limit cache size to 256 last responses by default.
|
||||
(#127 by @clue)
|
||||
|
||||
* Feature: Cooperatively resolve hosts to avoid running same query concurrently.
|
||||
(#125 by @clue)
|
||||
|
||||
## 0.4.17 (2019-04-01)
|
||||
|
||||
* Feature: Support parsing `authority` and `additional` records from DNS response.
|
||||
(#123 by @clue)
|
||||
|
||||
* Feature: Support dumping records as part of outgoing binary DNS message.
|
||||
(#124 by @clue)
|
||||
|
||||
* Feature: Forward compatibility with upcoming Cache v0.6 and Cache v1.0
|
||||
(#121 by @clue)
|
||||
|
||||
* Improve test suite to add forward compatibility with PHPUnit 7,
|
||||
test against PHP 7.3 and use legacy PHPUnit 5 on legacy HHVM.
|
||||
(#122 by @clue)
|
||||
|
||||
## 0.4.16 (2018-11-11)
|
||||
|
||||
* Feature: Improve promise cancellation for DNS lookup retries and clean up any garbage references.
|
||||
(#118 by @clue)
|
||||
|
||||
* Fix: Reject parsing malformed DNS response messages such as incomplete DNS response messages,
|
||||
malformed record data or malformed compressed domain name labels.
|
||||
(#115 and #117 by @clue)
|
||||
|
||||
* Fix: Fix interpretation of TTL as UINT32 with most significant bit unset.
|
||||
(#116 by @clue)
|
||||
|
||||
* Fix: Fix caching advanced MX/SRV/TXT/SOA structures.
|
||||
(#112 by @clue)
|
||||
|
||||
## 0.4.15 (2018-07-02)
|
||||
|
||||
* Feature: Add `resolveAll()` method to support custom query types in `Resolver`.
|
||||
(#110 by @clue and @WyriHaximus)
|
||||
|
||||
```php
|
||||
$resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
|
||||
echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
* Feature: Support parsing `NS`, `TXT`, `MX`, `SOA` and `SRV` records.
|
||||
(#104, #105, #106, #107 and #108 by @clue)
|
||||
|
||||
* Feature: Add support for `Message::TYPE_ANY` and parse unknown types as binary data.
|
||||
(#104 by @clue)
|
||||
|
||||
* Feature: Improve error messages for failed queries and improve documentation.
|
||||
(#109 by @clue)
|
||||
|
||||
* Feature: Add reverse DNS lookup example.
|
||||
(#111 by @clue)
|
||||
|
||||
## 0.4.14 (2018-06-26)
|
||||
|
||||
* Feature: Add `UdpTransportExecutor`, validate incoming DNS response messages
|
||||
to avoid cache poisoning attacks and deprecate legacy `Executor`.
|
||||
(#101 and #103 by @clue)
|
||||
|
||||
* Feature: Forward compatibility with Cache 0.5
|
||||
(#102 by @clue)
|
||||
|
||||
* Deprecate legacy `Query::$currentTime` and binary parser data attributes to clean up and simplify API.
|
||||
(#99 by @clue)
|
||||
|
||||
## 0.4.13 (2018-02-27)
|
||||
|
||||
* Add `Config::loadSystemConfigBlocking()` to load default system config
|
||||
and support parsing DNS config on all supported platforms
|
||||
(`/etc/resolv.conf` on Unix/Linux/Mac and WMIC on Windows)
|
||||
(#92, #93, #94 and #95 by @clue)
|
||||
|
||||
```php
|
||||
$config = Config::loadSystemConfigBlocking();
|
||||
$server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
|
||||
```
|
||||
|
||||
* Remove unneeded cyclic dependency on react/socket
|
||||
(#96 by @clue)
|
||||
|
||||
## 0.4.12 (2018-01-14)
|
||||
|
||||
* Improve test suite by adding forward compatibility with PHPUnit 6,
|
||||
test against PHP 7.2, fix forward compatibility with upcoming EventLoop releases,
|
||||
add test group to skip integration tests relying on internet connection
|
||||
and add minor documentation improvements.
|
||||
(#85 and #87 by @carusogabriel, #88 and #89 by @clue and #83 by @jsor)
|
||||
|
||||
## 0.4.11 (2017-08-25)
|
||||
|
||||
* Feature: Support resolving from default hosts file
|
||||
(#75, #76 and #77 by @clue)
|
||||
|
||||
This means that resolving hosts such as `localhost` will now work as
|
||||
expected across all platforms with no changes required:
|
||||
|
||||
```php
|
||||
$resolver->resolve('localhost')->then(function ($ip) {
|
||||
echo 'IP: ' . $ip;
|
||||
});
|
||||
```
|
||||
|
||||
The new `HostsExecutor` exists for advanced usage and is otherwise used
|
||||
internally for this feature.
|
||||
|
||||
## 0.4.10 (2017-08-10)
|
||||
|
||||
* Feature: Forward compatibility with EventLoop v1.0 and v0.5 and
|
||||
lock minimum dependencies and work around circular dependency for tests
|
||||
(#70 and #71 by @clue)
|
||||
|
||||
* Fix: Work around DNS timeout issues for Windows users
|
||||
(#74 by @clue)
|
||||
|
||||
* Documentation and examples for advanced usage
|
||||
(#66 by @WyriHaximus)
|
||||
|
||||
* Remove broken TCP code, do not retry with invalid TCP query
|
||||
(#73 by @clue)
|
||||
|
||||
* Improve test suite by fixing HHVM build for now again and ignore future HHVM build errors and
|
||||
lock Travis distro so new defaults will not break the build and
|
||||
fix failing tests for PHP 7.1
|
||||
(#68 by @WyriHaximus and #69 and #72 by @clue)
|
||||
|
||||
## 0.4.9 (2017-05-01)
|
||||
|
||||
* Feature: Forward compatibility with upcoming Socket v1.0 and v0.8
|
||||
(#61 by @clue)
|
||||
|
||||
## 0.4.8 (2017-04-16)
|
||||
|
||||
* Feature: Add support for the AAAA record type to the protocol parser
|
||||
(#58 by @othillo)
|
||||
|
||||
* Feature: Add support for the PTR record type to the protocol parser
|
||||
(#59 by @othillo)
|
||||
|
||||
## 0.4.7 (2017-03-31)
|
||||
|
||||
* Feature: Forward compatibility with upcoming Socket v0.6 and v0.7 component
|
||||
(#57 by @clue)
|
||||
|
||||
## 0.4.6 (2017-03-11)
|
||||
|
||||
* Fix: Fix DNS timeout issues for Windows users and add forward compatibility
|
||||
with Stream v0.5 and upcoming v0.6
|
||||
(#53 by @clue)
|
||||
|
||||
* Improve test suite by adding PHPUnit to `require-dev`
|
||||
(#54 by @clue)
|
||||
|
||||
## 0.4.5 (2017-03-02)
|
||||
|
||||
* Fix: Ensure we ignore the case of the answer
|
||||
(#51 by @WyriHaximus)
|
||||
|
||||
* Feature: Add `TimeoutExecutor` and simplify internal APIs to allow internal
|
||||
code re-use for upcoming versions.
|
||||
(#48 and #49 by @clue)
|
||||
|
||||
## 0.4.4 (2017-02-13)
|
||||
|
||||
* Fix: Fix handling connection and stream errors
|
||||
(#45 by @clue)
|
||||
|
||||
* Feature: Add examples and forward compatibility with upcoming Socket v0.5 component
|
||||
(#46 and #47 by @clue)
|
||||
|
||||
## 0.4.3 (2016-07-31)
|
||||
|
||||
* Feature: Allow for cache adapter injection (#38 by @WyriHaximus)
|
||||
|
||||
```php
|
||||
$factory = new React\Dns\Resolver\Factory();
|
||||
|
||||
$cache = new MyCustomCacheInstance();
|
||||
$resolver = $factory->createCached('8.8.8.8', $loop, $cache);
|
||||
```
|
||||
|
||||
* Feature: Support Promise cancellation (#35 by @clue)
|
||||
|
||||
```php
|
||||
$promise = $resolver->resolve('reactphp.org');
|
||||
|
||||
$promise->cancel();
|
||||
```
|
||||
|
||||
## 0.4.2 (2016-02-24)
|
||||
|
||||
* Repository maintenance, split off from main repo, improve test suite and documentation
|
||||
* First class support for PHP7 and HHVM (#34 by @clue)
|
||||
* Adjust compatibility to 5.3 (#30 by @clue)
|
||||
|
||||
## 0.4.1 (2014-04-13)
|
||||
|
||||
* Bug fix: Fixed PSR-4 autoload path (@marcj/WyriHaximus)
|
||||
|
||||
## 0.4.0 (2014-02-02)
|
||||
|
||||
* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks
|
||||
* BC break: Update to React/Promise 2.0
|
||||
* Bug fix: Properly resolve CNAME aliases
|
||||
* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0
|
||||
* Bump React dependencies to v0.4
|
||||
|
||||
## 0.3.2 (2013-05-10)
|
||||
|
||||
* Feature: Support default port for IPv6 addresses (@clue)
|
||||
|
||||
## 0.3.0 (2013-04-14)
|
||||
|
||||
* Bump React dependencies to v0.3
|
||||
|
||||
## 0.2.6 (2012-12-26)
|
||||
|
||||
* Feature: New cache component, used by DNS
|
||||
|
||||
## 0.2.5 (2012-11-26)
|
||||
|
||||
* Version bump
|
||||
|
||||
## 0.2.4 (2012-11-18)
|
||||
|
||||
* Feature: Change to promise-based API (@jsor)
|
||||
|
||||
## 0.2.3 (2012-11-14)
|
||||
|
||||
* Version bump
|
||||
|
||||
## 0.2.2 (2012-10-28)
|
||||
|
||||
* Feature: DNS executor timeout handling (@arnaud-lb)
|
||||
* Feature: DNS retry executor (@arnaud-lb)
|
||||
|
||||
## 0.2.1 (2012-10-14)
|
||||
|
||||
* Minor adjustments to DNS parser
|
||||
|
||||
## 0.2.0 (2012-09-10)
|
||||
|
||||
* Feature: DNS resolver
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Vendored
+453
@@ -0,0 +1,453 @@
|
||||
# DNS
|
||||
|
||||
[](https://github.com/reactphp/dns/actions)
|
||||
[](https://packagist.org/packages/react/dns)
|
||||
|
||||
Async DNS resolver for [ReactPHP](https://reactphp.org/).
|
||||
|
||||
The main point of the DNS component is to provide async DNS resolution.
|
||||
However, it is really a toolkit for working with DNS messages, and could
|
||||
easily be used to create a DNS server.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
* [Basic usage](#basic-usage)
|
||||
* [Caching](#caching)
|
||||
* [Custom cache adapter](#custom-cache-adapter)
|
||||
* [ResolverInterface](#resolverinterface)
|
||||
* [resolve()](#resolve)
|
||||
* [resolveAll()](#resolveall)
|
||||
* [Advanced usage](#advanced-usage)
|
||||
* [UdpTransportExecutor](#udptransportexecutor)
|
||||
* [TcpTransportExecutor](#tcptransportexecutor)
|
||||
* [SelectiveTransportExecutor](#selectivetransportexecutor)
|
||||
* [HostsFileExecutor](#hostsfileexecutor)
|
||||
* [Install](#install)
|
||||
* [Tests](#tests)
|
||||
* [License](#license)
|
||||
* [References](#references)
|
||||
|
||||
## Basic usage
|
||||
|
||||
The most basic usage is to just create a resolver through the resolver
|
||||
factory. All you need to give it is a nameserver, then you can start resolving
|
||||
names, baby!
|
||||
|
||||
```php
|
||||
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
|
||||
if (!$config->nameservers) {
|
||||
$config->nameservers[] = '8.8.8.8';
|
||||
}
|
||||
|
||||
$factory = new React\Dns\Resolver\Factory();
|
||||
$dns = $factory->create($config);
|
||||
|
||||
$dns->resolve('igor.io')->then(function ($ip) {
|
||||
echo "Host: $ip\n";
|
||||
});
|
||||
```
|
||||
|
||||
See also the [first example](examples).
|
||||
|
||||
The `Config` class can be used to load the system default config. This is an
|
||||
operation that may access the filesystem and block. Ideally, this method should
|
||||
thus be executed only once before the loop starts and not repeatedly while it is
|
||||
running.
|
||||
Note that this class may return an *empty* configuration if the system config
|
||||
can not be loaded. As such, you'll likely want to apply a default nameserver
|
||||
as above if none can be found.
|
||||
|
||||
> Note that the factory loads the hosts file from the filesystem once when
|
||||
creating the resolver instance.
|
||||
Ideally, this method should thus be executed only once before the loop starts
|
||||
and not repeatedly while it is running.
|
||||
|
||||
But there's more.
|
||||
|
||||
## Caching
|
||||
|
||||
You can cache results by configuring the resolver to use a `CachedExecutor`:
|
||||
|
||||
```php
|
||||
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
|
||||
if (!$config->nameservers) {
|
||||
$config->nameservers[] = '8.8.8.8';
|
||||
}
|
||||
|
||||
$factory = new React\Dns\Resolver\Factory();
|
||||
$dns = $factory->createCached($config);
|
||||
|
||||
$dns->resolve('igor.io')->then(function ($ip) {
|
||||
echo "Host: $ip\n";
|
||||
});
|
||||
|
||||
...
|
||||
|
||||
$dns->resolve('igor.io')->then(function ($ip) {
|
||||
echo "Host: $ip\n";
|
||||
});
|
||||
```
|
||||
|
||||
If the first call returns before the second, only one query will be executed.
|
||||
The second result will be served from an in memory cache.
|
||||
This is particularly useful for long running scripts where the same hostnames
|
||||
have to be looked up multiple times.
|
||||
|
||||
See also the [third example](examples).
|
||||
|
||||
### Custom cache adapter
|
||||
|
||||
By default, the above will use an in memory cache.
|
||||
|
||||
You can also specify a custom cache implementing [`CacheInterface`](https://github.com/reactphp/cache) to handle the record cache instead:
|
||||
|
||||
```php
|
||||
$cache = new React\Cache\ArrayCache();
|
||||
$factory = new React\Dns\Resolver\Factory();
|
||||
$dns = $factory->createCached('8.8.8.8', null, $cache);
|
||||
```
|
||||
|
||||
See also the wiki for possible [cache implementations](https://github.com/reactphp/react/wiki/Users#cache-implementations).
|
||||
|
||||
## ResolverInterface
|
||||
|
||||
<a id="resolver"><!-- legacy reference --></a>
|
||||
|
||||
### resolve()
|
||||
|
||||
The `resolve(string $domain): PromiseInterface<string>` method can be used to
|
||||
resolve the given $domain name to a single IPv4 address (type `A` query).
|
||||
|
||||
```php
|
||||
$resolver->resolve('reactphp.org')->then(function ($ip) {
|
||||
echo 'IP for reactphp.org is ' . $ip . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
This is one of the main methods in this package. It sends a DNS query
|
||||
for the given $domain name to your DNS server and returns a single IP
|
||||
address on success.
|
||||
|
||||
If the DNS server sends a DNS response message that contains more than
|
||||
one IP address for this query, it will randomly pick one of the IP
|
||||
addresses from the response. If you want the full list of IP addresses
|
||||
or want to send a different type of query, you should use the
|
||||
[`resolveAll()`](#resolveall) method instead.
|
||||
|
||||
If the DNS server sends a DNS response message that indicates an error
|
||||
code, this method will reject with a `RecordNotFoundException`. Its
|
||||
message and code can be used to check for the response code.
|
||||
|
||||
If the DNS communication fails and the server does not respond with a
|
||||
valid response message, this message will reject with an `Exception`.
|
||||
|
||||
Pending DNS queries can be cancelled by cancelling its pending promise like so:
|
||||
|
||||
```php
|
||||
$promise = $resolver->resolve('reactphp.org');
|
||||
|
||||
$promise->cancel();
|
||||
```
|
||||
|
||||
### resolveAll()
|
||||
|
||||
The `resolveAll(string $host, int $type): PromiseInterface<array>` method can be used to
|
||||
resolve all record values for the given $domain name and query $type.
|
||||
|
||||
```php
|
||||
$resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) {
|
||||
echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
|
||||
});
|
||||
|
||||
$resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
|
||||
echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
This is one of the main methods in this package. It sends a DNS query
|
||||
for the given $domain name to your DNS server and returns a list with all
|
||||
record values on success.
|
||||
|
||||
If the DNS server sends a DNS response message that contains one or more
|
||||
records for this query, it will return a list with all record values
|
||||
from the response. You can use the `Message::TYPE_*` constants to control
|
||||
which type of query will be sent. Note that this method always returns a
|
||||
list of record values, but each record value type depends on the query
|
||||
type. For example, it returns the IPv4 addresses for type `A` queries,
|
||||
the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`,
|
||||
`CNAME` and `PTR` queries and structured data for other queries. See also
|
||||
the `Record` documentation for more details.
|
||||
|
||||
If the DNS server sends a DNS response message that indicates an error
|
||||
code, this method will reject with a `RecordNotFoundException`. Its
|
||||
message and code can be used to check for the response code.
|
||||
|
||||
If the DNS communication fails and the server does not respond with a
|
||||
valid response message, this message will reject with an `Exception`.
|
||||
|
||||
Pending DNS queries can be cancelled by cancelling its pending promise like so:
|
||||
|
||||
```php
|
||||
$promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA);
|
||||
|
||||
$promise->cancel();
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### UdpTransportExecutor
|
||||
|
||||
The `UdpTransportExecutor` can be used to
|
||||
send DNS queries over a UDP transport.
|
||||
|
||||
This is the main class that sends a DNS query to your DNS server and is used
|
||||
internally by the `Resolver` for the actual message transport.
|
||||
|
||||
For more advanced usages one can utilize this class directly.
|
||||
The following example looks up the `IPv6` address for `igor.io`.
|
||||
|
||||
```php
|
||||
$executor = new UdpTransportExecutor('8.8.8.8:53');
|
||||
|
||||
$executor->query(
|
||||
new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
|
||||
)->then(function (Message $message) {
|
||||
foreach ($message->answers as $answer) {
|
||||
echo 'IPv6: ' . $answer->data . PHP_EOL;
|
||||
}
|
||||
}, 'printf');
|
||||
```
|
||||
|
||||
See also the [fourth example](examples).
|
||||
|
||||
Note that this executor does not implement a timeout, so you will very likely
|
||||
want to use this in combination with a `TimeoutExecutor` like this:
|
||||
|
||||
```php
|
||||
$executor = new TimeoutExecutor(
|
||||
new UdpTransportExecutor($nameserver),
|
||||
3.0
|
||||
);
|
||||
```
|
||||
|
||||
Also note that this executor uses an unreliable UDP transport and that it
|
||||
does not implement any retry logic, so you will likely want to use this in
|
||||
combination with a `RetryExecutor` like this:
|
||||
|
||||
```php
|
||||
$executor = new RetryExecutor(
|
||||
new TimeoutExecutor(
|
||||
new UdpTransportExecutor($nameserver),
|
||||
3.0
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
Note that this executor is entirely async and as such allows you to execute
|
||||
any number of queries concurrently. You should probably limit the number of
|
||||
concurrent queries in your application or you're very likely going to face
|
||||
rate limitations and bans on the resolver end. For many common applications,
|
||||
you may want to avoid sending the same query multiple times when the first
|
||||
one is still pending, so you will likely want to use this in combination with
|
||||
a `CoopExecutor` like this:
|
||||
|
||||
```php
|
||||
$executor = new CoopExecutor(
|
||||
new RetryExecutor(
|
||||
new TimeoutExecutor(
|
||||
new UdpTransportExecutor($nameserver),
|
||||
3.0
|
||||
)
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
> Internally, this class uses PHP's UDP sockets and does not take advantage
|
||||
of [react/datagram](https://github.com/reactphp/datagram) purely for
|
||||
organizational reasons to avoid a cyclic dependency between the two
|
||||
packages. Higher-level components should take advantage of the Datagram
|
||||
component instead of reimplementing this socket logic from scratch.
|
||||
|
||||
### TcpTransportExecutor
|
||||
|
||||
The `TcpTransportExecutor` class can be used to
|
||||
send DNS queries over a TCP/IP stream transport.
|
||||
|
||||
This is one of the main classes that send a DNS query to your DNS server.
|
||||
|
||||
For more advanced usages one can utilize this class directly.
|
||||
The following example looks up the `IPv6` address for `reactphp.org`.
|
||||
|
||||
```php
|
||||
$executor = new TcpTransportExecutor('8.8.8.8:53');
|
||||
|
||||
$executor->query(
|
||||
new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
|
||||
)->then(function (Message $message) {
|
||||
foreach ($message->answers as $answer) {
|
||||
echo 'IPv6: ' . $answer->data . PHP_EOL;
|
||||
}
|
||||
}, 'printf');
|
||||
```
|
||||
|
||||
See also [example #92](examples).
|
||||
|
||||
Note that this executor does not implement a timeout, so you will very likely
|
||||
want to use this in combination with a `TimeoutExecutor` like this:
|
||||
|
||||
```php
|
||||
$executor = new TimeoutExecutor(
|
||||
new TcpTransportExecutor($nameserver),
|
||||
3.0
|
||||
);
|
||||
```
|
||||
|
||||
Unlike the `UdpTransportExecutor`, this class uses a reliable TCP/IP
|
||||
transport, so you do not necessarily have to implement any retry logic.
|
||||
|
||||
Note that this executor is entirely async and as such allows you to execute
|
||||
queries concurrently. The first query will establish a TCP/IP socket
|
||||
connection to the DNS server which will be kept open for a short period.
|
||||
Additional queries will automatically reuse this existing socket connection
|
||||
to the DNS server, will pipeline multiple requests over this single
|
||||
connection and will keep an idle connection open for a short period. The
|
||||
initial TCP/IP connection overhead may incur a slight delay if you only send
|
||||
occasional queries – when sending a larger number of concurrent queries over
|
||||
an existing connection, it becomes increasingly more efficient and avoids
|
||||
creating many concurrent sockets like the UDP-based executor. You may still
|
||||
want to limit the number of (concurrent) queries in your application or you
|
||||
may be facing rate limitations and bans on the resolver end. For many common
|
||||
applications, you may want to avoid sending the same query multiple times
|
||||
when the first one is still pending, so you will likely want to use this in
|
||||
combination with a `CoopExecutor` like this:
|
||||
|
||||
```php
|
||||
$executor = new CoopExecutor(
|
||||
new TimeoutExecutor(
|
||||
new TcpTransportExecutor($nameserver),
|
||||
3.0
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
> Internally, this class uses PHP's TCP/IP sockets and does not take advantage
|
||||
of [react/socket](https://github.com/reactphp/socket) purely for
|
||||
organizational reasons to avoid a cyclic dependency between the two
|
||||
packages. Higher-level components should take advantage of the Socket
|
||||
component instead of reimplementing this socket logic from scratch.
|
||||
|
||||
### SelectiveTransportExecutor
|
||||
|
||||
The `SelectiveTransportExecutor` class can be used to
|
||||
Send DNS queries over a UDP or TCP/IP stream transport.
|
||||
|
||||
This class will automatically choose the correct transport protocol to send
|
||||
a DNS query to your DNS server. It will always try to send it over the more
|
||||
efficient UDP transport first. If this query yields a size related issue
|
||||
(truncated messages), it will retry over a streaming TCP/IP transport.
|
||||
|
||||
For more advanced usages one can utilize this class directly.
|
||||
The following example looks up the `IPv6` address for `reactphp.org`.
|
||||
|
||||
```php
|
||||
$executor = new SelectiveTransportExecutor($udpExecutor, $tcpExecutor);
|
||||
|
||||
$executor->query(
|
||||
new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
|
||||
)->then(function (Message $message) {
|
||||
foreach ($message->answers as $answer) {
|
||||
echo 'IPv6: ' . $answer->data . PHP_EOL;
|
||||
}
|
||||
}, 'printf');
|
||||
```
|
||||
|
||||
Note that this executor only implements the logic to select the correct
|
||||
transport for the given DNS query. Implementing the correct transport logic,
|
||||
implementing timeouts and any retry logic is left up to the given executors,
|
||||
see also [`UdpTransportExecutor`](#udptransportexecutor) and
|
||||
[`TcpTransportExecutor`](#tcptransportexecutor) for more details.
|
||||
|
||||
Note that this executor is entirely async and as such allows you to execute
|
||||
any number of queries concurrently. You should probably limit the number of
|
||||
concurrent queries in your application or you're very likely going to face
|
||||
rate limitations and bans on the resolver end. For many common applications,
|
||||
you may want to avoid sending the same query multiple times when the first
|
||||
one is still pending, so you will likely want to use this in combination with
|
||||
a `CoopExecutor` like this:
|
||||
|
||||
```php
|
||||
$executor = new CoopExecutor(
|
||||
new SelectiveTransportExecutor(
|
||||
$datagramExecutor,
|
||||
$streamExecutor
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### HostsFileExecutor
|
||||
|
||||
Note that the above `UdpTransportExecutor` class always performs an actual DNS query.
|
||||
If you also want to take entries from your hosts file into account, you may
|
||||
use this code:
|
||||
|
||||
```php
|
||||
$hosts = \React\Dns\Config\HostsFile::loadFromPathBlocking();
|
||||
|
||||
$executor = new UdpTransportExecutor('8.8.8.8:53');
|
||||
$executor = new HostsFileExecutor($hosts, $executor);
|
||||
|
||||
$executor->query(
|
||||
new Query('localhost', Message::TYPE_A, Message::CLASS_IN)
|
||||
);
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
The recommended way to install this library is [through Composer](https://getcomposer.org/).
|
||||
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
|
||||
|
||||
This project follows [SemVer](https://semver.org/).
|
||||
This will install the latest supported version:
|
||||
|
||||
```bash
|
||||
composer require react/dns:^1.14
|
||||
```
|
||||
|
||||
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
|
||||
|
||||
This project aims to run on any platform and thus does not require any PHP
|
||||
extensions and supports running on legacy PHP 5.3 through current PHP 8+ and
|
||||
HHVM.
|
||||
It's *highly recommended to use the latest supported PHP version* for this project.
|
||||
|
||||
## Tests
|
||||
|
||||
To run the test suite, you first need to clone this repo and then install all
|
||||
dependencies [through Composer](https://getcomposer.org/):
|
||||
|
||||
```bash
|
||||
composer install
|
||||
```
|
||||
|
||||
To run the test suite, go to the project root and run:
|
||||
|
||||
```bash
|
||||
vendor/bin/phpunit
|
||||
```
|
||||
|
||||
The test suite also contains a number of functional integration tests that rely
|
||||
on a stable internet connection.
|
||||
If you do not want to run these, they can simply be skipped like this:
|
||||
|
||||
```bash
|
||||
vendor/bin/phpunit --exclude-group internet
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT, see [LICENSE file](LICENSE).
|
||||
|
||||
## References
|
||||
|
||||
* [RFC 1034](https://tools.ietf.org/html/rfc1034) Domain Names - Concepts and Facilities
|
||||
* [RFC 1035](https://tools.ietf.org/html/rfc1035) Domain Names - Implementation and Specification
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "react/dns",
|
||||
"description": "Async DNS resolver for ReactPHP",
|
||||
"keywords": ["dns", "dns-resolver", "ReactPHP", "async"],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"homepage": "https://clue.engineering/",
|
||||
"email": "christian@clue.engineering"
|
||||
},
|
||||
{
|
||||
"name": "Cees-Jan Kiewiet",
|
||||
"homepage": "https://wyrihaximus.net/",
|
||||
"email": "reactphp@ceesjankiewiet.nl"
|
||||
},
|
||||
{
|
||||
"name": "Jan Sorgalla",
|
||||
"homepage": "https://sorgalla.com/",
|
||||
"email": "jsorgalla@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Chris Boden",
|
||||
"homepage": "https://cboden.dev/",
|
||||
"email": "cboden@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3.0",
|
||||
"react/cache": "^1.0 || ^0.6 || ^0.5",
|
||||
"react/event-loop": "^1.2",
|
||||
"react/promise": "^3.2 || ^2.7 || ^1.2.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
|
||||
"react/async": "^4.3 || ^3 || ^2",
|
||||
"react/promise-timer": "^1.11"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"React\\Dns\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"React\\Tests\\Dns\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns;
|
||||
|
||||
final class BadServerException extends \Exception
|
||||
{
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Config;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class Config
|
||||
{
|
||||
/**
|
||||
* Loads the system DNS configuration
|
||||
*
|
||||
* Note that this method may block while loading its internal files and/or
|
||||
* commands and should thus be used with care! While this should be
|
||||
* relatively fast for most systems, it remains unknown if this may block
|
||||
* under certain circumstances. In particular, this method should only be
|
||||
* executed before the loop starts, not while it is running.
|
||||
*
|
||||
* Note that this method will try to access its files and/or commands and
|
||||
* try to parse its output. Currently, this will only parse valid nameserver
|
||||
* entries from its output and will ignore all other output without
|
||||
* complaining.
|
||||
*
|
||||
* Note that the previous section implies that this may return an empty
|
||||
* `Config` object if no valid nameserver entries can be found.
|
||||
*
|
||||
* @return self
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function loadSystemConfigBlocking()
|
||||
{
|
||||
// Use WMIC output on Windows
|
||||
if (DIRECTORY_SEPARATOR === '\\') {
|
||||
return self::loadWmicBlocking();
|
||||
}
|
||||
|
||||
// otherwise (try to) load from resolv.conf
|
||||
try {
|
||||
return self::loadResolvConfBlocking();
|
||||
} catch (RuntimeException $ignored) {
|
||||
// return empty config if parsing fails (file not found)
|
||||
return new self();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a resolv.conf file (from the given path or default location)
|
||||
*
|
||||
* Note that this method blocks while loading the given path and should
|
||||
* thus be used with care! While this should be relatively fast for normal
|
||||
* resolv.conf files, this may be an issue if this file is located on a slow
|
||||
* device or contains an excessive number of entries. In particular, this
|
||||
* method should only be executed before the loop starts, not while it is
|
||||
* running.
|
||||
*
|
||||
* Note that this method will throw if the given file can not be loaded,
|
||||
* such as if it is not readable or does not exist. In particular, this file
|
||||
* is not available on Windows.
|
||||
*
|
||||
* Currently, this will only parse valid "nameserver X" lines from the
|
||||
* given file contents. Lines can be commented out with "#" and ";" and
|
||||
* invalid lines will be ignored without complaining. See also
|
||||
* `man resolv.conf` for more details.
|
||||
*
|
||||
* Note that the previous section implies that this may return an empty
|
||||
* `Config` object if no valid "nameserver X" lines can be found. See also
|
||||
* `man resolv.conf` which suggests that the DNS server on the localhost
|
||||
* should be used in this case. This is left up to higher level consumers
|
||||
* of this API.
|
||||
*
|
||||
* @param ?string $path (optional) path to resolv.conf file or null=load default location
|
||||
* @return self
|
||||
* @throws RuntimeException if the path can not be loaded (does not exist)
|
||||
*/
|
||||
public static function loadResolvConfBlocking($path = null)
|
||||
{
|
||||
if ($path === null) {
|
||||
$path = '/etc/resolv.conf';
|
||||
}
|
||||
|
||||
$contents = @file_get_contents($path);
|
||||
if ($contents === false) {
|
||||
throw new RuntimeException('Unable to load resolv.conf file "' . $path . '"');
|
||||
}
|
||||
|
||||
$matches = array();
|
||||
preg_match_all('/^nameserver\s+(\S+)\s*$/m', $contents, $matches);
|
||||
|
||||
$config = new self();
|
||||
foreach ($matches[1] as $ip) {
|
||||
// remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`)
|
||||
if (strpos($ip, ':') !== false && ($pos = strpos($ip, '%')) !== false) {
|
||||
$ip = substr($ip, 0, $pos);
|
||||
}
|
||||
|
||||
if (@inet_pton($ip) !== false) {
|
||||
$config->nameservers[] = $ip;
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the DNS configurations from Windows's WMIC (from the given command or default command)
|
||||
*
|
||||
* Note that this method blocks while loading the given command and should
|
||||
* thus be used with care! While this should be relatively fast for normal
|
||||
* WMIC commands, it remains unknown if this may block under certain
|
||||
* circumstances. In particular, this method should only be executed before
|
||||
* the loop starts, not while it is running.
|
||||
*
|
||||
* Note that this method will only try to execute the given command try to
|
||||
* parse its output, irrespective of whether this command exists. In
|
||||
* particular, this command is only available on Windows. Currently, this
|
||||
* will only parse valid nameserver entries from the command output and will
|
||||
* ignore all other output without complaining.
|
||||
*
|
||||
* Note that the previous section implies that this may return an empty
|
||||
* `Config` object if no valid nameserver entries can be found.
|
||||
*
|
||||
* @param ?string $command (advanced) should not be given (NULL) unless you know what you're doing
|
||||
* @return self
|
||||
* @link https://ss64.com/nt/wmic.html
|
||||
*/
|
||||
public static function loadWmicBlocking($command = null)
|
||||
{
|
||||
$contents = shell_exec($command === null ? 'wmic NICCONFIG get "DNSServerSearchOrder" /format:CSV' : $command);
|
||||
preg_match_all('/(?<=[{;,"])([\da-f.:]{4,})(?=[};,"])/i', $contents, $matches);
|
||||
|
||||
$config = new self();
|
||||
$config->nameservers = $matches[1];
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
public $nameservers = array();
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Config;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Represents a static hosts file which maps hostnames to IPs
|
||||
*
|
||||
* Hosts files are used on most systems to avoid actually hitting the DNS for
|
||||
* certain common hostnames.
|
||||
*
|
||||
* Most notably, this file usually contains an entry to map "localhost" to the
|
||||
* local IP. Windows is a notable exception here, as Windows does not actually
|
||||
* include "localhost" in this file by default. To compensate for this, this
|
||||
* class may explicitly be wrapped in another HostsFile instance which
|
||||
* hard-codes these entries for Windows (see also Factory).
|
||||
*
|
||||
* This class mostly exists to abstract the parsing/extraction process so this
|
||||
* can be replaced with a faster alternative in the future.
|
||||
*/
|
||||
class HostsFile
|
||||
{
|
||||
/**
|
||||
* Returns the default path for the hosts file on this system
|
||||
*
|
||||
* @return string
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function getDefaultPath()
|
||||
{
|
||||
// use static path for all Unix-based systems
|
||||
if (DIRECTORY_SEPARATOR !== '\\') {
|
||||
return '/etc/hosts';
|
||||
}
|
||||
|
||||
// Windows actually stores the path in the registry under
|
||||
// \HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\DataBasePath
|
||||
$path = '%SystemRoot%\\system32\drivers\etc\hosts';
|
||||
|
||||
$base = getenv('SystemRoot');
|
||||
if ($base === false) {
|
||||
$base = 'C:\\Windows';
|
||||
}
|
||||
|
||||
return str_replace('%SystemRoot%', $base, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a hosts file (from the given path or default location)
|
||||
*
|
||||
* Note that this method blocks while loading the given path and should
|
||||
* thus be used with care! While this should be relatively fast for normal
|
||||
* hosts file, this may be an issue if this file is located on a slow device
|
||||
* or contains an excessive number of entries. In particular, this method
|
||||
* should only be executed before the loop starts, not while it is running.
|
||||
*
|
||||
* @param ?string $path (optional) path to hosts file or null=load default location
|
||||
* @return self
|
||||
* @throws RuntimeException if the path can not be loaded (does not exist)
|
||||
*/
|
||||
public static function loadFromPathBlocking($path = null)
|
||||
{
|
||||
if ($path === null) {
|
||||
$path = self::getDefaultPath();
|
||||
}
|
||||
|
||||
$contents = @file_get_contents($path);
|
||||
if ($contents === false) {
|
||||
throw new RuntimeException('Unable to load hosts file "' . $path . '"');
|
||||
}
|
||||
|
||||
return new self($contents);
|
||||
}
|
||||
|
||||
private $contents;
|
||||
|
||||
/**
|
||||
* Instantiate new hosts file with the given hosts file contents
|
||||
*
|
||||
* @param string $contents
|
||||
*/
|
||||
public function __construct($contents)
|
||||
{
|
||||
// remove all comments from the contents
|
||||
$contents = preg_replace('/[ \t]*#.*/', '', strtolower($contents));
|
||||
|
||||
$this->contents = $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all IPs for the given hostname
|
||||
*
|
||||
* @param string $name
|
||||
* @return string[]
|
||||
*/
|
||||
public function getIpsForHost($name)
|
||||
{
|
||||
$name = strtolower($name);
|
||||
|
||||
$ips = array();
|
||||
foreach (preg_split('/\r?\n/', $this->contents) as $line) {
|
||||
$parts = preg_split('/\s+/', $line);
|
||||
$ip = array_shift($parts);
|
||||
if ($parts && array_search($name, $parts) !== false) {
|
||||
// remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`)
|
||||
if (strpos($ip, ':') !== false && ($pos = strpos($ip, '%')) !== false) {
|
||||
$ip = substr($ip, 0, $pos);
|
||||
}
|
||||
|
||||
if (@inet_pton($ip) !== false) {
|
||||
$ips[] = $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ips;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all hostnames for the given IPv4 or IPv6 address
|
||||
*
|
||||
* @param string $ip
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHostsForIp($ip)
|
||||
{
|
||||
// check binary representation of IP to avoid string case and short notation
|
||||
$ip = @inet_pton($ip);
|
||||
if ($ip === false) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$names = array();
|
||||
foreach (preg_split('/\r?\n/', $this->contents) as $line) {
|
||||
$parts = preg_split('/\s+/', $line, -1, PREG_SPLIT_NO_EMPTY);
|
||||
$addr = (string) array_shift($parts);
|
||||
|
||||
// remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`)
|
||||
if (strpos($addr, ':') !== false && ($pos = strpos($addr, '%')) !== false) {
|
||||
$addr = substr($addr, 0, $pos);
|
||||
}
|
||||
|
||||
if (@inet_pton($addr) === $ip) {
|
||||
foreach ($parts as $part) {
|
||||
$names[] = $part;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
}
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Model;
|
||||
|
||||
use React\Dns\Query\Query;
|
||||
|
||||
/**
|
||||
* This class represents an outgoing query message or an incoming response message
|
||||
*
|
||||
* @link https://tools.ietf.org/html/rfc1035#section-4.1.1
|
||||
*/
|
||||
final class Message
|
||||
{
|
||||
const TYPE_A = 1;
|
||||
const TYPE_NS = 2;
|
||||
const TYPE_CNAME = 5;
|
||||
const TYPE_SOA = 6;
|
||||
const TYPE_PTR = 12;
|
||||
const TYPE_MX = 15;
|
||||
const TYPE_TXT = 16;
|
||||
const TYPE_AAAA = 28;
|
||||
const TYPE_SRV = 33;
|
||||
const TYPE_SSHFP = 44;
|
||||
|
||||
/**
|
||||
* pseudo-type for EDNS0
|
||||
*
|
||||
* These are included in the additional section and usually not in answer section.
|
||||
* Defined in [RFC 6891](https://tools.ietf.org/html/rfc6891) (or older
|
||||
* [RFC 2671](https://tools.ietf.org/html/rfc2671)).
|
||||
*
|
||||
* The OPT record uses the "class" field to store the maximum size.
|
||||
*
|
||||
* The OPT record uses the "ttl" field to store additional flags.
|
||||
*/
|
||||
const TYPE_OPT = 41;
|
||||
|
||||
/**
|
||||
* Sender Policy Framework (SPF) had a dedicated SPF type which has been
|
||||
* deprecated in favor of reusing the existing TXT type.
|
||||
*
|
||||
* @deprecated https://datatracker.ietf.org/doc/html/rfc7208#section-3.1
|
||||
* @see self::TYPE_TXT
|
||||
*/
|
||||
const TYPE_SPF = 99;
|
||||
|
||||
const TYPE_ANY = 255;
|
||||
const TYPE_CAA = 257;
|
||||
|
||||
const CLASS_IN = 1;
|
||||
|
||||
const OPCODE_QUERY = 0;
|
||||
const OPCODE_IQUERY = 1; // inverse query
|
||||
const OPCODE_STATUS = 2;
|
||||
|
||||
const RCODE_OK = 0;
|
||||
const RCODE_FORMAT_ERROR = 1;
|
||||
const RCODE_SERVER_FAILURE = 2;
|
||||
const RCODE_NAME_ERROR = 3;
|
||||
const RCODE_NOT_IMPLEMENTED = 4;
|
||||
const RCODE_REFUSED = 5;
|
||||
|
||||
/**
|
||||
* The edns-tcp-keepalive EDNS0 Option
|
||||
*
|
||||
* Option value contains a `?float` with timeout in seconds (in 0.1s steps)
|
||||
* for DNS response or `null` for DNS query.
|
||||
*
|
||||
* @link https://tools.ietf.org/html/rfc7828
|
||||
*/
|
||||
const OPT_TCP_KEEPALIVE = 11;
|
||||
|
||||
/**
|
||||
* The EDNS(0) Padding Option
|
||||
*
|
||||
* Option value contains a `string` with binary data (usually variable
|
||||
* number of null bytes)
|
||||
*
|
||||
* @link https://tools.ietf.org/html/rfc7830
|
||||
*/
|
||||
const OPT_PADDING = 12;
|
||||
|
||||
/**
|
||||
* Creates a new request message for the given query
|
||||
*
|
||||
* @param Query $query
|
||||
* @return self
|
||||
*/
|
||||
public static function createRequestForQuery(Query $query)
|
||||
{
|
||||
$request = new Message();
|
||||
$request->id = self::generateId();
|
||||
$request->rd = true;
|
||||
$request->questions[] = $query;
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new response message for the given query with the given answer records
|
||||
*
|
||||
* @param Query $query
|
||||
* @param Record[] $answers
|
||||
* @return self
|
||||
*/
|
||||
public static function createResponseWithAnswersForQuery(Query $query, array $answers)
|
||||
{
|
||||
$response = new Message();
|
||||
$response->id = self::generateId();
|
||||
$response->qr = true;
|
||||
$response->rd = true;
|
||||
|
||||
$response->questions[] = $query;
|
||||
|
||||
foreach ($answers as $record) {
|
||||
$response->answers[] = $record;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* generates a random 16 bit message ID
|
||||
*
|
||||
* This uses a CSPRNG so that an outside attacker that is sending spoofed
|
||||
* DNS response messages can not guess the message ID to avoid possible
|
||||
* cache poisoning attacks.
|
||||
*
|
||||
* The `random_int()` function is only available on PHP 7+ or when
|
||||
* https://github.com/paragonie/random_compat is installed. As such, using
|
||||
* the latest supported PHP version is highly recommended. This currently
|
||||
* falls back to a less secure random number generator on older PHP versions
|
||||
* in the hope that this system is properly protected against outside
|
||||
* attackers, for example by using one of the common local DNS proxy stubs.
|
||||
*
|
||||
* @return int
|
||||
* @see self::getId()
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private static function generateId()
|
||||
{
|
||||
if (function_exists('random_int')) {
|
||||
return random_int(0, 0xffff);
|
||||
}
|
||||
return mt_rand(0, 0xffff);
|
||||
}
|
||||
|
||||
/**
|
||||
* The 16 bit message ID
|
||||
*
|
||||
* The response message ID has to match the request message ID. This allows
|
||||
* the receiver to verify this is the correct response message. An outside
|
||||
* attacker may try to inject fake responses by "guessing" the message ID,
|
||||
* so this should use a proper CSPRNG to avoid possible cache poisoning.
|
||||
*
|
||||
* @var int 16 bit message ID
|
||||
* @see self::generateId()
|
||||
*/
|
||||
public $id = 0;
|
||||
|
||||
/**
|
||||
* @var bool Query/Response flag, query=false or response=true
|
||||
*/
|
||||
public $qr = false;
|
||||
|
||||
/**
|
||||
* @var int specifies the kind of query (4 bit), see self::OPCODE_* constants
|
||||
* @see self::OPCODE_QUERY
|
||||
*/
|
||||
public $opcode = self::OPCODE_QUERY;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var bool Authoritative Answer
|
||||
*/
|
||||
public $aa = false;
|
||||
|
||||
/**
|
||||
* @var bool TrunCation
|
||||
*/
|
||||
public $tc = false;
|
||||
|
||||
/**
|
||||
* @var bool Recursion Desired
|
||||
*/
|
||||
public $rd = false;
|
||||
|
||||
/**
|
||||
* @var bool Recursion Available
|
||||
*/
|
||||
public $ra = false;
|
||||
|
||||
/**
|
||||
* @var int response code (4 bit), see self::RCODE_* constants
|
||||
* @see self::RCODE_OK
|
||||
*/
|
||||
public $rcode = Message::RCODE_OK;
|
||||
|
||||
/**
|
||||
* An array of Query objects
|
||||
*
|
||||
* ```php
|
||||
* $questions = array(
|
||||
* new Query(
|
||||
* 'reactphp.org',
|
||||
* Message::TYPE_A,
|
||||
* Message::CLASS_IN
|
||||
* )
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @var Query[]
|
||||
*/
|
||||
public $questions = array();
|
||||
|
||||
/**
|
||||
* @var Record[]
|
||||
*/
|
||||
public $answers = array();
|
||||
|
||||
/**
|
||||
* @var Record[]
|
||||
*/
|
||||
public $authority = array();
|
||||
|
||||
/**
|
||||
* @var Record[]
|
||||
*/
|
||||
public $additional = array();
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Model;
|
||||
|
||||
/**
|
||||
* This class represents a single resulting record in a response message
|
||||
*
|
||||
* It uses a structure similar to `\React\Dns\Query\Query`, but does include
|
||||
* fields for resulting TTL and resulting record data (IPs etc.).
|
||||
*
|
||||
* @link https://tools.ietf.org/html/rfc1035#section-4.1.3
|
||||
* @see \React\Dns\Query\Query
|
||||
*/
|
||||
final class Record
|
||||
{
|
||||
/**
|
||||
* @var string hostname without trailing dot, for example "reactphp.org"
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var int see Message::TYPE_* constants (UINT16)
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* Defines the network class, usually `Message::CLASS_IN`.
|
||||
*
|
||||
* For `OPT` records (EDNS0), this defines the maximum message size instead.
|
||||
*
|
||||
* @var int see Message::CLASS_IN constant (UINT16)
|
||||
* @see Message::CLASS_IN
|
||||
*/
|
||||
public $class;
|
||||
|
||||
/**
|
||||
* Defines the maximum time-to-live (TTL) in seconds
|
||||
*
|
||||
* For `OPT` records (EDNS0), this defines additional flags instead.
|
||||
*
|
||||
* @var int maximum TTL in seconds (UINT32, most significant bit always unset)
|
||||
* @link https://tools.ietf.org/html/rfc2181#section-8
|
||||
* @link https://tools.ietf.org/html/rfc6891#section-6.1.3 for `OPT` records (EDNS0)
|
||||
*/
|
||||
public $ttl;
|
||||
|
||||
/**
|
||||
* The payload data for this record
|
||||
*
|
||||
* The payload data format depends on the record type. As a rule of thumb,
|
||||
* this library will try to express this in a way that can be consumed
|
||||
* easily without having to worry about DNS internals and its binary transport:
|
||||
*
|
||||
* - A:
|
||||
* IPv4 address string, for example "192.168.1.1".
|
||||
*
|
||||
* - AAAA:
|
||||
* IPv6 address string, for example "::1".
|
||||
*
|
||||
* - CNAME / PTR / NS:
|
||||
* The hostname without trailing dot, for example "reactphp.org".
|
||||
*
|
||||
* - TXT:
|
||||
* List of string values, for example `["v=spf1 include:example.com"]`.
|
||||
* This is commonly a list with only a single string value, but this
|
||||
* technically allows multiple strings (0-255 bytes each) in a single
|
||||
* record. This is rarely used and depending on application you may want
|
||||
* to join these together or handle them separately. Each string can
|
||||
* transport any binary data, its character encoding is not defined (often
|
||||
* ASCII/UTF-8 in practice). [RFC 1464](https://tools.ietf.org/html/rfc1464)
|
||||
* suggests using key-value pairs such as `["name=test","version=1"]`, but
|
||||
* interpretation of this is not enforced and left up to consumers of this
|
||||
* library (used for DNS-SD/Zeroconf and others).
|
||||
*
|
||||
* - MX:
|
||||
* Mail server priority (UINT16) and target hostname without trailing dot,
|
||||
* for example `{"priority":10,"target":"mx.example.com"}`.
|
||||
* The payload data uses an associative array with fixed keys "priority"
|
||||
* (also commonly referred to as weight or preference) and "target" (also
|
||||
* referred to as exchange). If a response message contains multiple
|
||||
* records of this type, targets should be sorted by priority (lowest
|
||||
* first) - this is left up to consumers of this library (used for SMTP).
|
||||
*
|
||||
* - SRV:
|
||||
* Service priority (UINT16), service weight (UINT16), service port (UINT16)
|
||||
* and target hostname without trailing dot, for example
|
||||
* `{"priority":10,"weight":50,"port":8080,"target":"example.com"}`.
|
||||
* The payload data uses an associative array with fixed keys "priority",
|
||||
* "weight", "port" and "target" (also referred to as name).
|
||||
* The target may be an empty host name string if the service is decidedly
|
||||
* not available. If a response message contains multiple records of this
|
||||
* type, targets should be sorted by priority (lowest first) and selected
|
||||
* randomly according to their weight - this is left up to consumers of
|
||||
* this library, see also [RFC 2782](https://tools.ietf.org/html/rfc2782)
|
||||
* for more details.
|
||||
*
|
||||
* - SSHFP:
|
||||
* Includes algorithm (UNIT8), fingerprint type (UNIT8) and fingerprint
|
||||
* value as lower case hex string, for example:
|
||||
* `{"algorithm":1,"type":1,"fingerprint":"0123456789abcdef..."}`
|
||||
* See also https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml
|
||||
* for algorithm and fingerprint type assignments.
|
||||
*
|
||||
* - SOA:
|
||||
* Includes master hostname without trailing dot, responsible person email
|
||||
* as hostname without trailing dot and serial, refresh, retry, expire and
|
||||
* minimum times in seconds (UINT32 each), for example:
|
||||
* `{"mname":"ns.example.com","rname":"hostmaster.example.com","serial":
|
||||
* 2018082601,"refresh":3600,"retry":1800,"expire":60000,"minimum":3600}`.
|
||||
*
|
||||
* - CAA:
|
||||
* Includes flag (UNIT8), tag string and value string, for example:
|
||||
* `{"flag":128,"tag":"issue","value":"letsencrypt.org"}`
|
||||
*
|
||||
* - OPT:
|
||||
* Special pseudo-type for EDNS0. Includes an array of additional opt codes
|
||||
* with a value according to the respective OPT code. See `Message::OPT_*`
|
||||
* for list of supported OPT codes. Any other OPT code not currently
|
||||
* supported will be an opaque binary string containing the raw data
|
||||
* as transported in the DNS record. For forwards compatibility, you should
|
||||
* not rely on this format for unknown types. Future versions may add
|
||||
* support for new types and this may then parse the payload data
|
||||
* appropriately - this will not be considered a BC break. See also
|
||||
* [RFC 6891](https://tools.ietf.org/html/rfc6891) for more details.
|
||||
*
|
||||
* - Any other unknown type:
|
||||
* An opaque binary string containing the RDATA as transported in the DNS
|
||||
* record. For forwards compatibility, you should not rely on this format
|
||||
* for unknown types. Future versions may add support for new types and
|
||||
* this may then parse the payload data appropriately - this will not be
|
||||
* considered a BC break. See the format definition of known types above
|
||||
* for more details.
|
||||
*
|
||||
* @var string|string[]|array
|
||||
*/
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param int $type
|
||||
* @param int $class
|
||||
* @param int $ttl
|
||||
* @param string|string[]|array $data
|
||||
*/
|
||||
public function __construct($name, $type, $class, $ttl, $data)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->type = $type;
|
||||
$this->class = $class;
|
||||
$this->ttl = $ttl;
|
||||
$this->data = $data;
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Protocol;
|
||||
|
||||
use React\Dns\Model\Message;
|
||||
use React\Dns\Model\Record;
|
||||
use React\Dns\Query\Query;
|
||||
|
||||
final class BinaryDumper
|
||||
{
|
||||
/**
|
||||
* @param Message $message
|
||||
* @return string
|
||||
*/
|
||||
public function toBinary(Message $message)
|
||||
{
|
||||
$data = '';
|
||||
|
||||
$data .= $this->headerToBinary($message);
|
||||
$data .= $this->questionToBinary($message->questions);
|
||||
$data .= $this->recordsToBinary($message->answers);
|
||||
$data .= $this->recordsToBinary($message->authority);
|
||||
$data .= $this->recordsToBinary($message->additional);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Message $message
|
||||
* @return string
|
||||
*/
|
||||
private function headerToBinary(Message $message)
|
||||
{
|
||||
$data = '';
|
||||
|
||||
$data .= pack('n', $message->id);
|
||||
|
||||
$flags = 0x00;
|
||||
$flags = ($flags << 1) | ($message->qr ? 1 : 0);
|
||||
$flags = ($flags << 4) | $message->opcode;
|
||||
$flags = ($flags << 1) | ($message->aa ? 1 : 0);
|
||||
$flags = ($flags << 1) | ($message->tc ? 1 : 0);
|
||||
$flags = ($flags << 1) | ($message->rd ? 1 : 0);
|
||||
$flags = ($flags << 1) | ($message->ra ? 1 : 0);
|
||||
$flags = ($flags << 3) | 0; // skip unused zero bit
|
||||
$flags = ($flags << 4) | $message->rcode;
|
||||
|
||||
$data .= pack('n', $flags);
|
||||
|
||||
$data .= pack('n', count($message->questions));
|
||||
$data .= pack('n', count($message->answers));
|
||||
$data .= pack('n', count($message->authority));
|
||||
$data .= pack('n', count($message->additional));
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Query[] $questions
|
||||
* @return string
|
||||
*/
|
||||
private function questionToBinary(array $questions)
|
||||
{
|
||||
$data = '';
|
||||
|
||||
foreach ($questions as $question) {
|
||||
$data .= $this->domainNameToBinary($question->name);
|
||||
$data .= pack('n*', $question->type, $question->class);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Record[] $records
|
||||
* @return string
|
||||
*/
|
||||
private function recordsToBinary(array $records)
|
||||
{
|
||||
$data = '';
|
||||
|
||||
foreach ($records as $record) {
|
||||
/* @var $record Record */
|
||||
switch ($record->type) {
|
||||
case Message::TYPE_A:
|
||||
case Message::TYPE_AAAA:
|
||||
$binary = \inet_pton($record->data);
|
||||
break;
|
||||
case Message::TYPE_CNAME:
|
||||
case Message::TYPE_NS:
|
||||
case Message::TYPE_PTR:
|
||||
$binary = $this->domainNameToBinary($record->data);
|
||||
break;
|
||||
case Message::TYPE_TXT:
|
||||
case Message::TYPE_SPF:
|
||||
$binary = $this->textsToBinary($record->data);
|
||||
break;
|
||||
case Message::TYPE_MX:
|
||||
$binary = \pack(
|
||||
'n',
|
||||
$record->data['priority']
|
||||
);
|
||||
$binary .= $this->domainNameToBinary($record->data['target']);
|
||||
break;
|
||||
case Message::TYPE_SRV:
|
||||
$binary = \pack(
|
||||
'n*',
|
||||
$record->data['priority'],
|
||||
$record->data['weight'],
|
||||
$record->data['port']
|
||||
);
|
||||
$binary .= $this->domainNameToBinary($record->data['target']);
|
||||
break;
|
||||
case Message::TYPE_SOA:
|
||||
$binary = $this->domainNameToBinary($record->data['mname']);
|
||||
$binary .= $this->domainNameToBinary($record->data['rname']);
|
||||
$binary .= \pack(
|
||||
'N*',
|
||||
$record->data['serial'],
|
||||
$record->data['refresh'],
|
||||
$record->data['retry'],
|
||||
$record->data['expire'],
|
||||
$record->data['minimum']
|
||||
);
|
||||
break;
|
||||
case Message::TYPE_CAA:
|
||||
$binary = \pack(
|
||||
'C*',
|
||||
$record->data['flag'],
|
||||
\strlen($record->data['tag'])
|
||||
);
|
||||
$binary .= $record->data['tag'];
|
||||
$binary .= $record->data['value'];
|
||||
break;
|
||||
case Message::TYPE_SSHFP:
|
||||
$binary = \pack(
|
||||
'CCH*',
|
||||
$record->data['algorithm'],
|
||||
$record->data['type'],
|
||||
$record->data['fingerprint']
|
||||
);
|
||||
break;
|
||||
case Message::TYPE_OPT:
|
||||
$binary = '';
|
||||
foreach ($record->data as $opt => $value) {
|
||||
if ($opt === Message::OPT_TCP_KEEPALIVE && $value !== null) {
|
||||
$value = \pack('n', round($value * 10));
|
||||
}
|
||||
$binary .= \pack('n*', $opt, \strlen((string) $value)) . $value;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// RDATA is already stored as binary value for unknown record types
|
||||
$binary = $record->data;
|
||||
}
|
||||
|
||||
$data .= $this->domainNameToBinary($record->name);
|
||||
$data .= \pack('nnNn', $record->type, $record->class, $record->ttl, \strlen($binary));
|
||||
$data .= $binary;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $texts
|
||||
* @return string
|
||||
*/
|
||||
private function textsToBinary(array $texts)
|
||||
{
|
||||
$data = '';
|
||||
foreach ($texts as $text) {
|
||||
$data .= \chr(\strlen($text)) . $text;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $host
|
||||
* @return string
|
||||
*/
|
||||
private function domainNameToBinary($host)
|
||||
{
|
||||
if ($host === '') {
|
||||
return "\0";
|
||||
}
|
||||
|
||||
// break up domain name at each dot that is not preceeded by a backslash (escaped notation)
|
||||
return $this->textsToBinary(
|
||||
\array_map(
|
||||
'stripcslashes',
|
||||
\preg_split(
|
||||
'/(?<!\\\\)\./',
|
||||
$host . '.'
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+356
@@ -0,0 +1,356 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Protocol;
|
||||
|
||||
use React\Dns\Model\Message;
|
||||
use React\Dns\Model\Record;
|
||||
use React\Dns\Query\Query;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* DNS protocol parser
|
||||
*
|
||||
* Obsolete and uncommon types and classes are not implemented.
|
||||
*/
|
||||
final class Parser
|
||||
{
|
||||
/**
|
||||
* Parses the given raw binary message into a Message object
|
||||
*
|
||||
* @param string $data
|
||||
* @throws InvalidArgumentException
|
||||
* @return Message
|
||||
*/
|
||||
public function parseMessage($data)
|
||||
{
|
||||
$message = $this->parse($data, 0);
|
||||
if ($message === null) {
|
||||
throw new InvalidArgumentException('Unable to parse binary message');
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $data
|
||||
* @param int $consumed
|
||||
* @return ?Message
|
||||
*/
|
||||
private function parse($data, $consumed)
|
||||
{
|
||||
if (!isset($data[12 - 1])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', substr($data, 0, 12)));
|
||||
|
||||
$message = new Message();
|
||||
$message->id = $id;
|
||||
$message->rcode = $fields & 0xf;
|
||||
$message->ra = (($fields >> 7) & 1) === 1;
|
||||
$message->rd = (($fields >> 8) & 1) === 1;
|
||||
$message->tc = (($fields >> 9) & 1) === 1;
|
||||
$message->aa = (($fields >> 10) & 1) === 1;
|
||||
$message->opcode = ($fields >> 11) & 0xf;
|
||||
$message->qr = (($fields >> 15) & 1) === 1;
|
||||
$consumed += 12;
|
||||
|
||||
// parse all questions
|
||||
for ($i = $qdCount; $i > 0; --$i) {
|
||||
list($question, $consumed) = $this->parseQuestion($data, $consumed);
|
||||
if ($question === null) {
|
||||
return null;
|
||||
} else {
|
||||
$message->questions[] = $question;
|
||||
}
|
||||
}
|
||||
|
||||
// parse all answer records
|
||||
for ($i = $anCount; $i > 0; --$i) {
|
||||
list($record, $consumed) = $this->parseRecord($data, $consumed);
|
||||
if ($record === null) {
|
||||
return null;
|
||||
} else {
|
||||
$message->answers[] = $record;
|
||||
}
|
||||
}
|
||||
|
||||
// parse all authority records
|
||||
for ($i = $nsCount; $i > 0; --$i) {
|
||||
list($record, $consumed) = $this->parseRecord($data, $consumed);
|
||||
if ($record === null) {
|
||||
return null;
|
||||
} else {
|
||||
$message->authority[] = $record;
|
||||
}
|
||||
}
|
||||
|
||||
// parse all additional records
|
||||
for ($i = $arCount; $i > 0; --$i) {
|
||||
list($record, $consumed) = $this->parseRecord($data, $consumed);
|
||||
if ($record === null) {
|
||||
return null;
|
||||
} else {
|
||||
$message->additional[] = $record;
|
||||
}
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $data
|
||||
* @param int $consumed
|
||||
* @return array
|
||||
*/
|
||||
private function parseQuestion($data, $consumed)
|
||||
{
|
||||
list($labels, $consumed) = $this->readLabels($data, $consumed);
|
||||
|
||||
if ($labels === null || !isset($data[$consumed + 4 - 1])) {
|
||||
return array(null, null);
|
||||
}
|
||||
|
||||
list($type, $class) = array_values(unpack('n*', substr($data, $consumed, 4)));
|
||||
$consumed += 4;
|
||||
|
||||
return array(
|
||||
new Query(
|
||||
implode('.', $labels),
|
||||
$type,
|
||||
$class
|
||||
),
|
||||
$consumed
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $data
|
||||
* @param int $consumed
|
||||
* @return array An array with a parsed Record on success or array with null if data is invalid/incomplete
|
||||
*/
|
||||
private function parseRecord($data, $consumed)
|
||||
{
|
||||
list($name, $consumed) = $this->readDomain($data, $consumed);
|
||||
|
||||
if ($name === null || !isset($data[$consumed + 10 - 1])) {
|
||||
return array(null, null);
|
||||
}
|
||||
|
||||
list($type, $class) = array_values(unpack('n*', substr($data, $consumed, 4)));
|
||||
$consumed += 4;
|
||||
|
||||
list($ttl) = array_values(unpack('N', substr($data, $consumed, 4)));
|
||||
$consumed += 4;
|
||||
|
||||
// TTL is a UINT32 that must not have most significant bit set for BC reasons
|
||||
if ($ttl < 0 || $ttl >= 1 << 31) {
|
||||
$ttl = 0;
|
||||
}
|
||||
|
||||
list($rdLength) = array_values(unpack('n', substr($data, $consumed, 2)));
|
||||
$consumed += 2;
|
||||
|
||||
if (!isset($data[$consumed + $rdLength - 1])) {
|
||||
return array(null, null);
|
||||
}
|
||||
|
||||
$rdata = null;
|
||||
$expected = $consumed + $rdLength;
|
||||
|
||||
if (Message::TYPE_A === $type) {
|
||||
if ($rdLength === 4) {
|
||||
$rdata = inet_ntop(substr($data, $consumed, $rdLength));
|
||||
$consumed += $rdLength;
|
||||
}
|
||||
} elseif (Message::TYPE_AAAA === $type) {
|
||||
if ($rdLength === 16) {
|
||||
$rdata = inet_ntop(substr($data, $consumed, $rdLength));
|
||||
$consumed += $rdLength;
|
||||
}
|
||||
} elseif (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type || Message::TYPE_NS === $type) {
|
||||
list($rdata, $consumed) = $this->readDomain($data, $consumed);
|
||||
} elseif (Message::TYPE_TXT === $type || Message::TYPE_SPF === $type) {
|
||||
$rdata = array();
|
||||
while ($consumed < $expected) {
|
||||
$len = ord($data[$consumed]);
|
||||
$rdata[] = (string)substr($data, $consumed + 1, $len);
|
||||
$consumed += $len + 1;
|
||||
}
|
||||
} elseif (Message::TYPE_MX === $type) {
|
||||
if ($rdLength > 2) {
|
||||
list($priority) = array_values(unpack('n', substr($data, $consumed, 2)));
|
||||
list($target, $consumed) = $this->readDomain($data, $consumed + 2);
|
||||
|
||||
$rdata = array(
|
||||
'priority' => $priority,
|
||||
'target' => $target
|
||||
);
|
||||
}
|
||||
} elseif (Message::TYPE_SRV === $type) {
|
||||
if ($rdLength > 6) {
|
||||
list($priority, $weight, $port) = array_values(unpack('n*', substr($data, $consumed, 6)));
|
||||
list($target, $consumed) = $this->readDomain($data, $consumed + 6);
|
||||
|
||||
$rdata = array(
|
||||
'priority' => $priority,
|
||||
'weight' => $weight,
|
||||
'port' => $port,
|
||||
'target' => $target
|
||||
);
|
||||
}
|
||||
} elseif (Message::TYPE_SSHFP === $type) {
|
||||
if ($rdLength > 2) {
|
||||
list($algorithm, $hash) = \array_values(\unpack('C*', \substr($data, $consumed, 2)));
|
||||
$fingerprint = \bin2hex(\substr($data, $consumed + 2, $rdLength - 2));
|
||||
$consumed += $rdLength;
|
||||
|
||||
$rdata = array(
|
||||
'algorithm' => $algorithm,
|
||||
'type' => $hash,
|
||||
'fingerprint' => $fingerprint
|
||||
);
|
||||
}
|
||||
} elseif (Message::TYPE_SOA === $type) {
|
||||
list($mname, $consumed) = $this->readDomain($data, $consumed);
|
||||
list($rname, $consumed) = $this->readDomain($data, $consumed);
|
||||
|
||||
if ($mname !== null && $rname !== null && isset($data[$consumed + 20 - 1])) {
|
||||
list($serial, $refresh, $retry, $expire, $minimum) = array_values(unpack('N*', substr($data, $consumed, 20)));
|
||||
$consumed += 20;
|
||||
|
||||
$rdata = array(
|
||||
'mname' => $mname,
|
||||
'rname' => $rname,
|
||||
'serial' => $serial,
|
||||
'refresh' => $refresh,
|
||||
'retry' => $retry,
|
||||
'expire' => $expire,
|
||||
'minimum' => $minimum
|
||||
);
|
||||
}
|
||||
} elseif (Message::TYPE_OPT === $type) {
|
||||
$rdata = array();
|
||||
while (isset($data[$consumed + 4 - 1])) {
|
||||
list($code, $length) = array_values(unpack('n*', substr($data, $consumed, 4)));
|
||||
$value = (string) substr($data, $consumed + 4, $length);
|
||||
if ($code === Message::OPT_TCP_KEEPALIVE && $value === '') {
|
||||
$value = null;
|
||||
} elseif ($code === Message::OPT_TCP_KEEPALIVE && $length === 2) {
|
||||
list($value) = array_values(unpack('n', $value));
|
||||
$value = round($value * 0.1, 1);
|
||||
} elseif ($code === Message::OPT_TCP_KEEPALIVE) {
|
||||
break;
|
||||
}
|
||||
$rdata[$code] = $value;
|
||||
$consumed += 4 + $length;
|
||||
}
|
||||
} elseif (Message::TYPE_CAA === $type) {
|
||||
if ($rdLength > 3) {
|
||||
list($flag, $tagLength) = array_values(unpack('C*', substr($data, $consumed, 2)));
|
||||
|
||||
if ($tagLength > 0 && $rdLength - 2 - $tagLength > 0) {
|
||||
$tag = substr($data, $consumed + 2, $tagLength);
|
||||
$value = substr($data, $consumed + 2 + $tagLength, $rdLength - 2 - $tagLength);
|
||||
$consumed += $rdLength;
|
||||
|
||||
$rdata = array(
|
||||
'flag' => $flag,
|
||||
'tag' => $tag,
|
||||
'value' => $value
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// unknown types simply parse rdata as an opaque binary string
|
||||
$rdata = substr($data, $consumed, $rdLength);
|
||||
$consumed += $rdLength;
|
||||
}
|
||||
|
||||
// ensure parsing record data consumes expact number of bytes indicated in record length
|
||||
if ($consumed !== $expected || $rdata === null) {
|
||||
return array(null, null);
|
||||
}
|
||||
|
||||
return array(
|
||||
new Record($name, $type, $class, $ttl, $rdata),
|
||||
$consumed
|
||||
);
|
||||
}
|
||||
|
||||
private function readDomain($data, $consumed)
|
||||
{
|
||||
list ($labels, $consumed) = $this->readLabels($data, $consumed);
|
||||
|
||||
if ($labels === null) {
|
||||
return array(null, null);
|
||||
}
|
||||
|
||||
// use escaped notation for each label part, then join using dots
|
||||
return array(
|
||||
\implode(
|
||||
'.',
|
||||
\array_map(
|
||||
function ($label) {
|
||||
return \addcslashes($label, "\0..\40.\177");
|
||||
},
|
||||
$labels
|
||||
)
|
||||
),
|
||||
$consumed
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $data
|
||||
* @param int $consumed
|
||||
* @param int $compressionDepth maximum depth for compressed labels to avoid unreasonable recursion
|
||||
* @return array
|
||||
*/
|
||||
private function readLabels($data, $consumed, $compressionDepth = 127)
|
||||
{
|
||||
$labels = array();
|
||||
|
||||
while (true) {
|
||||
if (!isset($data[$consumed])) {
|
||||
return array(null, null);
|
||||
}
|
||||
|
||||
$length = \ord($data[$consumed]);
|
||||
|
||||
// end of labels reached
|
||||
if ($length === 0) {
|
||||
$consumed += 1;
|
||||
break;
|
||||
}
|
||||
|
||||
// first two bits set? this is a compressed label (14 bit pointer offset)
|
||||
if (($length & 0xc0) === 0xc0 && isset($data[$consumed + 1]) && $compressionDepth) {
|
||||
$offset = ($length & ~0xc0) << 8 | \ord($data[$consumed + 1]);
|
||||
if ($offset >= $consumed) {
|
||||
return array(null, null);
|
||||
}
|
||||
|
||||
$consumed += 2;
|
||||
list($newLabels) = $this->readLabels($data, $offset, $compressionDepth - 1);
|
||||
|
||||
if ($newLabels === null) {
|
||||
return array(null, null);
|
||||
}
|
||||
|
||||
$labels = array_merge($labels, $newLabels);
|
||||
break;
|
||||
}
|
||||
|
||||
// length MUST be 0-63 (6 bits only) and data has to be large enough
|
||||
if ($length & 0xc0 || !isset($data[$consumed + $length - 1])) {
|
||||
return array(null, null);
|
||||
}
|
||||
|
||||
$labels[] = substr($data, $consumed + 1, $length);
|
||||
$consumed += $length + 1;
|
||||
}
|
||||
|
||||
return array($labels, $consumed);
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Query;
|
||||
|
||||
use React\Cache\CacheInterface;
|
||||
use React\Dns\Model\Message;
|
||||
use React\Promise\Promise;
|
||||
|
||||
final class CachingExecutor implements ExecutorInterface
|
||||
{
|
||||
/**
|
||||
* Default TTL for negative responses (NXDOMAIN etc.).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
const TTL = 60;
|
||||
|
||||
private $executor;
|
||||
private $cache;
|
||||
|
||||
public function __construct(ExecutorInterface $executor, CacheInterface $cache)
|
||||
{
|
||||
$this->executor = $executor;
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
public function query(Query $query)
|
||||
{
|
||||
$id = $query->name . ':' . $query->type . ':' . $query->class;
|
||||
$cache = $this->cache;
|
||||
$that = $this;
|
||||
$executor = $this->executor;
|
||||
|
||||
$pending = $cache->get($id);
|
||||
return new Promise(function ($resolve, $reject) use ($query, $id, $cache, $executor, &$pending, $that) {
|
||||
$pending->then(
|
||||
function ($message) use ($query, $id, $cache, $executor, &$pending, $that) {
|
||||
// return cached response message on cache hit
|
||||
if ($message !== null) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
// perform DNS lookup if not already cached
|
||||
return $pending = $executor->query($query)->then(
|
||||
function (Message $message) use ($cache, $id, $that) {
|
||||
// DNS response message received => store in cache when not truncated and return
|
||||
if (!$message->tc) {
|
||||
$cache->set($id, $message, $that->ttl($message));
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
);
|
||||
}
|
||||
)->then($resolve, function ($e) use ($reject, &$pending) {
|
||||
$reject($e);
|
||||
$pending = null;
|
||||
});
|
||||
}, function ($_, $reject) use (&$pending, $query) {
|
||||
$reject(new \RuntimeException('DNS query for ' . $query->describe() . ' has been cancelled'));
|
||||
$pending->cancel();
|
||||
$pending = null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Message $message
|
||||
* @return int
|
||||
* @internal
|
||||
*/
|
||||
public function ttl(Message $message)
|
||||
{
|
||||
// select TTL from answers (should all be the same), use smallest value if available
|
||||
// @link https://tools.ietf.org/html/rfc2181#section-5.2
|
||||
$ttl = null;
|
||||
foreach ($message->answers as $answer) {
|
||||
if ($ttl === null || $answer->ttl < $ttl) {
|
||||
$ttl = $answer->ttl;
|
||||
}
|
||||
}
|
||||
|
||||
if ($ttl === null) {
|
||||
$ttl = self::TTL;
|
||||
}
|
||||
|
||||
return $ttl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Query;
|
||||
|
||||
final class CancellationException extends \RuntimeException
|
||||
{
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Query;
|
||||
|
||||
use React\Promise\Promise;
|
||||
|
||||
/**
|
||||
* Cooperatively resolves hosts via the given base executor to ensure same query is not run concurrently
|
||||
*
|
||||
* Wraps an existing `ExecutorInterface` to keep tracking of pending queries
|
||||
* and only starts a new query when the same query is not already pending. Once
|
||||
* the underlying query is fulfilled/rejected, it will forward its value to all
|
||||
* promises awaiting the same query.
|
||||
*
|
||||
* This means it will not limit concurrency for queries that differ, for example
|
||||
* when sending many queries for different host names or types.
|
||||
*
|
||||
* This is useful because all executors are entirely async and as such allow you
|
||||
* to execute any number of queries concurrently. You should probably limit the
|
||||
* number of concurrent queries in your application or you're very likely going
|
||||
* to face rate limitations and bans on the resolver end. For many common
|
||||
* applications, you may want to avoid sending the same query multiple times
|
||||
* when the first one is still pending, so you will likely want to use this in
|
||||
* combination with some other executor like this:
|
||||
*
|
||||
* ```php
|
||||
* $executor = new CoopExecutor(
|
||||
* new RetryExecutor(
|
||||
* new TimeoutExecutor(
|
||||
* new UdpTransportExecutor($nameserver),
|
||||
* 3.0
|
||||
* )
|
||||
* )
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
final class CoopExecutor implements ExecutorInterface
|
||||
{
|
||||
private $executor;
|
||||
private $pending = array();
|
||||
private $counts = array();
|
||||
|
||||
public function __construct(ExecutorInterface $base)
|
||||
{
|
||||
$this->executor = $base;
|
||||
}
|
||||
|
||||
public function query(Query $query)
|
||||
{
|
||||
$key = $this->serializeQueryToIdentity($query);
|
||||
if (isset($this->pending[$key])) {
|
||||
// same query is already pending, so use shared reference to pending query
|
||||
$promise = $this->pending[$key];
|
||||
++$this->counts[$key];
|
||||
} else {
|
||||
// no such query pending, so start new query and keep reference until it's fulfilled or rejected
|
||||
$promise = $this->executor->query($query);
|
||||
$this->pending[$key] = $promise;
|
||||
$this->counts[$key] = 1;
|
||||
|
||||
$pending =& $this->pending;
|
||||
$counts =& $this->counts;
|
||||
$promise->then(function () use ($key, &$pending, &$counts) {
|
||||
unset($pending[$key], $counts[$key]);
|
||||
}, function () use ($key, &$pending, &$counts) {
|
||||
unset($pending[$key], $counts[$key]);
|
||||
});
|
||||
}
|
||||
|
||||
// Return a child promise awaiting the pending query.
|
||||
// Cancelling this child promise should only cancel the pending query
|
||||
// when no other child promise is awaiting the same query.
|
||||
$pending =& $this->pending;
|
||||
$counts =& $this->counts;
|
||||
return new Promise(function ($resolve, $reject) use ($promise) {
|
||||
$promise->then($resolve, $reject);
|
||||
}, function () use (&$promise, $key, $query, &$pending, &$counts) {
|
||||
if (--$counts[$key] < 1) {
|
||||
unset($pending[$key], $counts[$key]);
|
||||
$promise->cancel();
|
||||
$promise = null;
|
||||
}
|
||||
throw new \RuntimeException('DNS query for ' . $query->describe() . ' has been cancelled');
|
||||
});
|
||||
}
|
||||
|
||||
private function serializeQueryToIdentity(Query $query)
|
||||
{
|
||||
return sprintf('%s:%s:%s', $query->name, $query->type, $query->class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Query;
|
||||
|
||||
interface ExecutorInterface
|
||||
{
|
||||
/**
|
||||
* Executes a query and will return a response message
|
||||
*
|
||||
* It returns a Promise which either fulfills with a response
|
||||
* `React\Dns\Model\Message` on success or rejects with an `Exception` if
|
||||
* the query is not successful. A response message may indicate an error
|
||||
* condition in its `rcode`, but this is considered a valid response message.
|
||||
*
|
||||
* ```php
|
||||
* $executor->query($query)->then(
|
||||
* function (React\Dns\Model\Message $response) {
|
||||
* // response message successfully received
|
||||
* var_dump($response->rcode, $response->answers);
|
||||
* },
|
||||
* function (Exception $error) {
|
||||
* // failed to query due to $error
|
||||
* }
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* The returned Promise MUST be implemented in such a way that it can be
|
||||
* cancelled when it is still pending. Cancelling a pending promise MUST
|
||||
* reject its value with an Exception. It SHOULD clean up any underlying
|
||||
* resources and references as applicable.
|
||||
*
|
||||
* ```php
|
||||
* $promise = $executor->query($query);
|
||||
*
|
||||
* $promise->cancel();
|
||||
* ```
|
||||
*
|
||||
* @param Query $query
|
||||
* @return \React\Promise\PromiseInterface<\React\Dns\Model\Message>
|
||||
* resolves with response message on success or rejects with an Exception on error
|
||||
*/
|
||||
public function query(Query $query);
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Query;
|
||||
|
||||
use React\Promise\Promise;
|
||||
|
||||
final class FallbackExecutor implements ExecutorInterface
|
||||
{
|
||||
private $executor;
|
||||
private $fallback;
|
||||
|
||||
public function __construct(ExecutorInterface $executor, ExecutorInterface $fallback)
|
||||
{
|
||||
$this->executor = $executor;
|
||||
$this->fallback = $fallback;
|
||||
}
|
||||
|
||||
public function query(Query $query)
|
||||
{
|
||||
$cancelled = false;
|
||||
$fallback = $this->fallback;
|
||||
$promise = $this->executor->query($query);
|
||||
|
||||
return new Promise(function ($resolve, $reject) use (&$promise, $fallback, $query, &$cancelled) {
|
||||
$promise->then($resolve, function (\Exception $e1) use ($fallback, $query, $resolve, $reject, &$cancelled, &$promise) {
|
||||
// reject if primary resolution rejected due to cancellation
|
||||
if ($cancelled) {
|
||||
$reject($e1);
|
||||
return;
|
||||
}
|
||||
|
||||
// start fallback query if primary query rejected
|
||||
$promise = $fallback->query($query)->then($resolve, function (\Exception $e2) use ($e1, $reject) {
|
||||
$append = $e2->getMessage();
|
||||
if (($pos = strpos($append, ':')) !== false) {
|
||||
$append = substr($append, $pos + 2);
|
||||
}
|
||||
|
||||
// reject with combined error message if both queries fail
|
||||
$reject(new \RuntimeException($e1->getMessage() . '. ' . $append));
|
||||
});
|
||||
});
|
||||
}, function () use (&$promise, &$cancelled) {
|
||||
// cancel pending query (primary or fallback)
|
||||
$cancelled = true;
|
||||
$promise->cancel();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Query;
|
||||
|
||||
use React\Dns\Config\HostsFile;
|
||||
use React\Dns\Model\Message;
|
||||
use React\Dns\Model\Record;
|
||||
use React\Promise;
|
||||
|
||||
/**
|
||||
* Resolves hosts from the given HostsFile or falls back to another executor
|
||||
*
|
||||
* If the host is found in the hosts file, it will not be passed to the actual
|
||||
* DNS executor. If the host is not found in the hosts file, it will be passed
|
||||
* to the DNS executor as a fallback.
|
||||
*/
|
||||
final class HostsFileExecutor implements ExecutorInterface
|
||||
{
|
||||
private $hosts;
|
||||
private $fallback;
|
||||
|
||||
public function __construct(HostsFile $hosts, ExecutorInterface $fallback)
|
||||
{
|
||||
$this->hosts = $hosts;
|
||||
$this->fallback = $fallback;
|
||||
}
|
||||
|
||||
public function query(Query $query)
|
||||
{
|
||||
if ($query->class === Message::CLASS_IN && ($query->type === Message::TYPE_A || $query->type === Message::TYPE_AAAA)) {
|
||||
// forward lookup for type A or AAAA
|
||||
$records = array();
|
||||
$expectsColon = $query->type === Message::TYPE_AAAA;
|
||||
foreach ($this->hosts->getIpsForHost($query->name) as $ip) {
|
||||
// ensure this is an IPv4/IPV6 address according to query type
|
||||
if ((strpos($ip, ':') !== false) === $expectsColon) {
|
||||
$records[] = new Record($query->name, $query->type, $query->class, 0, $ip);
|
||||
}
|
||||
}
|
||||
|
||||
if ($records) {
|
||||
return Promise\resolve(
|
||||
Message::createResponseWithAnswersForQuery($query, $records)
|
||||
);
|
||||
}
|
||||
} elseif ($query->class === Message::CLASS_IN && $query->type === Message::TYPE_PTR) {
|
||||
// reverse lookup: extract IPv4 or IPv6 from special `.arpa` domain
|
||||
$ip = $this->getIpFromHost($query->name);
|
||||
|
||||
if ($ip !== null) {
|
||||
$records = array();
|
||||
foreach ($this->hosts->getHostsForIp($ip) as $host) {
|
||||
$records[] = new Record($query->name, $query->type, $query->class, 0, $host);
|
||||
}
|
||||
|
||||
if ($records) {
|
||||
return Promise\resolve(
|
||||
Message::createResponseWithAnswersForQuery($query, $records)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->fallback->query($query);
|
||||
}
|
||||
|
||||
private function getIpFromHost($host)
|
||||
{
|
||||
if (substr($host, -13) === '.in-addr.arpa') {
|
||||
// IPv4: read as IP and reverse bytes
|
||||
$ip = @inet_pton(substr($host, 0, -13));
|
||||
if ($ip === false || isset($ip[4])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return inet_ntop(strrev($ip));
|
||||
} elseif (substr($host, -9) === '.ip6.arpa') {
|
||||
// IPv6: replace dots, reverse nibbles and interpret as hexadecimal string
|
||||
$ip = @inet_ntop(pack('H*', strrev(str_replace('.', '', substr($host, 0, -9)))));
|
||||
if ($ip === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $ip;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+69
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Query;
|
||||
|
||||
use React\Dns\Model\Message;
|
||||
|
||||
/**
|
||||
* This class represents a single question in a query/response message
|
||||
*
|
||||
* It uses a structure similar to `\React\Dns\Message\Record`, but does not
|
||||
* contain fields for resulting TTL and resulting record data (IPs etc.).
|
||||
*
|
||||
* @link https://tools.ietf.org/html/rfc1035#section-4.1.2
|
||||
* @see \React\Dns\Message\Record
|
||||
*/
|
||||
final class Query
|
||||
{
|
||||
/**
|
||||
* @var string query name, i.e. hostname to look up
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var int query type (aka QTYPE), see Message::TYPE_* constants
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* @var int query class (aka QCLASS), see Message::CLASS_IN constant
|
||||
*/
|
||||
public $class;
|
||||
|
||||
/**
|
||||
* @param string $name query name, i.e. hostname to look up
|
||||
* @param int $type query type, see Message::TYPE_* constants
|
||||
* @param int $class query class, see Message::CLASS_IN constant
|
||||
*/
|
||||
public function __construct($name, $type, $class)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->type = $type;
|
||||
$this->class = $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the hostname and query type/class for this query
|
||||
*
|
||||
* The output format is supposed to be human readable and is subject to change.
|
||||
* The format is inspired by RFC 3597 when handling unkown types/classes.
|
||||
*
|
||||
* @return string "example.com (A)" or "example.com (CLASS0 TYPE1234)"
|
||||
* @link https://tools.ietf.org/html/rfc3597
|
||||
*/
|
||||
public function describe()
|
||||
{
|
||||
$class = $this->class !== Message::CLASS_IN ? 'CLASS' . $this->class . ' ' : '';
|
||||
|
||||
$type = 'TYPE' . $this->type;
|
||||
$ref = new \ReflectionClass('React\Dns\Model\Message');
|
||||
foreach ($ref->getConstants() as $name => $value) {
|
||||
if ($value === $this->type && \strpos($name, 'TYPE_') === 0) {
|
||||
$type = \substr($name, 5);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->name . ' (' . $class . $type . ')';
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Query;
|
||||
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
final class RetryExecutor implements ExecutorInterface
|
||||
{
|
||||
private $executor;
|
||||
private $retries;
|
||||
|
||||
public function __construct(ExecutorInterface $executor, $retries = 2)
|
||||
{
|
||||
$this->executor = $executor;
|
||||
$this->retries = $retries;
|
||||
}
|
||||
|
||||
public function query(Query $query)
|
||||
{
|
||||
return $this->tryQuery($query, $this->retries);
|
||||
}
|
||||
|
||||
public function tryQuery(Query $query, $retries)
|
||||
{
|
||||
$deferred = new Deferred(function () use (&$promise) {
|
||||
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
|
||||
$promise->cancel();
|
||||
}
|
||||
});
|
||||
|
||||
$success = function ($value) use ($deferred, &$errorback) {
|
||||
$errorback = null;
|
||||
$deferred->resolve($value);
|
||||
};
|
||||
|
||||
$executor = $this->executor;
|
||||
$errorback = function ($e) use ($deferred, &$promise, $query, $success, &$errorback, &$retries, $executor) {
|
||||
if (!$e instanceof TimeoutException) {
|
||||
$errorback = null;
|
||||
$deferred->reject($e);
|
||||
} elseif ($retries <= 0) {
|
||||
$errorback = null;
|
||||
$deferred->reject($e = new \RuntimeException(
|
||||
'DNS query for ' . $query->describe() . ' failed: too many retries',
|
||||
0,
|
||||
$e
|
||||
));
|
||||
|
||||
// avoid garbage references by replacing all closures in call stack.
|
||||
// what a lovely piece of code!
|
||||
$r = new \ReflectionProperty('Exception', 'trace');
|
||||
if (\PHP_VERSION_ID < 80100) {
|
||||
$r->setAccessible(true);
|
||||
}
|
||||
$trace = $r->getValue($e);
|
||||
|
||||
// Exception trace arguments are not available on some PHP 7.4 installs
|
||||
// @codeCoverageIgnoreStart
|
||||
foreach ($trace as $ti => $one) {
|
||||
if (isset($one['args'])) {
|
||||
foreach ($one['args'] as $ai => $arg) {
|
||||
if ($arg instanceof \Closure) {
|
||||
$trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
$r->setValue($e, $trace);
|
||||
} else {
|
||||
--$retries;
|
||||
$promise = $executor->query($query)->then(
|
||||
$success,
|
||||
$errorback
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
$promise = $this->executor->query($query)->then(
|
||||
$success,
|
||||
$errorback
|
||||
);
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Query;
|
||||
|
||||
use React\Promise\Promise;
|
||||
|
||||
/**
|
||||
* Send DNS queries over a UDP or TCP/IP stream transport.
|
||||
*
|
||||
* This class will automatically choose the correct transport protocol to send
|
||||
* a DNS query to your DNS server. It will always try to send it over the more
|
||||
* efficient UDP transport first. If this query yields a size related issue
|
||||
* (truncated messages), it will retry over a streaming TCP/IP transport.
|
||||
*
|
||||
* For more advanced usages one can utilize this class directly.
|
||||
* The following example looks up the `IPv6` address for `reactphp.org`.
|
||||
*
|
||||
* ```php
|
||||
* $executor = new SelectiveTransportExecutor($udpExecutor, $tcpExecutor);
|
||||
*
|
||||
* $executor->query(
|
||||
* new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
|
||||
* )->then(function (Message $message) {
|
||||
* foreach ($message->answers as $answer) {
|
||||
* echo 'IPv6: ' . $answer->data . PHP_EOL;
|
||||
* }
|
||||
* }, 'printf');
|
||||
* ```
|
||||
*
|
||||
* Note that this executor only implements the logic to select the correct
|
||||
* transport for the given DNS query. Implementing the correct transport logic,
|
||||
* implementing timeouts and any retry logic is left up to the given executors,
|
||||
* see also [`UdpTransportExecutor`](#udptransportexecutor) and
|
||||
* [`TcpTransportExecutor`](#tcptransportexecutor) for more details.
|
||||
*
|
||||
* Note that this executor is entirely async and as such allows you to execute
|
||||
* any number of queries concurrently. You should probably limit the number of
|
||||
* concurrent queries in your application or you're very likely going to face
|
||||
* rate limitations and bans on the resolver end. For many common applications,
|
||||
* you may want to avoid sending the same query multiple times when the first
|
||||
* one is still pending, so you will likely want to use this in combination with
|
||||
* a `CoopExecutor` like this:
|
||||
*
|
||||
* ```php
|
||||
* $executor = new CoopExecutor(
|
||||
* new SelectiveTransportExecutor(
|
||||
* $datagramExecutor,
|
||||
* $streamExecutor
|
||||
* )
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
class SelectiveTransportExecutor implements ExecutorInterface
|
||||
{
|
||||
private $datagramExecutor;
|
||||
private $streamExecutor;
|
||||
|
||||
public function __construct(ExecutorInterface $datagramExecutor, ExecutorInterface $streamExecutor)
|
||||
{
|
||||
$this->datagramExecutor = $datagramExecutor;
|
||||
$this->streamExecutor = $streamExecutor;
|
||||
}
|
||||
|
||||
public function query(Query $query)
|
||||
{
|
||||
$stream = $this->streamExecutor;
|
||||
$pending = $this->datagramExecutor->query($query);
|
||||
|
||||
return new Promise(function ($resolve, $reject) use (&$pending, $stream, $query) {
|
||||
$pending->then(
|
||||
$resolve,
|
||||
function ($e) use (&$pending, $stream, $query, $resolve, $reject) {
|
||||
if ($e->getCode() === (\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90)) {
|
||||
$pending = $stream->query($query)->then($resolve, $reject);
|
||||
} else {
|
||||
$reject($e);
|
||||
}
|
||||
}
|
||||
);
|
||||
}, function () use (&$pending) {
|
||||
$pending->cancel();
|
||||
$pending = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
+382
@@ -0,0 +1,382 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Query;
|
||||
|
||||
use React\Dns\Model\Message;
|
||||
use React\Dns\Protocol\BinaryDumper;
|
||||
use React\Dns\Protocol\Parser;
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise\Deferred;
|
||||
|
||||
/**
|
||||
* Send DNS queries over a TCP/IP stream transport.
|
||||
*
|
||||
* This is one of the main classes that send a DNS query to your DNS server.
|
||||
*
|
||||
* For more advanced usages one can utilize this class directly.
|
||||
* The following example looks up the `IPv6` address for `reactphp.org`.
|
||||
*
|
||||
* ```php
|
||||
* $executor = new TcpTransportExecutor('8.8.8.8:53');
|
||||
*
|
||||
* $executor->query(
|
||||
* new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
|
||||
* )->then(function (Message $message) {
|
||||
* foreach ($message->answers as $answer) {
|
||||
* echo 'IPv6: ' . $answer->data . PHP_EOL;
|
||||
* }
|
||||
* }, 'printf');
|
||||
* ```
|
||||
*
|
||||
* See also [example #92](examples).
|
||||
*
|
||||
* Note that this executor does not implement a timeout, so you will very likely
|
||||
* want to use this in combination with a `TimeoutExecutor` like this:
|
||||
*
|
||||
* ```php
|
||||
* $executor = new TimeoutExecutor(
|
||||
* new TcpTransportExecutor($nameserver),
|
||||
* 3.0
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* Unlike the `UdpTransportExecutor`, this class uses a reliable TCP/IP
|
||||
* transport, so you do not necessarily have to implement any retry logic.
|
||||
*
|
||||
* Note that this executor is entirely async and as such allows you to execute
|
||||
* queries concurrently. The first query will establish a TCP/IP socket
|
||||
* connection to the DNS server which will be kept open for a short period.
|
||||
* Additional queries will automatically reuse this existing socket connection
|
||||
* to the DNS server, will pipeline multiple requests over this single
|
||||
* connection and will keep an idle connection open for a short period. The
|
||||
* initial TCP/IP connection overhead may incur a slight delay if you only send
|
||||
* occasional queries – when sending a larger number of concurrent queries over
|
||||
* an existing connection, it becomes increasingly more efficient and avoids
|
||||
* creating many concurrent sockets like the UDP-based executor. You may still
|
||||
* want to limit the number of (concurrent) queries in your application or you
|
||||
* may be facing rate limitations and bans on the resolver end. For many common
|
||||
* applications, you may want to avoid sending the same query multiple times
|
||||
* when the first one is still pending, so you will likely want to use this in
|
||||
* combination with a `CoopExecutor` like this:
|
||||
*
|
||||
* ```php
|
||||
* $executor = new CoopExecutor(
|
||||
* new TimeoutExecutor(
|
||||
* new TcpTransportExecutor($nameserver),
|
||||
* 3.0
|
||||
* )
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* > Internally, this class uses PHP's TCP/IP sockets and does not take advantage
|
||||
* of [react/socket](https://github.com/reactphp/socket) purely for
|
||||
* organizational reasons to avoid a cyclic dependency between the two
|
||||
* packages. Higher-level components should take advantage of the Socket
|
||||
* component instead of reimplementing this socket logic from scratch.
|
||||
*/
|
||||
class TcpTransportExecutor implements ExecutorInterface
|
||||
{
|
||||
private $nameserver;
|
||||
private $loop;
|
||||
private $parser;
|
||||
private $dumper;
|
||||
|
||||
/**
|
||||
* @var ?resource
|
||||
*/
|
||||
private $socket;
|
||||
|
||||
/**
|
||||
* @var Deferred[]
|
||||
*/
|
||||
private $pending = array();
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $names = array();
|
||||
|
||||
/**
|
||||
* Maximum idle time when socket is current unused (i.e. no pending queries outstanding)
|
||||
*
|
||||
* If a new query is to be sent during the idle period, we can reuse the
|
||||
* existing socket without having to wait for a new socket connection.
|
||||
* This uses a rather small, hard-coded value to not keep any unneeded
|
||||
* sockets open and to not keep the loop busy longer than needed.
|
||||
*
|
||||
* A future implementation may take advantage of `edns-tcp-keepalive` to keep
|
||||
* the socket open for longer periods. This will likely require explicit
|
||||
* configuration because this may consume additional resources and also keep
|
||||
* the loop busy for longer than expected in some applications.
|
||||
*
|
||||
* @var float
|
||||
* @link https://tools.ietf.org/html/rfc7766#section-6.2.1
|
||||
* @link https://tools.ietf.org/html/rfc7828
|
||||
*/
|
||||
private $idlePeriod = 0.001;
|
||||
|
||||
/**
|
||||
* @var ?\React\EventLoop\TimerInterface
|
||||
*/
|
||||
private $idleTimer;
|
||||
|
||||
private $writeBuffer = '';
|
||||
private $writePending = false;
|
||||
|
||||
private $readBuffer = '';
|
||||
private $readPending = false;
|
||||
|
||||
/** @var string */
|
||||
private $readChunk = 0xffff;
|
||||
|
||||
/**
|
||||
* @param string $nameserver
|
||||
* @param ?LoopInterface $loop
|
||||
*/
|
||||
public function __construct($nameserver, $loop = null)
|
||||
{
|
||||
if (\strpos($nameserver, '[') === false && \substr_count($nameserver, ':') >= 2 && \strpos($nameserver, '://') === false) {
|
||||
// several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets
|
||||
$nameserver = '[' . $nameserver . ']';
|
||||
}
|
||||
|
||||
$parts = \parse_url((\strpos($nameserver, '://') === false ? 'tcp://' : '') . $nameserver);
|
||||
if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'tcp' || @\inet_pton(\trim($parts['host'], '[]')) === false) {
|
||||
throw new \InvalidArgumentException('Invalid nameserver address given');
|
||||
}
|
||||
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
|
||||
$this->nameserver = 'tcp://' . $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53);
|
||||
$this->loop = $loop ?: Loop::get();
|
||||
$this->parser = new Parser();
|
||||
$this->dumper = new BinaryDumper();
|
||||
}
|
||||
|
||||
public function query(Query $query)
|
||||
{
|
||||
$request = Message::createRequestForQuery($query);
|
||||
|
||||
// keep shuffing message ID to avoid using the same message ID for two pending queries at the same time
|
||||
while (isset($this->pending[$request->id])) {
|
||||
$request->id = \mt_rand(0, 0xffff); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$queryData = $this->dumper->toBinary($request);
|
||||
$length = \strlen($queryData);
|
||||
if ($length > 0xffff) {
|
||||
return \React\Promise\reject(new \RuntimeException(
|
||||
'DNS query for ' . $query->describe() . ' failed: Query too large for TCP transport'
|
||||
));
|
||||
}
|
||||
|
||||
$queryData = \pack('n', $length) . $queryData;
|
||||
|
||||
if ($this->socket === null) {
|
||||
// create async TCP/IP connection (may take a while)
|
||||
$socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0, \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT);
|
||||
if ($socket === false) {
|
||||
return \React\Promise\reject(new \RuntimeException(
|
||||
'DNS query for ' . $query->describe() . ' failed: Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
|
||||
$errno
|
||||
));
|
||||
}
|
||||
|
||||
// set socket to non-blocking and wait for it to become writable (connection success/rejected)
|
||||
\stream_set_blocking($socket, false);
|
||||
if (\function_exists('stream_set_chunk_size')) {
|
||||
\stream_set_chunk_size($socket, $this->readChunk); // @codeCoverageIgnore
|
||||
}
|
||||
$this->socket = $socket;
|
||||
}
|
||||
|
||||
if ($this->idleTimer !== null) {
|
||||
$this->loop->cancelTimer($this->idleTimer);
|
||||
$this->idleTimer = null;
|
||||
}
|
||||
|
||||
// wait for socket to become writable to actually write out data
|
||||
$this->writeBuffer .= $queryData;
|
||||
if (!$this->writePending) {
|
||||
$this->writePending = true;
|
||||
$this->loop->addWriteStream($this->socket, array($this, 'handleWritable'));
|
||||
}
|
||||
|
||||
$names =& $this->names;
|
||||
$that = $this;
|
||||
$deferred = new Deferred(function () use ($that, &$names, $request) {
|
||||
// remove from list of pending names, but remember pending query
|
||||
$name = $names[$request->id];
|
||||
unset($names[$request->id]);
|
||||
$that->checkIdle();
|
||||
|
||||
throw new CancellationException('DNS query for ' . $name . ' has been cancelled');
|
||||
});
|
||||
|
||||
$this->pending[$request->id] = $deferred;
|
||||
$this->names[$request->id] = $query->describe();
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function handleWritable()
|
||||
{
|
||||
if ($this->readPending === false) {
|
||||
$name = @\stream_socket_get_name($this->socket, true);
|
||||
if ($name === false) {
|
||||
// Connection failed? Check socket error if available for underlying errno/errstr.
|
||||
// @codeCoverageIgnoreStart
|
||||
if (\function_exists('socket_import_stream')) {
|
||||
$socket = \socket_import_stream($this->socket);
|
||||
$errno = \socket_get_option($socket, \SOL_SOCKET, \SO_ERROR);
|
||||
$errstr = \socket_strerror($errno);
|
||||
} else {
|
||||
$errno = \defined('SOCKET_ECONNREFUSED') ? \SOCKET_ECONNREFUSED : 111;
|
||||
$errstr = 'Connection refused';
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
$this->closeError('Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')', $errno);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->readPending = true;
|
||||
$this->loop->addReadStream($this->socket, array($this, 'handleRead'));
|
||||
}
|
||||
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
\set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
|
||||
// Match errstr from PHP's warning message.
|
||||
// fwrite(): Send of 327712 bytes failed with errno=32 Broken pipe
|
||||
\preg_match('/errno=(\d+) (.+)/', $error, $m);
|
||||
$errno = isset($m[1]) ? (int) $m[1] : 0;
|
||||
$errstr = isset($m[2]) ? $m[2] : $error;
|
||||
});
|
||||
|
||||
$written = \fwrite($this->socket, $this->writeBuffer);
|
||||
|
||||
\restore_error_handler();
|
||||
|
||||
if ($written === false || $written === 0) {
|
||||
$this->closeError(
|
||||
'Unable to send query to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
|
||||
$errno
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($this->writeBuffer[$written])) {
|
||||
$this->writeBuffer = \substr($this->writeBuffer, $written);
|
||||
} else {
|
||||
$this->loop->removeWriteStream($this->socket);
|
||||
$this->writePending = false;
|
||||
$this->writeBuffer = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function handleRead()
|
||||
{
|
||||
// read one chunk of data from the DNS server
|
||||
// any error is fatal, this is a stream of TCP/IP data
|
||||
$chunk = @\fread($this->socket, $this->readChunk);
|
||||
if ($chunk === false || $chunk === '') {
|
||||
$this->closeError('Connection to DNS server ' . $this->nameserver . ' lost');
|
||||
return;
|
||||
}
|
||||
|
||||
// reassemble complete message by concatenating all chunks.
|
||||
$this->readBuffer .= $chunk;
|
||||
|
||||
// response message header contains at least 12 bytes
|
||||
while (isset($this->readBuffer[11])) {
|
||||
// read response message length from first 2 bytes and ensure we have length + data in buffer
|
||||
list(, $length) = \unpack('n', $this->readBuffer);
|
||||
if (!isset($this->readBuffer[$length + 1])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = \substr($this->readBuffer, 2, $length);
|
||||
$this->readBuffer = (string)substr($this->readBuffer, $length + 2);
|
||||
|
||||
try {
|
||||
$response = $this->parser->parseMessage($data);
|
||||
} catch (\Exception $e) {
|
||||
// reject all pending queries if we received an invalid message from remote server
|
||||
$this->closeError('Invalid message received from DNS server ' . $this->nameserver);
|
||||
return;
|
||||
}
|
||||
|
||||
// reject all pending queries if we received an unexpected response ID or truncated response
|
||||
if (!isset($this->pending[$response->id]) || $response->tc) {
|
||||
$this->closeError('Invalid response message received from DNS server ' . $this->nameserver);
|
||||
return;
|
||||
}
|
||||
|
||||
$deferred = $this->pending[$response->id];
|
||||
unset($this->pending[$response->id], $this->names[$response->id]);
|
||||
|
||||
$deferred->resolve($response);
|
||||
|
||||
$this->checkIdle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param string $reason
|
||||
* @param int $code
|
||||
*/
|
||||
public function closeError($reason, $code = 0)
|
||||
{
|
||||
$this->readBuffer = '';
|
||||
if ($this->readPending) {
|
||||
$this->loop->removeReadStream($this->socket);
|
||||
$this->readPending = false;
|
||||
}
|
||||
|
||||
$this->writeBuffer = '';
|
||||
if ($this->writePending) {
|
||||
$this->loop->removeWriteStream($this->socket);
|
||||
$this->writePending = false;
|
||||
}
|
||||
|
||||
if ($this->idleTimer !== null) {
|
||||
$this->loop->cancelTimer($this->idleTimer);
|
||||
$this->idleTimer = null;
|
||||
}
|
||||
|
||||
@\fclose($this->socket);
|
||||
$this->socket = null;
|
||||
|
||||
foreach ($this->names as $id => $name) {
|
||||
$this->pending[$id]->reject(new \RuntimeException(
|
||||
'DNS query for ' . $name . ' failed: ' . $reason,
|
||||
$code
|
||||
));
|
||||
}
|
||||
$this->pending = $this->names = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function checkIdle()
|
||||
{
|
||||
if ($this->idleTimer === null && !$this->names) {
|
||||
$that = $this;
|
||||
$this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () use ($that) {
|
||||
$that->closeError('Idle timeout');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Query;
|
||||
|
||||
final class TimeoutException extends \Exception
|
||||
{
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Query;
|
||||
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise\Promise;
|
||||
|
||||
final class TimeoutExecutor implements ExecutorInterface
|
||||
{
|
||||
private $executor;
|
||||
private $loop;
|
||||
private $timeout;
|
||||
|
||||
/**
|
||||
* @param ExecutorInterface $executor
|
||||
* @param float $timeout
|
||||
* @param ?LoopInterface $loop
|
||||
*/
|
||||
public function __construct(ExecutorInterface $executor, $timeout, $loop = null)
|
||||
{
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #3 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
|
||||
$this->executor = $executor;
|
||||
$this->loop = $loop ?: Loop::get();
|
||||
$this->timeout = $timeout;
|
||||
}
|
||||
|
||||
public function query(Query $query)
|
||||
{
|
||||
$promise = $this->executor->query($query);
|
||||
|
||||
$loop = $this->loop;
|
||||
$time = $this->timeout;
|
||||
return new Promise(function ($resolve, $reject) use ($loop, $time, $promise, $query) {
|
||||
$timer = null;
|
||||
$promise = $promise->then(function ($v) use (&$timer, $loop, $resolve) {
|
||||
if ($timer) {
|
||||
$loop->cancelTimer($timer);
|
||||
}
|
||||
$timer = false;
|
||||
$resolve($v);
|
||||
}, function ($v) use (&$timer, $loop, $reject) {
|
||||
if ($timer) {
|
||||
$loop->cancelTimer($timer);
|
||||
}
|
||||
$timer = false;
|
||||
$reject($v);
|
||||
});
|
||||
|
||||
// promise already resolved => no need to start timer
|
||||
if ($timer === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// start timeout timer which will cancel the pending promise
|
||||
$timer = $loop->addTimer($time, function () use ($time, &$promise, $reject, $query) {
|
||||
$reject(new TimeoutException(
|
||||
'DNS query for ' . $query->describe() . ' timed out'
|
||||
));
|
||||
|
||||
// Cancel pending query to clean up any underlying resources and references.
|
||||
// Avoid garbage references in call stack by passing pending promise by reference.
|
||||
assert(\method_exists($promise, 'cancel'));
|
||||
$promise->cancel();
|
||||
$promise = null;
|
||||
});
|
||||
}, function () use (&$promise) {
|
||||
// Cancelling this promise will cancel the pending query, thus triggering the rejection logic above.
|
||||
// Avoid garbage references in call stack by passing pending promise by reference.
|
||||
assert(\method_exists($promise, 'cancel'));
|
||||
$promise->cancel();
|
||||
$promise = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Query;
|
||||
|
||||
use React\Dns\Model\Message;
|
||||
use React\Dns\Protocol\BinaryDumper;
|
||||
use React\Dns\Protocol\Parser;
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise\Deferred;
|
||||
|
||||
/**
|
||||
* Send DNS queries over a UDP transport.
|
||||
*
|
||||
* This is the main class that sends a DNS query to your DNS server and is used
|
||||
* internally by the `Resolver` for the actual message transport.
|
||||
*
|
||||
* For more advanced usages one can utilize this class directly.
|
||||
* The following example looks up the `IPv6` address for `igor.io`.
|
||||
*
|
||||
* ```php
|
||||
* $executor = new UdpTransportExecutor('8.8.8.8:53');
|
||||
*
|
||||
* $executor->query(
|
||||
* new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
|
||||
* )->then(function (Message $message) {
|
||||
* foreach ($message->answers as $answer) {
|
||||
* echo 'IPv6: ' . $answer->data . PHP_EOL;
|
||||
* }
|
||||
* }, 'printf');
|
||||
* ```
|
||||
*
|
||||
* See also the [fourth example](examples).
|
||||
*
|
||||
* Note that this executor does not implement a timeout, so you will very likely
|
||||
* want to use this in combination with a `TimeoutExecutor` like this:
|
||||
*
|
||||
* ```php
|
||||
* $executor = new TimeoutExecutor(
|
||||
* new UdpTransportExecutor($nameserver),
|
||||
* 3.0
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* Also note that this executor uses an unreliable UDP transport and that it
|
||||
* does not implement any retry logic, so you will likely want to use this in
|
||||
* combination with a `RetryExecutor` like this:
|
||||
*
|
||||
* ```php
|
||||
* $executor = new RetryExecutor(
|
||||
* new TimeoutExecutor(
|
||||
* new UdpTransportExecutor($nameserver),
|
||||
* 3.0
|
||||
* )
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* Note that this executor is entirely async and as such allows you to execute
|
||||
* any number of queries concurrently. You should probably limit the number of
|
||||
* concurrent queries in your application or you're very likely going to face
|
||||
* rate limitations and bans on the resolver end. For many common applications,
|
||||
* you may want to avoid sending the same query multiple times when the first
|
||||
* one is still pending, so you will likely want to use this in combination with
|
||||
* a `CoopExecutor` like this:
|
||||
*
|
||||
* ```php
|
||||
* $executor = new CoopExecutor(
|
||||
* new RetryExecutor(
|
||||
* new TimeoutExecutor(
|
||||
* new UdpTransportExecutor($nameserver),
|
||||
* 3.0
|
||||
* )
|
||||
* )
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* > Internally, this class uses PHP's UDP sockets and does not take advantage
|
||||
* of [react/datagram](https://github.com/reactphp/datagram) purely for
|
||||
* organizational reasons to avoid a cyclic dependency between the two
|
||||
* packages. Higher-level components should take advantage of the Datagram
|
||||
* component instead of reimplementing this socket logic from scratch.
|
||||
*/
|
||||
final class UdpTransportExecutor implements ExecutorInterface
|
||||
{
|
||||
private $nameserver;
|
||||
private $loop;
|
||||
private $parser;
|
||||
private $dumper;
|
||||
|
||||
/**
|
||||
* maximum UDP packet size to send and receive
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $maxPacketSize = 512;
|
||||
|
||||
/**
|
||||
* @param string $nameserver
|
||||
* @param ?LoopInterface $loop
|
||||
*/
|
||||
public function __construct($nameserver, $loop = null)
|
||||
{
|
||||
if (\strpos($nameserver, '[') === false && \substr_count($nameserver, ':') >= 2 && \strpos($nameserver, '://') === false) {
|
||||
// several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets
|
||||
$nameserver = '[' . $nameserver . ']';
|
||||
}
|
||||
|
||||
$parts = \parse_url((\strpos($nameserver, '://') === false ? 'udp://' : '') . $nameserver);
|
||||
if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'udp' || @\inet_pton(\trim($parts['host'], '[]')) === false) {
|
||||
throw new \InvalidArgumentException('Invalid nameserver address given');
|
||||
}
|
||||
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
|
||||
$this->nameserver = 'udp://' . $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53);
|
||||
$this->loop = $loop ?: Loop::get();
|
||||
$this->parser = new Parser();
|
||||
$this->dumper = new BinaryDumper();
|
||||
}
|
||||
|
||||
public function query(Query $query)
|
||||
{
|
||||
$request = Message::createRequestForQuery($query);
|
||||
|
||||
$queryData = $this->dumper->toBinary($request);
|
||||
if (isset($queryData[$this->maxPacketSize])) {
|
||||
return \React\Promise\reject(new \RuntimeException(
|
||||
'DNS query for ' . $query->describe() . ' failed: Query too large for UDP transport',
|
||||
\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90
|
||||
));
|
||||
}
|
||||
|
||||
// UDP connections are instant, so try connection without a loop or timeout
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
$socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0);
|
||||
if ($socket === false) {
|
||||
return \React\Promise\reject(new \RuntimeException(
|
||||
'DNS query for ' . $query->describe() . ' failed: Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
|
||||
$errno
|
||||
));
|
||||
}
|
||||
|
||||
// set socket to non-blocking and immediately try to send (fill write buffer)
|
||||
\stream_set_blocking($socket, false);
|
||||
|
||||
\set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
|
||||
// Write may potentially fail, but most common errors are already caught by connection check above.
|
||||
// Among others, macOS is known to report here when trying to send to broadcast address.
|
||||
// This can also be reproduced by writing data exceeding `stream_set_chunk_size()` to a server refusing UDP data.
|
||||
// fwrite(): send of 8192 bytes failed with errno=111 Connection refused
|
||||
\preg_match('/errno=(\d+) (.+)/', $error, $m);
|
||||
$errno = isset($m[1]) ? (int) $m[1] : 0;
|
||||
$errstr = isset($m[2]) ? $m[2] : $error;
|
||||
});
|
||||
|
||||
$written = \fwrite($socket, $queryData);
|
||||
|
||||
\restore_error_handler();
|
||||
|
||||
if ($written !== \strlen($queryData)) {
|
||||
return \React\Promise\reject(new \RuntimeException(
|
||||
'DNS query for ' . $query->describe() . ' failed: Unable to send query to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
|
||||
$errno
|
||||
));
|
||||
}
|
||||
|
||||
$loop = $this->loop;
|
||||
$deferred = new Deferred(function () use ($loop, $socket, $query) {
|
||||
// cancellation should remove socket from loop and close socket
|
||||
$loop->removeReadStream($socket);
|
||||
\fclose($socket);
|
||||
|
||||
throw new CancellationException('DNS query for ' . $query->describe() . ' has been cancelled');
|
||||
});
|
||||
|
||||
$max = $this->maxPacketSize;
|
||||
$parser = $this->parser;
|
||||
$nameserver = $this->nameserver;
|
||||
$loop->addReadStream($socket, function ($socket) use ($loop, $deferred, $query, $parser, $request, $max, $nameserver) {
|
||||
// try to read a single data packet from the DNS server
|
||||
// ignoring any errors, this is uses UDP packets and not a stream of data
|
||||
$data = @\fread($socket, $max);
|
||||
if ($data === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $parser->parseMessage($data);
|
||||
} catch (\Exception $e) {
|
||||
// ignore and await next if we received an invalid message from remote server
|
||||
// this may as well be a fake response from an attacker (possible DOS)
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore and await next if we received an unexpected response ID
|
||||
// this may as well be a fake response from an attacker (possible cache poisoning)
|
||||
if ($response->id !== $request->id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we only react to the first valid message, so remove socket from loop and close
|
||||
$loop->removeReadStream($socket);
|
||||
\fclose($socket);
|
||||
|
||||
if ($response->tc) {
|
||||
$deferred->reject(new \RuntimeException(
|
||||
'DNS query for ' . $query->describe() . ' failed: The DNS server ' . $nameserver . ' returned a truncated result for a UDP query',
|
||||
\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
$deferred->resolve($response);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns;
|
||||
|
||||
final class RecordNotFoundException extends \Exception
|
||||
{
|
||||
}
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Resolver;
|
||||
|
||||
use React\Cache\ArrayCache;
|
||||
use React\Cache\CacheInterface;
|
||||
use React\Dns\Config\Config;
|
||||
use React\Dns\Config\HostsFile;
|
||||
use React\Dns\Query\CachingExecutor;
|
||||
use React\Dns\Query\CoopExecutor;
|
||||
use React\Dns\Query\ExecutorInterface;
|
||||
use React\Dns\Query\FallbackExecutor;
|
||||
use React\Dns\Query\HostsFileExecutor;
|
||||
use React\Dns\Query\RetryExecutor;
|
||||
use React\Dns\Query\SelectiveTransportExecutor;
|
||||
use React\Dns\Query\TcpTransportExecutor;
|
||||
use React\Dns\Query\TimeoutExecutor;
|
||||
use React\Dns\Query\UdpTransportExecutor;
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
final class Factory
|
||||
{
|
||||
/**
|
||||
* Creates a DNS resolver instance for the given DNS config
|
||||
*
|
||||
* As of v1.7.0 it's recommended to pass a `Config` object instead of a
|
||||
* single nameserver address. If the given config contains more than one DNS
|
||||
* nameserver, all DNS nameservers will be used in order. The primary DNS
|
||||
* server will always be used first before falling back to the secondary or
|
||||
* tertiary DNS server.
|
||||
*
|
||||
* @param Config|string $config DNS Config object (recommended) or single nameserver address
|
||||
* @param ?LoopInterface $loop
|
||||
* @return \React\Dns\Resolver\ResolverInterface
|
||||
* @throws \InvalidArgumentException for invalid DNS server address
|
||||
* @throws \UnderflowException when given DNS Config object has an empty list of nameservers
|
||||
*/
|
||||
public function create($config, $loop = null)
|
||||
{
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
|
||||
$executor = $this->decorateHostsFileExecutor($this->createExecutor($config, $loop ?: Loop::get()));
|
||||
|
||||
return new Resolver($executor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cached DNS resolver instance for the given DNS config and cache
|
||||
*
|
||||
* As of v1.7.0 it's recommended to pass a `Config` object instead of a
|
||||
* single nameserver address. If the given config contains more than one DNS
|
||||
* nameserver, all DNS nameservers will be used in order. The primary DNS
|
||||
* server will always be used first before falling back to the secondary or
|
||||
* tertiary DNS server.
|
||||
*
|
||||
* @param Config|string $config DNS Config object (recommended) or single nameserver address
|
||||
* @param ?LoopInterface $loop
|
||||
* @param ?CacheInterface $cache
|
||||
* @return \React\Dns\Resolver\ResolverInterface
|
||||
* @throws \InvalidArgumentException for invalid DNS server address
|
||||
* @throws \UnderflowException when given DNS Config object has an empty list of nameservers
|
||||
*/
|
||||
public function createCached($config, $loop = null, $cache = null)
|
||||
{
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
|
||||
if ($cache !== null && !$cache instanceof CacheInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #3 ($cache) expected null|React\Cache\CacheInterface');
|
||||
}
|
||||
|
||||
// default to keeping maximum of 256 responses in cache unless explicitly given
|
||||
if (!($cache instanceof CacheInterface)) {
|
||||
$cache = new ArrayCache(256);
|
||||
}
|
||||
|
||||
$executor = $this->createExecutor($config, $loop ?: Loop::get());
|
||||
$executor = new CachingExecutor($executor, $cache);
|
||||
$executor = $this->decorateHostsFileExecutor($executor);
|
||||
|
||||
return new Resolver($executor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to load the hosts file and decorates the given executor on success
|
||||
*
|
||||
* @param ExecutorInterface $executor
|
||||
* @return ExecutorInterface
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function decorateHostsFileExecutor(ExecutorInterface $executor)
|
||||
{
|
||||
try {
|
||||
$executor = new HostsFileExecutor(
|
||||
HostsFile::loadFromPathBlocking(),
|
||||
$executor
|
||||
);
|
||||
} catch (\RuntimeException $e) {
|
||||
// ignore this file if it can not be loaded
|
||||
}
|
||||
|
||||
// Windows does not store localhost in hosts file by default but handles this internally
|
||||
// To compensate for this, we explicitly use hard-coded defaults for localhost
|
||||
if (DIRECTORY_SEPARATOR === '\\') {
|
||||
$executor = new HostsFileExecutor(
|
||||
new HostsFile("127.0.0.1 localhost\n::1 localhost"),
|
||||
$executor
|
||||
);
|
||||
}
|
||||
|
||||
return $executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Config|string $nameserver
|
||||
* @param LoopInterface $loop
|
||||
* @return CoopExecutor
|
||||
* @throws \InvalidArgumentException for invalid DNS server address
|
||||
* @throws \UnderflowException when given DNS Config object has an empty list of nameservers
|
||||
*/
|
||||
private function createExecutor($nameserver, LoopInterface $loop)
|
||||
{
|
||||
if ($nameserver instanceof Config) {
|
||||
if (!$nameserver->nameservers) {
|
||||
throw new \UnderflowException('Empty config with no DNS servers');
|
||||
}
|
||||
|
||||
// Hard-coded to check up to 3 DNS servers to match default limits in place in most systems (see MAXNS config).
|
||||
// Note to future self: Recursion isn't too hard, but how deep do we really want to go?
|
||||
$primary = reset($nameserver->nameservers);
|
||||
$secondary = next($nameserver->nameservers);
|
||||
$tertiary = next($nameserver->nameservers);
|
||||
|
||||
if ($tertiary !== false) {
|
||||
// 3 DNS servers given => nest first with fallback for second and third
|
||||
return new CoopExecutor(
|
||||
new RetryExecutor(
|
||||
new FallbackExecutor(
|
||||
$this->createSingleExecutor($primary, $loop),
|
||||
new FallbackExecutor(
|
||||
$this->createSingleExecutor($secondary, $loop),
|
||||
$this->createSingleExecutor($tertiary, $loop)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
} elseif ($secondary !== false) {
|
||||
// 2 DNS servers given => fallback from first to second
|
||||
return new CoopExecutor(
|
||||
new RetryExecutor(
|
||||
new FallbackExecutor(
|
||||
$this->createSingleExecutor($primary, $loop),
|
||||
$this->createSingleExecutor($secondary, $loop)
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// 1 DNS server given => use single executor
|
||||
$nameserver = $primary;
|
||||
}
|
||||
}
|
||||
|
||||
return new CoopExecutor(new RetryExecutor($this->createSingleExecutor($nameserver, $loop)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $nameserver
|
||||
* @param LoopInterface $loop
|
||||
* @return ExecutorInterface
|
||||
* @throws \InvalidArgumentException for invalid DNS server address
|
||||
*/
|
||||
private function createSingleExecutor($nameserver, LoopInterface $loop)
|
||||
{
|
||||
$parts = \parse_url($nameserver);
|
||||
|
||||
if (isset($parts['scheme']) && $parts['scheme'] === 'tcp') {
|
||||
$executor = $this->createTcpExecutor($nameserver, $loop);
|
||||
} elseif (isset($parts['scheme']) && $parts['scheme'] === 'udp') {
|
||||
$executor = $this->createUdpExecutor($nameserver, $loop);
|
||||
} else {
|
||||
$executor = new SelectiveTransportExecutor(
|
||||
$this->createUdpExecutor($nameserver, $loop),
|
||||
$this->createTcpExecutor($nameserver, $loop)
|
||||
);
|
||||
}
|
||||
|
||||
return $executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $nameserver
|
||||
* @param LoopInterface $loop
|
||||
* @return TimeoutExecutor
|
||||
* @throws \InvalidArgumentException for invalid DNS server address
|
||||
*/
|
||||
private function createTcpExecutor($nameserver, LoopInterface $loop)
|
||||
{
|
||||
return new TimeoutExecutor(
|
||||
new TcpTransportExecutor($nameserver, $loop),
|
||||
5.0,
|
||||
$loop
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $nameserver
|
||||
* @param LoopInterface $loop
|
||||
* @return TimeoutExecutor
|
||||
* @throws \InvalidArgumentException for invalid DNS server address
|
||||
*/
|
||||
private function createUdpExecutor($nameserver, LoopInterface $loop)
|
||||
{
|
||||
return new TimeoutExecutor(
|
||||
new UdpTransportExecutor(
|
||||
$nameserver,
|
||||
$loop
|
||||
),
|
||||
5.0,
|
||||
$loop
|
||||
);
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Resolver;
|
||||
|
||||
use React\Dns\Model\Message;
|
||||
use React\Dns\Query\ExecutorInterface;
|
||||
use React\Dns\Query\Query;
|
||||
use React\Dns\RecordNotFoundException;
|
||||
|
||||
/**
|
||||
* @see ResolverInterface for the base interface
|
||||
*/
|
||||
final class Resolver implements ResolverInterface
|
||||
{
|
||||
private $executor;
|
||||
|
||||
public function __construct(ExecutorInterface $executor)
|
||||
{
|
||||
$this->executor = $executor;
|
||||
}
|
||||
|
||||
public function resolve($domain)
|
||||
{
|
||||
return $this->resolveAll($domain, Message::TYPE_A)->then(function (array $ips) {
|
||||
return $ips[array_rand($ips)];
|
||||
});
|
||||
}
|
||||
|
||||
public function resolveAll($domain, $type)
|
||||
{
|
||||
$query = new Query($domain, $type, Message::CLASS_IN);
|
||||
$that = $this;
|
||||
|
||||
return $this->executor->query(
|
||||
$query
|
||||
)->then(function (Message $response) use ($query, $that) {
|
||||
return $that->extractValues($query, $response);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* [Internal] extract all resource record values from response for this query
|
||||
*
|
||||
* @param Query $query
|
||||
* @param Message $response
|
||||
* @return array
|
||||
* @throws RecordNotFoundException when response indicates an error or contains no data
|
||||
* @internal
|
||||
*/
|
||||
public function extractValues(Query $query, Message $response)
|
||||
{
|
||||
// reject if response code indicates this is an error response message
|
||||
$code = $response->rcode;
|
||||
if ($code !== Message::RCODE_OK) {
|
||||
switch ($code) {
|
||||
case Message::RCODE_FORMAT_ERROR:
|
||||
$message = 'Format Error';
|
||||
break;
|
||||
case Message::RCODE_SERVER_FAILURE:
|
||||
$message = 'Server Failure';
|
||||
break;
|
||||
case Message::RCODE_NAME_ERROR:
|
||||
$message = 'Non-Existent Domain / NXDOMAIN';
|
||||
break;
|
||||
case Message::RCODE_NOT_IMPLEMENTED:
|
||||
$message = 'Not Implemented';
|
||||
break;
|
||||
case Message::RCODE_REFUSED:
|
||||
$message = 'Refused';
|
||||
break;
|
||||
default:
|
||||
$message = 'Unknown error response code ' . $code;
|
||||
}
|
||||
throw new RecordNotFoundException(
|
||||
'DNS query for ' . $query->describe() . ' returned an error response (' . $message . ')',
|
||||
$code
|
||||
);
|
||||
}
|
||||
|
||||
$answers = $response->answers;
|
||||
$addresses = $this->valuesByNameAndType($answers, $query->name, $query->type);
|
||||
|
||||
// reject if we did not receive a valid answer (domain is valid, but no record for this type could be found)
|
||||
if (0 === count($addresses)) {
|
||||
throw new RecordNotFoundException(
|
||||
'DNS query for ' . $query->describe() . ' did not return a valid answer (NOERROR / NODATA)'
|
||||
);
|
||||
}
|
||||
|
||||
return array_values($addresses);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \React\Dns\Model\Record[] $answers
|
||||
* @param string $name
|
||||
* @param int $type
|
||||
* @return array
|
||||
*/
|
||||
private function valuesByNameAndType(array $answers, $name, $type)
|
||||
{
|
||||
// return all record values for this name and type (if any)
|
||||
$named = $this->filterByName($answers, $name);
|
||||
$records = $this->filterByType($named, $type);
|
||||
if ($records) {
|
||||
return $this->mapRecordData($records);
|
||||
}
|
||||
|
||||
// no matching records found? check if there are any matching CNAMEs instead
|
||||
$cnameRecords = $this->filterByType($named, Message::TYPE_CNAME);
|
||||
if ($cnameRecords) {
|
||||
$cnames = $this->mapRecordData($cnameRecords);
|
||||
foreach ($cnames as $cname) {
|
||||
$records = array_merge(
|
||||
$records,
|
||||
$this->valuesByNameAndType($answers, $cname, $type)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
private function filterByName(array $answers, $name)
|
||||
{
|
||||
return $this->filterByField($answers, 'name', $name);
|
||||
}
|
||||
|
||||
private function filterByType(array $answers, $type)
|
||||
{
|
||||
return $this->filterByField($answers, 'type', $type);
|
||||
}
|
||||
|
||||
private function filterByField(array $answers, $field, $value)
|
||||
{
|
||||
$value = strtolower($value);
|
||||
return array_filter($answers, function ($answer) use ($field, $value) {
|
||||
return $value === strtolower($answer->$field);
|
||||
});
|
||||
}
|
||||
|
||||
private function mapRecordData(array $records)
|
||||
{
|
||||
return array_map(function ($record) {
|
||||
return $record->data;
|
||||
}, $records);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace React\Dns\Resolver;
|
||||
|
||||
interface ResolverInterface
|
||||
{
|
||||
/**
|
||||
* Resolves the given $domain name to a single IPv4 address (type `A` query).
|
||||
*
|
||||
* ```php
|
||||
* $resolver->resolve('reactphp.org')->then(function ($ip) {
|
||||
* echo 'IP for reactphp.org is ' . $ip . PHP_EOL;
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* This is one of the main methods in this package. It sends a DNS query
|
||||
* for the given $domain name to your DNS server and returns a single IP
|
||||
* address on success.
|
||||
*
|
||||
* If the DNS server sends a DNS response message that contains more than
|
||||
* one IP address for this query, it will randomly pick one of the IP
|
||||
* addresses from the response. If you want the full list of IP addresses
|
||||
* or want to send a different type of query, you should use the
|
||||
* [`resolveAll()`](#resolveall) method instead.
|
||||
*
|
||||
* If the DNS server sends a DNS response message that indicates an error
|
||||
* code, this method will reject with a `RecordNotFoundException`. Its
|
||||
* message and code can be used to check for the response code.
|
||||
*
|
||||
* If the DNS communication fails and the server does not respond with a
|
||||
* valid response message, this message will reject with an `Exception`.
|
||||
*
|
||||
* Pending DNS queries can be cancelled by cancelling its pending promise like so:
|
||||
*
|
||||
* ```php
|
||||
* $promise = $resolver->resolve('reactphp.org');
|
||||
*
|
||||
* $promise->cancel();
|
||||
* ```
|
||||
*
|
||||
* @param string $domain
|
||||
* @return \React\Promise\PromiseInterface<string>
|
||||
* resolves with a single IP address on success or rejects with an Exception on error.
|
||||
*/
|
||||
public function resolve($domain);
|
||||
|
||||
/**
|
||||
* Resolves all record values for the given $domain name and query $type.
|
||||
*
|
||||
* ```php
|
||||
* $resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) {
|
||||
* echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
|
||||
* });
|
||||
*
|
||||
* $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
|
||||
* echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* This is one of the main methods in this package. It sends a DNS query
|
||||
* for the given $domain name to your DNS server and returns a list with all
|
||||
* record values on success.
|
||||
*
|
||||
* If the DNS server sends a DNS response message that contains one or more
|
||||
* records for this query, it will return a list with all record values
|
||||
* from the response. You can use the `Message::TYPE_*` constants to control
|
||||
* which type of query will be sent. Note that this method always returns a
|
||||
* list of record values, but each record value type depends on the query
|
||||
* type. For example, it returns the IPv4 addresses for type `A` queries,
|
||||
* the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`,
|
||||
* `CNAME` and `PTR` queries and structured data for other queries. See also
|
||||
* the `Record` documentation for more details.
|
||||
*
|
||||
* If the DNS server sends a DNS response message that indicates an error
|
||||
* code, this method will reject with a `RecordNotFoundException`. Its
|
||||
* message and code can be used to check for the response code.
|
||||
*
|
||||
* If the DNS communication fails and the server does not respond with a
|
||||
* valid response message, this message will reject with an `Exception`.
|
||||
*
|
||||
* Pending DNS queries can be cancelled by cancelling its pending promise like so:
|
||||
*
|
||||
* ```php
|
||||
* $promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA);
|
||||
*
|
||||
* $promise->cancel();
|
||||
* ```
|
||||
*
|
||||
* @param string $domain
|
||||
* @return \React\Promise\PromiseInterface<array>
|
||||
* Resolves with all record values on success or rejects with an Exception on error.
|
||||
*/
|
||||
public function resolveAll($domain, $type);
|
||||
}
|
||||
Vendored
+473
@@ -0,0 +1,473 @@
|
||||
# Changelog
|
||||
|
||||
## 1.6.0 (2025-11-17)
|
||||
|
||||
* Feature: Improve PHP 8.5+ support by avoiding deprecated method calls.
|
||||
(#280 and #282 by @WyriHaximus)
|
||||
|
||||
## 1.5.0 (2023-11-13)
|
||||
|
||||
* Feature: Improve performance by using `spl_object_id()` on PHP 7.2+.
|
||||
(#267 by @samsonasik)
|
||||
|
||||
* Feature: Full PHP 8.3 compatibility.
|
||||
(#269 by @clue)
|
||||
|
||||
* Update tests for `ext-uv` on PHP 8+ and legacy PHP.
|
||||
(#270 by @clue and #268 by @SimonFrings)
|
||||
|
||||
## 1.4.0 (2023-05-05)
|
||||
|
||||
* Feature: Improve performance of `Loop` by avoiding unneeded method calls.
|
||||
(#266 by @clue)
|
||||
|
||||
* Feature: Support checking `EINTR` constant from `ext-pcntl` without `ext-sockets`.
|
||||
(#265 by @clue)
|
||||
|
||||
* Minor documentation improvements.
|
||||
(#254 by @nhedger)
|
||||
|
||||
* Improve test suite, run tests on PHP 8.2 and report failed assertions.
|
||||
(#258 by @WyriHaximus, #264 by @clue and #251, #261 and #262 by @SimonFrings)
|
||||
|
||||
## 1.3.0 (2022-03-17)
|
||||
|
||||
* Feature: Improve default `StreamSelectLoop` to report any warnings for invalid streams.
|
||||
(#245 by @clue)
|
||||
|
||||
* Feature: Improve performance of `StreamSelectLoop` when no timers are scheduled.
|
||||
(#246 by @clue)
|
||||
|
||||
* Fix: Fix periodic timer with zero interval for `ExtEvLoop` and legacy `ExtLibevLoop`.
|
||||
(#243 by @lucasnetau)
|
||||
|
||||
* Minor documentation improvements, update PHP version references.
|
||||
(#240, #248 and #250 by @SimonFrings, #241 by @dbu and #249 by @clue)
|
||||
|
||||
* Improve test suite and test against PHP 8.1.
|
||||
(#238 by @WyriHaximus and #242 by @clue)
|
||||
|
||||
## 1.2.0 (2021-07-11)
|
||||
|
||||
A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop).
|
||||
|
||||
* Feature: Introduce new concept of default loop with the new `Loop` class.
|
||||
(#226 by @WyriHaximus, #229, #231 and #232 by @clue)
|
||||
|
||||
The `Loop` class exists as a convenient global accessor for the event loop.
|
||||
It provides all methods that exist on the `LoopInterface` as static methods and
|
||||
will automatically execute the loop at the end of the program:
|
||||
|
||||
```php
|
||||
$timer = Loop::addPeriodicTimer(0.1, function () {
|
||||
echo 'Tick' . PHP_EOL;
|
||||
});
|
||||
|
||||
Loop::addTimer(1.0, function () use ($timer) {
|
||||
Loop::cancelTimer($timer);
|
||||
echo 'Done' . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
The explicit loop instructions are still valid and may still be useful in some applications,
|
||||
especially for a transition period towards the more concise style.
|
||||
The `Loop::get()` method can be used to get the currently active event loop instance.
|
||||
|
||||
```php
|
||||
// deprecated
|
||||
$loop = React\EventLoop\Factory::create();
|
||||
|
||||
// new
|
||||
$loop = React\EventLoop\Loop::get();
|
||||
```
|
||||
|
||||
* Minor documentation improvements and mark legacy extensions as deprecated.
|
||||
(#234 by @SimonFrings, #214 by @WyriHaximus and #233 and #235 by @nhedger)
|
||||
|
||||
* Improve test suite, use GitHub actions for continuous integration (CI),
|
||||
update PHPUnit config and run tests on PHP 8.
|
||||
(#212 and #215 by @SimonFrings and #230 by @clue)
|
||||
|
||||
## 1.1.1 (2020-01-01)
|
||||
|
||||
* Fix: Fix reporting connection refused errors with `ExtUvLoop` on Linux and `StreamSelectLoop` on Windows.
|
||||
(#207 and #208 by @clue)
|
||||
|
||||
* Fix: Fix unsupported EventConfig and `SEGFAULT` on shutdown with `ExtEventLoop` on Windows.
|
||||
(#205 by @clue)
|
||||
|
||||
* Fix: Prevent interval overflow for timers very far in the future with `ExtUvLoop`.
|
||||
(#196 by @PabloKowalczyk)
|
||||
|
||||
* Fix: Check PCNTL functions for signal support instead of PCNTL extension with `StreamSelectLoop`.
|
||||
(#195 by @clue)
|
||||
|
||||
* Add `.gitattributes` to exclude dev files from exports.
|
||||
(#201 by @reedy)
|
||||
|
||||
* Improve test suite to fix testing `ExtUvLoop` on Travis,
|
||||
fix Travis CI builds, do not install `libuv` on legacy PHP setups,
|
||||
fix failing test cases due to inaccurate timers,
|
||||
run tests on Windows via Travis CI and
|
||||
run tests on PHP 7.4 and simplify test matrix and test setup.
|
||||
(#197 by @WyriHaximus and #202, #203, #204 and #209 by @clue)
|
||||
|
||||
## 1.1.0 (2019-02-07)
|
||||
|
||||
* New UV based event loop (ext-uv).
|
||||
(#112 by @WyriHaximus)
|
||||
|
||||
* Use high resolution timer on PHP 7.3+.
|
||||
(#182 by @clue)
|
||||
|
||||
* Improve PCNTL signals by using async signal dispatching if available.
|
||||
(#179 by @CharlotteDunois)
|
||||
|
||||
* Improve test suite and test suite set up.
|
||||
(#174 by @WyriHaximus, #181 by @clue)
|
||||
|
||||
* Fix PCNTL signals edge case.
|
||||
(#183 by @clue)
|
||||
|
||||
## 1.0.0 (2018-07-11)
|
||||
|
||||
* First stable LTS release, now following [SemVer](https://semver.org/).
|
||||
We'd like to emphasize that this component is production ready and battle-tested.
|
||||
We plan to support all long-term support (LTS) releases for at least 24 months,
|
||||
so you have a rock-solid foundation to build on top of.
|
||||
|
||||
> Contains no other changes, so it's actually fully compatible with the v0.5.3 release.
|
||||
|
||||
## 0.5.3 (2018-07-09)
|
||||
|
||||
* Improve performance by importing global functions.
|
||||
(#167 by @Ocramius)
|
||||
|
||||
* Improve test suite by simplifying test bootstrap by using dev autoloader.
|
||||
(#169 by @lcobucci)
|
||||
|
||||
* Minor internal changes to improved backward compatibility with PHP 5.3.
|
||||
(#166 by @Donatello-za)
|
||||
|
||||
## 0.5.2 (2018-04-24)
|
||||
|
||||
* Feature: Improve memory consumption and runtime performance for `StreamSelectLoop` timers.
|
||||
(#164 by @clue)
|
||||
|
||||
* Improve test suite by removing I/O dependency at `StreamSelectLoopTest` to fix Mac OS X tests.
|
||||
(#161 by @nawarian)
|
||||
|
||||
## 0.5.1 (2018-04-09)
|
||||
|
||||
* Feature: New `ExtEvLoop` (PECL ext-ev) (#148 by @kaduev13)
|
||||
|
||||
## 0.5.0 (2018-04-05)
|
||||
|
||||
A major feature release with a significant documentation overhaul and long overdue API cleanup!
|
||||
|
||||
This update involves a number of BC breaks due to dropped support for deprecated
|
||||
functionality. We've tried hard to avoid BC breaks where possible and minimize
|
||||
impact otherwise. We expect that most consumers of this package will actually
|
||||
not be affected by any BC breaks, see below for more details.
|
||||
|
||||
We realize that the changes listed below may seem overwhelming, but we've tried
|
||||
to be very clear about any possible BC breaks. Don't worry: In fact, all ReactPHP
|
||||
components are already compatible and support both this new release as well as
|
||||
providing backwards compatibility with the last release.
|
||||
|
||||
* Feature / BC break: Add support for signal handling via new
|
||||
`LoopInterface::addSignal()` and `LoopInterface::removeSignal()` methods.
|
||||
(#104 by @WyriHaximus and #111 and #150 by @clue)
|
||||
|
||||
```php
|
||||
$loop->addSignal(SIGINT, function () {
|
||||
echo 'CTRL-C';
|
||||
});
|
||||
```
|
||||
|
||||
* Feature: Significant documentation updates for `LoopInterface` and `Factory`.
|
||||
(#100, #119, #126, #127, #159 and #160 by @clue, #113 by @WyriHaximus and #81 and #91 by @jsor)
|
||||
|
||||
* Feature: Add examples to ease getting started
|
||||
(#99, #100 and #125 by @clue, #59 by @WyriHaximus and #143 by @jsor)
|
||||
|
||||
* Feature: Documentation for advanced timer concepts, such as monotonic time source vs wall-clock time
|
||||
and high precision timers with millisecond accuracy or below.
|
||||
(#130 and #157 by @clue)
|
||||
|
||||
* Feature: Documentation for advanced stream concepts, such as edge-triggered event listeners
|
||||
and stream buffers and allow throwing Exception if stream resource is not supported.
|
||||
(#129 and #158 by @clue)
|
||||
|
||||
* Feature: Throw `BadMethodCallException` on manual loop creation when required extension isn't installed.
|
||||
(#153 by @WyriHaximus)
|
||||
|
||||
* Feature / BC break: First class support for legacy PHP 5.3 through PHP 7.2 and HHVM
|
||||
and remove all `callable` type hints for consistency reasons.
|
||||
(#141 and #151 by @clue)
|
||||
|
||||
* BC break: Documentation for timer API and clean up unneeded timer API.
|
||||
(#102 by @clue)
|
||||
|
||||
Remove `TimerInterface::cancel()`, use `LoopInterface::cancelTimer()` instead:
|
||||
|
||||
```php
|
||||
// old (method invoked on timer instance)
|
||||
$timer->cancel();
|
||||
|
||||
// already supported before: invoke method on loop instance
|
||||
$loop->cancelTimer($timer);
|
||||
```
|
||||
|
||||
Remove unneeded `TimerInterface::setData()` and `TimerInterface::getData()`,
|
||||
use closure binding to add arbitrary data to timer instead:
|
||||
|
||||
```php
|
||||
// old (limited setData() and getData() only allows single variable)
|
||||
$name = 'Tester';
|
||||
$timer = $loop->addTimer(1.0, function ($timer) {
|
||||
echo 'Hello ' . $timer->getData() . PHP_EOL;
|
||||
});
|
||||
$timer->setData($name);
|
||||
|
||||
// already supported before: closure binding allows any number of variables
|
||||
$name = 'Tester';
|
||||
$loop->addTimer(1.0, function () use ($name) {
|
||||
echo 'Hello ' . $name . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
Remove unneeded `TimerInterface::getLoop()`, use closure binding instead:
|
||||
|
||||
```php
|
||||
// old (getLoop() called on timer instance)
|
||||
$loop->addTimer(0.1, function ($timer) {
|
||||
$timer->getLoop()->stop();
|
||||
});
|
||||
|
||||
// already supported before: use closure binding as usual
|
||||
$loop->addTimer(0.1, function () use ($loop) {
|
||||
$loop->stop();
|
||||
});
|
||||
```
|
||||
|
||||
* BC break: Remove unneeded `LoopInterface::isTimerActive()` and
|
||||
`TimerInterface::isActive()` to reduce API surface.
|
||||
(#133 by @clue)
|
||||
|
||||
```php
|
||||
// old (method on timer instance or on loop instance)
|
||||
$timer->isActive();
|
||||
$loop->isTimerActive($timer);
|
||||
```
|
||||
|
||||
* BC break: Move `TimerInterface` one level up to `React\EventLoop\TimerInterface`.
|
||||
(#138 by @WyriHaximus)
|
||||
|
||||
```php
|
||||
// old (notice obsolete "Timer" namespace)
|
||||
assert($timer instanceof React\EventLoop\Timer\TimerInterface);
|
||||
|
||||
// new
|
||||
assert($timer instanceof React\EventLoop\TimerInterface);
|
||||
```
|
||||
|
||||
* BC break: Remove unneeded `LoopInterface::nextTick()` (and internal `NextTickQueue`),
|
||||
use `LoopInterface::futureTick()` instead.
|
||||
(#30 by @clue)
|
||||
|
||||
```php
|
||||
// old (removed)
|
||||
$loop->nextTick(function () {
|
||||
echo 'tick';
|
||||
});
|
||||
|
||||
// already supported before
|
||||
$loop->futureTick(function () {
|
||||
echo 'tick';
|
||||
});
|
||||
```
|
||||
|
||||
* BC break: Remove unneeded `$loop` argument for `LoopInterface::futureTick()`
|
||||
(and fix internal cyclic dependency).
|
||||
(#103 by @clue)
|
||||
|
||||
```php
|
||||
// old ($loop gets passed by default)
|
||||
$loop->futureTick(function ($loop) {
|
||||
$loop->stop();
|
||||
});
|
||||
|
||||
// already supported before: use closure binding as usual
|
||||
$loop->futureTick(function () use ($loop) {
|
||||
$loop->stop();
|
||||
});
|
||||
```
|
||||
|
||||
* BC break: Remove unneeded `LoopInterface::tick()`.
|
||||
(#72 by @jsor)
|
||||
|
||||
```php
|
||||
// old (removed)
|
||||
$loop->tick();
|
||||
|
||||
// suggested work around for testing purposes only
|
||||
$loop->futureTick(function () use ($loop) {
|
||||
$loop->stop();
|
||||
});
|
||||
```
|
||||
|
||||
* BC break: Documentation for advanced stream API and clean up unneeded stream API.
|
||||
(#110 by @clue)
|
||||
|
||||
Remove unneeded `$loop` argument for `LoopInterface::addReadStream()`
|
||||
and `LoopInterface::addWriteStream()`, use closure binding instead:
|
||||
|
||||
```php
|
||||
// old ($loop gets passed by default)
|
||||
$loop->addReadStream($stream, function ($stream, $loop) {
|
||||
$loop->removeReadStream($stream);
|
||||
});
|
||||
|
||||
// already supported before: use closure binding as usual
|
||||
$loop->addReadStream($stream, function ($stream) use ($loop) {
|
||||
$loop->removeReadStream($stream);
|
||||
});
|
||||
```
|
||||
|
||||
* BC break: Remove unneeded `LoopInterface::removeStream()` method,
|
||||
use `LoopInterface::removeReadStream()` and `LoopInterface::removeWriteStream()` instead.
|
||||
(#118 by @clue)
|
||||
|
||||
```php
|
||||
// old
|
||||
$loop->removeStream($stream);
|
||||
|
||||
// already supported before
|
||||
$loop->removeReadStream($stream);
|
||||
$loop->removeWriteStream($stream);
|
||||
```
|
||||
|
||||
* BC break: Rename `LibEventLoop` to `ExtLibeventLoop` and `LibEvLoop` to `ExtLibevLoop`
|
||||
for consistent naming for event loop implementations.
|
||||
(#128 by @clue)
|
||||
|
||||
* BC break: Remove optional `EventBaseConfig` argument from `ExtEventLoop`
|
||||
and make its `FEATURE_FDS` enabled by default.
|
||||
(#156 by @WyriHaximus)
|
||||
|
||||
* BC break: Mark all classes as final to discourage inheritance.
|
||||
(#131 by @clue)
|
||||
|
||||
* Fix: Fix `ExtEventLoop` to keep track of stream resources (refcount)
|
||||
(#123 by @clue)
|
||||
|
||||
* Fix: Ensure large timer interval does not overflow on 32bit systems
|
||||
(#132 by @clue)
|
||||
|
||||
* Fix: Fix separately removing readable and writable side of stream when closing
|
||||
(#139 by @clue)
|
||||
|
||||
* Fix: Properly clean up event watchers for `ext-event` and `ext-libev`
|
||||
(#149 by @clue)
|
||||
|
||||
* Fix: Minor code cleanup and remove unneeded references
|
||||
(#145 by @seregazhuk)
|
||||
|
||||
* Fix: Discourage outdated `ext-libevent` on PHP 7
|
||||
(#62 by @cboden)
|
||||
|
||||
* Improve test suite by adding forward compatibility with PHPUnit 6 and PHPUnit 5,
|
||||
lock Travis distro so new defaults will not break the build,
|
||||
improve test suite to be less fragile and increase test timeouts,
|
||||
test against PHP 7.2 and reduce fwrite() call length to one chunk.
|
||||
(#106 and #144 by @clue, #120 and #124 by @carusogabriel, #147 by nawarian and #92 by @kelunik)
|
||||
|
||||
* A number of changes were originally planned for this release but have been backported
|
||||
to the last `v0.4.3` already: #74, #76, #79, #81 (refs #65, #66, #67), #88 and #93
|
||||
|
||||
## 0.4.3 (2017-04-27)
|
||||
|
||||
* Bug fix: Bugfix in the usage sample code #57 (@dandelionred)
|
||||
* Improvement: Remove branch-alias definition #53 (@WyriHaximus)
|
||||
* Improvement: StreamSelectLoop: Use fresh time so Timers added during stream events are accurate #51 (@andrewminerd)
|
||||
* Improvement: Avoid deprecation warnings in test suite due to deprecation of getMock() in PHPUnit #68 (@martinschroeder)
|
||||
* Improvement: Add PHPUnit 4.8 to require-dev #69 (@shaunbramley)
|
||||
* Improvement: Increase test timeouts for HHVM and unify timeout handling #70 (@clue)
|
||||
* Improvement: Travis improvements (backported from #74) #75 (@clue)
|
||||
* Improvement: Test suite now uses socket pairs instead of memory streams #66 (@martinschroeder)
|
||||
* Improvement: StreamSelectLoop: Test suite uses signal constant names in data provider #67 (@martinschroeder)
|
||||
* Improvement: ExtEventLoop: No longer suppress all errors #65 (@mamciek)
|
||||
* Improvement: Readme cleanup #89 (@jsor)
|
||||
* Improvement: Restructure and improve README #90 (@jsor)
|
||||
* Bug fix: StreamSelectLoop: Fix erroneous zero-time sleep (backport to 0.4) #94 (@jsor)
|
||||
|
||||
## 0.4.2 (2016-03-07)
|
||||
|
||||
* Bug fix: No longer error when signals sent to StreamSelectLoop
|
||||
* Support HHVM and PHP7 (@ondrejmirtes, @cebe)
|
||||
* Feature: Added support for EventConfig for ExtEventLoop (@steverhoades)
|
||||
* Bug fix: Fixed an issue loading loop extension libs via autoloader (@czarpino)
|
||||
|
||||
## 0.4.1 (2014-04-13)
|
||||
|
||||
* Bug fix: null timeout in StreamSelectLoop causing 100% CPU usage (@clue)
|
||||
* Bug fix: v0.3.4 changes merged for v0.4.1
|
||||
|
||||
## 0.4.0 (2014-02-02)
|
||||
|
||||
* Feature: Added `EventLoopInterface::nextTick()`, implemented in all event loops (@jmalloc)
|
||||
* Feature: Added `EventLoopInterface::futureTick()`, implemented in all event loops (@jmalloc)
|
||||
* Feature: Added `ExtEventLoop` implementation using pecl/event (@jmalloc)
|
||||
* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks
|
||||
* BC break: New method: `EventLoopInterface::nextTick()`
|
||||
* BC break: New method: `EventLoopInterface::futureTick()`
|
||||
* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0
|
||||
|
||||
## 0.3.5 (2016-12-28)
|
||||
|
||||
This is a compatibility release that eases upgrading to the v0.4 release branch.
|
||||
You should consider upgrading to the v0.4 release branch.
|
||||
|
||||
* Feature: Cap min timer interval at 1µs, thus improving compatibility with v0.4
|
||||
(#47 by @clue)
|
||||
|
||||
## 0.3.4 (2014-03-30)
|
||||
|
||||
* Bug fix: Changed StreamSelectLoop to use non-blocking behavior on tick() (@astephens25)
|
||||
|
||||
## 0.3.3 (2013-07-08)
|
||||
|
||||
* Bug fix: No error on removing non-existent streams (@clue)
|
||||
* Bug fix: Do not silently remove feof listeners in `LibEvLoop`
|
||||
|
||||
## 0.3.0 (2013-04-14)
|
||||
|
||||
* BC break: New timers API (@nrk)
|
||||
* BC break: Remove check on return value from stream callbacks (@nrk)
|
||||
|
||||
## 0.2.7 (2013-01-05)
|
||||
|
||||
* Bug fix: Fix libevent timers with PHP 5.3
|
||||
* Bug fix: Fix libevent timer cancellation (@nrk)
|
||||
|
||||
## 0.2.6 (2012-12-26)
|
||||
|
||||
* Bug fix: Plug memory issue in libevent timers (@cameronjacobson)
|
||||
* Bug fix: Correctly pause LibEvLoop on stop()
|
||||
|
||||
## 0.2.3 (2012-11-14)
|
||||
|
||||
* Feature: LibEvLoop, integration of `php-libev`
|
||||
|
||||
## 0.2.0 (2012-09-10)
|
||||
|
||||
* Version bump
|
||||
|
||||
## 0.1.1 (2012-07-12)
|
||||
|
||||
* Version bump
|
||||
|
||||
## 0.1.0 (2012-07-11)
|
||||
|
||||
* First tagged release
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Vendored
+930
@@ -0,0 +1,930 @@
|
||||
# EventLoop
|
||||
|
||||
[](https://github.com/reactphp/event-loop/actions)
|
||||
[](https://packagist.org/packages/react/event-loop)
|
||||
|
||||
[ReactPHP](https://reactphp.org/)'s core reactor event loop that libraries can use for evented I/O.
|
||||
|
||||
In order for async based libraries to be interoperable, they need to use the
|
||||
same event loop. This component provides a common `LoopInterface` that any
|
||||
library can target. This allows them to be used in the same loop, with one
|
||||
single [`run()`](#run) call that is controlled by the user.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
* [Quickstart example](#quickstart-example)
|
||||
* [Usage](#usage)
|
||||
* [Loop](#loop)
|
||||
* [Loop methods](#loop-methods)
|
||||
* [Loop autorun](#loop-autorun)
|
||||
* [get()](#get)
|
||||
* [~~Factory~~](#factory)
|
||||
* [~~create()~~](#create)
|
||||
* [Loop implementations](#loop-implementations)
|
||||
* [StreamSelectLoop](#streamselectloop)
|
||||
* [ExtEventLoop](#exteventloop)
|
||||
* [ExtEvLoop](#extevloop)
|
||||
* [ExtUvLoop](#extuvloop)
|
||||
* [~~ExtLibeventLoop~~](#extlibeventloop)
|
||||
* [~~ExtLibevLoop~~](#extlibevloop)
|
||||
* [LoopInterface](#loopinterface)
|
||||
* [run()](#run)
|
||||
* [stop()](#stop)
|
||||
* [addTimer()](#addtimer)
|
||||
* [addPeriodicTimer()](#addperiodictimer)
|
||||
* [cancelTimer()](#canceltimer)
|
||||
* [futureTick()](#futuretick)
|
||||
* [addSignal()](#addsignal)
|
||||
* [removeSignal()](#removesignal)
|
||||
* [addReadStream()](#addreadstream)
|
||||
* [addWriteStream()](#addwritestream)
|
||||
* [removeReadStream()](#removereadstream)
|
||||
* [removeWriteStream()](#removewritestream)
|
||||
* [Install](#install)
|
||||
* [Tests](#tests)
|
||||
* [License](#license)
|
||||
* [More](#more)
|
||||
|
||||
## Quickstart example
|
||||
|
||||
Here is an async HTTP server built with just the event loop.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use React\EventLoop\Loop;
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
$server = stream_socket_server('tcp://127.0.0.1:8080');
|
||||
stream_set_blocking($server, false);
|
||||
|
||||
Loop::addReadStream($server, function ($server) {
|
||||
$conn = stream_socket_accept($server);
|
||||
$data = "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nHi\n";
|
||||
Loop::addWriteStream($conn, function ($conn) use (&$data) {
|
||||
$written = fwrite($conn, $data);
|
||||
if ($written === strlen($data)) {
|
||||
fclose($conn);
|
||||
Loop::removeWriteStream($conn);
|
||||
} else {
|
||||
$data = substr($data, $written);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Loop::addPeriodicTimer(5, function () {
|
||||
$memory = memory_get_usage() / 1024;
|
||||
$formatted = number_format($memory, 3).'K';
|
||||
echo "Current memory usage: {$formatted}\n";
|
||||
});
|
||||
```
|
||||
|
||||
See also the [examples](examples).
|
||||
|
||||
## Usage
|
||||
|
||||
Typical applications would use the [`Loop` class](#loop) to use the default
|
||||
event loop like this:
|
||||
|
||||
```php
|
||||
use React\EventLoop\Loop;
|
||||
|
||||
$timer = Loop::addPeriodicTimer(0.1, function () {
|
||||
echo 'Tick' . PHP_EOL;
|
||||
});
|
||||
|
||||
Loop::addTimer(1.0, function () use ($timer) {
|
||||
Loop::cancelTimer($timer);
|
||||
echo 'Done' . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
As an alternative, you can also explicitly create an event loop instance at the
|
||||
beginning, reuse it throughout your program and finally run it at the end of the
|
||||
program like this:
|
||||
|
||||
```php
|
||||
$loop = React\EventLoop\Loop::get(); // or deprecated React\EventLoop\Factory::create();
|
||||
|
||||
$timer = $loop->addPeriodicTimer(0.1, function () {
|
||||
echo 'Tick' . PHP_EOL;
|
||||
});
|
||||
|
||||
$loop->addTimer(1.0, function () use ($loop, $timer) {
|
||||
$loop->cancelTimer($timer);
|
||||
echo 'Done' . PHP_EOL;
|
||||
});
|
||||
|
||||
$loop->run();
|
||||
```
|
||||
|
||||
While the former is more concise, the latter is more explicit.
|
||||
In both cases, the program would perform the exact same steps.
|
||||
|
||||
1. The event loop instance is created at the beginning of the program. This is
|
||||
implicitly done the first time you call the [`Loop` class](#loop) or
|
||||
explicitly when using the deprecated [`Factory::create()` method](#create)
|
||||
(or manually instantiating any of the [loop implementations](#loop-implementations)).
|
||||
2. The event loop is used directly or passed as an instance to library and
|
||||
application code. In this example, a periodic timer is registered with the
|
||||
event loop which simply outputs `Tick` every fraction of a second until another
|
||||
timer stops the periodic timer after a second.
|
||||
3. The event loop is run at the end of the program. This is automatically done
|
||||
when using the [`Loop` class](#loop) or explicitly with a single [`run()`](#run)
|
||||
call at the end of the program.
|
||||
|
||||
As of `v1.2.0`, we highly recommend using the [`Loop` class](#loop).
|
||||
The explicit loop instructions are still valid and may still be useful in some
|
||||
applications, especially for a transition period towards the more concise style.
|
||||
|
||||
### Loop
|
||||
|
||||
The `Loop` class exists as a convenient global accessor for the event loop.
|
||||
|
||||
#### Loop methods
|
||||
|
||||
The `Loop` class provides all methods that exist on the [`LoopInterface`](#loopinterface)
|
||||
as static methods:
|
||||
|
||||
* [run()](#run)
|
||||
* [stop()](#stop)
|
||||
* [addTimer()](#addtimer)
|
||||
* [addPeriodicTimer()](#addperiodictimer)
|
||||
* [cancelTimer()](#canceltimer)
|
||||
* [futureTick()](#futuretick)
|
||||
* [addSignal()](#addsignal)
|
||||
* [removeSignal()](#removesignal)
|
||||
* [addReadStream()](#addreadstream)
|
||||
* [addWriteStream()](#addwritestream)
|
||||
* [removeReadStream()](#removereadstream)
|
||||
* [removeWriteStream()](#removewritestream)
|
||||
|
||||
If you're working with the event loop in your application code, it's often
|
||||
easiest to directly interface with the static methods defined on the `Loop` class
|
||||
like this:
|
||||
|
||||
```php
|
||||
use React\EventLoop\Loop;
|
||||
|
||||
$timer = Loop::addPeriodicTimer(0.1, function () {
|
||||
echo 'Tick' . PHP_EOL;
|
||||
});
|
||||
|
||||
Loop::addTimer(1.0, function () use ($timer) {
|
||||
Loop::cancelTimer($timer);
|
||||
echo 'Done' . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
On the other hand, if you're familiar with object-oriented programming (OOP) and
|
||||
dependency injection (DI), you may want to inject an event loop instance and
|
||||
invoke instance methods on the `LoopInterface` like this:
|
||||
|
||||
```php
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
class Greeter
|
||||
{
|
||||
private $loop;
|
||||
|
||||
public function __construct(LoopInterface $loop)
|
||||
{
|
||||
$this->loop = $loop;
|
||||
}
|
||||
|
||||
public function greet(string $name)
|
||||
{
|
||||
$this->loop->addTimer(1.0, function () use ($name) {
|
||||
echo 'Hello ' . $name . '!' . PHP_EOL;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$greeter = new Greeter(Loop::get());
|
||||
$greeter->greet('Alice');
|
||||
$greeter->greet('Bob');
|
||||
```
|
||||
|
||||
Each static method call will be forwarded as-is to the underlying event loop
|
||||
instance by using the [`Loop::get()`](#get) call internally.
|
||||
See [`LoopInterface`](#loopinterface) for more details about available methods.
|
||||
|
||||
#### Loop autorun
|
||||
|
||||
When using the `Loop` class, it will automatically execute the loop at the end of
|
||||
the program. This means the following example will schedule a timer and will
|
||||
automatically execute the program until the timer event fires:
|
||||
|
||||
```php
|
||||
use React\EventLoop\Loop;
|
||||
|
||||
Loop::addTimer(1.0, function () {
|
||||
echo 'Hello' . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
As of `v1.2.0`, we highly recommend using the `Loop` class this way and omitting any
|
||||
explicit [`run()`](#run) calls. For BC reasons, the explicit [`run()`](#run)
|
||||
method is still valid and may still be useful in some applications, especially
|
||||
for a transition period towards the more concise style.
|
||||
|
||||
If you don't want the `Loop` to run automatically, you can either explicitly
|
||||
[`run()`](#run) or [`stop()`](#stop) it. This can be useful if you're using
|
||||
a global exception handler like this:
|
||||
|
||||
```php
|
||||
use React\EventLoop\Loop;
|
||||
|
||||
Loop::addTimer(10.0, function () {
|
||||
echo 'Never happens';
|
||||
});
|
||||
|
||||
set_exception_handler(function (Throwable $e) {
|
||||
echo 'Error: ' . $e->getMessage() . PHP_EOL;
|
||||
Loop::stop();
|
||||
});
|
||||
|
||||
throw new RuntimeException('Demo');
|
||||
```
|
||||
|
||||
#### get()
|
||||
|
||||
The `get(): LoopInterface` method can be used to
|
||||
get the currently active event loop instance.
|
||||
|
||||
This method will always return the same event loop instance throughout the
|
||||
lifetime of your application.
|
||||
|
||||
```php
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
$loop = Loop::get();
|
||||
|
||||
assert($loop instanceof LoopInterface);
|
||||
assert($loop === Loop::get());
|
||||
```
|
||||
|
||||
This is particularly useful if you're using object-oriented programming (OOP)
|
||||
and dependency injection (DI). In this case, you may want to inject an event
|
||||
loop instance and invoke instance methods on the `LoopInterface` like this:
|
||||
|
||||
```php
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
class Greeter
|
||||
{
|
||||
private $loop;
|
||||
|
||||
public function __construct(LoopInterface $loop)
|
||||
{
|
||||
$this->loop = $loop;
|
||||
}
|
||||
|
||||
public function greet(string $name)
|
||||
{
|
||||
$this->loop->addTimer(1.0, function () use ($name) {
|
||||
echo 'Hello ' . $name . '!' . PHP_EOL;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$greeter = new Greeter(Loop::get());
|
||||
$greeter->greet('Alice');
|
||||
$greeter->greet('Bob');
|
||||
```
|
||||
|
||||
See [`LoopInterface`](#loopinterface) for more details about available methods.
|
||||
|
||||
### ~~Factory~~
|
||||
|
||||
> Deprecated since v1.2.0, see [`Loop` class](#loop) instead.
|
||||
|
||||
The deprecated `Factory` class exists as a convenient way to pick the best available
|
||||
[event loop implementation](#loop-implementations).
|
||||
|
||||
#### ~~create()~~
|
||||
|
||||
> Deprecated since v1.2.0, see [`Loop::get()`](#get) instead.
|
||||
|
||||
The deprecated `create(): LoopInterface` method can be used to
|
||||
create a new event loop instance:
|
||||
|
||||
```php
|
||||
// deprecated
|
||||
$loop = React\EventLoop\Factory::create();
|
||||
|
||||
// new
|
||||
$loop = React\EventLoop\Loop::get();
|
||||
```
|
||||
|
||||
This method always returns an instance implementing [`LoopInterface`](#loopinterface),
|
||||
the actual [event loop implementation](#loop-implementations) is an implementation detail.
|
||||
|
||||
This method should usually only be called once at the beginning of the program.
|
||||
|
||||
### Loop implementations
|
||||
|
||||
In addition to the [`LoopInterface`](#loopinterface), there are a number of
|
||||
event loop implementations provided.
|
||||
|
||||
All of the event loops support these features:
|
||||
|
||||
* File descriptor polling
|
||||
* One-off timers
|
||||
* Periodic timers
|
||||
* Deferred execution on future loop tick
|
||||
|
||||
For most consumers of this package, the underlying event loop implementation is
|
||||
an implementation detail.
|
||||
You should use the [`Loop` class](#loop) to automatically create a new instance.
|
||||
|
||||
Advanced! If you explicitly need a certain event loop implementation, you can
|
||||
manually instantiate one of the following classes.
|
||||
Note that you may have to install the required PHP extensions for the respective
|
||||
event loop implementation first or they will throw a `BadMethodCallException` on creation.
|
||||
|
||||
#### StreamSelectLoop
|
||||
|
||||
A `stream_select()` based event loop.
|
||||
|
||||
This uses the [`stream_select()`](https://www.php.net/manual/en/function.stream-select.php)
|
||||
function and is the only implementation that works out of the box with PHP.
|
||||
|
||||
This event loop works out of the box on PHP 5.3 through PHP 8+ and HHVM.
|
||||
This means that no installation is required and this library works on all
|
||||
platforms and supported PHP versions.
|
||||
Accordingly, the [`Loop` class](#loop) and the deprecated [`Factory`](#factory)
|
||||
will use this event loop by default if you do not install any of the event loop
|
||||
extensions listed below.
|
||||
|
||||
Under the hood, it does a simple `select` system call.
|
||||
This system call is limited to the maximum file descriptor number of
|
||||
`FD_SETSIZE` (platform dependent, commonly 1024) and scales with `O(m)`
|
||||
(`m` being the maximum file descriptor number passed).
|
||||
This means that you may run into issues when handling thousands of streams
|
||||
concurrently and you may want to look into using one of the alternative
|
||||
event loop implementations listed below in this case.
|
||||
If your use case is among the many common use cases that involve handling only
|
||||
dozens or a few hundred streams at once, then this event loop implementation
|
||||
performs really well.
|
||||
|
||||
If you want to use signal handling (see also [`addSignal()`](#addsignal) below),
|
||||
this event loop implementation requires `ext-pcntl`.
|
||||
This extension is only available for Unix-like platforms and does not support
|
||||
Windows.
|
||||
It is commonly installed as part of many PHP distributions.
|
||||
If this extension is missing (or you're running on Windows), signal handling is
|
||||
not supported and throws a `BadMethodCallException` instead.
|
||||
|
||||
This event loop is known to rely on wall-clock time to schedule future timers
|
||||
when using any version before PHP 7.3, because a monotonic time source is
|
||||
only available as of PHP 7.3 (`hrtime()`).
|
||||
While this does not affect many common use cases, this is an important
|
||||
distinction for programs that rely on a high time precision or on systems
|
||||
that are subject to discontinuous time adjustments (time jumps).
|
||||
This means that if you schedule a timer to trigger in 30s on PHP < 7.3 and
|
||||
then adjust your system time forward by 20s, the timer may trigger in 10s.
|
||||
See also [`addTimer()`](#addtimer) for more details.
|
||||
|
||||
#### ExtEventLoop
|
||||
|
||||
An `ext-event` based event loop.
|
||||
|
||||
This uses the [`event` PECL extension](https://pecl.php.net/package/event),
|
||||
that provides an interface to `libevent` library.
|
||||
`libevent` itself supports a number of system-specific backends (epoll, kqueue).
|
||||
|
||||
This loop is known to work with PHP 5.4 through PHP 8+.
|
||||
|
||||
#### ExtEvLoop
|
||||
|
||||
An `ext-ev` based event loop.
|
||||
|
||||
This loop uses the [`ev` PECL extension](https://pecl.php.net/package/ev),
|
||||
that provides an interface to `libev` library.
|
||||
`libev` itself supports a number of system-specific backends (epoll, kqueue).
|
||||
|
||||
|
||||
This loop is known to work with PHP 5.4 through PHP 8+.
|
||||
|
||||
#### ExtUvLoop
|
||||
|
||||
An `ext-uv` based event loop.
|
||||
|
||||
This loop uses the [`uv` PECL extension](https://pecl.php.net/package/uv),
|
||||
that provides an interface to `libuv` library.
|
||||
`libuv` itself supports a number of system-specific backends (epoll, kqueue).
|
||||
|
||||
This loop is known to work with PHP 7+.
|
||||
|
||||
#### ~~ExtLibeventLoop~~
|
||||
|
||||
> Deprecated since v1.2.0, use [`ExtEventLoop`](#exteventloop) instead.
|
||||
|
||||
An `ext-libevent` based event loop.
|
||||
|
||||
This uses the [`libevent` PECL extension](https://pecl.php.net/package/libevent),
|
||||
that provides an interface to `libevent` library.
|
||||
`libevent` itself supports a number of system-specific backends (epoll, kqueue).
|
||||
|
||||
This event loop does only work with PHP 5.
|
||||
An [unofficial update](https://github.com/php/pecl-event-libevent/pull/2) for
|
||||
PHP 7 does exist, but it is known to cause regular crashes due to `SEGFAULT`s.
|
||||
To reiterate: Using this event loop on PHP 7 is not recommended.
|
||||
Accordingly, neither the [`Loop` class](#loop) nor the deprecated
|
||||
[`Factory` class](#factory) will try to use this event loop on PHP 7.
|
||||
|
||||
This event loop is known to trigger a readable listener only if
|
||||
the stream *becomes* readable (edge-triggered) and may not trigger if the
|
||||
stream has already been readable from the beginning.
|
||||
This also implies that a stream may not be recognized as readable when data
|
||||
is still left in PHP's internal stream buffers.
|
||||
As such, it's recommended to use `stream_set_read_buffer($stream, 0);`
|
||||
to disable PHP's internal read buffer in this case.
|
||||
See also [`addReadStream()`](#addreadstream) for more details.
|
||||
|
||||
#### ~~ExtLibevLoop~~
|
||||
|
||||
> Deprecated since v1.2.0, use [`ExtEvLoop`](#extevloop) instead.
|
||||
|
||||
An `ext-libev` based event loop.
|
||||
|
||||
This uses an [unofficial `libev` extension](https://github.com/m4rw3r/php-libev),
|
||||
that provides an interface to `libev` library.
|
||||
`libev` itself supports a number of system-specific backends (epoll, kqueue).
|
||||
|
||||
This loop does only work with PHP 5.
|
||||
An update for PHP 7 is [unlikely](https://github.com/m4rw3r/php-libev/issues/8)
|
||||
to happen any time soon.
|
||||
|
||||
### LoopInterface
|
||||
|
||||
#### run()
|
||||
|
||||
The `run(): void` method can be used to
|
||||
run the event loop until there are no more tasks to perform.
|
||||
|
||||
For many applications, this method is the only directly visible
|
||||
invocation on the event loop.
|
||||
As a rule of thumb, it is usually recommended to attach everything to the
|
||||
same loop instance and then run the loop once at the bottom end of the
|
||||
application.
|
||||
|
||||
```php
|
||||
$loop->run();
|
||||
```
|
||||
|
||||
This method will keep the loop running until there are no more tasks
|
||||
to perform. In other words: This method will block until the last
|
||||
timer, stream and/or signal has been removed.
|
||||
|
||||
Likewise, it is imperative to ensure the application actually invokes
|
||||
this method once. Adding listeners to the loop and missing to actually
|
||||
run it will result in the application exiting without actually waiting
|
||||
for any of the attached listeners.
|
||||
|
||||
This method MUST NOT be called while the loop is already running.
|
||||
This method MAY be called more than once after it has explicitly been
|
||||
[`stop()`ped](#stop) or after it automatically stopped because it
|
||||
previously did no longer have anything to do.
|
||||
|
||||
#### stop()
|
||||
|
||||
The `stop(): void` method can be used to
|
||||
instruct a running event loop to stop.
|
||||
|
||||
This method is considered advanced usage and should be used with care.
|
||||
As a rule of thumb, it is usually recommended to let the loop stop
|
||||
only automatically when it no longer has anything to do.
|
||||
|
||||
This method can be used to explicitly instruct the event loop to stop:
|
||||
|
||||
```php
|
||||
$loop->addTimer(3.0, function () use ($loop) {
|
||||
$loop->stop();
|
||||
});
|
||||
```
|
||||
|
||||
Calling this method on a loop instance that is not currently running or
|
||||
on a loop instance that has already been stopped has no effect.
|
||||
|
||||
#### addTimer()
|
||||
|
||||
The `addTimer(float $interval, callable $callback): TimerInterface` method can be used to
|
||||
enqueue a callback to be invoked once after the given interval.
|
||||
|
||||
The second parameter MUST be a timer callback function that accepts
|
||||
the timer instance as its only parameter.
|
||||
If you don't use the timer instance inside your timer callback function
|
||||
you MAY use a function which has no parameters at all.
|
||||
|
||||
The timer callback function MUST NOT throw an `Exception`.
|
||||
The return value of the timer callback function will be ignored and has
|
||||
no effect, so for performance reasons you're recommended to not return
|
||||
any excessive data structures.
|
||||
|
||||
This method returns a timer instance. The same timer instance will also be
|
||||
passed into the timer callback function as described above.
|
||||
You can invoke [`cancelTimer`](#canceltimer) to cancel a pending timer.
|
||||
Unlike [`addPeriodicTimer()`](#addperiodictimer), this method will ensure
|
||||
the callback will be invoked only once after the given interval.
|
||||
|
||||
```php
|
||||
$loop->addTimer(0.8, function () {
|
||||
echo 'world!' . PHP_EOL;
|
||||
});
|
||||
|
||||
$loop->addTimer(0.3, function () {
|
||||
echo 'hello ';
|
||||
});
|
||||
```
|
||||
|
||||
See also [example #1](examples).
|
||||
|
||||
If you want to access any variables within your callback function, you
|
||||
can bind arbitrary data to a callback closure like this:
|
||||
|
||||
```php
|
||||
function hello($name, LoopInterface $loop)
|
||||
{
|
||||
$loop->addTimer(1.0, function () use ($name) {
|
||||
echo "hello $name\n";
|
||||
});
|
||||
}
|
||||
|
||||
hello('Tester', $loop);
|
||||
```
|
||||
|
||||
This interface does not enforce any particular timer resolution, so
|
||||
special care may have to be taken if you rely on very high precision with
|
||||
millisecond accuracy or below. Event loop implementations SHOULD work on
|
||||
a best effort basis and SHOULD provide at least millisecond accuracy
|
||||
unless otherwise noted. Many existing event loop implementations are
|
||||
known to provide microsecond accuracy, but it's generally not recommended
|
||||
to rely on this high precision.
|
||||
|
||||
Similarly, the execution order of timers scheduled to execute at the
|
||||
same time (within its possible accuracy) is not guaranteed.
|
||||
|
||||
This interface suggests that event loop implementations SHOULD use a
|
||||
monotonic time source if available. Given that a monotonic time source is
|
||||
only available as of PHP 7.3 by default, event loop implementations MAY
|
||||
fall back to using wall-clock time.
|
||||
While this does not affect many common use cases, this is an important
|
||||
distinction for programs that rely on a high time precision or on systems
|
||||
that are subject to discontinuous time adjustments (time jumps).
|
||||
This means that if you schedule a timer to trigger in 30s and then adjust
|
||||
your system time forward by 20s, the timer SHOULD still trigger in 30s.
|
||||
See also [event loop implementations](#loop-implementations) for more details.
|
||||
|
||||
#### addPeriodicTimer()
|
||||
|
||||
The `addPeriodicTimer(float $interval, callable $callback): TimerInterface` method can be used to
|
||||
enqueue a callback to be invoked repeatedly after the given interval.
|
||||
|
||||
The second parameter MUST be a timer callback function that accepts
|
||||
the timer instance as its only parameter.
|
||||
If you don't use the timer instance inside your timer callback function
|
||||
you MAY use a function which has no parameters at all.
|
||||
|
||||
The timer callback function MUST NOT throw an `Exception`.
|
||||
The return value of the timer callback function will be ignored and has
|
||||
no effect, so for performance reasons you're recommended to not return
|
||||
any excessive data structures.
|
||||
|
||||
This method returns a timer instance. The same timer instance will also be
|
||||
passed into the timer callback function as described above.
|
||||
Unlike [`addTimer()`](#addtimer), this method will ensure the callback
|
||||
will be invoked infinitely after the given interval or until you invoke
|
||||
[`cancelTimer`](#canceltimer).
|
||||
|
||||
```php
|
||||
$timer = $loop->addPeriodicTimer(0.1, function () {
|
||||
echo 'tick!' . PHP_EOL;
|
||||
});
|
||||
|
||||
$loop->addTimer(1.0, function () use ($loop, $timer) {
|
||||
$loop->cancelTimer($timer);
|
||||
echo 'Done' . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
See also [example #2](examples).
|
||||
|
||||
If you want to limit the number of executions, you can bind
|
||||
arbitrary data to a callback closure like this:
|
||||
|
||||
```php
|
||||
function hello($name, LoopInterface $loop)
|
||||
{
|
||||
$n = 3;
|
||||
$loop->addPeriodicTimer(1.0, function ($timer) use ($name, $loop, &$n) {
|
||||
if ($n > 0) {
|
||||
--$n;
|
||||
echo "hello $name\n";
|
||||
} else {
|
||||
$loop->cancelTimer($timer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hello('Tester', $loop);
|
||||
```
|
||||
|
||||
This interface does not enforce any particular timer resolution, so
|
||||
special care may have to be taken if you rely on very high precision with
|
||||
millisecond accuracy or below. Event loop implementations SHOULD work on
|
||||
a best effort basis and SHOULD provide at least millisecond accuracy
|
||||
unless otherwise noted. Many existing event loop implementations are
|
||||
known to provide microsecond accuracy, but it's generally not recommended
|
||||
to rely on this high precision.
|
||||
|
||||
Similarly, the execution order of timers scheduled to execute at the
|
||||
same time (within its possible accuracy) is not guaranteed.
|
||||
|
||||
This interface suggests that event loop implementations SHOULD use a
|
||||
monotonic time source if available. Given that a monotonic time source is
|
||||
only available as of PHP 7.3 by default, event loop implementations MAY
|
||||
fall back to using wall-clock time.
|
||||
While this does not affect many common use cases, this is an important
|
||||
distinction for programs that rely on a high time precision or on systems
|
||||
that are subject to discontinuous time adjustments (time jumps).
|
||||
This means that if you schedule a timer to trigger in 30s and then adjust
|
||||
your system time forward by 20s, the timer SHOULD still trigger in 30s.
|
||||
See also [event loop implementations](#loop-implementations) for more details.
|
||||
|
||||
Additionally, periodic timers may be subject to timer drift due to
|
||||
re-scheduling after each invocation. As such, it's generally not
|
||||
recommended to rely on this for high precision intervals with millisecond
|
||||
accuracy or below.
|
||||
|
||||
#### cancelTimer()
|
||||
|
||||
The `cancelTimer(TimerInterface $timer): void` method can be used to
|
||||
cancel a pending timer.
|
||||
|
||||
See also [`addPeriodicTimer()`](#addperiodictimer) and [example #2](examples).
|
||||
|
||||
Calling this method on a timer instance that has not been added to this
|
||||
loop instance or on a timer that has already been cancelled has no effect.
|
||||
|
||||
#### futureTick()
|
||||
|
||||
The `futureTick(callable $listener): void` method can be used to
|
||||
schedule a callback to be invoked on a future tick of the event loop.
|
||||
|
||||
This works very much similar to timers with an interval of zero seconds,
|
||||
but does not require the overhead of scheduling a timer queue.
|
||||
|
||||
The tick callback function MUST be able to accept zero parameters.
|
||||
|
||||
The tick callback function MUST NOT throw an `Exception`.
|
||||
The return value of the tick callback function will be ignored and has
|
||||
no effect, so for performance reasons you're recommended to not return
|
||||
any excessive data structures.
|
||||
|
||||
If you want to access any variables within your callback function, you
|
||||
can bind arbitrary data to a callback closure like this:
|
||||
|
||||
```php
|
||||
function hello($name, LoopInterface $loop)
|
||||
{
|
||||
$loop->futureTick(function () use ($name) {
|
||||
echo "hello $name\n";
|
||||
});
|
||||
}
|
||||
|
||||
hello('Tester', $loop);
|
||||
```
|
||||
|
||||
Unlike timers, tick callbacks are guaranteed to be executed in the order
|
||||
they are enqueued.
|
||||
Also, once a callback is enqueued, there's no way to cancel this operation.
|
||||
|
||||
This is often used to break down bigger tasks into smaller steps (a form
|
||||
of cooperative multitasking).
|
||||
|
||||
```php
|
||||
$loop->futureTick(function () {
|
||||
echo 'b';
|
||||
});
|
||||
$loop->futureTick(function () {
|
||||
echo 'c';
|
||||
});
|
||||
echo 'a';
|
||||
```
|
||||
|
||||
See also [example #3](examples).
|
||||
|
||||
#### addSignal()
|
||||
|
||||
The `addSignal(int $signal, callable $listener): void` method can be used to
|
||||
register a listener to be notified when a signal has been caught by this process.
|
||||
|
||||
This is useful to catch user interrupt signals or shutdown signals from
|
||||
tools like `supervisor` or `systemd`.
|
||||
|
||||
The second parameter MUST be a listener callback function that accepts
|
||||
the signal as its only parameter.
|
||||
If you don't use the signal inside your listener callback function
|
||||
you MAY use a function which has no parameters at all.
|
||||
|
||||
The listener callback function MUST NOT throw an `Exception`.
|
||||
The return value of the listener callback function will be ignored and has
|
||||
no effect, so for performance reasons you're recommended to not return
|
||||
any excessive data structures.
|
||||
|
||||
```php
|
||||
$loop->addSignal(SIGINT, function (int $signal) {
|
||||
echo 'Caught user interrupt signal' . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
See also [example #4](examples).
|
||||
|
||||
Signaling is only available on Unix-like platforms, Windows isn't
|
||||
supported due to operating system limitations.
|
||||
This method may throw a `BadMethodCallException` if signals aren't
|
||||
supported on this platform, for example when required extensions are
|
||||
missing.
|
||||
|
||||
**Note: A listener can only be added once to the same signal, any
|
||||
attempts to add it more than once will be ignored.**
|
||||
|
||||
#### removeSignal()
|
||||
|
||||
The `removeSignal(int $signal, callable $listener): void` method can be used to
|
||||
remove a previously added signal listener.
|
||||
|
||||
```php
|
||||
$loop->removeSignal(SIGINT, $listener);
|
||||
```
|
||||
|
||||
Any attempts to remove listeners that aren't registered will be ignored.
|
||||
|
||||
#### addReadStream()
|
||||
|
||||
> Advanced! Note that this low-level API is considered advanced usage.
|
||||
Most use cases should probably use the higher-level
|
||||
[readable Stream API](https://github.com/reactphp/stream#readablestreaminterface)
|
||||
instead.
|
||||
|
||||
The `addReadStream(resource $stream, callable $callback): void` method can be used to
|
||||
register a listener to be notified when a stream is ready to read.
|
||||
|
||||
The first parameter MUST be a valid stream resource that supports
|
||||
checking whether it is ready to read by this loop implementation.
|
||||
A single stream resource MUST NOT be added more than once.
|
||||
Instead, either call [`removeReadStream()`](#removereadstream) first or
|
||||
react to this event with a single listener and then dispatch from this
|
||||
listener. This method MAY throw an `Exception` if the given resource type
|
||||
is not supported by this loop implementation.
|
||||
|
||||
The second parameter MUST be a listener callback function that accepts
|
||||
the stream resource as its only parameter.
|
||||
If you don't use the stream resource inside your listener callback function
|
||||
you MAY use a function which has no parameters at all.
|
||||
|
||||
The listener callback function MUST NOT throw an `Exception`.
|
||||
The return value of the listener callback function will be ignored and has
|
||||
no effect, so for performance reasons you're recommended to not return
|
||||
any excessive data structures.
|
||||
|
||||
If you want to access any variables within your callback function, you
|
||||
can bind arbitrary data to a callback closure like this:
|
||||
|
||||
```php
|
||||
$loop->addReadStream($stream, function ($stream) use ($name) {
|
||||
echo $name . ' said: ' . fread($stream);
|
||||
});
|
||||
```
|
||||
|
||||
See also [example #11](examples).
|
||||
|
||||
You can invoke [`removeReadStream()`](#removereadstream) to remove the
|
||||
read event listener for this stream.
|
||||
|
||||
The execution order of listeners when multiple streams become ready at
|
||||
the same time is not guaranteed.
|
||||
|
||||
Some event loop implementations are known to only trigger the listener if
|
||||
the stream *becomes* readable (edge-triggered) and may not trigger if the
|
||||
stream has already been readable from the beginning.
|
||||
This also implies that a stream may not be recognized as readable when data
|
||||
is still left in PHP's internal stream buffers.
|
||||
As such, it's recommended to use `stream_set_read_buffer($stream, 0);`
|
||||
to disable PHP's internal read buffer in this case.
|
||||
|
||||
#### addWriteStream()
|
||||
|
||||
> Advanced! Note that this low-level API is considered advanced usage.
|
||||
Most use cases should probably use the higher-level
|
||||
[writable Stream API](https://github.com/reactphp/stream#writablestreaminterface)
|
||||
instead.
|
||||
|
||||
The `addWriteStream(resource $stream, callable $callback): void` method can be used to
|
||||
register a listener to be notified when a stream is ready to write.
|
||||
|
||||
The first parameter MUST be a valid stream resource that supports
|
||||
checking whether it is ready to write by this loop implementation.
|
||||
A single stream resource MUST NOT be added more than once.
|
||||
Instead, either call [`removeWriteStream()`](#removewritestream) first or
|
||||
react to this event with a single listener and then dispatch from this
|
||||
listener. This method MAY throw an `Exception` if the given resource type
|
||||
is not supported by this loop implementation.
|
||||
|
||||
The second parameter MUST be a listener callback function that accepts
|
||||
the stream resource as its only parameter.
|
||||
If you don't use the stream resource inside your listener callback function
|
||||
you MAY use a function which has no parameters at all.
|
||||
|
||||
The listener callback function MUST NOT throw an `Exception`.
|
||||
The return value of the listener callback function will be ignored and has
|
||||
no effect, so for performance reasons you're recommended to not return
|
||||
any excessive data structures.
|
||||
|
||||
If you want to access any variables within your callback function, you
|
||||
can bind arbitrary data to a callback closure like this:
|
||||
|
||||
```php
|
||||
$loop->addWriteStream($stream, function ($stream) use ($name) {
|
||||
fwrite($stream, 'Hello ' . $name);
|
||||
});
|
||||
```
|
||||
|
||||
See also [example #12](examples).
|
||||
|
||||
You can invoke [`removeWriteStream()`](#removewritestream) to remove the
|
||||
write event listener for this stream.
|
||||
|
||||
The execution order of listeners when multiple streams become ready at
|
||||
the same time is not guaranteed.
|
||||
|
||||
#### removeReadStream()
|
||||
|
||||
The `removeReadStream(resource $stream): void` method can be used to
|
||||
remove the read event listener for the given stream.
|
||||
|
||||
Removing a stream from the loop that has already been removed or trying
|
||||
to remove a stream that was never added or is invalid has no effect.
|
||||
|
||||
#### removeWriteStream()
|
||||
|
||||
The `removeWriteStream(resource $stream): void` method can be used to
|
||||
remove the write event listener for the given stream.
|
||||
|
||||
Removing a stream from the loop that has already been removed or trying
|
||||
to remove a stream that was never added or is invalid has no effect.
|
||||
|
||||
## Install
|
||||
|
||||
The recommended way to install this library is [through Composer](https://getcomposer.org/).
|
||||
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
|
||||
|
||||
This project follows [SemVer](https://semver.org/).
|
||||
This will install the latest supported version:
|
||||
|
||||
```bash
|
||||
composer require react/event-loop:^1.6
|
||||
```
|
||||
|
||||
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
|
||||
|
||||
This project aims to run on any platform and thus does not require any PHP
|
||||
extensions and supports running on legacy PHP 5.3 through current PHP 8+ and
|
||||
HHVM.
|
||||
It's *highly recommended to use the latest supported PHP version* for this project.
|
||||
|
||||
Installing any of the event loop extensions is suggested, but entirely optional.
|
||||
See also [event loop implementations](#loop-implementations) for more details.
|
||||
|
||||
## Tests
|
||||
|
||||
To run the test suite, you first need to clone this repo and then install all
|
||||
dependencies [through Composer](https://getcomposer.org/):
|
||||
|
||||
```bash
|
||||
composer install
|
||||
```
|
||||
|
||||
To run the test suite, go to the project root and run:
|
||||
|
||||
```bash
|
||||
vendor/bin/phpunit
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT, see [LICENSE file](LICENSE).
|
||||
|
||||
## More
|
||||
|
||||
* See our [Stream component](https://github.com/reactphp/stream) for more
|
||||
information on how streams are used in real-world applications.
|
||||
* See our [users wiki](https://github.com/reactphp/react/wiki/Users) and the
|
||||
[dependents on Packagist](https://packagist.org/packages/react/event-loop/dependents)
|
||||
for a list of packages that use the EventLoop in real-world applications.
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "react/event-loop",
|
||||
"description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.",
|
||||
"keywords": ["event-loop", "asynchronous"],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"homepage": "https://clue.engineering/",
|
||||
"email": "christian@clue.engineering"
|
||||
},
|
||||
{
|
||||
"name": "Cees-Jan Kiewiet",
|
||||
"homepage": "https://wyrihaximus.net/",
|
||||
"email": "reactphp@ceesjankiewiet.nl"
|
||||
},
|
||||
{
|
||||
"name": "Jan Sorgalla",
|
||||
"homepage": "https://sorgalla.com/",
|
||||
"email": "jsorgalla@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Chris Boden",
|
||||
"homepage": "https://cboden.dev/",
|
||||
"email": "cboden@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-pcntl": "For signal handling support when using the StreamSelectLoop"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"React\\EventLoop\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"React\\Tests\\EventLoop\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
namespace React\EventLoop;
|
||||
|
||||
use Ev;
|
||||
use EvIo;
|
||||
use EvLoop;
|
||||
use React\EventLoop\Tick\FutureTickQueue;
|
||||
use React\EventLoop\Timer\Timer;
|
||||
use SplObjectStorage;
|
||||
|
||||
/**
|
||||
* An `ext-ev` based event loop.
|
||||
*
|
||||
* This loop uses the [`ev` PECL extension](https://pecl.php.net/package/ev),
|
||||
* that provides an interface to `libev` library.
|
||||
* `libev` itself supports a number of system-specific backends (epoll, kqueue).
|
||||
*
|
||||
* This loop is known to work with PHP 5.4 through PHP 8+.
|
||||
*
|
||||
* @see http://php.net/manual/en/book.ev.php
|
||||
* @see https://bitbucket.org/osmanov/pecl-ev/overview
|
||||
*/
|
||||
class ExtEvLoop implements LoopInterface
|
||||
{
|
||||
/**
|
||||
* @var EvLoop
|
||||
*/
|
||||
private $loop;
|
||||
|
||||
/**
|
||||
* @var FutureTickQueue
|
||||
*/
|
||||
private $futureTickQueue;
|
||||
|
||||
/**
|
||||
* @var SplObjectStorage
|
||||
*/
|
||||
private $timers;
|
||||
|
||||
/**
|
||||
* @var EvIo[]
|
||||
*/
|
||||
private $readStreams = array();
|
||||
|
||||
/**
|
||||
* @var EvIo[]
|
||||
*/
|
||||
private $writeStreams = array();
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $running;
|
||||
|
||||
/**
|
||||
* @var SignalsHandler
|
||||
*/
|
||||
private $signals;
|
||||
|
||||
/**
|
||||
* @var \EvSignal[]
|
||||
*/
|
||||
private $signalEvents = array();
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->loop = new EvLoop();
|
||||
$this->futureTickQueue = new FutureTickQueue();
|
||||
$this->timers = new SplObjectStorage();
|
||||
$this->signals = new SignalsHandler();
|
||||
}
|
||||
|
||||
public function addReadStream($stream, $listener)
|
||||
{
|
||||
$key = (int)$stream;
|
||||
|
||||
if (isset($this->readStreams[$key])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$callback = $this->getStreamListenerClosure($stream, $listener);
|
||||
$event = $this->loop->io($stream, Ev::READ, $callback);
|
||||
$this->readStreams[$key] = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $stream
|
||||
* @param callable $listener
|
||||
*
|
||||
* @return \Closure
|
||||
*/
|
||||
private function getStreamListenerClosure($stream, $listener)
|
||||
{
|
||||
return function () use ($stream, $listener) {
|
||||
\call_user_func($listener, $stream);
|
||||
};
|
||||
}
|
||||
|
||||
public function addWriteStream($stream, $listener)
|
||||
{
|
||||
$key = (int)$stream;
|
||||
|
||||
if (isset($this->writeStreams[$key])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$callback = $this->getStreamListenerClosure($stream, $listener);
|
||||
$event = $this->loop->io($stream, Ev::WRITE, $callback);
|
||||
$this->writeStreams[$key] = $event;
|
||||
}
|
||||
|
||||
public function removeReadStream($stream)
|
||||
{
|
||||
$key = (int)$stream;
|
||||
|
||||
if (!isset($this->readStreams[$key])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->readStreams[$key]->stop();
|
||||
unset($this->readStreams[$key]);
|
||||
}
|
||||
|
||||
public function removeWriteStream($stream)
|
||||
{
|
||||
$key = (int)$stream;
|
||||
|
||||
if (!isset($this->writeStreams[$key])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->writeStreams[$key]->stop();
|
||||
unset($this->writeStreams[$key]);
|
||||
}
|
||||
|
||||
public function addTimer($interval, $callback)
|
||||
{
|
||||
$timer = new Timer($interval, $callback, false);
|
||||
|
||||
$that = $this;
|
||||
$timers = $this->timers;
|
||||
$callback = function () use ($timer, $timers, $that) {
|
||||
\call_user_func($timer->getCallback(), $timer);
|
||||
|
||||
if ($timers->offsetExists($timer)) {
|
||||
$that->cancelTimer($timer);
|
||||
}
|
||||
};
|
||||
|
||||
$event = $this->loop->timer($timer->getInterval(), 0.0, $callback);
|
||||
$this->timers->offsetSet($timer, $event);
|
||||
|
||||
return $timer;
|
||||
}
|
||||
|
||||
public function addPeriodicTimer($interval, $callback)
|
||||
{
|
||||
$timer = new Timer($interval, $callback, true);
|
||||
|
||||
$callback = function () use ($timer) {
|
||||
\call_user_func($timer->getCallback(), $timer);
|
||||
};
|
||||
|
||||
$event = $this->loop->timer($timer->getInterval(), $timer->getInterval(), $callback);
|
||||
$this->timers->offsetSet($timer, $event);
|
||||
|
||||
return $timer;
|
||||
}
|
||||
|
||||
public function cancelTimer(TimerInterface $timer)
|
||||
{
|
||||
if (!isset($this->timers[$timer])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event = $this->timers[$timer];
|
||||
$event->stop();
|
||||
$this->timers->offsetUnset($timer);
|
||||
}
|
||||
|
||||
public function futureTick($listener)
|
||||
{
|
||||
$this->futureTickQueue->add($listener);
|
||||
}
|
||||
|
||||
public function run()
|
||||
{
|
||||
$this->running = true;
|
||||
|
||||
while ($this->running) {
|
||||
$this->futureTickQueue->tick();
|
||||
|
||||
$hasPendingCallbacks = !$this->futureTickQueue->isEmpty();
|
||||
$wasJustStopped = !$this->running;
|
||||
$nothingLeftToDo = !$this->readStreams
|
||||
&& !$this->writeStreams
|
||||
&& !$this->timers->count()
|
||||
&& $this->signals->isEmpty();
|
||||
|
||||
$flags = Ev::RUN_ONCE;
|
||||
if ($wasJustStopped || $hasPendingCallbacks) {
|
||||
$flags |= Ev::RUN_NOWAIT;
|
||||
} elseif ($nothingLeftToDo) {
|
||||
break;
|
||||
}
|
||||
|
||||
$this->loop->run($flags);
|
||||
}
|
||||
}
|
||||
|
||||
public function stop()
|
||||
{
|
||||
$this->running = false;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
/** @var TimerInterface $timer */
|
||||
foreach ($this->timers as $timer) {
|
||||
$this->cancelTimer($timer);
|
||||
}
|
||||
|
||||
foreach ($this->readStreams as $key => $stream) {
|
||||
$this->removeReadStream($key);
|
||||
}
|
||||
|
||||
foreach ($this->writeStreams as $key => $stream) {
|
||||
$this->removeWriteStream($key);
|
||||
}
|
||||
}
|
||||
|
||||
public function addSignal($signal, $listener)
|
||||
{
|
||||
$this->signals->add($signal, $listener);
|
||||
|
||||
if (!isset($this->signalEvents[$signal])) {
|
||||
$this->signalEvents[$signal] = $this->loop->signal($signal, function() use ($signal) {
|
||||
$this->signals->call($signal);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function removeSignal($signal, $listener)
|
||||
{
|
||||
$this->signals->remove($signal, $listener);
|
||||
|
||||
if (isset($this->signalEvents[$signal])) {
|
||||
$this->signalEvents[$signal]->stop();
|
||||
unset($this->signalEvents[$signal]);
|
||||
}
|
||||
}
|
||||
}
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
namespace React\EventLoop;
|
||||
|
||||
use BadMethodCallException;
|
||||
use Event;
|
||||
use EventBase;
|
||||
use React\EventLoop\Tick\FutureTickQueue;
|
||||
use React\EventLoop\Timer\Timer;
|
||||
use SplObjectStorage;
|
||||
|
||||
/**
|
||||
* An `ext-event` based event loop.
|
||||
*
|
||||
* This uses the [`event` PECL extension](https://pecl.php.net/package/event),
|
||||
* that provides an interface to `libevent` library.
|
||||
* `libevent` itself supports a number of system-specific backends (epoll, kqueue).
|
||||
*
|
||||
* This loop is known to work with PHP 5.4 through PHP 8+.
|
||||
*
|
||||
* @link https://pecl.php.net/package/event
|
||||
*/
|
||||
final class ExtEventLoop implements LoopInterface
|
||||
{
|
||||
private $eventBase;
|
||||
private $futureTickQueue;
|
||||
private $timerCallback;
|
||||
private $timerEvents;
|
||||
private $streamCallback;
|
||||
private $readEvents = array();
|
||||
private $writeEvents = array();
|
||||
private $readListeners = array();
|
||||
private $writeListeners = array();
|
||||
private $readRefs = array();
|
||||
private $writeRefs = array();
|
||||
private $running;
|
||||
private $signals;
|
||||
private $signalEvents = array();
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (!\class_exists('EventBase', false)) {
|
||||
throw new BadMethodCallException('Cannot create ExtEventLoop, ext-event extension missing');
|
||||
}
|
||||
|
||||
// support arbitrary file descriptors and not just sockets
|
||||
// Windows only has limited file descriptor support, so do not require this (will fail otherwise)
|
||||
// @link http://www.wangafu.net/~nickm/libevent-book/Ref2_eventbase.html#_setting_up_a_complicated_event_base
|
||||
$config = new \EventConfig();
|
||||
if (\DIRECTORY_SEPARATOR !== '\\') {
|
||||
$config->requireFeatures(\EventConfig::FEATURE_FDS);
|
||||
}
|
||||
|
||||
$this->eventBase = new EventBase($config);
|
||||
$this->futureTickQueue = new FutureTickQueue();
|
||||
$this->timerEvents = new SplObjectStorage();
|
||||
$this->signals = new SignalsHandler();
|
||||
|
||||
$this->createTimerCallback();
|
||||
$this->createStreamCallback();
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
// explicitly clear all references to Event objects to prevent SEGFAULTs on Windows
|
||||
foreach ($this->timerEvents as $timer) {
|
||||
$this->timerEvents->offsetUnset($timer);
|
||||
}
|
||||
|
||||
$this->readEvents = array();
|
||||
$this->writeEvents = array();
|
||||
}
|
||||
|
||||
public function addReadStream($stream, $listener)
|
||||
{
|
||||
$key = (int) $stream;
|
||||
if (isset($this->readListeners[$key])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event = new Event($this->eventBase, $stream, Event::PERSIST | Event::READ, $this->streamCallback);
|
||||
$event->add();
|
||||
$this->readEvents[$key] = $event;
|
||||
$this->readListeners[$key] = $listener;
|
||||
|
||||
// ext-event does not increase refcount on stream resources for PHP 7+
|
||||
// manually keep track of stream resource to prevent premature garbage collection
|
||||
if (\PHP_VERSION_ID >= 70000) {
|
||||
$this->readRefs[$key] = $stream;
|
||||
}
|
||||
}
|
||||
|
||||
public function addWriteStream($stream, $listener)
|
||||
{
|
||||
$key = (int) $stream;
|
||||
if (isset($this->writeListeners[$key])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event = new Event($this->eventBase, $stream, Event::PERSIST | Event::WRITE, $this->streamCallback);
|
||||
$event->add();
|
||||
$this->writeEvents[$key] = $event;
|
||||
$this->writeListeners[$key] = $listener;
|
||||
|
||||
// ext-event does not increase refcount on stream resources for PHP 7+
|
||||
// manually keep track of stream resource to prevent premature garbage collection
|
||||
if (\PHP_VERSION_ID >= 70000) {
|
||||
$this->writeRefs[$key] = $stream;
|
||||
}
|
||||
}
|
||||
|
||||
public function removeReadStream($stream)
|
||||
{
|
||||
$key = (int) $stream;
|
||||
|
||||
if (isset($this->readEvents[$key])) {
|
||||
$this->readEvents[$key]->free();
|
||||
unset(
|
||||
$this->readEvents[$key],
|
||||
$this->readListeners[$key],
|
||||
$this->readRefs[$key]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function removeWriteStream($stream)
|
||||
{
|
||||
$key = (int) $stream;
|
||||
|
||||
if (isset($this->writeEvents[$key])) {
|
||||
$this->writeEvents[$key]->free();
|
||||
unset(
|
||||
$this->writeEvents[$key],
|
||||
$this->writeListeners[$key],
|
||||
$this->writeRefs[$key]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function addTimer($interval, $callback)
|
||||
{
|
||||
$timer = new Timer($interval, $callback, false);
|
||||
|
||||
$this->scheduleTimer($timer);
|
||||
|
||||
return $timer;
|
||||
}
|
||||
|
||||
public function addPeriodicTimer($interval, $callback)
|
||||
{
|
||||
$timer = new Timer($interval, $callback, true);
|
||||
|
||||
$this->scheduleTimer($timer);
|
||||
|
||||
return $timer;
|
||||
}
|
||||
|
||||
public function cancelTimer(TimerInterface $timer)
|
||||
{
|
||||
if ($this->timerEvents->offsetExists($timer)) {
|
||||
$this->timerEvents[$timer]->free();
|
||||
$this->timerEvents->offsetUnset($timer);
|
||||
}
|
||||
}
|
||||
|
||||
public function futureTick($listener)
|
||||
{
|
||||
$this->futureTickQueue->add($listener);
|
||||
}
|
||||
|
||||
public function addSignal($signal, $listener)
|
||||
{
|
||||
$this->signals->add($signal, $listener);
|
||||
|
||||
if (!isset($this->signalEvents[$signal])) {
|
||||
$this->signalEvents[$signal] = Event::signal($this->eventBase, $signal, array($this->signals, 'call'));
|
||||
$this->signalEvents[$signal]->add();
|
||||
}
|
||||
}
|
||||
|
||||
public function removeSignal($signal, $listener)
|
||||
{
|
||||
$this->signals->remove($signal, $listener);
|
||||
|
||||
if (isset($this->signalEvents[$signal]) && $this->signals->count($signal) === 0) {
|
||||
$this->signalEvents[$signal]->free();
|
||||
unset($this->signalEvents[$signal]);
|
||||
}
|
||||
}
|
||||
|
||||
public function run()
|
||||
{
|
||||
$this->running = true;
|
||||
|
||||
while ($this->running) {
|
||||
$this->futureTickQueue->tick();
|
||||
|
||||
$flags = EventBase::LOOP_ONCE;
|
||||
if (!$this->running || !$this->futureTickQueue->isEmpty()) {
|
||||
$flags |= EventBase::LOOP_NONBLOCK;
|
||||
} elseif (!$this->readEvents && !$this->writeEvents && !$this->timerEvents->count() && $this->signals->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$this->eventBase->loop($flags);
|
||||
}
|
||||
}
|
||||
|
||||
public function stop()
|
||||
{
|
||||
$this->running = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a timer for execution.
|
||||
*
|
||||
* @param TimerInterface $timer
|
||||
*/
|
||||
private function scheduleTimer(TimerInterface $timer)
|
||||
{
|
||||
$flags = Event::TIMEOUT;
|
||||
|
||||
if ($timer->isPeriodic()) {
|
||||
$flags |= Event::PERSIST;
|
||||
}
|
||||
|
||||
$event = new Event($this->eventBase, -1, $flags, $this->timerCallback, $timer);
|
||||
$this->timerEvents[$timer] = $event;
|
||||
|
||||
$event->add($timer->getInterval());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a callback used as the target of timer events.
|
||||
*
|
||||
* A reference is kept to the callback for the lifetime of the loop
|
||||
* to prevent "Cannot destroy active lambda function" fatal error from
|
||||
* the event extension.
|
||||
*/
|
||||
private function createTimerCallback()
|
||||
{
|
||||
$timers = $this->timerEvents;
|
||||
$this->timerCallback = function ($_, $__, $timer) use ($timers) {
|
||||
\call_user_func($timer->getCallback(), $timer);
|
||||
|
||||
if (!$timer->isPeriodic() && $timers->offsetExists($timer)) {
|
||||
$this->cancelTimer($timer);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a callback used as the target of stream events.
|
||||
*
|
||||
* A reference is kept to the callback for the lifetime of the loop
|
||||
* to prevent "Cannot destroy active lambda function" fatal error from
|
||||
* the event extension.
|
||||
*/
|
||||
private function createStreamCallback()
|
||||
{
|
||||
$read =& $this->readListeners;
|
||||
$write =& $this->writeListeners;
|
||||
$this->streamCallback = function ($stream, $flags) use (&$read, &$write) {
|
||||
$key = (int) $stream;
|
||||
|
||||
if (Event::READ === (Event::READ & $flags) && isset($read[$key])) {
|
||||
\call_user_func($read[$key], $stream);
|
||||
}
|
||||
|
||||
if (Event::WRITE === (Event::WRITE & $flags) && isset($write[$key])) {
|
||||
\call_user_func($write[$key], $stream);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace React\EventLoop;
|
||||
|
||||
use BadMethodCallException;
|
||||
use libev\EventLoop;
|
||||
use libev\IOEvent;
|
||||
use libev\SignalEvent;
|
||||
use libev\TimerEvent;
|
||||
use React\EventLoop\Tick\FutureTickQueue;
|
||||
use React\EventLoop\Timer\Timer;
|
||||
use SplObjectStorage;
|
||||
|
||||
/**
|
||||
* [Deprecated] An `ext-libev` based event loop.
|
||||
*
|
||||
* This uses an [unofficial `libev` extension](https://github.com/m4rw3r/php-libev),
|
||||
* that provides an interface to `libev` library.
|
||||
* `libev` itself supports a number of system-specific backends (epoll, kqueue).
|
||||
*
|
||||
* This loop does only work with PHP 5.
|
||||
* An update for PHP 7 is [unlikely](https://github.com/m4rw3r/php-libev/issues/8)
|
||||
* to happen any time soon.
|
||||
*
|
||||
* @see https://github.com/m4rw3r/php-libev
|
||||
* @see https://gist.github.com/1688204
|
||||
* @deprecated 1.2.0, use [`ExtEvLoop`](#extevloop) instead.
|
||||
*/
|
||||
final class ExtLibevLoop implements LoopInterface
|
||||
{
|
||||
private $loop;
|
||||
private $futureTickQueue;
|
||||
private $timerEvents;
|
||||
private $readEvents = array();
|
||||
private $writeEvents = array();
|
||||
private $running;
|
||||
private $signals;
|
||||
private $signalEvents = array();
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (!\class_exists('libev\EventLoop', false)) {
|
||||
throw new BadMethodCallException('Cannot create ExtLibevLoop, ext-libev extension missing');
|
||||
}
|
||||
|
||||
$this->loop = new EventLoop();
|
||||
$this->futureTickQueue = new FutureTickQueue();
|
||||
$this->timerEvents = new SplObjectStorage();
|
||||
$this->signals = new SignalsHandler();
|
||||
}
|
||||
|
||||
public function addReadStream($stream, $listener)
|
||||
{
|
||||
if (isset($this->readEvents[(int) $stream])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$callback = function () use ($stream, $listener) {
|
||||
\call_user_func($listener, $stream);
|
||||
};
|
||||
|
||||
$event = new IOEvent($callback, $stream, IOEvent::READ);
|
||||
$this->loop->add($event);
|
||||
|
||||
$this->readEvents[(int) $stream] = $event;
|
||||
}
|
||||
|
||||
public function addWriteStream($stream, $listener)
|
||||
{
|
||||
if (isset($this->writeEvents[(int) $stream])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$callback = function () use ($stream, $listener) {
|
||||
\call_user_func($listener, $stream);
|
||||
};
|
||||
|
||||
$event = new IOEvent($callback, $stream, IOEvent::WRITE);
|
||||
$this->loop->add($event);
|
||||
|
||||
$this->writeEvents[(int) $stream] = $event;
|
||||
}
|
||||
|
||||
public function removeReadStream($stream)
|
||||
{
|
||||
$key = (int) $stream;
|
||||
|
||||
if (isset($this->readEvents[$key])) {
|
||||
$this->readEvents[$key]->stop();
|
||||
$this->loop->remove($this->readEvents[$key]);
|
||||
unset($this->readEvents[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
public function removeWriteStream($stream)
|
||||
{
|
||||
$key = (int) $stream;
|
||||
|
||||
if (isset($this->writeEvents[$key])) {
|
||||
$this->writeEvents[$key]->stop();
|
||||
$this->loop->remove($this->writeEvents[$key]);
|
||||
unset($this->writeEvents[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
public function addTimer($interval, $callback)
|
||||
{
|
||||
$timer = new Timer( $interval, $callback, false);
|
||||
|
||||
$that = $this;
|
||||
$timers = $this->timerEvents;
|
||||
$callback = function () use ($timer, $timers, $that) {
|
||||
\call_user_func($timer->getCallback(), $timer);
|
||||
|
||||
if ($timers->contains($timer)) {
|
||||
$that->cancelTimer($timer);
|
||||
}
|
||||
};
|
||||
|
||||
$event = new TimerEvent($callback, $timer->getInterval());
|
||||
$this->timerEvents->attach($timer, $event);
|
||||
$this->loop->add($event);
|
||||
|
||||
return $timer;
|
||||
}
|
||||
|
||||
public function addPeriodicTimer($interval, $callback)
|
||||
{
|
||||
$timer = new Timer($interval, $callback, true);
|
||||
|
||||
$callback = function () use ($timer) {
|
||||
\call_user_func($timer->getCallback(), $timer);
|
||||
};
|
||||
|
||||
$event = new TimerEvent($callback, $timer->getInterval(), $timer->getInterval());
|
||||
$this->timerEvents->attach($timer, $event);
|
||||
$this->loop->add($event);
|
||||
|
||||
return $timer;
|
||||
}
|
||||
|
||||
public function cancelTimer(TimerInterface $timer)
|
||||
{
|
||||
if (isset($this->timerEvents[$timer])) {
|
||||
$this->loop->remove($this->timerEvents[$timer]);
|
||||
$this->timerEvents->detach($timer);
|
||||
}
|
||||
}
|
||||
|
||||
public function futureTick($listener)
|
||||
{
|
||||
$this->futureTickQueue->add($listener);
|
||||
}
|
||||
|
||||
public function addSignal($signal, $listener)
|
||||
{
|
||||
$this->signals->add($signal, $listener);
|
||||
|
||||
if (!isset($this->signalEvents[$signal])) {
|
||||
$signals = $this->signals;
|
||||
$this->signalEvents[$signal] = new SignalEvent(function () use ($signals, $signal) {
|
||||
$signals->call($signal);
|
||||
}, $signal);
|
||||
$this->loop->add($this->signalEvents[$signal]);
|
||||
}
|
||||
}
|
||||
|
||||
public function removeSignal($signal, $listener)
|
||||
{
|
||||
$this->signals->remove($signal, $listener);
|
||||
|
||||
if (isset($this->signalEvents[$signal]) && $this->signals->count($signal) === 0) {
|
||||
$this->signalEvents[$signal]->stop();
|
||||
$this->loop->remove($this->signalEvents[$signal]);
|
||||
unset($this->signalEvents[$signal]);
|
||||
}
|
||||
}
|
||||
|
||||
public function run()
|
||||
{
|
||||
$this->running = true;
|
||||
|
||||
while ($this->running) {
|
||||
$this->futureTickQueue->tick();
|
||||
|
||||
$flags = EventLoop::RUN_ONCE;
|
||||
if (!$this->running || !$this->futureTickQueue->isEmpty()) {
|
||||
$flags |= EventLoop::RUN_NOWAIT;
|
||||
} elseif (!$this->readEvents && !$this->writeEvents && !$this->timerEvents->count() && $this->signals->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$this->loop->run($flags);
|
||||
}
|
||||
}
|
||||
|
||||
public function stop()
|
||||
{
|
||||
$this->running = false;
|
||||
}
|
||||
}
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
|
||||
namespace React\EventLoop;
|
||||
|
||||
use BadMethodCallException;
|
||||
use Event;
|
||||
use EventBase;
|
||||
use React\EventLoop\Tick\FutureTickQueue;
|
||||
use React\EventLoop\Timer\Timer;
|
||||
use SplObjectStorage;
|
||||
|
||||
/**
|
||||
* [Deprecated] An `ext-libevent` based event loop.
|
||||
*
|
||||
* This uses the [`libevent` PECL extension](https://pecl.php.net/package/libevent),
|
||||
* that provides an interface to `libevent` library.
|
||||
* `libevent` itself supports a number of system-specific backends (epoll, kqueue).
|
||||
*
|
||||
* This event loop does only work with PHP 5.
|
||||
* An [unofficial update](https://github.com/php/pecl-event-libevent/pull/2) for
|
||||
* PHP 7 does exist, but it is known to cause regular crashes due to `SEGFAULT`s.
|
||||
* To reiterate: Using this event loop on PHP 7 is not recommended.
|
||||
* Accordingly, neither the [`Loop` class](#loop) nor the deprecated
|
||||
* [`Factory` class](#factory) will try to use this event loop on PHP 7.
|
||||
*
|
||||
* This event loop is known to trigger a readable listener only if
|
||||
* the stream *becomes* readable (edge-triggered) and may not trigger if the
|
||||
* stream has already been readable from the beginning.
|
||||
* This also implies that a stream may not be recognized as readable when data
|
||||
* is still left in PHP's internal stream buffers.
|
||||
* As such, it's recommended to use `stream_set_read_buffer($stream, 0);`
|
||||
* to disable PHP's internal read buffer in this case.
|
||||
* See also [`addReadStream()`](#addreadstream) for more details.
|
||||
*
|
||||
* @link https://pecl.php.net/package/libevent
|
||||
* @deprecated 1.2.0, use [`ExtEventLoop`](#exteventloop) instead.
|
||||
*/
|
||||
final class ExtLibeventLoop implements LoopInterface
|
||||
{
|
||||
/** @internal */
|
||||
const MICROSECONDS_PER_SECOND = 1000000;
|
||||
|
||||
private $eventBase;
|
||||
private $futureTickQueue;
|
||||
private $timerCallback;
|
||||
private $timerEvents;
|
||||
private $streamCallback;
|
||||
private $readEvents = array();
|
||||
private $writeEvents = array();
|
||||
private $readListeners = array();
|
||||
private $writeListeners = array();
|
||||
private $running;
|
||||
private $signals;
|
||||
private $signalEvents = array();
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (!\function_exists('event_base_new')) {
|
||||
throw new BadMethodCallException('Cannot create ExtLibeventLoop, ext-libevent extension missing');
|
||||
}
|
||||
|
||||
$this->eventBase = \event_base_new();
|
||||
$this->futureTickQueue = new FutureTickQueue();
|
||||
$this->timerEvents = new SplObjectStorage();
|
||||
$this->signals = new SignalsHandler();
|
||||
|
||||
$this->createTimerCallback();
|
||||
$this->createStreamCallback();
|
||||
}
|
||||
|
||||
public function addReadStream($stream, $listener)
|
||||
{
|
||||
$key = (int) $stream;
|
||||
if (isset($this->readListeners[$key])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event = \event_new();
|
||||
\event_set($event, $stream, \EV_PERSIST | \EV_READ, $this->streamCallback);
|
||||
\event_base_set($event, $this->eventBase);
|
||||
\event_add($event);
|
||||
|
||||
$this->readEvents[$key] = $event;
|
||||
$this->readListeners[$key] = $listener;
|
||||
}
|
||||
|
||||
public function addWriteStream($stream, $listener)
|
||||
{
|
||||
$key = (int) $stream;
|
||||
if (isset($this->writeListeners[$key])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event = \event_new();
|
||||
\event_set($event, $stream, \EV_PERSIST | \EV_WRITE, $this->streamCallback);
|
||||
\event_base_set($event, $this->eventBase);
|
||||
\event_add($event);
|
||||
|
||||
$this->writeEvents[$key] = $event;
|
||||
$this->writeListeners[$key] = $listener;
|
||||
}
|
||||
|
||||
public function removeReadStream($stream)
|
||||
{
|
||||
$key = (int) $stream;
|
||||
|
||||
if (isset($this->readListeners[$key])) {
|
||||
$event = $this->readEvents[$key];
|
||||
\event_del($event);
|
||||
\event_free($event);
|
||||
|
||||
unset(
|
||||
$this->readEvents[$key],
|
||||
$this->readListeners[$key]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function removeWriteStream($stream)
|
||||
{
|
||||
$key = (int) $stream;
|
||||
|
||||
if (isset($this->writeListeners[$key])) {
|
||||
$event = $this->writeEvents[$key];
|
||||
\event_del($event);
|
||||
\event_free($event);
|
||||
|
||||
unset(
|
||||
$this->writeEvents[$key],
|
||||
$this->writeListeners[$key]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function addTimer($interval, $callback)
|
||||
{
|
||||
$timer = new Timer($interval, $callback, false);
|
||||
|
||||
$this->scheduleTimer($timer);
|
||||
|
||||
return $timer;
|
||||
}
|
||||
|
||||
public function addPeriodicTimer($interval, $callback)
|
||||
{
|
||||
$timer = new Timer($interval, $callback, true);
|
||||
|
||||
$this->scheduleTimer($timer);
|
||||
|
||||
return $timer;
|
||||
}
|
||||
|
||||
public function cancelTimer(TimerInterface $timer)
|
||||
{
|
||||
if ($this->timerEvents->contains($timer)) {
|
||||
$event = $this->timerEvents[$timer];
|
||||
\event_del($event);
|
||||
\event_free($event);
|
||||
|
||||
$this->timerEvents->detach($timer);
|
||||
}
|
||||
}
|
||||
|
||||
public function futureTick($listener)
|
||||
{
|
||||
$this->futureTickQueue->add($listener);
|
||||
}
|
||||
|
||||
public function addSignal($signal, $listener)
|
||||
{
|
||||
$this->signals->add($signal, $listener);
|
||||
|
||||
if (!isset($this->signalEvents[$signal])) {
|
||||
$this->signalEvents[$signal] = \event_new();
|
||||
\event_set($this->signalEvents[$signal], $signal, \EV_PERSIST | \EV_SIGNAL, array($this->signals, 'call'));
|
||||
\event_base_set($this->signalEvents[$signal], $this->eventBase);
|
||||
\event_add($this->signalEvents[$signal]);
|
||||
}
|
||||
}
|
||||
|
||||
public function removeSignal($signal, $listener)
|
||||
{
|
||||
$this->signals->remove($signal, $listener);
|
||||
|
||||
if (isset($this->signalEvents[$signal]) && $this->signals->count($signal) === 0) {
|
||||
\event_del($this->signalEvents[$signal]);
|
||||
\event_free($this->signalEvents[$signal]);
|
||||
unset($this->signalEvents[$signal]);
|
||||
}
|
||||
}
|
||||
|
||||
public function run()
|
||||
{
|
||||
$this->running = true;
|
||||
|
||||
while ($this->running) {
|
||||
$this->futureTickQueue->tick();
|
||||
|
||||
$flags = \EVLOOP_ONCE;
|
||||
if (!$this->running || !$this->futureTickQueue->isEmpty()) {
|
||||
$flags |= \EVLOOP_NONBLOCK;
|
||||
} elseif (!$this->readEvents && !$this->writeEvents && !$this->timerEvents->count() && $this->signals->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
\event_base_loop($this->eventBase, $flags);
|
||||
}
|
||||
}
|
||||
|
||||
public function stop()
|
||||
{
|
||||
$this->running = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a timer for execution.
|
||||
*
|
||||
* @param TimerInterface $timer
|
||||
*/
|
||||
private function scheduleTimer(TimerInterface $timer)
|
||||
{
|
||||
$this->timerEvents[$timer] = $event = \event_timer_new();
|
||||
|
||||
\event_timer_set($event, $this->timerCallback, $timer);
|
||||
\event_base_set($event, $this->eventBase);
|
||||
\event_add($event, $timer->getInterval() * self::MICROSECONDS_PER_SECOND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a callback used as the target of timer events.
|
||||
*
|
||||
* A reference is kept to the callback for the lifetime of the loop
|
||||
* to prevent "Cannot destroy active lambda function" fatal error from
|
||||
* the event extension.
|
||||
*/
|
||||
private function createTimerCallback()
|
||||
{
|
||||
$that = $this;
|
||||
$timers = $this->timerEvents;
|
||||
$this->timerCallback = function ($_, $__, $timer) use ($timers, $that) {
|
||||
\call_user_func($timer->getCallback(), $timer);
|
||||
|
||||
// Timer already cancelled ...
|
||||
if (!$timers->contains($timer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reschedule periodic timers ...
|
||||
if ($timer->isPeriodic()) {
|
||||
\event_add(
|
||||
$timers[$timer],
|
||||
$timer->getInterval() * ExtLibeventLoop::MICROSECONDS_PER_SECOND
|
||||
);
|
||||
|
||||
// Clean-up one shot timers ...
|
||||
} else {
|
||||
$that->cancelTimer($timer);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a callback used as the target of stream events.
|
||||
*
|
||||
* A reference is kept to the callback for the lifetime of the loop
|
||||
* to prevent "Cannot destroy active lambda function" fatal error from
|
||||
* the event extension.
|
||||
*/
|
||||
private function createStreamCallback()
|
||||
{
|
||||
$read =& $this->readListeners;
|
||||
$write =& $this->writeListeners;
|
||||
$this->streamCallback = function ($stream, $flags) use (&$read, &$write) {
|
||||
$key = (int) $stream;
|
||||
|
||||
if (\EV_READ === (\EV_READ & $flags) && isset($read[$key])) {
|
||||
\call_user_func($read[$key], $stream);
|
||||
}
|
||||
|
||||
if (\EV_WRITE === (\EV_WRITE & $flags) && isset($write[$key])) {
|
||||
\call_user_func($write[$key], $stream);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+350
@@ -0,0 +1,350 @@
|
||||
<?php
|
||||
|
||||
namespace React\EventLoop;
|
||||
|
||||
use React\EventLoop\Tick\FutureTickQueue;
|
||||
use React\EventLoop\Timer\Timer;
|
||||
use SplObjectStorage;
|
||||
|
||||
/**
|
||||
* An `ext-uv` based event loop.
|
||||
*
|
||||
* This loop uses the [`uv` PECL extension](https://pecl.php.net/package/uv),
|
||||
* that provides an interface to `libuv` library.
|
||||
* `libuv` itself supports a number of system-specific backends (epoll, kqueue).
|
||||
*
|
||||
* This loop is known to work with PHP 7+.
|
||||
*
|
||||
* @see https://github.com/bwoebi/php-uv
|
||||
*/
|
||||
final class ExtUvLoop implements LoopInterface
|
||||
{
|
||||
private $uv;
|
||||
private $futureTickQueue;
|
||||
private $timers;
|
||||
private $streamEvents = array();
|
||||
private $readStreams = array();
|
||||
private $writeStreams = array();
|
||||
private $running;
|
||||
private $signals;
|
||||
private $signalEvents = array();
|
||||
private $streamListener;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (!\function_exists('uv_loop_new')) {
|
||||
throw new \BadMethodCallException('Cannot create LibUvLoop, ext-uv extension missing');
|
||||
}
|
||||
|
||||
$this->uv = \uv_loop_new();
|
||||
$this->futureTickQueue = new FutureTickQueue();
|
||||
$this->timers = new SplObjectStorage();
|
||||
$this->streamListener = $this->createStreamListener();
|
||||
$this->signals = new SignalsHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying ext-uv event loop. (Internal ReactPHP use only.)
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @return resource
|
||||
*/
|
||||
public function getUvLoop()
|
||||
{
|
||||
return $this->uv;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addReadStream($stream, $listener)
|
||||
{
|
||||
if (isset($this->readStreams[(int) $stream])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->readStreams[(int) $stream] = $listener;
|
||||
$this->addStream($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addWriteStream($stream, $listener)
|
||||
{
|
||||
if (isset($this->writeStreams[(int) $stream])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->writeStreams[(int) $stream] = $listener;
|
||||
$this->addStream($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function removeReadStream($stream)
|
||||
{
|
||||
if (!isset($this->streamEvents[(int) $stream])) {
|
||||
return;
|
||||
}
|
||||
|
||||
unset($this->readStreams[(int) $stream]);
|
||||
$this->removeStream($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function removeWriteStream($stream)
|
||||
{
|
||||
if (!isset($this->streamEvents[(int) $stream])) {
|
||||
return;
|
||||
}
|
||||
|
||||
unset($this->writeStreams[(int) $stream]);
|
||||
$this->removeStream($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addTimer($interval, $callback)
|
||||
{
|
||||
$timer = new Timer($interval, $callback, false);
|
||||
|
||||
$that = $this;
|
||||
$timers = $this->timers;
|
||||
$callback = function () use ($timer, $timers, $that) {
|
||||
\call_user_func($timer->getCallback(), $timer);
|
||||
|
||||
if ($timers->offsetExists($timer)) {
|
||||
$that->cancelTimer($timer);
|
||||
}
|
||||
};
|
||||
|
||||
$event = \uv_timer_init($this->uv);
|
||||
$this->timers->offsetSet($timer, $event);
|
||||
\uv_timer_start(
|
||||
$event,
|
||||
$this->convertFloatSecondsToMilliseconds($interval),
|
||||
0,
|
||||
$callback
|
||||
);
|
||||
|
||||
return $timer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addPeriodicTimer($interval, $callback)
|
||||
{
|
||||
$timer = new Timer($interval, $callback, true);
|
||||
|
||||
$callback = function () use ($timer) {
|
||||
\call_user_func($timer->getCallback(), $timer);
|
||||
};
|
||||
|
||||
$interval = $this->convertFloatSecondsToMilliseconds($interval);
|
||||
$event = \uv_timer_init($this->uv);
|
||||
$this->timers->offsetSet($timer, $event);
|
||||
\uv_timer_start(
|
||||
$event,
|
||||
$interval,
|
||||
(int) $interval === 0 ? 1 : $interval,
|
||||
$callback
|
||||
);
|
||||
|
||||
return $timer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cancelTimer(TimerInterface $timer)
|
||||
{
|
||||
if (isset($this->timers[$timer])) {
|
||||
@\uv_timer_stop($this->timers[$timer]);
|
||||
$this->timers->offsetUnset($timer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function futureTick($listener)
|
||||
{
|
||||
$this->futureTickQueue->add($listener);
|
||||
}
|
||||
|
||||
public function addSignal($signal, $listener)
|
||||
{
|
||||
$this->signals->add($signal, $listener);
|
||||
|
||||
if (!isset($this->signalEvents[$signal])) {
|
||||
$signals = $this->signals;
|
||||
$this->signalEvents[$signal] = \uv_signal_init($this->uv);
|
||||
\uv_signal_start($this->signalEvents[$signal], function () use ($signals, $signal) {
|
||||
$signals->call($signal);
|
||||
}, $signal);
|
||||
}
|
||||
}
|
||||
|
||||
public function removeSignal($signal, $listener)
|
||||
{
|
||||
$this->signals->remove($signal, $listener);
|
||||
|
||||
if (isset($this->signalEvents[$signal]) && $this->signals->count($signal) === 0) {
|
||||
\uv_signal_stop($this->signalEvents[$signal]);
|
||||
unset($this->signalEvents[$signal]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
$this->running = true;
|
||||
|
||||
while ($this->running) {
|
||||
$this->futureTickQueue->tick();
|
||||
|
||||
$hasPendingCallbacks = !$this->futureTickQueue->isEmpty();
|
||||
$wasJustStopped = !$this->running;
|
||||
$nothingLeftToDo = !$this->readStreams
|
||||
&& !$this->writeStreams
|
||||
&& !$this->timers->count()
|
||||
&& $this->signals->isEmpty();
|
||||
|
||||
// Use UV::RUN_ONCE when there are only I/O events active in the loop and block until one of those triggers,
|
||||
// otherwise use UV::RUN_NOWAIT.
|
||||
// @link http://docs.libuv.org/en/v1.x/loop.html#c.uv_run
|
||||
$flags = \UV::RUN_ONCE;
|
||||
if ($wasJustStopped || $hasPendingCallbacks) {
|
||||
$flags = \UV::RUN_NOWAIT;
|
||||
} elseif ($nothingLeftToDo) {
|
||||
break;
|
||||
}
|
||||
|
||||
\uv_run($this->uv, $flags);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stop()
|
||||
{
|
||||
$this->running = false;
|
||||
}
|
||||
|
||||
private function addStream($stream)
|
||||
{
|
||||
if (!isset($this->streamEvents[(int) $stream])) {
|
||||
$this->streamEvents[(int)$stream] = \uv_poll_init_socket($this->uv, $stream);
|
||||
}
|
||||
|
||||
if ($this->streamEvents[(int) $stream] !== false) {
|
||||
$this->pollStream($stream);
|
||||
}
|
||||
}
|
||||
|
||||
private function removeStream($stream)
|
||||
{
|
||||
if (!isset($this->streamEvents[(int) $stream])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($this->readStreams[(int) $stream])
|
||||
&& !isset($this->writeStreams[(int) $stream])) {
|
||||
\uv_poll_stop($this->streamEvents[(int) $stream]);
|
||||
\uv_close($this->streamEvents[(int) $stream]);
|
||||
unset($this->streamEvents[(int) $stream]);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->pollStream($stream);
|
||||
}
|
||||
|
||||
private function pollStream($stream)
|
||||
{
|
||||
if (!isset($this->streamEvents[(int) $stream])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$flags = 0;
|
||||
if (isset($this->readStreams[(int) $stream])) {
|
||||
$flags |= \UV::READABLE;
|
||||
}
|
||||
|
||||
if (isset($this->writeStreams[(int) $stream])) {
|
||||
$flags |= \UV::WRITABLE;
|
||||
}
|
||||
|
||||
\uv_poll_start($this->streamEvents[(int) $stream], $flags, $this->streamListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a stream listener
|
||||
*
|
||||
* @return callable Returns a callback
|
||||
*/
|
||||
private function createStreamListener()
|
||||
{
|
||||
$callback = function ($event, $status, $events, $stream) {
|
||||
// libuv automatically stops polling on error, re-enable polling to match other loop implementations
|
||||
if ($status !== 0) {
|
||||
$this->pollStream($stream);
|
||||
|
||||
// libuv may report no events on error, but this should still invoke stream listeners to report closed connections
|
||||
// re-enable both readable and writable, correct listeners will be checked below anyway
|
||||
if ($events === 0) {
|
||||
$events = \UV::READABLE | \UV::WRITABLE;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->readStreams[(int) $stream]) && ($events & \UV::READABLE)) {
|
||||
\call_user_func($this->readStreams[(int) $stream], $stream);
|
||||
}
|
||||
|
||||
if (isset($this->writeStreams[(int) $stream]) && ($events & \UV::WRITABLE)) {
|
||||
\call_user_func($this->writeStreams[(int) $stream], $stream);
|
||||
}
|
||||
};
|
||||
|
||||
return $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $interval
|
||||
* @return int
|
||||
*/
|
||||
private function convertFloatSecondsToMilliseconds($interval)
|
||||
{
|
||||
if ($interval < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$maxValue = (int) (\PHP_INT_MAX / 1000);
|
||||
$intervalOverflow = false;
|
||||
if (PHP_VERSION_ID > 80499 && $interval >= \PHP_INT_MAX + 1) {
|
||||
$intervalOverflow = true;
|
||||
} else {
|
||||
$intInterval = (int) $interval;
|
||||
if (($intInterval <= 0 && $interval > 1) || $intInterval >= $maxValue) {
|
||||
$intervalOverflow = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($intervalOverflow) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Interval overflow, value must be lower than '{$maxValue}', but '{$interval}' passed."
|
||||
);
|
||||
}
|
||||
|
||||
return (int) \floor($interval * 1000);
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace React\EventLoop;
|
||||
|
||||
/**
|
||||
* [Deprecated] The `Factory` class exists as a convenient way to pick the best available event loop implementation.
|
||||
*
|
||||
* @deprecated 1.2.0 See Loop instead.
|
||||
* @see Loop
|
||||
*/
|
||||
final class Factory
|
||||
{
|
||||
/**
|
||||
* [Deprecated] Creates a new event loop instance
|
||||
*
|
||||
* ```php
|
||||
* // deprecated
|
||||
* $loop = React\EventLoop\Factory::create();
|
||||
*
|
||||
* // new
|
||||
* $loop = React\EventLoop\Loop::get();
|
||||
* ```
|
||||
*
|
||||
* This method always returns an instance implementing `LoopInterface`,
|
||||
* the actual event loop implementation is an implementation detail.
|
||||
*
|
||||
* This method should usually only be called once at the beginning of the program.
|
||||
*
|
||||
* @deprecated 1.2.0 See Loop::get() instead.
|
||||
* @see Loop::get()
|
||||
*
|
||||
* @return LoopInterface
|
||||
*/
|
||||
public static function create()
|
||||
{
|
||||
$loop = self::construct();
|
||||
|
||||
Loop::set($loop);
|
||||
|
||||
return $loop;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @return LoopInterface
|
||||
*/
|
||||
private static function construct()
|
||||
{
|
||||
// @codeCoverageIgnoreStart
|
||||
if (\function_exists('uv_loop_new')) {
|
||||
// only use ext-uv on PHP 7
|
||||
return new ExtUvLoop();
|
||||
}
|
||||
|
||||
if (\class_exists('libev\EventLoop', false)) {
|
||||
return new ExtLibevLoop();
|
||||
}
|
||||
|
||||
if (\class_exists('EvLoop', false)) {
|
||||
return new ExtEvLoop();
|
||||
}
|
||||
|
||||
if (\class_exists('EventBase', false)) {
|
||||
return new ExtEventLoop();
|
||||
}
|
||||
|
||||
if (\function_exists('event_base_new') && \PHP_MAJOR_VERSION === 5) {
|
||||
// only use ext-libevent on PHP 5 for now
|
||||
return new ExtLibeventLoop();
|
||||
}
|
||||
|
||||
return new StreamSelectLoop();
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
}
|
||||
Vendored
+266
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
namespace React\EventLoop;
|
||||
|
||||
/**
|
||||
* The `Loop` class exists as a convenient way to get the currently relevant loop
|
||||
*/
|
||||
final class Loop
|
||||
{
|
||||
/**
|
||||
* @var ?LoopInterface
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/** @var bool */
|
||||
private static $stopped = false;
|
||||
|
||||
/**
|
||||
* Returns the event loop.
|
||||
* When no loop is set, it will call the factory to create one.
|
||||
*
|
||||
* This method always returns an instance implementing `LoopInterface`,
|
||||
* the actual event loop implementation is an implementation detail.
|
||||
*
|
||||
* This method is the preferred way to get the event loop and using
|
||||
* Factory::create has been deprecated.
|
||||
*
|
||||
* @return LoopInterface
|
||||
*/
|
||||
public static function get()
|
||||
{
|
||||
if (self::$instance instanceof LoopInterface) {
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
self::$instance = $loop = Factory::create();
|
||||
|
||||
// Automatically run loop at end of program, unless already started or stopped explicitly.
|
||||
// This is tested using child processes, so coverage is actually 100%, see BinTest.
|
||||
// @codeCoverageIgnoreStart
|
||||
$hasRun = false;
|
||||
$loop->futureTick(function () use (&$hasRun) {
|
||||
$hasRun = true;
|
||||
});
|
||||
|
||||
$stopped =& self::$stopped;
|
||||
register_shutdown_function(function () use ($loop, &$hasRun, &$stopped) {
|
||||
// Don't run if we're coming from a fatal error (uncaught exception).
|
||||
$error = error_get_last();
|
||||
if ((isset($error['type']) ? $error['type'] : 0) & (E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$hasRun && !$stopped) {
|
||||
$loop->run();
|
||||
}
|
||||
});
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal undocumented method, behavior might change or throw in the
|
||||
* future. Use with caution and at your own risk.
|
||||
*
|
||||
* @internal
|
||||
* @return void
|
||||
*/
|
||||
public static function set(LoopInterface $loop)
|
||||
{
|
||||
self::$instance = $loop;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Advanced] Register a listener to be notified when a stream is ready to read.
|
||||
*
|
||||
* @param resource $stream
|
||||
* @param callable $listener
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @see LoopInterface::addReadStream()
|
||||
*/
|
||||
public static function addReadStream($stream, $listener)
|
||||
{
|
||||
// create loop instance on demand (legacy PHP < 7 doesn't like ternaries in method calls)
|
||||
if (self::$instance === null) {
|
||||
self::get();
|
||||
}
|
||||
self::$instance->addReadStream($stream, $listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* [Advanced] Register a listener to be notified when a stream is ready to write.
|
||||
*
|
||||
* @param resource $stream
|
||||
* @param callable $listener
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @see LoopInterface::addWriteStream()
|
||||
*/
|
||||
public static function addWriteStream($stream, $listener)
|
||||
{
|
||||
// create loop instance on demand (legacy PHP < 7 doesn't like ternaries in method calls)
|
||||
if (self::$instance === null) {
|
||||
self::get();
|
||||
}
|
||||
self::$instance->addWriteStream($stream, $listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the read event listener for the given stream.
|
||||
*
|
||||
* @param resource $stream
|
||||
* @return void
|
||||
* @see LoopInterface::removeReadStream()
|
||||
*/
|
||||
public static function removeReadStream($stream)
|
||||
{
|
||||
if (self::$instance !== null) {
|
||||
self::$instance->removeReadStream($stream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the write event listener for the given stream.
|
||||
*
|
||||
* @param resource $stream
|
||||
* @return void
|
||||
* @see LoopInterface::removeWriteStream()
|
||||
*/
|
||||
public static function removeWriteStream($stream)
|
||||
{
|
||||
if (self::$instance !== null) {
|
||||
self::$instance->removeWriteStream($stream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue a callback to be invoked once after the given interval.
|
||||
*
|
||||
* @param float $interval
|
||||
* @param callable $callback
|
||||
* @return TimerInterface
|
||||
* @see LoopInterface::addTimer()
|
||||
*/
|
||||
public static function addTimer($interval, $callback)
|
||||
{
|
||||
// create loop instance on demand (legacy PHP < 7 doesn't like ternaries in method calls)
|
||||
if (self::$instance === null) {
|
||||
self::get();
|
||||
}
|
||||
return self::$instance->addTimer($interval, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue a callback to be invoked repeatedly after the given interval.
|
||||
*
|
||||
* @param float $interval
|
||||
* @param callable $callback
|
||||
* @return TimerInterface
|
||||
* @see LoopInterface::addPeriodicTimer()
|
||||
*/
|
||||
public static function addPeriodicTimer($interval, $callback)
|
||||
{
|
||||
// create loop instance on demand (legacy PHP < 7 doesn't like ternaries in method calls)
|
||||
if (self::$instance === null) {
|
||||
self::get();
|
||||
}
|
||||
return self::$instance->addPeriodicTimer($interval, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a pending timer.
|
||||
*
|
||||
* @param TimerInterface $timer
|
||||
* @return void
|
||||
* @see LoopInterface::cancelTimer()
|
||||
*/
|
||||
public static function cancelTimer(TimerInterface $timer)
|
||||
{
|
||||
if (self::$instance !== null) {
|
||||
self::$instance->cancelTimer($timer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a callback to be invoked on a future tick of the event loop.
|
||||
*
|
||||
* @param callable $listener
|
||||
* @return void
|
||||
* @see LoopInterface::futureTick()
|
||||
*/
|
||||
public static function futureTick($listener)
|
||||
{
|
||||
// create loop instance on demand (legacy PHP < 7 doesn't like ternaries in method calls)
|
||||
if (self::$instance === null) {
|
||||
self::get();
|
||||
}
|
||||
|
||||
self::$instance->futureTick($listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a listener to be notified when a signal has been caught by this process.
|
||||
*
|
||||
* @param int $signal
|
||||
* @param callable $listener
|
||||
* @return void
|
||||
* @see LoopInterface::addSignal()
|
||||
*/
|
||||
public static function addSignal($signal, $listener)
|
||||
{
|
||||
// create loop instance on demand (legacy PHP < 7 doesn't like ternaries in method calls)
|
||||
if (self::$instance === null) {
|
||||
self::get();
|
||||
}
|
||||
|
||||
self::$instance->addSignal($signal, $listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a previously added signal listener.
|
||||
*
|
||||
* @param int $signal
|
||||
* @param callable $listener
|
||||
* @return void
|
||||
* @see LoopInterface::removeSignal()
|
||||
*/
|
||||
public static function removeSignal($signal, $listener)
|
||||
{
|
||||
if (self::$instance !== null) {
|
||||
self::$instance->removeSignal($signal, $listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the event loop until there are no more tasks to perform.
|
||||
*
|
||||
* @return void
|
||||
* @see LoopInterface::run()
|
||||
*/
|
||||
public static function run()
|
||||
{
|
||||
// create loop instance on demand (legacy PHP < 7 doesn't like ternaries in method calls)
|
||||
if (self::$instance === null) {
|
||||
self::get();
|
||||
}
|
||||
|
||||
self::$instance->run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruct a running event loop to stop.
|
||||
*
|
||||
* @return void
|
||||
* @see LoopInterface::stop()
|
||||
*/
|
||||
public static function stop()
|
||||
{
|
||||
self::$stopped = true;
|
||||
if (self::$instance !== null) {
|
||||
self::$instance->stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
+472
@@ -0,0 +1,472 @@
|
||||
<?php
|
||||
|
||||
namespace React\EventLoop;
|
||||
|
||||
interface LoopInterface
|
||||
{
|
||||
/**
|
||||
* [Advanced] Register a listener to be notified when a stream is ready to read.
|
||||
*
|
||||
* Note that this low-level API is considered advanced usage.
|
||||
* Most use cases should probably use the higher-level
|
||||
* [readable Stream API](https://github.com/reactphp/stream#readablestreaminterface)
|
||||
* instead.
|
||||
*
|
||||
* The first parameter MUST be a valid stream resource that supports
|
||||
* checking whether it is ready to read by this loop implementation.
|
||||
* A single stream resource MUST NOT be added more than once.
|
||||
* Instead, either call [`removeReadStream()`](#removereadstream) first or
|
||||
* react to this event with a single listener and then dispatch from this
|
||||
* listener. This method MAY throw an `Exception` if the given resource type
|
||||
* is not supported by this loop implementation.
|
||||
*
|
||||
* The second parameter MUST be a listener callback function that accepts
|
||||
* the stream resource as its only parameter.
|
||||
* If you don't use the stream resource inside your listener callback function
|
||||
* you MAY use a function which has no parameters at all.
|
||||
*
|
||||
* The listener callback function MUST NOT throw an `Exception`.
|
||||
* The return value of the listener callback function will be ignored and has
|
||||
* no effect, so for performance reasons you're recommended to not return
|
||||
* any excessive data structures.
|
||||
*
|
||||
* If you want to access any variables within your callback function, you
|
||||
* can bind arbitrary data to a callback closure like this:
|
||||
*
|
||||
* ```php
|
||||
* $loop->addReadStream($stream, function ($stream) use ($name) {
|
||||
* echo $name . ' said: ' . fread($stream);
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* See also [example #11](examples).
|
||||
*
|
||||
* You can invoke [`removeReadStream()`](#removereadstream) to remove the
|
||||
* read event listener for this stream.
|
||||
*
|
||||
* The execution order of listeners when multiple streams become ready at
|
||||
* the same time is not guaranteed.
|
||||
*
|
||||
* @param resource $stream The PHP stream resource to check.
|
||||
* @param callable $listener Invoked when the stream is ready.
|
||||
* @throws \Exception if the given resource type is not supported by this loop implementation
|
||||
* @see self::removeReadStream()
|
||||
*/
|
||||
public function addReadStream($stream, $listener);
|
||||
|
||||
/**
|
||||
* [Advanced] Register a listener to be notified when a stream is ready to write.
|
||||
*
|
||||
* Note that this low-level API is considered advanced usage.
|
||||
* Most use cases should probably use the higher-level
|
||||
* [writable Stream API](https://github.com/reactphp/stream#writablestreaminterface)
|
||||
* instead.
|
||||
*
|
||||
* The first parameter MUST be a valid stream resource that supports
|
||||
* checking whether it is ready to write by this loop implementation.
|
||||
* A single stream resource MUST NOT be added more than once.
|
||||
* Instead, either call [`removeWriteStream()`](#removewritestream) first or
|
||||
* react to this event with a single listener and then dispatch from this
|
||||
* listener. This method MAY throw an `Exception` if the given resource type
|
||||
* is not supported by this loop implementation.
|
||||
*
|
||||
* The second parameter MUST be a listener callback function that accepts
|
||||
* the stream resource as its only parameter.
|
||||
* If you don't use the stream resource inside your listener callback function
|
||||
* you MAY use a function which has no parameters at all.
|
||||
*
|
||||
* The listener callback function MUST NOT throw an `Exception`.
|
||||
* The return value of the listener callback function will be ignored and has
|
||||
* no effect, so for performance reasons you're recommended to not return
|
||||
* any excessive data structures.
|
||||
*
|
||||
* If you want to access any variables within your callback function, you
|
||||
* can bind arbitrary data to a callback closure like this:
|
||||
*
|
||||
* ```php
|
||||
* $loop->addWriteStream($stream, function ($stream) use ($name) {
|
||||
* fwrite($stream, 'Hello ' . $name);
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* See also [example #12](examples).
|
||||
*
|
||||
* You can invoke [`removeWriteStream()`](#removewritestream) to remove the
|
||||
* write event listener for this stream.
|
||||
*
|
||||
* The execution order of listeners when multiple streams become ready at
|
||||
* the same time is not guaranteed.
|
||||
*
|
||||
* Some event loop implementations are known to only trigger the listener if
|
||||
* the stream *becomes* readable (edge-triggered) and may not trigger if the
|
||||
* stream has already been readable from the beginning.
|
||||
* This also implies that a stream may not be recognized as readable when data
|
||||
* is still left in PHP's internal stream buffers.
|
||||
* As such, it's recommended to use `stream_set_read_buffer($stream, 0);`
|
||||
* to disable PHP's internal read buffer in this case.
|
||||
*
|
||||
* @param resource $stream The PHP stream resource to check.
|
||||
* @param callable $listener Invoked when the stream is ready.
|
||||
* @throws \Exception if the given resource type is not supported by this loop implementation
|
||||
* @see self::removeWriteStream()
|
||||
*/
|
||||
public function addWriteStream($stream, $listener);
|
||||
|
||||
/**
|
||||
* Remove the read event listener for the given stream.
|
||||
*
|
||||
* Removing a stream from the loop that has already been removed or trying
|
||||
* to remove a stream that was never added or is invalid has no effect.
|
||||
*
|
||||
* @param resource $stream The PHP stream resource.
|
||||
*/
|
||||
public function removeReadStream($stream);
|
||||
|
||||
/**
|
||||
* Remove the write event listener for the given stream.
|
||||
*
|
||||
* Removing a stream from the loop that has already been removed or trying
|
||||
* to remove a stream that was never added or is invalid has no effect.
|
||||
*
|
||||
* @param resource $stream The PHP stream resource.
|
||||
*/
|
||||
public function removeWriteStream($stream);
|
||||
|
||||
/**
|
||||
* Enqueue a callback to be invoked once after the given interval.
|
||||
*
|
||||
* The second parameter MUST be a timer callback function that accepts
|
||||
* the timer instance as its only parameter.
|
||||
* If you don't use the timer instance inside your timer callback function
|
||||
* you MAY use a function which has no parameters at all.
|
||||
*
|
||||
* The timer callback function MUST NOT throw an `Exception`.
|
||||
* The return value of the timer callback function will be ignored and has
|
||||
* no effect, so for performance reasons you're recommended to not return
|
||||
* any excessive data structures.
|
||||
*
|
||||
* This method returns a timer instance. The same timer instance will also be
|
||||
* passed into the timer callback function as described above.
|
||||
* You can invoke [`cancelTimer`](#canceltimer) to cancel a pending timer.
|
||||
* Unlike [`addPeriodicTimer()`](#addperiodictimer), this method will ensure
|
||||
* the callback will be invoked only once after the given interval.
|
||||
*
|
||||
* ```php
|
||||
* $loop->addTimer(0.8, function () {
|
||||
* echo 'world!' . PHP_EOL;
|
||||
* });
|
||||
*
|
||||
* $loop->addTimer(0.3, function () {
|
||||
* echo 'hello ';
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* See also [example #1](examples).
|
||||
*
|
||||
* If you want to access any variables within your callback function, you
|
||||
* can bind arbitrary data to a callback closure like this:
|
||||
*
|
||||
* ```php
|
||||
* function hello($name, LoopInterface $loop)
|
||||
* {
|
||||
* $loop->addTimer(1.0, function () use ($name) {
|
||||
* echo "hello $name\n";
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* hello('Tester', $loop);
|
||||
* ```
|
||||
*
|
||||
* This interface does not enforce any particular timer resolution, so
|
||||
* special care may have to be taken if you rely on very high precision with
|
||||
* millisecond accuracy or below. Event loop implementations SHOULD work on
|
||||
* a best effort basis and SHOULD provide at least millisecond accuracy
|
||||
* unless otherwise noted. Many existing event loop implementations are
|
||||
* known to provide microsecond accuracy, but it's generally not recommended
|
||||
* to rely on this high precision.
|
||||
*
|
||||
* Similarly, the execution order of timers scheduled to execute at the
|
||||
* same time (within its possible accuracy) is not guaranteed.
|
||||
*
|
||||
* This interface suggests that event loop implementations SHOULD use a
|
||||
* monotonic time source if available. Given that a monotonic time source is
|
||||
* only available as of PHP 7.3 by default, event loop implementations MAY
|
||||
* fall back to using wall-clock time.
|
||||
* While this does not affect many common use cases, this is an important
|
||||
* distinction for programs that rely on a high time precision or on systems
|
||||
* that are subject to discontinuous time adjustments (time jumps).
|
||||
* This means that if you schedule a timer to trigger in 30s and then adjust
|
||||
* your system time forward by 20s, the timer SHOULD still trigger in 30s.
|
||||
* See also [event loop implementations](#loop-implementations) for more details.
|
||||
*
|
||||
* @param int|float $interval The number of seconds to wait before execution.
|
||||
* @param callable $callback The callback to invoke.
|
||||
*
|
||||
* @return TimerInterface
|
||||
*/
|
||||
public function addTimer($interval, $callback);
|
||||
|
||||
/**
|
||||
* Enqueue a callback to be invoked repeatedly after the given interval.
|
||||
*
|
||||
* The second parameter MUST be a timer callback function that accepts
|
||||
* the timer instance as its only parameter.
|
||||
* If you don't use the timer instance inside your timer callback function
|
||||
* you MAY use a function which has no parameters at all.
|
||||
*
|
||||
* The timer callback function MUST NOT throw an `Exception`.
|
||||
* The return value of the timer callback function will be ignored and has
|
||||
* no effect, so for performance reasons you're recommended to not return
|
||||
* any excessive data structures.
|
||||
*
|
||||
* This method returns a timer instance. The same timer instance will also be
|
||||
* passed into the timer callback function as described above.
|
||||
* Unlike [`addTimer()`](#addtimer), this method will ensure the callback
|
||||
* will be invoked infinitely after the given interval or until you invoke
|
||||
* [`cancelTimer`](#canceltimer).
|
||||
*
|
||||
* ```php
|
||||
* $timer = $loop->addPeriodicTimer(0.1, function () {
|
||||
* echo 'tick!' . PHP_EOL;
|
||||
* });
|
||||
*
|
||||
* $loop->addTimer(1.0, function () use ($loop, $timer) {
|
||||
* $loop->cancelTimer($timer);
|
||||
* echo 'Done' . PHP_EOL;
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* See also [example #2](examples).
|
||||
*
|
||||
* If you want to limit the number of executions, you can bind
|
||||
* arbitrary data to a callback closure like this:
|
||||
*
|
||||
* ```php
|
||||
* function hello($name, LoopInterface $loop)
|
||||
* {
|
||||
* $n = 3;
|
||||
* $loop->addPeriodicTimer(1.0, function ($timer) use ($name, $loop, &$n) {
|
||||
* if ($n > 0) {
|
||||
* --$n;
|
||||
* echo "hello $name\n";
|
||||
* } else {
|
||||
* $loop->cancelTimer($timer);
|
||||
* }
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* hello('Tester', $loop);
|
||||
* ```
|
||||
*
|
||||
* This interface does not enforce any particular timer resolution, so
|
||||
* special care may have to be taken if you rely on very high precision with
|
||||
* millisecond accuracy or below. Event loop implementations SHOULD work on
|
||||
* a best effort basis and SHOULD provide at least millisecond accuracy
|
||||
* unless otherwise noted. Many existing event loop implementations are
|
||||
* known to provide microsecond accuracy, but it's generally not recommended
|
||||
* to rely on this high precision.
|
||||
*
|
||||
* Similarly, the execution order of timers scheduled to execute at the
|
||||
* same time (within its possible accuracy) is not guaranteed.
|
||||
*
|
||||
* This interface suggests that event loop implementations SHOULD use a
|
||||
* monotonic time source if available. Given that a monotonic time source is
|
||||
* only available as of PHP 7.3 by default, event loop implementations MAY
|
||||
* fall back to using wall-clock time.
|
||||
* While this does not affect many common use cases, this is an important
|
||||
* distinction for programs that rely on a high time precision or on systems
|
||||
* that are subject to discontinuous time adjustments (time jumps).
|
||||
* This means that if you schedule a timer to trigger in 30s and then adjust
|
||||
* your system time forward by 20s, the timer SHOULD still trigger in 30s.
|
||||
* See also [event loop implementations](#loop-implementations) for more details.
|
||||
*
|
||||
* Additionally, periodic timers may be subject to timer drift due to
|
||||
* re-scheduling after each invocation. As such, it's generally not
|
||||
* recommended to rely on this for high precision intervals with millisecond
|
||||
* accuracy or below.
|
||||
*
|
||||
* @param int|float $interval The number of seconds to wait before execution.
|
||||
* @param callable $callback The callback to invoke.
|
||||
*
|
||||
* @return TimerInterface
|
||||
*/
|
||||
public function addPeriodicTimer($interval, $callback);
|
||||
|
||||
/**
|
||||
* Cancel a pending timer.
|
||||
*
|
||||
* See also [`addPeriodicTimer()`](#addperiodictimer) and [example #2](examples).
|
||||
*
|
||||
* Calling this method on a timer instance that has not been added to this
|
||||
* loop instance or on a timer that has already been cancelled has no effect.
|
||||
*
|
||||
* @param TimerInterface $timer The timer to cancel.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function cancelTimer(TimerInterface $timer);
|
||||
|
||||
/**
|
||||
* Schedule a callback to be invoked on a future tick of the event loop.
|
||||
*
|
||||
* This works very much similar to timers with an interval of zero seconds,
|
||||
* but does not require the overhead of scheduling a timer queue.
|
||||
*
|
||||
* The tick callback function MUST be able to accept zero parameters.
|
||||
*
|
||||
* The tick callback function MUST NOT throw an `Exception`.
|
||||
* The return value of the tick callback function will be ignored and has
|
||||
* no effect, so for performance reasons you're recommended to not return
|
||||
* any excessive data structures.
|
||||
*
|
||||
* If you want to access any variables within your callback function, you
|
||||
* can bind arbitrary data to a callback closure like this:
|
||||
*
|
||||
* ```php
|
||||
* function hello($name, LoopInterface $loop)
|
||||
* {
|
||||
* $loop->futureTick(function () use ($name) {
|
||||
* echo "hello $name\n";
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* hello('Tester', $loop);
|
||||
* ```
|
||||
*
|
||||
* Unlike timers, tick callbacks are guaranteed to be executed in the order
|
||||
* they are enqueued.
|
||||
* Also, once a callback is enqueued, there's no way to cancel this operation.
|
||||
*
|
||||
* This is often used to break down bigger tasks into smaller steps (a form
|
||||
* of cooperative multitasking).
|
||||
*
|
||||
* ```php
|
||||
* $loop->futureTick(function () {
|
||||
* echo 'b';
|
||||
* });
|
||||
* $loop->futureTick(function () {
|
||||
* echo 'c';
|
||||
* });
|
||||
* echo 'a';
|
||||
* ```
|
||||
*
|
||||
* See also [example #3](examples).
|
||||
*
|
||||
* @param callable $listener The callback to invoke.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function futureTick($listener);
|
||||
|
||||
/**
|
||||
* Register a listener to be notified when a signal has been caught by this process.
|
||||
*
|
||||
* This is useful to catch user interrupt signals or shutdown signals from
|
||||
* tools like `supervisor` or `systemd`.
|
||||
*
|
||||
* The second parameter MUST be a listener callback function that accepts
|
||||
* the signal as its only parameter.
|
||||
* If you don't use the signal inside your listener callback function
|
||||
* you MAY use a function which has no parameters at all.
|
||||
*
|
||||
* The listener callback function MUST NOT throw an `Exception`.
|
||||
* The return value of the listener callback function will be ignored and has
|
||||
* no effect, so for performance reasons you're recommended to not return
|
||||
* any excessive data structures.
|
||||
*
|
||||
* ```php
|
||||
* $loop->addSignal(SIGINT, function (int $signal) {
|
||||
* echo 'Caught user interrupt signal' . PHP_EOL;
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* See also [example #4](examples).
|
||||
*
|
||||
* Signaling is only available on Unix-like platforms, Windows isn't
|
||||
* supported due to operating system limitations.
|
||||
* This method may throw a `BadMethodCallException` if signals aren't
|
||||
* supported on this platform, for example when required extensions are
|
||||
* missing.
|
||||
*
|
||||
* **Note: A listener can only be added once to the same signal, any
|
||||
* attempts to add it more than once will be ignored.**
|
||||
*
|
||||
* @param int $signal
|
||||
* @param callable $listener
|
||||
*
|
||||
* @throws \BadMethodCallException when signals aren't supported on this
|
||||
* platform, for example when required extensions are missing.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addSignal($signal, $listener);
|
||||
|
||||
/**
|
||||
* Removes a previously added signal listener.
|
||||
*
|
||||
* ```php
|
||||
* $loop->removeSignal(SIGINT, $listener);
|
||||
* ```
|
||||
*
|
||||
* Any attempts to remove listeners that aren't registered will be ignored.
|
||||
*
|
||||
* @param int $signal
|
||||
* @param callable $listener
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function removeSignal($signal, $listener);
|
||||
|
||||
/**
|
||||
* Run the event loop until there are no more tasks to perform.
|
||||
*
|
||||
* For many applications, this method is the only directly visible
|
||||
* invocation on the event loop.
|
||||
* As a rule of thumb, it is usually recommended to attach everything to the
|
||||
* same loop instance and then run the loop once at the bottom end of the
|
||||
* application.
|
||||
*
|
||||
* ```php
|
||||
* $loop->run();
|
||||
* ```
|
||||
*
|
||||
* This method will keep the loop running until there are no more tasks
|
||||
* to perform. In other words: This method will block until the last
|
||||
* timer, stream and/or signal has been removed.
|
||||
*
|
||||
* Likewise, it is imperative to ensure the application actually invokes
|
||||
* this method once. Adding listeners to the loop and missing to actually
|
||||
* run it will result in the application exiting without actually waiting
|
||||
* for any of the attached listeners.
|
||||
*
|
||||
* This method MUST NOT be called while the loop is already running.
|
||||
* This method MAY be called more than once after it has explicitly been
|
||||
* [`stop()`ped](#stop) or after it automatically stopped because it
|
||||
* previously did no longer have anything to do.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run();
|
||||
|
||||
/**
|
||||
* Instruct a running event loop to stop.
|
||||
*
|
||||
* This method is considered advanced usage and should be used with care.
|
||||
* As a rule of thumb, it is usually recommended to let the loop stop
|
||||
* only automatically when it no longer has anything to do.
|
||||
*
|
||||
* This method can be used to explicitly instruct the event loop to stop:
|
||||
*
|
||||
* ```php
|
||||
* $loop->addTimer(3.0, function () use ($loop) {
|
||||
* $loop->stop();
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Calling this method on a loop instance that is not currently running or
|
||||
* on a loop instance that has already been stopped has no effect.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function stop();
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace React\EventLoop;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class SignalsHandler
|
||||
{
|
||||
private $signals = array();
|
||||
|
||||
public function add($signal, $listener)
|
||||
{
|
||||
if (!isset($this->signals[$signal])) {
|
||||
$this->signals[$signal] = array();
|
||||
}
|
||||
|
||||
if (\in_array($listener, $this->signals[$signal])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->signals[$signal][] = $listener;
|
||||
}
|
||||
|
||||
public function remove($signal, $listener)
|
||||
{
|
||||
if (!isset($this->signals[$signal])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$index = \array_search($listener, $this->signals[$signal], true);
|
||||
unset($this->signals[$signal][$index]);
|
||||
|
||||
if (isset($this->signals[$signal]) && \count($this->signals[$signal]) === 0) {
|
||||
unset($this->signals[$signal]);
|
||||
}
|
||||
}
|
||||
|
||||
public function call($signal)
|
||||
{
|
||||
if (!isset($this->signals[$signal])) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->signals[$signal] as $listener) {
|
||||
\call_user_func($listener, $signal);
|
||||
}
|
||||
}
|
||||
|
||||
public function count($signal)
|
||||
{
|
||||
if (!isset($this->signals[$signal])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return \count($this->signals[$signal]);
|
||||
}
|
||||
|
||||
public function isEmpty()
|
||||
{
|
||||
return !$this->signals;
|
||||
}
|
||||
}
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
<?php
|
||||
|
||||
namespace React\EventLoop;
|
||||
|
||||
use React\EventLoop\Tick\FutureTickQueue;
|
||||
use React\EventLoop\Timer\Timer;
|
||||
use React\EventLoop\Timer\Timers;
|
||||
|
||||
/**
|
||||
* A `stream_select()` based event loop.
|
||||
*
|
||||
* This uses the [`stream_select()`](https://www.php.net/manual/en/function.stream-select.php)
|
||||
* function and is the only implementation that works out of the box with PHP.
|
||||
*
|
||||
* This event loop works out of the box on PHP 5.4 through PHP 8+ and HHVM.
|
||||
* This means that no installation is required and this library works on all
|
||||
* platforms and supported PHP versions.
|
||||
* Accordingly, the [`Loop` class](#loop) and the deprecated [`Factory`](#factory)
|
||||
* will use this event loop by default if you do not install any of the event loop
|
||||
* extensions listed below.
|
||||
*
|
||||
* Under the hood, it does a simple `select` system call.
|
||||
* This system call is limited to the maximum file descriptor number of
|
||||
* `FD_SETSIZE` (platform dependent, commonly 1024) and scales with `O(m)`
|
||||
* (`m` being the maximum file descriptor number passed).
|
||||
* This means that you may run into issues when handling thousands of streams
|
||||
* concurrently and you may want to look into using one of the alternative
|
||||
* event loop implementations listed below in this case.
|
||||
* If your use case is among the many common use cases that involve handling only
|
||||
* dozens or a few hundred streams at once, then this event loop implementation
|
||||
* performs really well.
|
||||
*
|
||||
* If you want to use signal handling (see also [`addSignal()`](#addsignal) below),
|
||||
* this event loop implementation requires `ext-pcntl`.
|
||||
* This extension is only available for Unix-like platforms and does not support
|
||||
* Windows.
|
||||
* It is commonly installed as part of many PHP distributions.
|
||||
* If this extension is missing (or you're running on Windows), signal handling is
|
||||
* not supported and throws a `BadMethodCallException` instead.
|
||||
*
|
||||
* This event loop is known to rely on wall-clock time to schedule future timers
|
||||
* when using any version before PHP 7.3, because a monotonic time source is
|
||||
* only available as of PHP 7.3 (`hrtime()`).
|
||||
* While this does not affect many common use cases, this is an important
|
||||
* distinction for programs that rely on a high time precision or on systems
|
||||
* that are subject to discontinuous time adjustments (time jumps).
|
||||
* This means that if you schedule a timer to trigger in 30s on PHP < 7.3 and
|
||||
* then adjust your system time forward by 20s, the timer may trigger in 10s.
|
||||
* See also [`addTimer()`](#addtimer) for more details.
|
||||
*
|
||||
* @link https://www.php.net/manual/en/function.stream-select.php
|
||||
*/
|
||||
final class StreamSelectLoop implements LoopInterface
|
||||
{
|
||||
/** @internal */
|
||||
const MICROSECONDS_PER_SECOND = 1000000;
|
||||
|
||||
private $futureTickQueue;
|
||||
private $timers;
|
||||
private $readStreams = array();
|
||||
private $readListeners = array();
|
||||
private $writeStreams = array();
|
||||
private $writeListeners = array();
|
||||
private $running;
|
||||
private $pcntl = false;
|
||||
private $pcntlPoll = false;
|
||||
private $signals;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->futureTickQueue = new FutureTickQueue();
|
||||
$this->timers = new Timers();
|
||||
$this->pcntl = \function_exists('pcntl_signal') && \function_exists('pcntl_signal_dispatch');
|
||||
$this->pcntlPoll = $this->pcntl && !\function_exists('pcntl_async_signals');
|
||||
$this->signals = new SignalsHandler();
|
||||
|
||||
// prefer async signals if available (PHP 7.1+) or fall back to dispatching on each tick
|
||||
if ($this->pcntl && !$this->pcntlPoll) {
|
||||
\pcntl_async_signals(true);
|
||||
}
|
||||
}
|
||||
|
||||
public function addReadStream($stream, $listener)
|
||||
{
|
||||
$key = (int) $stream;
|
||||
|
||||
if (!isset($this->readStreams[$key])) {
|
||||
$this->readStreams[$key] = $stream;
|
||||
$this->readListeners[$key] = $listener;
|
||||
}
|
||||
}
|
||||
|
||||
public function addWriteStream($stream, $listener)
|
||||
{
|
||||
$key = (int) $stream;
|
||||
|
||||
if (!isset($this->writeStreams[$key])) {
|
||||
$this->writeStreams[$key] = $stream;
|
||||
$this->writeListeners[$key] = $listener;
|
||||
}
|
||||
}
|
||||
|
||||
public function removeReadStream($stream)
|
||||
{
|
||||
$key = (int) $stream;
|
||||
|
||||
unset(
|
||||
$this->readStreams[$key],
|
||||
$this->readListeners[$key]
|
||||
);
|
||||
}
|
||||
|
||||
public function removeWriteStream($stream)
|
||||
{
|
||||
$key = (int) $stream;
|
||||
|
||||
unset(
|
||||
$this->writeStreams[$key],
|
||||
$this->writeListeners[$key]
|
||||
);
|
||||
}
|
||||
|
||||
public function addTimer($interval, $callback)
|
||||
{
|
||||
$timer = new Timer($interval, $callback, false);
|
||||
|
||||
$this->timers->add($timer);
|
||||
|
||||
return $timer;
|
||||
}
|
||||
|
||||
public function addPeriodicTimer($interval, $callback)
|
||||
{
|
||||
$timer = new Timer($interval, $callback, true);
|
||||
|
||||
$this->timers->add($timer);
|
||||
|
||||
return $timer;
|
||||
}
|
||||
|
||||
public function cancelTimer(TimerInterface $timer)
|
||||
{
|
||||
$this->timers->cancel($timer);
|
||||
}
|
||||
|
||||
public function futureTick($listener)
|
||||
{
|
||||
$this->futureTickQueue->add($listener);
|
||||
}
|
||||
|
||||
public function addSignal($signal, $listener)
|
||||
{
|
||||
if ($this->pcntl === false) {
|
||||
throw new \BadMethodCallException('Event loop feature "signals" isn\'t supported by the "StreamSelectLoop"');
|
||||
}
|
||||
|
||||
$first = $this->signals->count($signal) === 0;
|
||||
$this->signals->add($signal, $listener);
|
||||
|
||||
if ($first) {
|
||||
\pcntl_signal($signal, array($this->signals, 'call'));
|
||||
}
|
||||
}
|
||||
|
||||
public function removeSignal($signal, $listener)
|
||||
{
|
||||
if (!$this->signals->count($signal)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->signals->remove($signal, $listener);
|
||||
|
||||
if ($this->signals->count($signal) === 0) {
|
||||
\pcntl_signal($signal, \SIG_DFL);
|
||||
}
|
||||
}
|
||||
|
||||
public function run()
|
||||
{
|
||||
$this->running = true;
|
||||
|
||||
while ($this->running) {
|
||||
$this->futureTickQueue->tick();
|
||||
|
||||
$this->timers->tick();
|
||||
|
||||
// Future-tick queue has pending callbacks ...
|
||||
if (!$this->running || !$this->futureTickQueue->isEmpty()) {
|
||||
$timeout = 0;
|
||||
|
||||
// There is a pending timer, only block until it is due ...
|
||||
} elseif ($scheduledAt = $this->timers->getFirst()) {
|
||||
$timeout = $scheduledAt - $this->timers->getTime();
|
||||
if ($timeout < 0) {
|
||||
$timeout = 0;
|
||||
} else {
|
||||
// Convert float seconds to int microseconds.
|
||||
// Ensure we do not exceed maximum integer size, which may
|
||||
// cause the loop to tick once every ~35min on 32bit systems.
|
||||
$timeout *= self::MICROSECONDS_PER_SECOND;
|
||||
$timeout = $timeout > \PHP_INT_MAX ? \PHP_INT_MAX : (int)$timeout;
|
||||
}
|
||||
|
||||
// The only possible event is stream or signal activity, so wait forever ...
|
||||
} elseif ($this->readStreams || $this->writeStreams || !$this->signals->isEmpty()) {
|
||||
$timeout = null;
|
||||
|
||||
// There's nothing left to do ...
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
$this->waitForStreamActivity($timeout);
|
||||
}
|
||||
}
|
||||
|
||||
public function stop()
|
||||
{
|
||||
$this->running = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait/check for stream activity, or until the next timer is due.
|
||||
*
|
||||
* @param integer|null $timeout Activity timeout in microseconds, or null to wait forever.
|
||||
*/
|
||||
private function waitForStreamActivity($timeout)
|
||||
{
|
||||
$read = $this->readStreams;
|
||||
$write = $this->writeStreams;
|
||||
|
||||
$available = $this->streamSelect($read, $write, $timeout);
|
||||
if ($this->pcntlPoll) {
|
||||
\pcntl_signal_dispatch();
|
||||
}
|
||||
if (false === $available) {
|
||||
// if a system call has been interrupted,
|
||||
// we cannot rely on it's outcome
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($read as $stream) {
|
||||
$key = (int) $stream;
|
||||
|
||||
if (isset($this->readListeners[$key])) {
|
||||
\call_user_func($this->readListeners[$key], $stream);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($write as $stream) {
|
||||
$key = (int) $stream;
|
||||
|
||||
if (isset($this->writeListeners[$key])) {
|
||||
\call_user_func($this->writeListeners[$key], $stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulate a stream_select() implementation that does not break when passed
|
||||
* empty stream arrays.
|
||||
*
|
||||
* @param array $read An array of read streams to select upon.
|
||||
* @param array $write An array of write streams to select upon.
|
||||
* @param int|null $timeout Activity timeout in microseconds, or null to wait forever.
|
||||
*
|
||||
* @return int|false The total number of streams that are ready for read/write.
|
||||
* Can return false if stream_select() is interrupted by a signal.
|
||||
*/
|
||||
private function streamSelect(array &$read, array &$write, $timeout)
|
||||
{
|
||||
if ($read || $write) {
|
||||
// We do not usually use or expose the `exceptfds` parameter passed to the underlying `select`.
|
||||
// However, Windows does not report failed connection attempts in `writefds` passed to `select` like most other platforms.
|
||||
// Instead, it uses `writefds` only for successful connection attempts and `exceptfds` for failed connection attempts.
|
||||
// We work around this by adding all sockets that look like a pending connection attempt to `exceptfds` automatically on Windows and merge it back later.
|
||||
// This ensures the public API matches other loop implementations across all platforms (see also test suite or rather test matrix).
|
||||
// Lacking better APIs, every write-only socket that has not yet read any data is assumed to be in a pending connection attempt state.
|
||||
// @link https://docs.microsoft.com/de-de/windows/win32/api/winsock2/nf-winsock2-select
|
||||
$except = null;
|
||||
if (\DIRECTORY_SEPARATOR === '\\') {
|
||||
$except = array();
|
||||
foreach ($write as $key => $socket) {
|
||||
if (!isset($read[$key]) && @\ftell($socket) === 0) {
|
||||
$except[$key] = $socket;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @var ?callable $previous */
|
||||
$previous = \set_error_handler(function ($errno, $errstr) use (&$previous) {
|
||||
// suppress warnings that occur when `stream_select()` is interrupted by a signal
|
||||
// PHP defines `EINTR` through `ext-sockets` or `ext-pcntl`, otherwise use common default (Linux & Mac)
|
||||
$eintr = \defined('SOCKET_EINTR') ? \SOCKET_EINTR : (\defined('PCNTL_EINTR') ? \PCNTL_EINTR : 4);
|
||||
if ($errno === \E_WARNING && \strpos($errstr, '[' . $eintr .']: ') !== false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// forward any other error to registered error handler or print warning
|
||||
return ($previous !== null) ? \call_user_func_array($previous, \func_get_args()) : false;
|
||||
});
|
||||
|
||||
try {
|
||||
$ret = \stream_select($read, $write, $except, $timeout === null ? null : 0, $timeout);
|
||||
\restore_error_handler();
|
||||
} catch (\Throwable $e) { // @codeCoverageIgnoreStart
|
||||
\restore_error_handler();
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
\restore_error_handler();
|
||||
throw $e;
|
||||
} // @codeCoverageIgnoreEnd
|
||||
|
||||
if ($except) {
|
||||
$write = \array_merge($write, $except);
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
if ($timeout > 0) {
|
||||
\usleep($timeout);
|
||||
} elseif ($timeout === null) {
|
||||
// wait forever (we only reach this if we're only awaiting signals)
|
||||
// this may be interrupted and return earlier when a signal is received
|
||||
\sleep(PHP_INT_MAX);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace React\EventLoop\Tick;
|
||||
|
||||
use SplQueue;
|
||||
|
||||
/**
|
||||
* A tick queue implementation that can hold multiple callback functions
|
||||
*
|
||||
* This class should only be used internally, see LoopInterface instead.
|
||||
*
|
||||
* @see LoopInterface
|
||||
* @internal
|
||||
*/
|
||||
final class FutureTickQueue
|
||||
{
|
||||
private $queue;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->queue = new SplQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a callback to be invoked on a future tick of the event loop.
|
||||
*
|
||||
* Callbacks are guaranteed to be executed in the order they are enqueued.
|
||||
*
|
||||
* @param callable $listener The callback to invoke.
|
||||
*/
|
||||
public function add($listener)
|
||||
{
|
||||
$this->queue->enqueue($listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the callback queue.
|
||||
*/
|
||||
public function tick()
|
||||
{
|
||||
// Only invoke as many callbacks as were on the queue when tick() was called.
|
||||
$count = $this->queue->count();
|
||||
|
||||
while ($count--) {
|
||||
\call_user_func(
|
||||
$this->queue->dequeue()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the next tick queue is empty.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isEmpty()
|
||||
{
|
||||
return $this->queue->isEmpty();
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace React\EventLoop\Timer;
|
||||
|
||||
use React\EventLoop\TimerInterface;
|
||||
|
||||
/**
|
||||
* The actual connection implementation for TimerInterface
|
||||
*
|
||||
* This class should only be used internally, see TimerInterface instead.
|
||||
*
|
||||
* @see TimerInterface
|
||||
* @internal
|
||||
*/
|
||||
final class Timer implements TimerInterface
|
||||
{
|
||||
const MIN_INTERVAL = 0.000001;
|
||||
|
||||
private $interval;
|
||||
private $callback;
|
||||
private $periodic;
|
||||
|
||||
/**
|
||||
* Constructor initializes the fields of the Timer
|
||||
*
|
||||
* @param float $interval The interval after which this timer will execute, in seconds
|
||||
* @param callable $callback The callback that will be executed when this timer elapses
|
||||
* @param bool $periodic Whether the time is periodic
|
||||
*/
|
||||
public function __construct($interval, $callback, $periodic = false)
|
||||
{
|
||||
if ($interval < self::MIN_INTERVAL) {
|
||||
$interval = self::MIN_INTERVAL;
|
||||
}
|
||||
|
||||
$this->interval = (float) $interval;
|
||||
$this->callback = $callback;
|
||||
$this->periodic = (bool) $periodic;
|
||||
}
|
||||
|
||||
public function getInterval()
|
||||
{
|
||||
return $this->interval;
|
||||
}
|
||||
|
||||
public function getCallback()
|
||||
{
|
||||
return $this->callback;
|
||||
}
|
||||
|
||||
public function isPeriodic()
|
||||
{
|
||||
return $this->periodic;
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace React\EventLoop\Timer;
|
||||
|
||||
use React\EventLoop\TimerInterface;
|
||||
|
||||
/**
|
||||
* A scheduler implementation that can hold multiple timer instances
|
||||
*
|
||||
* This class should only be used internally, see TimerInterface instead.
|
||||
*
|
||||
* @see TimerInterface
|
||||
* @internal
|
||||
*/
|
||||
final class Timers
|
||||
{
|
||||
private $time;
|
||||
private $timers = array();
|
||||
private $schedule = array();
|
||||
private $sorted = true;
|
||||
private $useHighResolution;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// prefer high-resolution timer, available as of PHP 7.3+
|
||||
$this->useHighResolution = \function_exists('hrtime');
|
||||
}
|
||||
|
||||
public function updateTime()
|
||||
{
|
||||
return $this->time = $this->useHighResolution ? \hrtime(true) * 1e-9 : \microtime(true);
|
||||
}
|
||||
|
||||
public function getTime()
|
||||
{
|
||||
return $this->time ?: $this->updateTime();
|
||||
}
|
||||
|
||||
public function add(TimerInterface $timer)
|
||||
{
|
||||
$id = \PHP_VERSION_ID < 70200 ? \spl_object_hash($timer) : \spl_object_id($timer);
|
||||
$this->timers[$id] = $timer;
|
||||
$this->schedule[$id] = $timer->getInterval() + $this->updateTime();
|
||||
$this->sorted = false;
|
||||
}
|
||||
|
||||
public function contains(TimerInterface $timer)
|
||||
{
|
||||
$id = \PHP_VERSION_ID < 70200 ? \spl_object_hash($timer) : \spl_object_id($timer);
|
||||
return isset($this->timers[$id]);
|
||||
}
|
||||
|
||||
public function cancel(TimerInterface $timer)
|
||||
{
|
||||
$id = \PHP_VERSION_ID < 70200 ? \spl_object_hash($timer) : \spl_object_id($timer);
|
||||
unset($this->timers[$id], $this->schedule[$id]);
|
||||
}
|
||||
|
||||
public function getFirst()
|
||||
{
|
||||
// ensure timers are sorted to simply accessing next (first) one
|
||||
if (!$this->sorted) {
|
||||
$this->sorted = true;
|
||||
\asort($this->schedule);
|
||||
}
|
||||
|
||||
return \reset($this->schedule);
|
||||
}
|
||||
|
||||
public function isEmpty()
|
||||
{
|
||||
return \count($this->timers) === 0;
|
||||
}
|
||||
|
||||
public function tick()
|
||||
{
|
||||
// hot path: skip timers if nothing is scheduled
|
||||
if (!$this->schedule) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure timers are sorted so we can execute in order
|
||||
if (!$this->sorted) {
|
||||
$this->sorted = true;
|
||||
\asort($this->schedule);
|
||||
}
|
||||
|
||||
$time = $this->updateTime();
|
||||
|
||||
foreach ($this->schedule as $id => $scheduled) {
|
||||
// schedule is ordered, so loop until first timer that is not scheduled for execution now
|
||||
if ($scheduled >= $time) {
|
||||
break;
|
||||
}
|
||||
|
||||
// skip any timers that are removed while we process the current schedule
|
||||
if (!isset($this->schedule[$id]) || $this->schedule[$id] !== $scheduled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$timer = $this->timers[$id];
|
||||
\call_user_func($timer->getCallback(), $timer);
|
||||
|
||||
// re-schedule if this is a periodic timer and it has not been cancelled explicitly already
|
||||
if ($timer->isPeriodic() && isset($this->timers[$id])) {
|
||||
$this->schedule[$id] = $timer->getInterval() + $time;
|
||||
$this->sorted = false;
|
||||
} else {
|
||||
unset($this->timers[$id], $this->schedule[$id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace React\EventLoop;
|
||||
|
||||
interface TimerInterface
|
||||
{
|
||||
/**
|
||||
* Get the interval after which this timer will execute, in seconds
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getInterval();
|
||||
|
||||
/**
|
||||
* Get the callback that will be executed when this timer elapses
|
||||
*
|
||||
* @return callable
|
||||
*/
|
||||
public function getCallback();
|
||||
|
||||
/**
|
||||
* Determine whether the time is periodic
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isPeriodic();
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
# Changelog
|
||||
|
||||
## 1.11.0 (2024-06-04)
|
||||
|
||||
* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable type declarations.
|
||||
(#70 by @clue)
|
||||
|
||||
* Feature: Full PHP 8.3 compatibility.
|
||||
(#68 by @clue)
|
||||
|
||||
## 1.10.0 (2023-07-20)
|
||||
|
||||
* Feature: Use Promise v3 template types.
|
||||
(#67 by @clue and #63 and #64 by @WyriHaximus)
|
||||
|
||||
* Minor documentation improvements.
|
||||
(#59 by @nhedger)
|
||||
|
||||
* Improve test suite, avoid unhandled promise rejections and report failed assertions.
|
||||
(#66 and #62 by @clue and #61 by @WyriHaximus)
|
||||
|
||||
## 1.9.0 (2022-06-13)
|
||||
|
||||
* Feature: Improve forward compatibility with upcoming Promise v3 API.
|
||||
(#54 and #55 by @clue)
|
||||
|
||||
* Minor documentation improvements for upcoming Promise v3.
|
||||
(#58 by @clue and #56 by @SimonFrings)
|
||||
|
||||
* Improve test suite, fix legacy HHVM build by downgrading Composer.
|
||||
(#57 by @SimonFrings)
|
||||
|
||||
## 1.8.0 (2021-12-06)
|
||||
|
||||
* Feature: Add new `sleep()` function and deprecate `resolve()` and `reject()` functions.
|
||||
(#51 by @clue)
|
||||
|
||||
```php
|
||||
// deprecated
|
||||
React\Promise\Timer\resolve($time);
|
||||
React\Promise\Timer\reject($time);
|
||||
|
||||
// new
|
||||
React\Promise\Timer\sleep($time);
|
||||
```
|
||||
|
||||
* Feature: Support PHP 8.1 release.
|
||||
(#50 by @Thomas-Gelf, #52 by @clue and #48 by @SimonFrings)
|
||||
|
||||
* Improve API documentation and add parameter types and return types.
|
||||
(#49 by @clue and #47 by @SimonFrings)
|
||||
|
||||
## 1.7.0 (2021-07-11)
|
||||
|
||||
A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop).
|
||||
|
||||
* Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop).
|
||||
(#46 by @clue)
|
||||
|
||||
```php
|
||||
// old (still supported)
|
||||
$promise = timeout($promise, $time, $loop);
|
||||
$promise = resolve($time, $loop);
|
||||
$promise = reject($time, $loop);
|
||||
|
||||
// new (using default loop)
|
||||
$promise = timeout($promise, $time);
|
||||
$promise = resolve($time);
|
||||
$promise = reject($time);
|
||||
```
|
||||
|
||||
* Improve test suite, use GitHub actions for continuous integration (CI),
|
||||
update PHPUnit config, run tests on PHP 8 and add full core team to the license.
|
||||
(#43 by @WyriHaximus, #44 and #45 by @SimonFrings)
|
||||
|
||||
## 1.6.0 (2020-07-10)
|
||||
|
||||
* Feature: Forward compatibility with react/promise v3.
|
||||
(#37 by @WyriHaximus)
|
||||
|
||||
* Improve test suite and add `.gitattributes` to exclude dev files from exports.
|
||||
Run tests on PHPUnit 9 and PHP 7.4 and clean up test suite.
|
||||
(#38 by @WyriHaximus, #39 by @reedy, #41 by @clue and #42 by @SimonFrings)
|
||||
|
||||
## 1.5.1 (2019-03-27)
|
||||
|
||||
* Fix: Typo in readme
|
||||
(#35 by @aak74)
|
||||
|
||||
* Improvement: Only include functions file when functions aren't defined
|
||||
(#36 by @Niko9911)
|
||||
|
||||
## 1.5.0 (2018-06-13)
|
||||
|
||||
* Feature: Improve memory consumption by cleaning up garbage references to pending promise without canceller.
|
||||
(#34 by @clue)
|
||||
|
||||
## 1.4.0 (2018-06-11)
|
||||
|
||||
* Feature: Improve memory consumption by cleaning up garbage references.
|
||||
(#33 by @clue)
|
||||
|
||||
## 1.3.0 (2018-04-24)
|
||||
|
||||
* Feature: Improve memory consumption by cleaning up unneeded references.
|
||||
(#32 by @clue)
|
||||
|
||||
## 1.2.1 (2017-12-22)
|
||||
|
||||
* README improvements
|
||||
(#28 by @jsor)
|
||||
|
||||
* Improve test suite by adding forward compatiblity with PHPUnit 6 and
|
||||
fix test suite forward compatibility with upcoming EventLoop releases
|
||||
(#30 and #31 by @clue)
|
||||
|
||||
## 1.2.0 (2017-08-08)
|
||||
|
||||
* Feature: Only start timers if input Promise is still pending and
|
||||
return a settled output promise if the input is already settled.
|
||||
(#25 by @clue)
|
||||
|
||||
* Feature: Cap minimum timer interval at 1µs across all versions
|
||||
(#23 by @clue)
|
||||
|
||||
* Feature: Forward compatibility with EventLoop v1.0 and v0.5
|
||||
(#27 by @clue)
|
||||
|
||||
* Improve test suite by adding PHPUnit to require-dev and
|
||||
lock Travis distro so new defaults will not break the build
|
||||
(#24 and #26 by @clue)
|
||||
|
||||
## 1.1.1 (2016-12-27)
|
||||
|
||||
* Improve test suite to use PSR-4 autoloader and proper namespaces.
|
||||
(#21 by @clue)
|
||||
|
||||
## 1.1.0 (2016-02-29)
|
||||
|
||||
* Feature: Support promise cancellation for all timer primitives
|
||||
(#18 by @clue)
|
||||
|
||||
## 1.0.0 (2015-09-29)
|
||||
|
||||
* First tagged release
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Vendored
+318
@@ -0,0 +1,318 @@
|
||||
# PromiseTimer
|
||||
|
||||
[](https://github.com/reactphp/promise-timer/actions)
|
||||
[](https://packagist.org/packages/react/promise-timer)
|
||||
|
||||
A trivial implementation of timeouts for `Promise`s, built on top of [ReactPHP](https://reactphp.org/).
|
||||
|
||||
**Table of contents**
|
||||
|
||||
* [Usage](#usage)
|
||||
* [timeout()](#timeout)
|
||||
* [sleep()](#sleep)
|
||||
* [~~resolve()~~](#resolve)
|
||||
* [~~reject()~~](#reject)
|
||||
* [TimeoutException](#timeoutexception)
|
||||
* [getTimeout()](#gettimeout)
|
||||
* [Install](#install)
|
||||
* [Tests](#tests)
|
||||
* [License](#license)
|
||||
|
||||
## Usage
|
||||
|
||||
This lightweight library consists only of a few simple functions.
|
||||
All functions reside under the `React\Promise\Timer` namespace.
|
||||
|
||||
The below examples refer to all functions with their fully-qualified names like this:
|
||||
|
||||
```php
|
||||
React\Promise\Timer\timeout(…);
|
||||
```
|
||||
|
||||
As of PHP 5.6+ you can also import each required function into your code like this:
|
||||
|
||||
```php
|
||||
use function React\Promise\Timer\timeout;
|
||||
|
||||
timeout(…);
|
||||
```
|
||||
|
||||
Alternatively, you can also use an import statement similar to this:
|
||||
|
||||
```php
|
||||
use React\Promise\Timer;
|
||||
|
||||
Timer\timeout(…);
|
||||
```
|
||||
|
||||
### timeout()
|
||||
|
||||
The `timeout(PromiseInterface<T> $promise, float $time, ?LoopInterface $loop = null): PromiseInterface<T>` function can be used to
|
||||
cancel operations that take *too long*.
|
||||
|
||||
You need to pass in an input `$promise` that represents a pending operation
|
||||
and timeout parameters. It returns a new promise with the following
|
||||
resolution behavior:
|
||||
|
||||
- If the input `$promise` resolves before `$time` seconds, resolve the
|
||||
resulting promise with its fulfillment value.
|
||||
|
||||
- If the input `$promise` rejects before `$time` seconds, reject the
|
||||
resulting promise with its rejection value.
|
||||
|
||||
- If the input `$promise` does not settle before `$time` seconds, *cancel*
|
||||
the operation and reject the resulting promise with a [`TimeoutException`](#timeoutexception).
|
||||
|
||||
Internally, the given `$time` value will be used to start a timer that will
|
||||
*cancel* the pending operation once it triggers. This implies that if you
|
||||
pass a really small (or negative) value, it will still start a timer and will
|
||||
thus trigger at the earliest possible time in the future.
|
||||
|
||||
If the input `$promise` is already settled, then the resulting promise will
|
||||
resolve or reject immediately without starting a timer at all.
|
||||
|
||||
This function takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
pass the event loop instance to use. You can use a `null` value here in order to
|
||||
use the [default loop](https://github.com/reactphp/event-loop#loop). This value
|
||||
SHOULD NOT be given unless you're sure you want to explicitly use a given event
|
||||
loop instance.
|
||||
|
||||
A common use case for handling only resolved values looks like this:
|
||||
|
||||
```php
|
||||
$promise = accessSomeRemoteResource();
|
||||
React\Promise\Timer\timeout($promise, 10.0)->then(function ($value) {
|
||||
// the operation finished within 10.0 seconds
|
||||
});
|
||||
```
|
||||
|
||||
A more complete example could look like this:
|
||||
|
||||
```php
|
||||
$promise = accessSomeRemoteResource();
|
||||
React\Promise\Timer\timeout($promise, 10.0)->then(
|
||||
function ($value) {
|
||||
// the operation finished within 10.0 seconds
|
||||
},
|
||||
function ($error) {
|
||||
if ($error instanceof React\Promise\Timer\TimeoutException) {
|
||||
// the operation has failed due to a timeout
|
||||
} else {
|
||||
// the input operation has failed due to some other error
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Or if you're using [react/promise v3](https://github.com/reactphp/promise):
|
||||
|
||||
```php
|
||||
React\Promise\Timer\timeout($promise, 10.0)->then(function ($value) {
|
||||
// the operation finished within 10.0 seconds
|
||||
})->catch(function (React\Promise\Timer\TimeoutException $error) {
|
||||
// the operation has failed due to a timeout
|
||||
})->catch(function (Throwable $error) {
|
||||
// the input operation has failed due to some other error
|
||||
});
|
||||
```
|
||||
|
||||
As discussed above, the [`timeout()`](#timeout) function will take care of
|
||||
the underlying operation if it takes *too long*. In this case, you can be
|
||||
sure the resulting promise will always be rejected with a
|
||||
[`TimeoutException`](#timeoutexception). On top of this, the function will
|
||||
try to *cancel* the underlying operation. Responsibility for this
|
||||
cancellation logic is left up to the underlying operation.
|
||||
|
||||
- A common use case involves cleaning up any resources like open network
|
||||
sockets or file handles or terminating external processes or timers.
|
||||
|
||||
- If the given input `$promise` does not support cancellation, then this is a
|
||||
NO-OP. This means that while the resulting promise will still be rejected,
|
||||
the underlying input `$promise` may still be pending and can hence continue
|
||||
consuming resources
|
||||
|
||||
On top of this, the returned promise is implemented in such a way that it can
|
||||
be cancelled when it is still pending. Cancelling a pending promise will
|
||||
cancel the underlying operation. As discussed above, responsibility for this
|
||||
cancellation logic is left up to the underlying operation.
|
||||
|
||||
```php
|
||||
$promise = accessSomeRemoteResource();
|
||||
$timeout = React\Promise\Timer\timeout($promise, 10.0);
|
||||
|
||||
$timeout->cancel();
|
||||
```
|
||||
|
||||
For more details on the promise cancellation, please refer to the
|
||||
[Promise documentation](https://github.com/reactphp/promise#cancellablepromiseinterface).
|
||||
|
||||
If you want to wait for multiple promises to resolve, you can use the normal
|
||||
promise primitives like this:
|
||||
|
||||
```php
|
||||
$promises = array(
|
||||
accessSomeRemoteResource(),
|
||||
accessSomeRemoteResource(),
|
||||
accessSomeRemoteResource()
|
||||
);
|
||||
|
||||
$promise = React\Promise\all($promises);
|
||||
|
||||
React\Promise\Timer\timeout($promise, 10)->then(function ($values) {
|
||||
// *all* promises resolved
|
||||
});
|
||||
```
|
||||
|
||||
The applies to all promise collection primitives alike, i.e. `all()`,
|
||||
`race()`, `any()`, `some()` etc.
|
||||
|
||||
For more details on the promise primitives, please refer to the
|
||||
[Promise documentation](https://github.com/reactphp/promise#functions).
|
||||
|
||||
### sleep()
|
||||
|
||||
The `sleep(float $time, ?LoopInterface $loop = null): PromiseInterface<void>` function can be used to
|
||||
create a new promise that resolves in `$time` seconds.
|
||||
|
||||
```php
|
||||
React\Promise\Timer\sleep(1.5)->then(function () {
|
||||
echo 'Thanks for waiting!' . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
Internally, the given `$time` value will be used to start a timer that will
|
||||
resolve the promise once it triggers. This implies that if you pass a really
|
||||
small (or negative) value, it will still start a timer and will thus trigger
|
||||
at the earliest possible time in the future.
|
||||
|
||||
This function takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
pass the event loop instance to use. You can use a `null` value here in order to
|
||||
use the [default loop](https://github.com/reactphp/event-loop#loop). This value
|
||||
SHOULD NOT be given unless you're sure you want to explicitly use a given event
|
||||
loop instance.
|
||||
|
||||
The returned promise is implemented in such a way that it can be cancelled
|
||||
when it is still pending. Cancelling a pending promise will reject its value
|
||||
with a `RuntimeException` and clean up any pending timers.
|
||||
|
||||
```php
|
||||
$timer = React\Promise\Timer\sleep(2.0);
|
||||
|
||||
$timer->cancel();
|
||||
```
|
||||
|
||||
### ~~resolve()~~
|
||||
|
||||
> Deprecated since v1.8.0, see [`sleep()`](#sleep) instead.
|
||||
|
||||
The `resolve(float $time, ?LoopInterface $loop = null): PromiseInterface<float>` function can be used to
|
||||
create a new promise that resolves in `$time` seconds with the `$time` as the fulfillment value.
|
||||
|
||||
```php
|
||||
React\Promise\Timer\resolve(1.5)->then(function ($time) {
|
||||
echo 'Thanks for waiting ' . $time . ' seconds' . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
Internally, the given `$time` value will be used to start a timer that will
|
||||
resolve the promise once it triggers. This implies that if you pass a really
|
||||
small (or negative) value, it will still start a timer and will thus trigger
|
||||
at the earliest possible time in the future.
|
||||
|
||||
This function takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
pass the event loop instance to use. You can use a `null` value here in order to
|
||||
use the [default loop](https://github.com/reactphp/event-loop#loop). This value
|
||||
SHOULD NOT be given unless you're sure you want to explicitly use a given event
|
||||
loop instance.
|
||||
|
||||
The returned promise is implemented in such a way that it can be cancelled
|
||||
when it is still pending. Cancelling a pending promise will reject its value
|
||||
with a `RuntimeException` and clean up any pending timers.
|
||||
|
||||
```php
|
||||
$timer = React\Promise\Timer\resolve(2.0);
|
||||
|
||||
$timer->cancel();
|
||||
```
|
||||
|
||||
### ~~reject()~~
|
||||
|
||||
> Deprecated since v1.8.0, see [`sleep()`](#sleep) instead.
|
||||
|
||||
The `reject(float $time, ?LoopInterface $loop = null): PromiseInterface<never>` function can be used to
|
||||
create a new promise which rejects in `$time` seconds with a `TimeoutException`.
|
||||
|
||||
```php
|
||||
React\Promise\Timer\reject(2.0)->then(null, function (React\Promise\Timer\TimeoutException $e) {
|
||||
echo 'Rejected after ' . $e->getTimeout() . ' seconds ' . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
Internally, the given `$time` value will be used to start a timer that will
|
||||
reject the promise once it triggers. This implies that if you pass a really
|
||||
small (or negative) value, it will still start a timer and will thus trigger
|
||||
at the earliest possible time in the future.
|
||||
|
||||
This function takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
pass the event loop instance to use. You can use a `null` value here in order to
|
||||
use the [default loop](https://github.com/reactphp/event-loop#loop). This value
|
||||
SHOULD NOT be given unless you're sure you want to explicitly use a given event
|
||||
loop instance.
|
||||
|
||||
The returned promise is implemented in such a way that it can be cancelled
|
||||
when it is still pending. Cancelling a pending promise will reject its value
|
||||
with a `RuntimeException` and clean up any pending timers.
|
||||
|
||||
```php
|
||||
$timer = React\Promise\Timer\reject(2.0);
|
||||
|
||||
$timer->cancel();
|
||||
```
|
||||
|
||||
### TimeoutException
|
||||
|
||||
The `TimeoutException` extends PHP's built-in `RuntimeException`.
|
||||
|
||||
|
||||
#### getTimeout()
|
||||
|
||||
The `getTimeout(): float` method can be used to
|
||||
get the timeout value in seconds.
|
||||
|
||||
## Install
|
||||
|
||||
The recommended way to install this library is [through Composer](https://getcomposer.org/).
|
||||
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
|
||||
|
||||
This project follows [SemVer](https://semver.org/).
|
||||
This will install the latest supported version:
|
||||
|
||||
```bash
|
||||
composer require react/promise-timer:^1.11
|
||||
```
|
||||
|
||||
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
|
||||
|
||||
This project aims to run on any platform and thus does not require any PHP
|
||||
extensions and supports running on legacy PHP 5.3 through current PHP 8+ and
|
||||
HHVM.
|
||||
It's *highly recommended to use the latest supported PHP version* for this project.
|
||||
|
||||
## Tests
|
||||
|
||||
To run the test suite, you first need to clone this repo and then install all
|
||||
dependencies [through Composer](https://getcomposer.org/):
|
||||
|
||||
```bash
|
||||
composer install
|
||||
```
|
||||
|
||||
To run the test suite, go to the project root and run:
|
||||
|
||||
```bash
|
||||
vendor/bin/phpunit
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT, see [LICENSE file](LICENSE).
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "react/promise-timer",
|
||||
"description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.",
|
||||
"keywords": ["Promise", "timeout", "timer", "event-loop", "ReactPHP", "async"],
|
||||
"homepage": "https://github.com/reactphp/promise-timer",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"homepage": "https://clue.engineering/",
|
||||
"email": "christian@clue.engineering"
|
||||
},
|
||||
{
|
||||
"name": "Cees-Jan Kiewiet",
|
||||
"homepage": "https://wyrihaximus.net/",
|
||||
"email": "reactphp@ceesjankiewiet.nl"
|
||||
},
|
||||
{
|
||||
"name": "Jan Sorgalla",
|
||||
"homepage": "https://sorgalla.com/",
|
||||
"email": "jsorgalla@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Chris Boden",
|
||||
"homepage": "https://cboden.dev/",
|
||||
"email": "cboden@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3",
|
||||
"react/event-loop": "^1.2",
|
||||
"react/promise": "^3.2 || ^2.7.0 || ^1.2.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"React\\Promise\\Timer\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/functions_include.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"React\\Tests\\Promise\\Timer\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace React\Promise\Timer;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class TimeoutException extends RuntimeException
|
||||
{
|
||||
/** @var float */
|
||||
private $timeout;
|
||||
|
||||
/**
|
||||
* @param float $timeout
|
||||
* @param string|null $message
|
||||
* @param int|null $code
|
||||
* @param null|\Exception|\Throwable $previous
|
||||
*/
|
||||
public function __construct($timeout, $message = '', $code = 0, $previous = null)
|
||||
{
|
||||
// Preserve compatibility with our former nullable signature, but avoid invalid arguments for the parent constructor:
|
||||
parent::__construct((string) $message, (int) $code, $previous);
|
||||
|
||||
$this->timeout = (float) $timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timeout value in seconds.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getTimeout()
|
||||
{
|
||||
return $this->timeout;
|
||||
}
|
||||
}
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
<?php
|
||||
|
||||
namespace React\Promise\Timer;
|
||||
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise\Promise;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
/**
|
||||
* Cancel operations that take *too long*.
|
||||
*
|
||||
* You need to pass in an input `$promise` that represents a pending operation
|
||||
* and timeout parameters. It returns a new promise with the following
|
||||
* resolution behavior:
|
||||
*
|
||||
* - If the input `$promise` resolves before `$time` seconds, resolve the
|
||||
* resulting promise with its fulfillment value.
|
||||
*
|
||||
* - If the input `$promise` rejects before `$time` seconds, reject the
|
||||
* resulting promise with its rejection value.
|
||||
*
|
||||
* - If the input `$promise` does not settle before `$time` seconds, *cancel*
|
||||
* the operation and reject the resulting promise with a [`TimeoutException`](#timeoutexception).
|
||||
*
|
||||
* Internally, the given `$time` value will be used to start a timer that will
|
||||
* *cancel* the pending operation once it triggers. This implies that if you
|
||||
* pass a really small (or negative) value, it will still start a timer and will
|
||||
* thus trigger at the earliest possible time in the future.
|
||||
*
|
||||
* If the input `$promise` is already settled, then the resulting promise will
|
||||
* resolve or reject immediately without starting a timer at all.
|
||||
*
|
||||
* This function takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
* pass the event loop instance to use. You can use a `null` value here in order to
|
||||
* use the [default loop](https://github.com/reactphp/event-loop#loop). This value
|
||||
* SHOULD NOT be given unless you're sure you want to explicitly use a given event
|
||||
* loop instance.
|
||||
*
|
||||
* A common use case for handling only resolved values looks like this:
|
||||
*
|
||||
* ```php
|
||||
* $promise = accessSomeRemoteResource();
|
||||
* React\Promise\Timer\timeout($promise, 10.0)->then(function ($value) {
|
||||
* // the operation finished within 10.0 seconds
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* A more complete example could look like this:
|
||||
*
|
||||
* ```php
|
||||
* $promise = accessSomeRemoteResource();
|
||||
* React\Promise\Timer\timeout($promise, 10.0)->then(
|
||||
* function ($value) {
|
||||
* // the operation finished within 10.0 seconds
|
||||
* },
|
||||
* function ($error) {
|
||||
* if ($error instanceof React\Promise\Timer\TimeoutException) {
|
||||
* // the operation has failed due to a timeout
|
||||
* } else {
|
||||
* // the input operation has failed due to some other error
|
||||
* }
|
||||
* }
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* Or if you're using [react/promise v3](https://github.com/reactphp/promise):
|
||||
*
|
||||
* ```php
|
||||
* React\Promise\Timer\timeout($promise, 10.0)->then(function ($value) {
|
||||
* // the operation finished within 10.0 seconds
|
||||
* })->catch(function (React\Promise\Timer\TimeoutException $error) {
|
||||
* // the operation has failed due to a timeout
|
||||
* })->catch(function (Throwable $error) {
|
||||
* // the input operation has failed due to some other error
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* As discussed above, the [`timeout()`](#timeout) function will take care of
|
||||
* the underlying operation if it takes *too long*. In this case, you can be
|
||||
* sure the resulting promise will always be rejected with a
|
||||
* [`TimeoutException`](#timeoutexception). On top of this, the function will
|
||||
* try to *cancel* the underlying operation. Responsibility for this
|
||||
* cancellation logic is left up to the underlying operation.
|
||||
*
|
||||
* - A common use case involves cleaning up any resources like open network
|
||||
* sockets or file handles or terminating external processes or timers.
|
||||
*
|
||||
* - If the given input `$promise` does not support cancellation, then this is a
|
||||
* NO-OP. This means that while the resulting promise will still be rejected,
|
||||
* the underlying input `$promise` may still be pending and can hence continue
|
||||
* consuming resources
|
||||
*
|
||||
* On top of this, the returned promise is implemented in such a way that it can
|
||||
* be cancelled when it is still pending. Cancelling a pending promise will
|
||||
* cancel the underlying operation. As discussed above, responsibility for this
|
||||
* cancellation logic is left up to the underlying operation.
|
||||
*
|
||||
* ```php
|
||||
* $promise = accessSomeRemoteResource();
|
||||
* $timeout = React\Promise\Timer\timeout($promise, 10.0);
|
||||
*
|
||||
* $timeout->cancel();
|
||||
* ```
|
||||
*
|
||||
* For more details on the promise cancellation, please refer to the
|
||||
* [Promise documentation](https://github.com/reactphp/promise#cancellablepromiseinterface).
|
||||
*
|
||||
* If you want to wait for multiple promises to resolve, you can use the normal
|
||||
* promise primitives like this:
|
||||
*
|
||||
* ```php
|
||||
* $promises = array(
|
||||
* accessSomeRemoteResource(),
|
||||
* accessSomeRemoteResource(),
|
||||
* accessSomeRemoteResource()
|
||||
* );
|
||||
*
|
||||
* $promise = React\Promise\all($promises);
|
||||
*
|
||||
* React\Promise\Timer\timeout($promise, 10)->then(function ($values) {
|
||||
* // *all* promises resolved
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* The applies to all promise collection primitives alike, i.e. `all()`,
|
||||
* `race()`, `any()`, `some()` etc.
|
||||
*
|
||||
* For more details on the promise primitives, please refer to the
|
||||
* [Promise documentation](https://github.com/reactphp/promise#functions).
|
||||
*
|
||||
* @template T
|
||||
* @param PromiseInterface<T> $promise
|
||||
* @param float $time
|
||||
* @param ?LoopInterface $loop
|
||||
* @return PromiseInterface<T>
|
||||
*/
|
||||
function timeout(PromiseInterface $promise, $time, $loop = null)
|
||||
{
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #3 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
|
||||
// cancelling this promise will only try to cancel the input promise,
|
||||
// thus leaving responsibility to the input promise.
|
||||
$canceller = null;
|
||||
if (\method_exists($promise, 'cancel')) {
|
||||
// pass promise by reference to clean reference after cancellation handler
|
||||
// has been invoked once in order to avoid garbage references in call stack.
|
||||
$canceller = function () use (&$promise) {
|
||||
$promise->cancel();
|
||||
$promise = null;
|
||||
};
|
||||
}
|
||||
|
||||
if ($loop === null) {
|
||||
$loop = Loop::get();
|
||||
}
|
||||
|
||||
return new Promise(function ($resolve, $reject) use ($loop, $time, $promise) {
|
||||
$timer = null;
|
||||
$promise = $promise->then(function ($v) use (&$timer, $loop, $resolve) {
|
||||
if ($timer) {
|
||||
$loop->cancelTimer($timer);
|
||||
}
|
||||
$timer = false;
|
||||
$resolve($v);
|
||||
}, function ($v) use (&$timer, $loop, $reject) {
|
||||
if ($timer) {
|
||||
$loop->cancelTimer($timer);
|
||||
}
|
||||
$timer = false;
|
||||
$reject($v);
|
||||
});
|
||||
|
||||
// promise already resolved => no need to start timer
|
||||
if ($timer === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// start timeout timer which will cancel the input promise
|
||||
$timer = $loop->addTimer($time, function () use ($time, &$promise, $reject) {
|
||||
$reject(new TimeoutException($time, 'Timed out after ' . $time . ' seconds'));
|
||||
|
||||
// try to invoke cancellation handler of input promise and then clean
|
||||
// reference in order to avoid garbage references in call stack.
|
||||
if (\method_exists($promise, 'cancel')) {
|
||||
$promise->cancel();
|
||||
}
|
||||
$promise = null;
|
||||
});
|
||||
}, $canceller);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new promise that resolves in `$time` seconds.
|
||||
*
|
||||
* ```php
|
||||
* React\Promise\Timer\sleep(1.5)->then(function () {
|
||||
* echo 'Thanks for waiting!' . PHP_EOL;
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Internally, the given `$time` value will be used to start a timer that will
|
||||
* resolve the promise once it triggers. This implies that if you pass a really
|
||||
* small (or negative) value, it will still start a timer and will thus trigger
|
||||
* at the earliest possible time in the future.
|
||||
*
|
||||
* This function takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
* pass the event loop instance to use. You can use a `null` value here in order to
|
||||
* use the [default loop](https://github.com/reactphp/event-loop#loop). This value
|
||||
* SHOULD NOT be given unless you're sure you want to explicitly use a given event
|
||||
* loop instance.
|
||||
*
|
||||
* The returned promise is implemented in such a way that it can be cancelled
|
||||
* when it is still pending. Cancelling a pending promise will reject its value
|
||||
* with a `RuntimeException` and clean up any pending timers.
|
||||
*
|
||||
* ```php
|
||||
* $timer = React\Promise\Timer\sleep(2.0);
|
||||
*
|
||||
* $timer->cancel();
|
||||
* ```
|
||||
*
|
||||
* @param float $time
|
||||
* @param ?LoopInterface $loop
|
||||
* @return PromiseInterface<void>
|
||||
*/
|
||||
function sleep($time, $loop = null)
|
||||
{
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
|
||||
if ($loop === null) {
|
||||
$loop = Loop::get();
|
||||
}
|
||||
|
||||
$timer = null;
|
||||
return new Promise(function ($resolve) use ($loop, $time, &$timer) {
|
||||
// resolve the promise when the timer fires in $time seconds
|
||||
$timer = $loop->addTimer($time, function () use ($resolve) {
|
||||
$resolve(null);
|
||||
});
|
||||
}, function () use (&$timer, $loop) {
|
||||
// cancelling this promise will cancel the timer, clean the reference
|
||||
// in order to avoid garbage references in call stack and then reject.
|
||||
$loop->cancelTimer($timer);
|
||||
$timer = null;
|
||||
|
||||
throw new \RuntimeException('Timer cancelled');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* [Deprecated] Create a new promise that resolves in `$time` seconds with the `$time` as the fulfillment value.
|
||||
*
|
||||
* ```php
|
||||
* React\Promise\Timer\resolve(1.5)->then(function ($time) {
|
||||
* echo 'Thanks for waiting ' . $time . ' seconds' . PHP_EOL;
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Internally, the given `$time` value will be used to start a timer that will
|
||||
* resolve the promise once it triggers. This implies that if you pass a really
|
||||
* small (or negative) value, it will still start a timer and will thus trigger
|
||||
* at the earliest possible time in the future.
|
||||
*
|
||||
* This function takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
* pass the event loop instance to use. You can use a `null` value here in order to
|
||||
* use the [default loop](https://github.com/reactphp/event-loop#loop). This value
|
||||
* SHOULD NOT be given unless you're sure you want to explicitly use a given event
|
||||
* loop instance.
|
||||
*
|
||||
* The returned promise is implemented in such a way that it can be cancelled
|
||||
* when it is still pending. Cancelling a pending promise will reject its value
|
||||
* with a `RuntimeException` and clean up any pending timers.
|
||||
*
|
||||
* ```php
|
||||
* $timer = React\Promise\Timer\resolve(2.0);
|
||||
*
|
||||
* $timer->cancel();
|
||||
* ```
|
||||
*
|
||||
* @param float $time
|
||||
* @param ?LoopInterface $loop
|
||||
* @return PromiseInterface<float>
|
||||
* @deprecated 1.8.0 See `sleep()` instead
|
||||
* @see sleep()
|
||||
*/
|
||||
function resolve($time, $loop = null)
|
||||
{
|
||||
return sleep($time, $loop)->then(function() use ($time) {
|
||||
return $time;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* [Deprecated] Create a new promise which rejects in `$time` seconds with a `TimeoutException`.
|
||||
*
|
||||
* ```php
|
||||
* React\Promise\Timer\reject(2.0)->then(null, function (React\Promise\Timer\TimeoutException $e) {
|
||||
* echo 'Rejected after ' . $e->getTimeout() . ' seconds ' . PHP_EOL;
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Internally, the given `$time` value will be used to start a timer that will
|
||||
* reject the promise once it triggers. This implies that if you pass a really
|
||||
* small (or negative) value, it will still start a timer and will thus trigger
|
||||
* at the earliest possible time in the future.
|
||||
*
|
||||
* This function takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
* pass the event loop instance to use. You can use a `null` value here in order to
|
||||
* use the [default loop](https://github.com/reactphp/event-loop#loop). This value
|
||||
* SHOULD NOT be given unless you're sure you want to explicitly use a given event
|
||||
* loop instance.
|
||||
*
|
||||
* The returned promise is implemented in such a way that it can be cancelled
|
||||
* when it is still pending. Cancelling a pending promise will reject its value
|
||||
* with a `RuntimeException` and clean up any pending timers.
|
||||
*
|
||||
* ```php
|
||||
* $timer = React\Promise\Timer\reject(2.0);
|
||||
*
|
||||
* $timer->cancel();
|
||||
* ```
|
||||
*
|
||||
* @param float $time
|
||||
* @param ?LoopInterface $loop
|
||||
* @return PromiseInterface<never>
|
||||
* @deprecated 1.8.0 See `sleep()` instead
|
||||
* @see sleep()
|
||||
*/
|
||||
function reject($time, $loop = null)
|
||||
{
|
||||
return sleep($time, $loop)->then(function () use ($time) {
|
||||
throw new TimeoutException($time, 'Timer expired after ' . $time . ' seconds');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace React\Promise\Timer;
|
||||
|
||||
if (!function_exists('React\\Promise\\Timer\\timeout')) {
|
||||
require __DIR__ . '/functions.php';
|
||||
}
|
||||
Vendored
+156
@@ -0,0 +1,156 @@
|
||||
# Changelog
|
||||
|
||||
## 3.2.0 (2024-05-24)
|
||||
|
||||
* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable type declarations.
|
||||
(#260 by @Ayesh)
|
||||
|
||||
* Feature: Include previous exceptions when reporting unhandled promise rejections.
|
||||
(#262 by @clue)
|
||||
|
||||
* Update test suite to improve PHP 8.4+ support.
|
||||
(#261 by @SimonFrings)
|
||||
|
||||
## 3.1.0 (2023-11-16)
|
||||
|
||||
* Feature: Full PHP 8.3 compatibility.
|
||||
(#255 by @clue)
|
||||
|
||||
* Feature: Describe all callable arguments with types for `Promise` and `Deferred`.
|
||||
(#253 by @clue)
|
||||
|
||||
* Update test suite and minor documentation improvements.
|
||||
(#251 by @ondrejmirtes and #250 by @SQKo)
|
||||
|
||||
## 3.0.0 (2023-07-11)
|
||||
|
||||
A major new feature release, see [**release announcement**](https://clue.engineering/2023/announcing-reactphp-promise-v3).
|
||||
|
||||
* We'd like to emphasize that this component is production ready and battle-tested.
|
||||
We plan to support all long-term support (LTS) releases for at least 24 months,
|
||||
so you have a rock-solid foundation to build on top of.
|
||||
|
||||
* The v3 release will be the way forward for this package. However, we will still
|
||||
actively support v2 and v1 to provide a smooth upgrade path for those not yet
|
||||
on the latest versions.
|
||||
|
||||
This update involves some major new features and a minor BC break over the
|
||||
`v2.0.0` release. We've tried hard to avoid BC breaks where possible and
|
||||
minimize impact otherwise. We expect that most consumers of this package will be
|
||||
affected by BC breaks, but updating should take no longer than a few minutes.
|
||||
See below for more details:
|
||||
|
||||
* BC break: PHP 8.1+ recommended, PHP 7.1+ required.
|
||||
(#138 and #149 by @WyriHaximus)
|
||||
|
||||
* Feature / BC break: The `PromiseInterface` now includes the functionality of the old ~~`ExtendedPromiseInterface`~~ and ~~`CancellablePromiseInterface`~~.
|
||||
Each promise now always includes the `then()`, `catch()`, `finally()` and `cancel()` methods.
|
||||
The new `catch()` and `finally()` methods replace the deprecated ~~`otherwise()`~~ and ~~`always()`~~ methods which continue to exist for BC reasons.
|
||||
The old ~~`ExtendedPromiseInterface`~~ and ~~`CancellablePromiseInterface`~~ are no longer needed and have been removed as a consequence.
|
||||
(#75 by @jsor and #208 by @clue and @WyriHaximus)
|
||||
|
||||
```php
|
||||
// old (multiple interfaces may or may not be implemented)
|
||||
assert($promise instanceof PromiseInterface);
|
||||
assert(method_exists($promise, 'then'));
|
||||
if ($promise instanceof ExtendedPromiseInterface) { assert(method_exists($promise, 'otherwise')); }
|
||||
if ($promise instanceof ExtendedPromiseInterface) { assert(method_exists($promise, 'always')); }
|
||||
if ($promise instanceof CancellablePromiseInterface) { assert(method_exists($promise, 'cancel')); }
|
||||
|
||||
// new (single PromiseInterface with all methods)
|
||||
assert($promise instanceof PromiseInterface);
|
||||
assert(method_exists($promise, 'then'));
|
||||
assert(method_exists($promise, 'catch'));
|
||||
assert(method_exists($promise, 'finally'));
|
||||
assert(method_exists($promise, 'cancel'));
|
||||
```
|
||||
|
||||
* Feature / BC break: Improve type safety of promises. Require `mixed` fulfillment value argument and `Throwable` (or `Exception`) as rejection reason.
|
||||
Add PHPStan template types to ensure strict types for `resolve(T $value): PromiseInterface<T>` and `reject(Throwable $reason): PromiseInterface<never>`.
|
||||
It is no longer possible to resolve a promise without a value (use `null` instead) or reject a promise without a reason (use `Throwable` instead).
|
||||
(#93, #141 and #142 by @jsor, #138, #149 and #247 by @WyriHaximus and #213 and #246 by @clue)
|
||||
|
||||
```php
|
||||
// old (arguments used to be optional)
|
||||
$promise = resolve();
|
||||
$promise = reject();
|
||||
|
||||
// new (already supported before)
|
||||
$promise = resolve(null);
|
||||
$promise = reject(new RuntimeException());
|
||||
```
|
||||
|
||||
* Feature / BC break: Report all unhandled rejections by default and remove ~~`done()`~~ method.
|
||||
Add new `set_rejection_handler()` function to set the global rejection handler for unhandled promise rejections.
|
||||
(#248, #249 and #224 by @clue)
|
||||
|
||||
```php
|
||||
// Unhandled promise rejection with RuntimeException: Unhandled in example.php:2
|
||||
reject(new RuntimeException('Unhandled'));
|
||||
```
|
||||
|
||||
* BC break: Remove all deprecated APIs and reduce API surface.
|
||||
Remove ~~`some()`~~, ~~`map()`~~, ~~`reduce()`~~ functions, use `any()` and `all()` functions instead.
|
||||
Remove internal ~~`FulfilledPromise`~~ and ~~`RejectedPromise`~~ classes, use `resolve()` and `reject()` functions instead.
|
||||
Remove legacy promise progress API (deprecated third argument to `then()` method) and deprecated ~~`LazyPromise`~~ class.
|
||||
(#32 and #98 by @jsor and #164, #219 and #220 by @clue)
|
||||
|
||||
* BC break: Make all classes final to encourage composition over inheritance.
|
||||
(#80 by @jsor)
|
||||
|
||||
* Feature / BC break: Require `array` (or `iterable`) type for `all()` + `race()` + `any()` functions and bring in line with ES6 specification.
|
||||
These functions now require a single argument with a variable number of promises or values as input.
|
||||
(#225 by @clue and #35 by @jsor)
|
||||
|
||||
* Fix / BC break: Fix `race()` to return a forever pending promise when called with an empty `array` (or `iterable`) and bring in line with ES6 specification.
|
||||
(#83 by @jsor and #225 by @clue)
|
||||
|
||||
* Minor performance improvements by initializing `Deferred` in the constructor and avoiding `call_user_func()` calls.
|
||||
(#151 by @WyriHaximus and #171 by @Kubo2)
|
||||
|
||||
* Minor documentation improvements.
|
||||
(#110 by @seregazhuk, #132 by @CharlotteDunois, #145 by @danielecr, #178 by @WyriHaximus, #189 by @srdante, #212 by @clue, #214, #239 and #243 by @SimonFrings and #231 by @nhedger)
|
||||
|
||||
The following changes had to be ported to this release due to our branching
|
||||
strategy, but also appeared in the [`2.x` branch](https://github.com/reactphp/promise/tree/2.x):
|
||||
|
||||
* Feature: Support union types and address deprecation of `ReflectionType::getClass()` (PHP 8+).
|
||||
(#197 by @cdosoftei and @SimonFrings)
|
||||
|
||||
* Feature: Support intersection types (PHP 8.1+).
|
||||
(#209 by @bzikarsky)
|
||||
|
||||
* Feature: Support DNS types (PHP 8.2+).
|
||||
(#236 by @nhedger)
|
||||
|
||||
* Feature: Port all memory improvements from `2.x` to `3.x`.
|
||||
(#150 by @clue and @WyriHaximus)
|
||||
|
||||
* Fix: Fix checking whether cancellable promise is an object and avoid possible warning.
|
||||
(#161 by @smscr)
|
||||
|
||||
* Improve performance by prefixing all global functions calls with \ to skip the look up and resolve process and go straight to the global function.
|
||||
(#134 by @WyriHaximus)
|
||||
|
||||
* Improve test suite, update PHPUnit and PHP versions and add `.gitattributes` to exclude dev files from exports.
|
||||
(#107 by @carusogabriel, #148 and #234 by @WyriHaximus, #153 by @reedy, #162, #230 and #240 by @clue, #173, #177, #185 and #199 by @SimonFrings, #193 by @woodongwong and #210 by @bzikarsky)
|
||||
|
||||
The following changes were originally planned for this release but later reverted
|
||||
and are not part of the final release:
|
||||
|
||||
* Add iterative callback queue handler to avoid recursion (later removed to improve Fiber support).
|
||||
(#28, #82 and #86 by @jsor, #158 by @WyriHaximus and #229 and #238 by @clue)
|
||||
|
||||
* Trigger an `E_USER_ERROR` instead of throwing an exception from `done()` (later removed entire `done()` method to globally report unhandled rejections).
|
||||
(#97 by @jsor and #224 and #248 by @clue)
|
||||
|
||||
* Add type declarations for `some()` (later removed entire `some()` function).
|
||||
(#172 by @WyriHaximus and #219 by @clue)
|
||||
|
||||
## 2.0.0 (2013-12-10)
|
||||
|
||||
See [`2.x` CHANGELOG](https://github.com/reactphp/promise/blob/2.x/CHANGELOG.md) for more details.
|
||||
|
||||
## 1.0.0 (2012-11-07)
|
||||
|
||||
See [`1.x` CHANGELOG](https://github.com/reactphp/promise/blob/1.x/CHANGELOG.md) for more details.
|
||||
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012 Jan Sorgalla, Christian Lück, Cees-Jan Kiewiet, Chris Boden
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
Vendored
+722
@@ -0,0 +1,722 @@
|
||||
Promise
|
||||
=======
|
||||
|
||||
A lightweight implementation of
|
||||
[CommonJS Promises/A](http://wiki.commonjs.org/wiki/Promises/A) for PHP.
|
||||
|
||||
[](https://github.com/reactphp/promise/actions)
|
||||
[](https://packagist.org/packages/react/promise)
|
||||
|
||||
Table of Contents
|
||||
-----------------
|
||||
|
||||
1. [Introduction](#introduction)
|
||||
2. [Concepts](#concepts)
|
||||
* [Deferred](#deferred)
|
||||
* [Promise](#promise-1)
|
||||
3. [API](#api)
|
||||
* [Deferred](#deferred-1)
|
||||
* [Deferred::promise()](#deferredpromise)
|
||||
* [Deferred::resolve()](#deferredresolve)
|
||||
* [Deferred::reject()](#deferredreject)
|
||||
* [PromiseInterface](#promiseinterface)
|
||||
* [PromiseInterface::then()](#promiseinterfacethen)
|
||||
* [PromiseInterface::catch()](#promiseinterfacecatch)
|
||||
* [PromiseInterface::finally()](#promiseinterfacefinally)
|
||||
* [PromiseInterface::cancel()](#promiseinterfacecancel)
|
||||
* [~~PromiseInterface::otherwise()~~](#promiseinterfaceotherwise)
|
||||
* [~~PromiseInterface::always()~~](#promiseinterfacealways)
|
||||
* [Promise](#promise-2)
|
||||
* [Functions](#functions)
|
||||
* [resolve()](#resolve)
|
||||
* [reject()](#reject)
|
||||
* [all()](#all)
|
||||
* [race()](#race)
|
||||
* [any()](#any)
|
||||
* [set_rejection_handler()](#set_rejection_handler)
|
||||
4. [Examples](#examples)
|
||||
* [How to use Deferred](#how-to-use-deferred)
|
||||
* [How promise forwarding works](#how-promise-forwarding-works)
|
||||
* [Resolution forwarding](#resolution-forwarding)
|
||||
* [Rejection forwarding](#rejection-forwarding)
|
||||
* [Mixed resolution and rejection forwarding](#mixed-resolution-and-rejection-forwarding)
|
||||
5. [Install](#install)
|
||||
6. [Tests](#tests)
|
||||
7. [Credits](#credits)
|
||||
8. [License](#license)
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
Promise is a library implementing
|
||||
[CommonJS Promises/A](http://wiki.commonjs.org/wiki/Promises/A) for PHP.
|
||||
|
||||
It also provides several other useful promise-related concepts, such as joining
|
||||
multiple promises and mapping and reducing collections of promises.
|
||||
|
||||
If you've never heard about promises before,
|
||||
[read this first](https://gist.github.com/domenic/3889970).
|
||||
|
||||
Concepts
|
||||
--------
|
||||
|
||||
### Deferred
|
||||
|
||||
A **Deferred** represents a computation or unit of work that may not have
|
||||
completed yet. Typically (but not always), that computation will be something
|
||||
that executes asynchronously and completes at some point in the future.
|
||||
|
||||
### Promise
|
||||
|
||||
While a deferred represents the computation itself, a **Promise** represents
|
||||
the result of that computation. Thus, each deferred has a promise that acts as
|
||||
a placeholder for its actual result.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
### Deferred
|
||||
|
||||
A deferred represents an operation whose resolution is pending. It has separate
|
||||
promise and resolver parts.
|
||||
|
||||
```php
|
||||
$deferred = new React\Promise\Deferred();
|
||||
|
||||
$promise = $deferred->promise();
|
||||
|
||||
$deferred->resolve(mixed $value);
|
||||
$deferred->reject(\Throwable $reason);
|
||||
```
|
||||
|
||||
The `promise` method returns the promise of the deferred.
|
||||
|
||||
The `resolve` and `reject` methods control the state of the deferred.
|
||||
|
||||
The constructor of the `Deferred` accepts an optional `$canceller` argument.
|
||||
See [Promise](#promise-2) for more information.
|
||||
|
||||
#### Deferred::promise()
|
||||
|
||||
```php
|
||||
$promise = $deferred->promise();
|
||||
```
|
||||
|
||||
Returns the promise of the deferred, which you can hand out to others while
|
||||
keeping the authority to modify its state to yourself.
|
||||
|
||||
#### Deferred::resolve()
|
||||
|
||||
```php
|
||||
$deferred->resolve(mixed $value);
|
||||
```
|
||||
|
||||
Resolves the promise returned by `promise()`. All consumers are notified by
|
||||
having `$onFulfilled` (which they registered via `$promise->then()`) called with
|
||||
`$value`.
|
||||
|
||||
If `$value` itself is a promise, the promise will transition to the state of
|
||||
this promise once it is resolved.
|
||||
|
||||
See also the [`resolve()` function](#resolve).
|
||||
|
||||
#### Deferred::reject()
|
||||
|
||||
```php
|
||||
$deferred->reject(\Throwable $reason);
|
||||
```
|
||||
|
||||
Rejects the promise returned by `promise()`, signalling that the deferred's
|
||||
computation failed.
|
||||
All consumers are notified by having `$onRejected` (which they registered via
|
||||
`$promise->then()`) called with `$reason`.
|
||||
|
||||
See also the [`reject()` function](#reject).
|
||||
|
||||
### PromiseInterface
|
||||
|
||||
The promise interface provides the common interface for all promise
|
||||
implementations.
|
||||
See [Promise](#promise-2) for the only public implementation exposed by this
|
||||
package.
|
||||
|
||||
A promise represents an eventual outcome, which is either fulfillment (success)
|
||||
and an associated value, or rejection (failure) and an associated reason.
|
||||
|
||||
Once in the fulfilled or rejected state, a promise becomes immutable.
|
||||
Neither its state nor its result (or error) can be modified.
|
||||
|
||||
#### PromiseInterface::then()
|
||||
|
||||
```php
|
||||
$transformedPromise = $promise->then(callable $onFulfilled = null, callable $onRejected = null);
|
||||
```
|
||||
|
||||
Transforms a promise's value by applying a function to the promise's fulfillment
|
||||
or rejection value. Returns a new promise for the transformed result.
|
||||
|
||||
The `then()` method registers new fulfilled and rejection handlers with a promise
|
||||
(all parameters are optional):
|
||||
|
||||
* `$onFulfilled` will be invoked once the promise is fulfilled and passed
|
||||
the result as the first argument.
|
||||
* `$onRejected` will be invoked once the promise is rejected and passed the
|
||||
reason as the first argument.
|
||||
|
||||
It returns a new promise that will fulfill with the return value of either
|
||||
`$onFulfilled` or `$onRejected`, whichever is called, or will reject with
|
||||
the thrown exception if either throws.
|
||||
|
||||
A promise makes the following guarantees about handlers registered in
|
||||
the same call to `then()`:
|
||||
|
||||
1. Only one of `$onFulfilled` or `$onRejected` will be called,
|
||||
never both.
|
||||
2. `$onFulfilled` and `$onRejected` will never be called more
|
||||
than once.
|
||||
|
||||
#### See also
|
||||
|
||||
* [resolve()](#resolve) - Creating a resolved promise
|
||||
* [reject()](#reject) - Creating a rejected promise
|
||||
|
||||
#### PromiseInterface::catch()
|
||||
|
||||
```php
|
||||
$promise->catch(callable $onRejected);
|
||||
```
|
||||
|
||||
Registers a rejection handler for promise. It is a shortcut for:
|
||||
|
||||
```php
|
||||
$promise->then(null, $onRejected);
|
||||
```
|
||||
|
||||
Additionally, you can type hint the `$reason` argument of `$onRejected` to catch
|
||||
only specific errors.
|
||||
|
||||
```php
|
||||
$promise
|
||||
->catch(function (\RuntimeException $reason) {
|
||||
// Only catch \RuntimeException instances
|
||||
// All other types of errors will propagate automatically
|
||||
})
|
||||
->catch(function (\Throwable $reason) {
|
||||
// Catch other errors
|
||||
});
|
||||
```
|
||||
|
||||
#### PromiseInterface::finally()
|
||||
|
||||
```php
|
||||
$newPromise = $promise->finally(callable $onFulfilledOrRejected);
|
||||
```
|
||||
|
||||
Allows you to execute "cleanup" type tasks in a promise chain.
|
||||
|
||||
It arranges for `$onFulfilledOrRejected` to be called, with no arguments,
|
||||
when the promise is either fulfilled or rejected.
|
||||
|
||||
* If `$promise` fulfills, and `$onFulfilledOrRejected` returns successfully,
|
||||
`$newPromise` will fulfill with the same value as `$promise`.
|
||||
* If `$promise` fulfills, and `$onFulfilledOrRejected` throws or returns a
|
||||
rejected promise, `$newPromise` will reject with the thrown exception or
|
||||
rejected promise's reason.
|
||||
* If `$promise` rejects, and `$onFulfilledOrRejected` returns successfully,
|
||||
`$newPromise` will reject with the same reason as `$promise`.
|
||||
* If `$promise` rejects, and `$onFulfilledOrRejected` throws or returns a
|
||||
rejected promise, `$newPromise` will reject with the thrown exception or
|
||||
rejected promise's reason.
|
||||
|
||||
`finally()` behaves similarly to the synchronous finally statement. When combined
|
||||
with `catch()`, `finally()` allows you to write code that is similar to the familiar
|
||||
synchronous catch/finally pair.
|
||||
|
||||
Consider the following synchronous code:
|
||||
|
||||
```php
|
||||
try {
|
||||
return doSomething();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
```
|
||||
|
||||
Similar asynchronous code (with `doSomething()` that returns a promise) can be
|
||||
written:
|
||||
|
||||
```php
|
||||
return doSomething()
|
||||
->catch('handleError')
|
||||
->finally('cleanup');
|
||||
```
|
||||
|
||||
#### PromiseInterface::cancel()
|
||||
|
||||
``` php
|
||||
$promise->cancel();
|
||||
```
|
||||
|
||||
The `cancel()` method notifies the creator of the promise that there is no
|
||||
further interest in the results of the operation.
|
||||
|
||||
Once a promise is settled (either fulfilled or rejected), calling `cancel()` on
|
||||
a promise has no effect.
|
||||
|
||||
#### ~~PromiseInterface::otherwise()~~
|
||||
|
||||
> Deprecated since v3.0.0, see [`catch()`](#promiseinterfacecatch) instead.
|
||||
|
||||
The `otherwise()` method registers a rejection handler for a promise.
|
||||
|
||||
This method continues to exist only for BC reasons and to ease upgrading
|
||||
between versions. It is an alias for:
|
||||
|
||||
```php
|
||||
$promise->catch($onRejected);
|
||||
```
|
||||
|
||||
#### ~~PromiseInterface::always()~~
|
||||
|
||||
> Deprecated since v3.0.0, see [`finally()`](#promiseinterfacefinally) instead.
|
||||
|
||||
The `always()` method allows you to execute "cleanup" type tasks in a promise chain.
|
||||
|
||||
This method continues to exist only for BC reasons and to ease upgrading
|
||||
between versions. It is an alias for:
|
||||
|
||||
```php
|
||||
$promise->finally($onFulfilledOrRejected);
|
||||
```
|
||||
|
||||
### Promise
|
||||
|
||||
Creates a promise whose state is controlled by the functions passed to
|
||||
`$resolver`.
|
||||
|
||||
```php
|
||||
$resolver = function (callable $resolve, callable $reject) {
|
||||
// Do some work, possibly asynchronously, and then
|
||||
// resolve or reject.
|
||||
|
||||
$resolve($awesomeResult);
|
||||
// or throw new Exception('Promise rejected');
|
||||
// or $resolve($anotherPromise);
|
||||
// or $reject($nastyError);
|
||||
};
|
||||
|
||||
$canceller = function () {
|
||||
// Cancel/abort any running operations like network connections, streams etc.
|
||||
|
||||
// Reject promise by throwing an exception
|
||||
throw new Exception('Promise cancelled');
|
||||
};
|
||||
|
||||
$promise = new React\Promise\Promise($resolver, $canceller);
|
||||
```
|
||||
|
||||
The promise constructor receives a resolver function and an optional canceller
|
||||
function which both will be called with two arguments:
|
||||
|
||||
* `$resolve($value)` - Primary function that seals the fate of the
|
||||
returned promise. Accepts either a non-promise value, or another promise.
|
||||
When called with a non-promise value, fulfills promise with that value.
|
||||
When called with another promise, e.g. `$resolve($otherPromise)`, promise's
|
||||
fate will be equivalent to that of `$otherPromise`.
|
||||
* `$reject($reason)` - Function that rejects the promise. It is recommended to
|
||||
just throw an exception instead of using `$reject()`.
|
||||
|
||||
If the resolver or canceller throw an exception, the promise will be rejected
|
||||
with that thrown exception as the rejection reason.
|
||||
|
||||
The resolver function will be called immediately, the canceller function only
|
||||
once all consumers called the `cancel()` method of the promise.
|
||||
|
||||
### Functions
|
||||
|
||||
Useful functions for creating and joining collections of promises.
|
||||
|
||||
All functions working on promise collections (like `all()`, `race()`,
|
||||
etc.) support cancellation. This means, if you call `cancel()` on the returned
|
||||
promise, all promises in the collection are cancelled.
|
||||
|
||||
#### resolve()
|
||||
|
||||
```php
|
||||
$promise = React\Promise\resolve(mixed $promiseOrValue);
|
||||
```
|
||||
|
||||
Creates a promise for the supplied `$promiseOrValue`.
|
||||
|
||||
If `$promiseOrValue` is a value, it will be the resolution value of the
|
||||
returned promise.
|
||||
|
||||
If `$promiseOrValue` is a thenable (any object that provides a `then()` method),
|
||||
a trusted promise that follows the state of the thenable is returned.
|
||||
|
||||
If `$promiseOrValue` is a promise, it will be returned as is.
|
||||
|
||||
The resulting `$promise` implements the [`PromiseInterface`](#promiseinterface)
|
||||
and can be consumed like any other promise:
|
||||
|
||||
```php
|
||||
$promise = React\Promise\resolve(42);
|
||||
|
||||
$promise->then(function (int $result): void {
|
||||
var_dump($result);
|
||||
}, function (\Throwable $e): void {
|
||||
echo 'Error: ' . $e->getMessage() . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
#### reject()
|
||||
|
||||
```php
|
||||
$promise = React\Promise\reject(\Throwable $reason);
|
||||
```
|
||||
|
||||
Creates a rejected promise for the supplied `$reason`.
|
||||
|
||||
Note that the [`\Throwable`](https://www.php.net/manual/en/class.throwable.php) interface introduced in PHP 7 covers
|
||||
both user land [`\Exception`](https://www.php.net/manual/en/class.exception.php)'s and
|
||||
[`\Error`](https://www.php.net/manual/en/class.error.php) internal PHP errors. By enforcing `\Throwable` as reason to
|
||||
reject a promise, any language error or user land exception can be used to reject a promise.
|
||||
|
||||
The resulting `$promise` implements the [`PromiseInterface`](#promiseinterface)
|
||||
and can be consumed like any other promise:
|
||||
|
||||
```php
|
||||
$promise = React\Promise\reject(new RuntimeException('Request failed'));
|
||||
|
||||
$promise->then(function (int $result): void {
|
||||
var_dump($result);
|
||||
}, function (\Throwable $e): void {
|
||||
echo 'Error: ' . $e->getMessage() . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
Note that rejected promises should always be handled similar to how any
|
||||
exceptions should always be caught in a `try` + `catch` block. If you remove the
|
||||
last reference to a rejected promise that has not been handled, it will
|
||||
report an unhandled promise rejection:
|
||||
|
||||
```php
|
||||
function incorrect(): int
|
||||
{
|
||||
$promise = React\Promise\reject(new RuntimeException('Request failed'));
|
||||
|
||||
// Commented out: No rejection handler registered here.
|
||||
// $promise->then(null, function (\Throwable $e): void { /* ignore */ });
|
||||
|
||||
// Returning from a function will remove all local variable references, hence why
|
||||
// this will report an unhandled promise rejection here.
|
||||
return 42;
|
||||
}
|
||||
|
||||
// Calling this function will log an error message plus its stack trace:
|
||||
// Unhandled promise rejection with RuntimeException: Request failed in example.php:10
|
||||
incorrect();
|
||||
```
|
||||
|
||||
A rejected promise will be considered "handled" if you catch the rejection
|
||||
reason with either the [`then()` method](#promiseinterfacethen), the
|
||||
[`catch()` method](#promiseinterfacecatch), or the
|
||||
[`finally()` method](#promiseinterfacefinally). Note that each of these methods
|
||||
return a new promise that may again be rejected if you re-throw an exception.
|
||||
|
||||
A rejected promise will also be considered "handled" if you abort the operation
|
||||
with the [`cancel()` method](#promiseinterfacecancel) (which in turn would
|
||||
usually reject the promise if it is still pending).
|
||||
|
||||
See also the [`set_rejection_handler()` function](#set_rejection_handler).
|
||||
|
||||
#### all()
|
||||
|
||||
```php
|
||||
$promise = React\Promise\all(iterable $promisesOrValues);
|
||||
```
|
||||
|
||||
Returns a promise that will resolve only once all the items in
|
||||
`$promisesOrValues` have resolved. The resolution value of the returned promise
|
||||
will be an array containing the resolution values of each of the items in
|
||||
`$promisesOrValues`.
|
||||
|
||||
#### race()
|
||||
|
||||
```php
|
||||
$promise = React\Promise\race(iterable $promisesOrValues);
|
||||
```
|
||||
|
||||
Initiates a competitive race that allows one winner. Returns a promise which is
|
||||
resolved in the same way the first settled promise resolves.
|
||||
|
||||
The returned promise will become **infinitely pending** if `$promisesOrValues`
|
||||
contains 0 items.
|
||||
|
||||
#### any()
|
||||
|
||||
```php
|
||||
$promise = React\Promise\any(iterable $promisesOrValues);
|
||||
```
|
||||
|
||||
Returns a promise that will resolve when any one of the items in
|
||||
`$promisesOrValues` resolves. The resolution value of the returned promise
|
||||
will be the resolution value of the triggering item.
|
||||
|
||||
The returned promise will only reject if *all* items in `$promisesOrValues` are
|
||||
rejected. The rejection value will be a `React\Promise\Exception\CompositeException`
|
||||
which holds all rejection reasons. The rejection reasons can be obtained with
|
||||
`CompositeException::getThrowables()`.
|
||||
|
||||
The returned promise will also reject with a `React\Promise\Exception\LengthException`
|
||||
if `$promisesOrValues` contains 0 items.
|
||||
|
||||
#### set_rejection_handler()
|
||||
|
||||
```php
|
||||
React\Promise\set_rejection_handler(?callable $callback): ?callable;
|
||||
```
|
||||
|
||||
Sets the global rejection handler for unhandled promise rejections.
|
||||
|
||||
Note that rejected promises should always be handled similar to how any
|
||||
exceptions should always be caught in a `try` + `catch` block. If you remove
|
||||
the last reference to a rejected promise that has not been handled, it will
|
||||
report an unhandled promise rejection. See also the [`reject()` function](#reject)
|
||||
for more details.
|
||||
|
||||
The `?callable $callback` argument MUST be a valid callback function that
|
||||
accepts a single `Throwable` argument or a `null` value to restore the
|
||||
default promise rejection handler. The return value of the callback function
|
||||
will be ignored and has no effect, so you SHOULD return a `void` value. The
|
||||
callback function MUST NOT throw or the program will be terminated with a
|
||||
fatal error.
|
||||
|
||||
The function returns the previous rejection handler or `null` if using the
|
||||
default promise rejection handler.
|
||||
|
||||
The default promise rejection handler will log an error message plus its stack
|
||||
trace:
|
||||
|
||||
```php
|
||||
// Unhandled promise rejection with RuntimeException: Unhandled in example.php:2
|
||||
React\Promise\reject(new RuntimeException('Unhandled'));
|
||||
```
|
||||
|
||||
The promise rejection handler may be used to use customize the log message or
|
||||
write to custom log targets. As a rule of thumb, this function should only be
|
||||
used as a last resort and promise rejections are best handled with either the
|
||||
[`then()` method](#promiseinterfacethen), the
|
||||
[`catch()` method](#promiseinterfacecatch), or the
|
||||
[`finally()` method](#promiseinterfacefinally).
|
||||
See also the [`reject()` function](#reject) for more details.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
### How to use Deferred
|
||||
|
||||
```php
|
||||
function getAwesomeResultPromise()
|
||||
{
|
||||
$deferred = new React\Promise\Deferred();
|
||||
|
||||
// Execute a Node.js-style function using the callback pattern
|
||||
computeAwesomeResultAsynchronously(function (\Throwable $error, $result) use ($deferred) {
|
||||
if ($error) {
|
||||
$deferred->reject($error);
|
||||
} else {
|
||||
$deferred->resolve($result);
|
||||
}
|
||||
});
|
||||
|
||||
// Return the promise
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
getAwesomeResultPromise()
|
||||
->then(
|
||||
function ($value) {
|
||||
// Deferred resolved, do something with $value
|
||||
},
|
||||
function (\Throwable $reason) {
|
||||
// Deferred rejected, do something with $reason
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### How promise forwarding works
|
||||
|
||||
A few simple examples to show how the mechanics of Promises/A forwarding works.
|
||||
These examples are contrived, of course, and in real usage, promise chains will
|
||||
typically be spread across several function calls, or even several levels of
|
||||
your application architecture.
|
||||
|
||||
#### Resolution forwarding
|
||||
|
||||
Resolved promises forward resolution values to the next promise.
|
||||
The first promise, `$deferred->promise()`, will resolve with the value passed
|
||||
to `$deferred->resolve()` below.
|
||||
|
||||
Each call to `then()` returns a new promise that will resolve with the return
|
||||
value of the previous handler. This creates a promise "pipeline".
|
||||
|
||||
```php
|
||||
$deferred = new React\Promise\Deferred();
|
||||
|
||||
$deferred->promise()
|
||||
->then(function ($x) {
|
||||
// $x will be the value passed to $deferred->resolve() below
|
||||
// and returns a *new promise* for $x + 1
|
||||
return $x + 1;
|
||||
})
|
||||
->then(function ($x) {
|
||||
// $x === 2
|
||||
// This handler receives the return value of the
|
||||
// previous handler.
|
||||
return $x + 1;
|
||||
})
|
||||
->then(function ($x) {
|
||||
// $x === 3
|
||||
// This handler receives the return value of the
|
||||
// previous handler.
|
||||
return $x + 1;
|
||||
})
|
||||
->then(function ($x) {
|
||||
// $x === 4
|
||||
// This handler receives the return value of the
|
||||
// previous handler.
|
||||
echo 'Resolve ' . $x;
|
||||
});
|
||||
|
||||
$deferred->resolve(1); // Prints "Resolve 4"
|
||||
```
|
||||
|
||||
#### Rejection forwarding
|
||||
|
||||
Rejected promises behave similarly, and also work similarly to try/catch:
|
||||
When you catch an exception, you must rethrow for it to propagate.
|
||||
|
||||
Similarly, when you handle a rejected promise, to propagate the rejection,
|
||||
"rethrow" it by either returning a rejected promise, or actually throwing
|
||||
(since promise translates thrown exceptions into rejections)
|
||||
|
||||
```php
|
||||
$deferred = new React\Promise\Deferred();
|
||||
|
||||
$deferred->promise()
|
||||
->then(function ($x) {
|
||||
throw new \Exception($x + 1);
|
||||
})
|
||||
->catch(function (\Exception $x) {
|
||||
// Propagate the rejection
|
||||
throw $x;
|
||||
})
|
||||
->catch(function (\Exception $x) {
|
||||
// Can also propagate by returning another rejection
|
||||
return React\Promise\reject(
|
||||
new \Exception($x->getMessage() + 1)
|
||||
);
|
||||
})
|
||||
->catch(function ($x) {
|
||||
echo 'Reject ' . $x->getMessage(); // 3
|
||||
});
|
||||
|
||||
$deferred->resolve(1); // Prints "Reject 3"
|
||||
```
|
||||
|
||||
#### Mixed resolution and rejection forwarding
|
||||
|
||||
Just like try/catch, you can choose to propagate or not. Mixing resolutions and
|
||||
rejections will still forward handler results in a predictable way.
|
||||
|
||||
```php
|
||||
$deferred = new React\Promise\Deferred();
|
||||
|
||||
$deferred->promise()
|
||||
->then(function ($x) {
|
||||
return $x + 1;
|
||||
})
|
||||
->then(function ($x) {
|
||||
throw new \Exception($x + 1);
|
||||
})
|
||||
->catch(function (\Exception $x) {
|
||||
// Handle the rejection, and don't propagate.
|
||||
// This is like catch without a rethrow
|
||||
return $x->getMessage() + 1;
|
||||
})
|
||||
->then(function ($x) {
|
||||
echo 'Mixed ' . $x; // 4
|
||||
});
|
||||
|
||||
$deferred->resolve(1); // Prints "Mixed 4"
|
||||
```
|
||||
|
||||
Install
|
||||
-------
|
||||
|
||||
The recommended way to install this library is [through Composer](https://getcomposer.org/).
|
||||
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
|
||||
|
||||
This project follows [SemVer](https://semver.org/).
|
||||
This will install the latest supported version from this branch:
|
||||
|
||||
```bash
|
||||
composer require react/promise:^3.2
|
||||
```
|
||||
|
||||
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
|
||||
|
||||
This project aims to run on any platform and thus does not require any PHP
|
||||
extensions and supports running on PHP 7.1 through current PHP 8+.
|
||||
It's *highly recommended to use the latest supported PHP version* for this project.
|
||||
|
||||
We're committed to providing long-term support (LTS) options and to provide a
|
||||
smooth upgrade path. If you're using an older PHP version, you may use the
|
||||
[`2.x` branch](https://github.com/reactphp/promise/tree/2.x) (PHP 5.4+) or
|
||||
[`1.x` branch](https://github.com/reactphp/promise/tree/1.x) (PHP 5.3+) which both
|
||||
provide a compatible API but do not take advantage of newer language features.
|
||||
You may target multiple versions at the same time to support a wider range of
|
||||
PHP versions like this:
|
||||
|
||||
```bash
|
||||
composer require "react/promise:^3 || ^2 || ^1"
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
To run the test suite, you first need to clone this repo and then install all
|
||||
dependencies [through Composer](https://getcomposer.org/):
|
||||
|
||||
```bash
|
||||
composer install
|
||||
```
|
||||
|
||||
To run the test suite, go to the project root and run:
|
||||
|
||||
```bash
|
||||
vendor/bin/phpunit
|
||||
```
|
||||
|
||||
On top of this, we use PHPStan on max level to ensure type safety across the project:
|
||||
|
||||
```bash
|
||||
vendor/bin/phpstan
|
||||
```
|
||||
|
||||
Credits
|
||||
-------
|
||||
|
||||
Promise is a port of [when.js](https://github.com/cujojs/when)
|
||||
by [Brian Cavalier](https://github.com/briancavalier).
|
||||
|
||||
Also, large parts of the documentation have been ported from the when.js
|
||||
[Wiki](https://github.com/cujojs/when/wiki) and the
|
||||
[API docs](https://github.com/cujojs/when/blob/master/docs/api.md).
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Released under the [MIT](LICENSE) license.
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "react/promise",
|
||||
"description": "A lightweight implementation of CommonJS Promises/A for PHP",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jan Sorgalla",
|
||||
"homepage": "https://sorgalla.com/",
|
||||
"email": "jsorgalla@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"homepage": "https://clue.engineering/",
|
||||
"email": "christian@clue.engineering"
|
||||
},
|
||||
{
|
||||
"name": "Cees-Jan Kiewiet",
|
||||
"homepage": "https://wyrihaximus.net/",
|
||||
"email": "reactphp@ceesjankiewiet.nl"
|
||||
},
|
||||
{
|
||||
"name": "Chris Boden",
|
||||
"homepage": "https://cboden.dev/",
|
||||
"email": "cboden@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "1.12.28 || 1.4.10",
|
||||
"phpunit/phpunit": "^9.6 || ^7.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"React\\Promise\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/functions_include.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"React\\Promise\\": [
|
||||
"tests/fixtures/",
|
||||
"tests/"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"tests/Fiber.php"
|
||||
]
|
||||
},
|
||||
"keywords": [
|
||||
"promise",
|
||||
"promises"
|
||||
]
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace React\Promise;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
final class Deferred
|
||||
{
|
||||
/**
|
||||
* @var PromiseInterface<T>
|
||||
*/
|
||||
private $promise;
|
||||
|
||||
/** @var callable(T):void */
|
||||
private $resolveCallback;
|
||||
|
||||
/** @var callable(\Throwable):void */
|
||||
private $rejectCallback;
|
||||
|
||||
/**
|
||||
* @param (callable(callable(T):void,callable(\Throwable):void):void)|null $canceller
|
||||
*/
|
||||
public function __construct(?callable $canceller = null)
|
||||
{
|
||||
$this->promise = new Promise(function ($resolve, $reject): void {
|
||||
$this->resolveCallback = $resolve;
|
||||
$this->rejectCallback = $reject;
|
||||
}, $canceller);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PromiseInterface<T>
|
||||
*/
|
||||
public function promise(): PromiseInterface
|
||||
{
|
||||
return $this->promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param T $value
|
||||
*/
|
||||
public function resolve($value): void
|
||||
{
|
||||
($this->resolveCallback)($value);
|
||||
}
|
||||
|
||||
public function reject(\Throwable $reason): void
|
||||
{
|
||||
($this->rejectCallback)($reason);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace React\Promise\Exception;
|
||||
|
||||
/**
|
||||
* Represents an exception that is a composite of one or more other exceptions.
|
||||
*
|
||||
* This exception is useful in situations where a promise must be rejected
|
||||
* with multiple exceptions. It is used for example to reject the returned
|
||||
* promise from `some()` and `any()` when too many input promises reject.
|
||||
*/
|
||||
class CompositeException extends \Exception
|
||||
{
|
||||
/** @var \Throwable[] */
|
||||
private $throwables;
|
||||
|
||||
/** @param \Throwable[] $throwables */
|
||||
public function __construct(array $throwables, string $message = '', int $code = 0, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
|
||||
$this->throwables = $throwables;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Throwable[]
|
||||
*/
|
||||
public function getThrowables(): array
|
||||
{
|
||||
return $this->throwables;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace React\Promise\Exception;
|
||||
|
||||
class LengthException extends \LengthException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace React\Promise\Internal;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class CancellationQueue
|
||||
{
|
||||
/** @var bool */
|
||||
private $started = false;
|
||||
|
||||
/** @var object[] */
|
||||
private $queue = [];
|
||||
|
||||
public function __invoke(): void
|
||||
{
|
||||
if ($this->started) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->started = true;
|
||||
$this->drain();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $cancellable
|
||||
*/
|
||||
public function enqueue($cancellable): void
|
||||
{
|
||||
if (!\is_object($cancellable) || !\method_exists($cancellable, 'then') || !\method_exists($cancellable, 'cancel')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$length = \array_push($this->queue, $cancellable);
|
||||
|
||||
if ($this->started && 1 === $length) {
|
||||
$this->drain();
|
||||
}
|
||||
}
|
||||
|
||||
private function drain(): void
|
||||
{
|
||||
for ($i = \key($this->queue); isset($this->queue[$i]); $i++) {
|
||||
$cancellable = $this->queue[$i];
|
||||
assert(\method_exists($cancellable, 'cancel'));
|
||||
|
||||
$exception = null;
|
||||
|
||||
try {
|
||||
$cancellable->cancel();
|
||||
} catch (\Throwable $exception) {
|
||||
}
|
||||
|
||||
unset($this->queue[$i]);
|
||||
|
||||
if ($exception) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
$this->queue = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace React\Promise\Internal;
|
||||
|
||||
use React\Promise\PromiseInterface;
|
||||
use function React\Promise\resolve;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @template T
|
||||
* @template-implements PromiseInterface<T>
|
||||
*/
|
||||
final class FulfilledPromise implements PromiseInterface
|
||||
{
|
||||
/** @var T */
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @param T $value
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct($value = null)
|
||||
{
|
||||
if ($value instanceof PromiseInterface) {
|
||||
throw new \InvalidArgumentException('You cannot create React\Promise\FulfilledPromise with a promise. Use React\Promise\resolve($promiseOrValue) instead.');
|
||||
}
|
||||
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template TFulfilled
|
||||
* @param ?(callable((T is void ? null : T)): (PromiseInterface<TFulfilled>|TFulfilled)) $onFulfilled
|
||||
* @return PromiseInterface<($onFulfilled is null ? T : TFulfilled)>
|
||||
*/
|
||||
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface
|
||||
{
|
||||
if (null === $onFulfilled) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* @var PromiseInterface<T>|T $result
|
||||
*/
|
||||
$result = $onFulfilled($this->value);
|
||||
return resolve($result);
|
||||
} catch (\Throwable $exception) {
|
||||
return new RejectedPromise($exception);
|
||||
}
|
||||
}
|
||||
|
||||
public function catch(callable $onRejected): PromiseInterface
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function finally(callable $onFulfilledOrRejected): PromiseInterface
|
||||
{
|
||||
return $this->then(function ($value) use ($onFulfilledOrRejected): PromiseInterface {
|
||||
/** @var T $value */
|
||||
return resolve($onFulfilledOrRejected())->then(function () use ($value) {
|
||||
return $value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public function cancel(): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `catch()` instead
|
||||
* @see self::catch()
|
||||
*/
|
||||
public function otherwise(callable $onRejected): PromiseInterface
|
||||
{
|
||||
return $this->catch($onRejected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `finally()` instead
|
||||
* @see self::finally()
|
||||
*/
|
||||
public function always(callable $onFulfilledOrRejected): PromiseInterface
|
||||
{
|
||||
return $this->finally($onFulfilledOrRejected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace React\Promise\Internal;
|
||||
|
||||
use React\Promise\PromiseInterface;
|
||||
use function React\Promise\_checkTypehint;
|
||||
use function React\Promise\resolve;
|
||||
use function React\Promise\set_rejection_handler;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @template-implements PromiseInterface<never>
|
||||
*/
|
||||
final class RejectedPromise implements PromiseInterface
|
||||
{
|
||||
/** @var \Throwable */
|
||||
private $reason;
|
||||
|
||||
/** @var bool */
|
||||
private $handled = false;
|
||||
|
||||
/**
|
||||
* @param \Throwable $reason
|
||||
*/
|
||||
public function __construct(\Throwable $reason)
|
||||
{
|
||||
$this->reason = $reason;
|
||||
}
|
||||
|
||||
/** @throws void */
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->handled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$handler = set_rejection_handler(null);
|
||||
if ($handler === null) {
|
||||
$message = 'Unhandled promise rejection with ' . $this->reason;
|
||||
|
||||
\error_log($message);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$handler($this->reason);
|
||||
} catch (\Throwable $e) {
|
||||
\preg_match('/^([^:\s]++)(.*+)$/sm', (string) $e, $match);
|
||||
\assert(isset($match[1], $match[2]));
|
||||
$message = 'Fatal error: Uncaught ' . $match[1] . ' from unhandled promise rejection handler' . $match[2];
|
||||
|
||||
\error_log($message);
|
||||
exit(255);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template TRejected
|
||||
* @param ?callable $onFulfilled
|
||||
* @param ?(callable(\Throwable): (PromiseInterface<TRejected>|TRejected)) $onRejected
|
||||
* @return PromiseInterface<($onRejected is null ? never : TRejected)>
|
||||
*/
|
||||
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface
|
||||
{
|
||||
if (null === $onRejected) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->handled = true;
|
||||
|
||||
try {
|
||||
return resolve($onRejected($this->reason));
|
||||
} catch (\Throwable $exception) {
|
||||
return new RejectedPromise($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template TThrowable of \Throwable
|
||||
* @template TRejected
|
||||
* @param callable(TThrowable): (PromiseInterface<TRejected>|TRejected) $onRejected
|
||||
* @return PromiseInterface<TRejected>
|
||||
*/
|
||||
public function catch(callable $onRejected): PromiseInterface
|
||||
{
|
||||
if (!_checkTypehint($onRejected, $this->reason)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var callable(\Throwable):(PromiseInterface<TRejected>|TRejected) $onRejected
|
||||
*/
|
||||
return $this->then(null, $onRejected);
|
||||
}
|
||||
|
||||
public function finally(callable $onFulfilledOrRejected): PromiseInterface
|
||||
{
|
||||
return $this->then(null, function (\Throwable $reason) use ($onFulfilledOrRejected): PromiseInterface {
|
||||
return resolve($onFulfilledOrRejected())->then(function () use ($reason): PromiseInterface {
|
||||
return new RejectedPromise($reason);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public function cancel(): void
|
||||
{
|
||||
$this->handled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `catch()` instead
|
||||
* @see self::catch()
|
||||
*/
|
||||
public function otherwise(callable $onRejected): PromiseInterface
|
||||
{
|
||||
return $this->catch($onRejected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `always()` instead
|
||||
* @see self::always()
|
||||
*/
|
||||
public function always(callable $onFulfilledOrRejected): PromiseInterface
|
||||
{
|
||||
return $this->finally($onFulfilledOrRejected);
|
||||
}
|
||||
}
|
||||
Vendored
+304
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
namespace React\Promise;
|
||||
|
||||
use React\Promise\Internal\RejectedPromise;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template-implements PromiseInterface<T>
|
||||
*/
|
||||
final class Promise implements PromiseInterface
|
||||
{
|
||||
/** @var (callable(callable(T):void,callable(\Throwable):void):void)|null */
|
||||
private $canceller;
|
||||
|
||||
/** @var ?PromiseInterface<T> */
|
||||
private $result;
|
||||
|
||||
/** @var list<callable(PromiseInterface<T>):void> */
|
||||
private $handlers = [];
|
||||
|
||||
/** @var int */
|
||||
private $requiredCancelRequests = 0;
|
||||
|
||||
/** @var bool */
|
||||
private $cancelled = false;
|
||||
|
||||
/**
|
||||
* @param callable(callable(T):void,callable(\Throwable):void):void $resolver
|
||||
* @param (callable(callable(T):void,callable(\Throwable):void):void)|null $canceller
|
||||
*/
|
||||
public function __construct(callable $resolver, ?callable $canceller = null)
|
||||
{
|
||||
$this->canceller = $canceller;
|
||||
|
||||
// Explicitly overwrite arguments with null values before invoking
|
||||
// resolver function. This ensure that these arguments do not show up
|
||||
// in the stack trace in PHP 7+ only.
|
||||
$cb = $resolver;
|
||||
$resolver = $canceller = null;
|
||||
$this->call($cb);
|
||||
}
|
||||
|
||||
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface
|
||||
{
|
||||
if (null !== $this->result) {
|
||||
return $this->result->then($onFulfilled, $onRejected);
|
||||
}
|
||||
|
||||
if (null === $this->canceller) {
|
||||
return new static($this->resolver($onFulfilled, $onRejected));
|
||||
}
|
||||
|
||||
// This promise has a canceller, so we create a new child promise which
|
||||
// has a canceller that invokes the parent canceller if all other
|
||||
// followers are also cancelled. We keep a reference to this promise
|
||||
// instance for the static canceller function and clear this to avoid
|
||||
// keeping a cyclic reference between parent and follower.
|
||||
$parent = $this;
|
||||
++$parent->requiredCancelRequests;
|
||||
|
||||
return new static(
|
||||
$this->resolver($onFulfilled, $onRejected),
|
||||
static function () use (&$parent): void {
|
||||
assert($parent instanceof self);
|
||||
--$parent->requiredCancelRequests;
|
||||
|
||||
if ($parent->requiredCancelRequests <= 0) {
|
||||
$parent->cancel();
|
||||
}
|
||||
|
||||
$parent = null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template TThrowable of \Throwable
|
||||
* @template TRejected
|
||||
* @param callable(TThrowable): (PromiseInterface<TRejected>|TRejected) $onRejected
|
||||
* @return PromiseInterface<T|TRejected>
|
||||
*/
|
||||
public function catch(callable $onRejected): PromiseInterface
|
||||
{
|
||||
return $this->then(null, static function (\Throwable $reason) use ($onRejected) {
|
||||
if (!_checkTypehint($onRejected, $reason)) {
|
||||
return new RejectedPromise($reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* @var callable(\Throwable):(PromiseInterface<TRejected>|TRejected) $onRejected
|
||||
*/
|
||||
return $onRejected($reason);
|
||||
});
|
||||
}
|
||||
|
||||
public function finally(callable $onFulfilledOrRejected): PromiseInterface
|
||||
{
|
||||
return $this->then(static function ($value) use ($onFulfilledOrRejected): PromiseInterface {
|
||||
/** @var T $value */
|
||||
return resolve($onFulfilledOrRejected())->then(function () use ($value) {
|
||||
return $value;
|
||||
});
|
||||
}, static function (\Throwable $reason) use ($onFulfilledOrRejected): PromiseInterface {
|
||||
return resolve($onFulfilledOrRejected())->then(function () use ($reason): RejectedPromise {
|
||||
return new RejectedPromise($reason);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public function cancel(): void
|
||||
{
|
||||
$this->cancelled = true;
|
||||
$canceller = $this->canceller;
|
||||
$this->canceller = null;
|
||||
|
||||
$parentCanceller = null;
|
||||
|
||||
if (null !== $this->result) {
|
||||
// Forward cancellation to rejected promise to avoid reporting unhandled rejection
|
||||
if ($this->result instanceof RejectedPromise) {
|
||||
$this->result->cancel();
|
||||
}
|
||||
|
||||
// Go up the promise chain and reach the top most promise which is
|
||||
// itself not following another promise
|
||||
$root = $this->unwrap($this->result);
|
||||
|
||||
// Return if the root promise is already resolved or a
|
||||
// FulfilledPromise or RejectedPromise
|
||||
if (!$root instanceof self || null !== $root->result) {
|
||||
return;
|
||||
}
|
||||
|
||||
$root->requiredCancelRequests--;
|
||||
|
||||
if ($root->requiredCancelRequests <= 0) {
|
||||
$parentCanceller = [$root, 'cancel'];
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $canceller) {
|
||||
$this->call($canceller);
|
||||
}
|
||||
|
||||
// For BC, we call the parent canceller after our own canceller
|
||||
if ($parentCanceller) {
|
||||
$parentCanceller();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `catch()` instead
|
||||
* @see self::catch()
|
||||
*/
|
||||
public function otherwise(callable $onRejected): PromiseInterface
|
||||
{
|
||||
return $this->catch($onRejected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `finally()` instead
|
||||
* @see self::finally()
|
||||
*/
|
||||
public function always(callable $onFulfilledOrRejected): PromiseInterface
|
||||
{
|
||||
return $this->finally($onFulfilledOrRejected);
|
||||
}
|
||||
|
||||
private function resolver(?callable $onFulfilled = null, ?callable $onRejected = null): callable
|
||||
{
|
||||
return function (callable $resolve, callable $reject) use ($onFulfilled, $onRejected): void {
|
||||
$this->handlers[] = static function (PromiseInterface $promise) use ($onFulfilled, $onRejected, $resolve, $reject): void {
|
||||
$promise = $promise->then($onFulfilled, $onRejected);
|
||||
|
||||
if ($promise instanceof self && $promise->result === null) {
|
||||
$promise->handlers[] = static function (PromiseInterface $promise) use ($resolve, $reject): void {
|
||||
$promise->then($resolve, $reject);
|
||||
};
|
||||
} else {
|
||||
$promise->then($resolve, $reject);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
private function reject(\Throwable $reason): void
|
||||
{
|
||||
if (null !== $this->result) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->settle(reject($reason));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PromiseInterface<T> $result
|
||||
*/
|
||||
private function settle(PromiseInterface $result): void
|
||||
{
|
||||
$result = $this->unwrap($result);
|
||||
|
||||
if ($result === $this) {
|
||||
$result = new RejectedPromise(
|
||||
new \LogicException('Cannot resolve a promise with itself.')
|
||||
);
|
||||
}
|
||||
|
||||
if ($result instanceof self) {
|
||||
$result->requiredCancelRequests++;
|
||||
} else {
|
||||
// Unset canceller only when not following a pending promise
|
||||
$this->canceller = null;
|
||||
}
|
||||
|
||||
$handlers = $this->handlers;
|
||||
|
||||
$this->handlers = [];
|
||||
$this->result = $result;
|
||||
|
||||
foreach ($handlers as $handler) {
|
||||
$handler($result);
|
||||
}
|
||||
|
||||
// Forward cancellation to rejected promise to avoid reporting unhandled rejection
|
||||
if ($this->cancelled && $result instanceof RejectedPromise) {
|
||||
$result->cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PromiseInterface<T> $promise
|
||||
* @return PromiseInterface<T>
|
||||
*/
|
||||
private function unwrap(PromiseInterface $promise): PromiseInterface
|
||||
{
|
||||
while ($promise instanceof self && null !== $promise->result) {
|
||||
/** @var PromiseInterface<T> $promise */
|
||||
$promise = $promise->result;
|
||||
}
|
||||
|
||||
return $promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(callable(mixed):void,callable(\Throwable):void):void $cb
|
||||
*/
|
||||
private function call(callable $cb): void
|
||||
{
|
||||
// Explicitly overwrite argument with null value. This ensure that this
|
||||
// argument does not show up in the stack trace in PHP 7+ only.
|
||||
$callback = $cb;
|
||||
$cb = null;
|
||||
|
||||
// Use reflection to inspect number of arguments expected by this callback.
|
||||
// We did some careful benchmarking here: Using reflection to avoid unneeded
|
||||
// function arguments is actually faster than blindly passing them.
|
||||
// Also, this helps avoiding unnecessary function arguments in the call stack
|
||||
// if the callback creates an Exception (creating garbage cycles).
|
||||
if (\is_array($callback)) {
|
||||
$ref = new \ReflectionMethod($callback[0], $callback[1]);
|
||||
} elseif (\is_object($callback) && !$callback instanceof \Closure) {
|
||||
$ref = new \ReflectionMethod($callback, '__invoke');
|
||||
} else {
|
||||
assert($callback instanceof \Closure || \is_string($callback));
|
||||
$ref = new \ReflectionFunction($callback);
|
||||
}
|
||||
$args = $ref->getNumberOfParameters();
|
||||
|
||||
try {
|
||||
if ($args === 0) {
|
||||
$callback();
|
||||
} else {
|
||||
// Keep references to this promise instance for the static resolve/reject functions.
|
||||
// By using static callbacks that are not bound to this instance
|
||||
// and passing the target promise instance by reference, we can
|
||||
// still execute its resolving logic and still clear this
|
||||
// reference when settling the promise. This helps avoiding
|
||||
// garbage cycles if any callback creates an Exception.
|
||||
// These assumptions are covered by the test suite, so if you ever feel like
|
||||
// refactoring this, go ahead, any alternative suggestions are welcome!
|
||||
$target =& $this;
|
||||
|
||||
$callback(
|
||||
static function ($value) use (&$target): void {
|
||||
if ($target !== null) {
|
||||
$target->settle(resolve($value));
|
||||
$target = null;
|
||||
}
|
||||
},
|
||||
static function (\Throwable $reason) use (&$target): void {
|
||||
if ($target !== null) {
|
||||
$target->reject($reason);
|
||||
$target = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$target = null;
|
||||
$this->reject($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace React\Promise;
|
||||
|
||||
/**
|
||||
* @template-covariant T
|
||||
*/
|
||||
interface PromiseInterface
|
||||
{
|
||||
/**
|
||||
* Transforms a promise's value by applying a function to the promise's fulfillment
|
||||
* or rejection value. Returns a new promise for the transformed result.
|
||||
*
|
||||
* The `then()` method registers new fulfilled and rejection handlers with a promise
|
||||
* (all parameters are optional):
|
||||
*
|
||||
* * `$onFulfilled` will be invoked once the promise is fulfilled and passed
|
||||
* the result as the first argument.
|
||||
* * `$onRejected` will be invoked once the promise is rejected and passed the
|
||||
* reason as the first argument.
|
||||
*
|
||||
* It returns a new promise that will fulfill with the return value of either
|
||||
* `$onFulfilled` or `$onRejected`, whichever is called, or will reject with
|
||||
* the thrown exception if either throws.
|
||||
*
|
||||
* A promise makes the following guarantees about handlers registered in
|
||||
* the same call to `then()`:
|
||||
*
|
||||
* 1. Only one of `$onFulfilled` or `$onRejected` will be called,
|
||||
* never both.
|
||||
* 2. `$onFulfilled` and `$onRejected` will never be called more
|
||||
* than once.
|
||||
*
|
||||
* @template TFulfilled
|
||||
* @template TRejected
|
||||
* @param ?(callable((T is void ? null : T)): (PromiseInterface<TFulfilled>|TFulfilled)) $onFulfilled
|
||||
* @param ?(callable(\Throwable): (PromiseInterface<TRejected>|TRejected)) $onRejected
|
||||
* @return PromiseInterface<($onRejected is null ? ($onFulfilled is null ? T : TFulfilled) : ($onFulfilled is null ? T|TRejected : TFulfilled|TRejected))>
|
||||
*/
|
||||
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface;
|
||||
|
||||
/**
|
||||
* Registers a rejection handler for promise. It is a shortcut for:
|
||||
*
|
||||
* ```php
|
||||
* $promise->then(null, $onRejected);
|
||||
* ```
|
||||
*
|
||||
* Additionally, you can type hint the `$reason` argument of `$onRejected` to catch
|
||||
* only specific errors.
|
||||
*
|
||||
* @template TThrowable of \Throwable
|
||||
* @template TRejected
|
||||
* @param callable(TThrowable): (PromiseInterface<TRejected>|TRejected) $onRejected
|
||||
* @return PromiseInterface<T|TRejected>
|
||||
*/
|
||||
public function catch(callable $onRejected): PromiseInterface;
|
||||
|
||||
/**
|
||||
* Allows you to execute "cleanup" type tasks in a promise chain.
|
||||
*
|
||||
* It arranges for `$onFulfilledOrRejected` to be called, with no arguments,
|
||||
* when the promise is either fulfilled or rejected.
|
||||
*
|
||||
* * If `$promise` fulfills, and `$onFulfilledOrRejected` returns successfully,
|
||||
* `$newPromise` will fulfill with the same value as `$promise`.
|
||||
* * If `$promise` fulfills, and `$onFulfilledOrRejected` throws or returns a
|
||||
* rejected promise, `$newPromise` will reject with the thrown exception or
|
||||
* rejected promise's reason.
|
||||
* * If `$promise` rejects, and `$onFulfilledOrRejected` returns successfully,
|
||||
* `$newPromise` will reject with the same reason as `$promise`.
|
||||
* * If `$promise` rejects, and `$onFulfilledOrRejected` throws or returns a
|
||||
* rejected promise, `$newPromise` will reject with the thrown exception or
|
||||
* rejected promise's reason.
|
||||
*
|
||||
* `finally()` behaves similarly to the synchronous finally statement. When combined
|
||||
* with `catch()`, `finally()` allows you to write code that is similar to the familiar
|
||||
* synchronous catch/finally pair.
|
||||
*
|
||||
* Consider the following synchronous code:
|
||||
*
|
||||
* ```php
|
||||
* try {
|
||||
* return doSomething();
|
||||
* } catch(\Exception $e) {
|
||||
* return handleError($e);
|
||||
* } finally {
|
||||
* cleanup();
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Similar asynchronous code (with `doSomething()` that returns a promise) can be
|
||||
* written:
|
||||
*
|
||||
* ```php
|
||||
* return doSomething()
|
||||
* ->catch('handleError')
|
||||
* ->finally('cleanup');
|
||||
* ```
|
||||
*
|
||||
* @param callable(): (void|PromiseInterface<void>) $onFulfilledOrRejected
|
||||
* @return PromiseInterface<T>
|
||||
*/
|
||||
public function finally(callable $onFulfilledOrRejected): PromiseInterface;
|
||||
|
||||
/**
|
||||
* The `cancel()` method notifies the creator of the promise that there is no
|
||||
* further interest in the results of the operation.
|
||||
*
|
||||
* Once a promise is settled (either fulfilled or rejected), calling `cancel()` on
|
||||
* a promise has no effect.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function cancel(): void;
|
||||
|
||||
/**
|
||||
* [Deprecated] Registers a rejection handler for a promise.
|
||||
*
|
||||
* This method continues to exist only for BC reasons and to ease upgrading
|
||||
* between versions. It is an alias for:
|
||||
*
|
||||
* ```php
|
||||
* $promise->catch($onRejected);
|
||||
* ```
|
||||
*
|
||||
* @template TThrowable of \Throwable
|
||||
* @template TRejected
|
||||
* @param callable(TThrowable): (PromiseInterface<TRejected>|TRejected) $onRejected
|
||||
* @return PromiseInterface<T|TRejected>
|
||||
* @deprecated 3.0.0 Use catch() instead
|
||||
* @see self::catch()
|
||||
*/
|
||||
public function otherwise(callable $onRejected): PromiseInterface;
|
||||
|
||||
/**
|
||||
* [Deprecated] Allows you to execute "cleanup" type tasks in a promise chain.
|
||||
*
|
||||
* This method continues to exist only for BC reasons and to ease upgrading
|
||||
* between versions. It is an alias for:
|
||||
*
|
||||
* ```php
|
||||
* $promise->finally($onFulfilledOrRejected);
|
||||
* ```
|
||||
*
|
||||
* @param callable(): (void|PromiseInterface<void>) $onFulfilledOrRejected
|
||||
* @return PromiseInterface<T>
|
||||
* @deprecated 3.0.0 Use finally() instead
|
||||
* @see self::finally()
|
||||
*/
|
||||
public function always(callable $onFulfilledOrRejected): PromiseInterface;
|
||||
}
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
|
||||
namespace React\Promise;
|
||||
|
||||
use React\Promise\Exception\CompositeException;
|
||||
use React\Promise\Internal\FulfilledPromise;
|
||||
use React\Promise\Internal\RejectedPromise;
|
||||
|
||||
/**
|
||||
* Creates a promise for the supplied `$promiseOrValue`.
|
||||
*
|
||||
* If `$promiseOrValue` is a value, it will be the resolution value of the
|
||||
* returned promise.
|
||||
*
|
||||
* If `$promiseOrValue` is a thenable (any object that provides a `then()` method),
|
||||
* a trusted promise that follows the state of the thenable is returned.
|
||||
*
|
||||
* If `$promiseOrValue` is a promise, it will be returned as is.
|
||||
*
|
||||
* @template T
|
||||
* @param PromiseInterface<T>|T $promiseOrValue
|
||||
* @return PromiseInterface<T>
|
||||
*/
|
||||
function resolve($promiseOrValue): PromiseInterface
|
||||
{
|
||||
if ($promiseOrValue instanceof PromiseInterface) {
|
||||
return $promiseOrValue;
|
||||
}
|
||||
|
||||
if (\is_object($promiseOrValue) && \method_exists($promiseOrValue, 'then')) {
|
||||
$canceller = null;
|
||||
|
||||
if (\method_exists($promiseOrValue, 'cancel')) {
|
||||
$canceller = [$promiseOrValue, 'cancel'];
|
||||
assert(\is_callable($canceller));
|
||||
}
|
||||
|
||||
/** @var Promise<T> */
|
||||
return new Promise(function (callable $resolve, callable $reject) use ($promiseOrValue): void {
|
||||
$promiseOrValue->then($resolve, $reject);
|
||||
}, $canceller);
|
||||
}
|
||||
|
||||
return new FulfilledPromise($promiseOrValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a rejected promise for the supplied `$reason`.
|
||||
*
|
||||
* If `$reason` is a value, it will be the rejection value of the
|
||||
* returned promise.
|
||||
*
|
||||
* If `$reason` is a promise, its completion value will be the rejected
|
||||
* value of the returned promise.
|
||||
*
|
||||
* This can be useful in situations where you need to reject a promise without
|
||||
* throwing an exception. For example, it allows you to propagate a rejection with
|
||||
* the value of another promise.
|
||||
*
|
||||
* @return PromiseInterface<never>
|
||||
*/
|
||||
function reject(\Throwable $reason): PromiseInterface
|
||||
{
|
||||
return new RejectedPromise($reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that will resolve only once all the items in
|
||||
* `$promisesOrValues` have resolved. The resolution value of the returned promise
|
||||
* will be an array containing the resolution values of each of the items in
|
||||
* `$promisesOrValues`.
|
||||
*
|
||||
* @template T
|
||||
* @param iterable<PromiseInterface<T>|T> $promisesOrValues
|
||||
* @return PromiseInterface<array<T>>
|
||||
*/
|
||||
function all(iterable $promisesOrValues): PromiseInterface
|
||||
{
|
||||
$cancellationQueue = new Internal\CancellationQueue();
|
||||
|
||||
/** @var Promise<array<T>> */
|
||||
return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void {
|
||||
$toResolve = 0;
|
||||
/** @var bool */
|
||||
$continue = true;
|
||||
$values = [];
|
||||
|
||||
foreach ($promisesOrValues as $i => $promiseOrValue) {
|
||||
$cancellationQueue->enqueue($promiseOrValue);
|
||||
$values[$i] = null;
|
||||
++$toResolve;
|
||||
|
||||
resolve($promiseOrValue)->then(
|
||||
function ($value) use ($i, &$values, &$toResolve, &$continue, $resolve): void {
|
||||
$values[$i] = $value;
|
||||
|
||||
if (0 === --$toResolve && !$continue) {
|
||||
$resolve($values);
|
||||
}
|
||||
},
|
||||
function (\Throwable $reason) use (&$continue, $reject): void {
|
||||
$continue = false;
|
||||
$reject($reason);
|
||||
}
|
||||
);
|
||||
|
||||
if (!$continue && !\is_array($promisesOrValues)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$continue = false;
|
||||
if ($toResolve === 0) {
|
||||
$resolve($values);
|
||||
}
|
||||
}, $cancellationQueue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a competitive race that allows one winner. Returns a promise which is
|
||||
* resolved in the same way the first settled promise resolves.
|
||||
*
|
||||
* The returned promise will become **infinitely pending** if `$promisesOrValues`
|
||||
* contains 0 items.
|
||||
*
|
||||
* @template T
|
||||
* @param iterable<PromiseInterface<T>|T> $promisesOrValues
|
||||
* @return PromiseInterface<T>
|
||||
*/
|
||||
function race(iterable $promisesOrValues): PromiseInterface
|
||||
{
|
||||
$cancellationQueue = new Internal\CancellationQueue();
|
||||
|
||||
/** @var Promise<T> */
|
||||
return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void {
|
||||
$continue = true;
|
||||
|
||||
foreach ($promisesOrValues as $promiseOrValue) {
|
||||
$cancellationQueue->enqueue($promiseOrValue);
|
||||
|
||||
resolve($promiseOrValue)->then($resolve, $reject)->finally(function () use (&$continue): void {
|
||||
$continue = false;
|
||||
});
|
||||
|
||||
if (!$continue && !\is_array($promisesOrValues)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, $cancellationQueue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that will resolve when any one of the items in
|
||||
* `$promisesOrValues` resolves. The resolution value of the returned promise
|
||||
* will be the resolution value of the triggering item.
|
||||
*
|
||||
* The returned promise will only reject if *all* items in `$promisesOrValues` are
|
||||
* rejected. The rejection value will be an array of all rejection reasons.
|
||||
*
|
||||
* The returned promise will also reject with a `React\Promise\Exception\LengthException`
|
||||
* if `$promisesOrValues` contains 0 items.
|
||||
*
|
||||
* @template T
|
||||
* @param iterable<PromiseInterface<T>|T> $promisesOrValues
|
||||
* @return PromiseInterface<T>
|
||||
*/
|
||||
function any(iterable $promisesOrValues): PromiseInterface
|
||||
{
|
||||
$cancellationQueue = new Internal\CancellationQueue();
|
||||
|
||||
/** @var Promise<T> */
|
||||
return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void {
|
||||
$toReject = 0;
|
||||
$continue = true;
|
||||
$reasons = [];
|
||||
|
||||
foreach ($promisesOrValues as $i => $promiseOrValue) {
|
||||
$cancellationQueue->enqueue($promiseOrValue);
|
||||
++$toReject;
|
||||
|
||||
resolve($promiseOrValue)->then(
|
||||
function ($value) use ($resolve, &$continue): void {
|
||||
$continue = false;
|
||||
$resolve($value);
|
||||
},
|
||||
function (\Throwable $reason) use ($i, &$reasons, &$toReject, $reject, &$continue): void {
|
||||
$reasons[$i] = $reason;
|
||||
|
||||
if (0 === --$toReject && !$continue) {
|
||||
$reject(new CompositeException(
|
||||
$reasons,
|
||||
'All promises rejected.'
|
||||
));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!$continue && !\is_array($promisesOrValues)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$continue = false;
|
||||
if ($toReject === 0 && !$reasons) {
|
||||
$reject(new Exception\LengthException(
|
||||
'Must contain at least 1 item but contains only 0 items.'
|
||||
));
|
||||
} elseif ($toReject === 0) {
|
||||
$reject(new CompositeException(
|
||||
$reasons,
|
||||
'All promises rejected.'
|
||||
));
|
||||
}
|
||||
}, $cancellationQueue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the global rejection handler for unhandled promise rejections.
|
||||
*
|
||||
* Note that rejected promises should always be handled similar to how any
|
||||
* exceptions should always be caught in a `try` + `catch` block. If you remove
|
||||
* the last reference to a rejected promise that has not been handled, it will
|
||||
* report an unhandled promise rejection. See also the [`reject()` function](#reject)
|
||||
* for more details.
|
||||
*
|
||||
* The `?callable $callback` argument MUST be a valid callback function that
|
||||
* accepts a single `Throwable` argument or a `null` value to restore the
|
||||
* default promise rejection handler. The return value of the callback function
|
||||
* will be ignored and has no effect, so you SHOULD return a `void` value. The
|
||||
* callback function MUST NOT throw or the program will be terminated with a
|
||||
* fatal error.
|
||||
*
|
||||
* The function returns the previous rejection handler or `null` if using the
|
||||
* default promise rejection handler.
|
||||
*
|
||||
* The default promise rejection handler will log an error message plus its
|
||||
* stack trace:
|
||||
*
|
||||
* ```php
|
||||
* // Unhandled promise rejection with RuntimeException: Unhandled in example.php:2
|
||||
* React\Promise\reject(new RuntimeException('Unhandled'));
|
||||
* ```
|
||||
*
|
||||
* The promise rejection handler may be used to use customize the log message or
|
||||
* write to custom log targets. As a rule of thumb, this function should only be
|
||||
* used as a last resort and promise rejections are best handled with either the
|
||||
* [`then()` method](#promiseinterfacethen), the
|
||||
* [`catch()` method](#promiseinterfacecatch), or the
|
||||
* [`finally()` method](#promiseinterfacefinally).
|
||||
* See also the [`reject()` function](#reject) for more details.
|
||||
*
|
||||
* @param callable(\Throwable):void|null $callback
|
||||
* @return callable(\Throwable):void|null
|
||||
*/
|
||||
function set_rejection_handler(?callable $callback): ?callable
|
||||
{
|
||||
static $current = null;
|
||||
$previous = $current;
|
||||
$current = $callback;
|
||||
|
||||
return $previous;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
function _checkTypehint(callable $callback, \Throwable $reason): bool
|
||||
{
|
||||
if (\is_array($callback)) {
|
||||
$callbackReflection = new \ReflectionMethod($callback[0], $callback[1]);
|
||||
} elseif (\is_object($callback) && !$callback instanceof \Closure) {
|
||||
$callbackReflection = new \ReflectionMethod($callback, '__invoke');
|
||||
} else {
|
||||
assert($callback instanceof \Closure || \is_string($callback));
|
||||
$callbackReflection = new \ReflectionFunction($callback);
|
||||
}
|
||||
|
||||
$parameters = $callbackReflection->getParameters();
|
||||
|
||||
if (!isset($parameters[0])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$expectedException = $parameters[0];
|
||||
|
||||
// Extract the type of the argument and handle different possibilities
|
||||
$type = $expectedException->getType();
|
||||
|
||||
$isTypeUnion = true;
|
||||
$types = [];
|
||||
|
||||
switch (true) {
|
||||
case $type === null:
|
||||
break;
|
||||
case $type instanceof \ReflectionNamedType:
|
||||
$types = [$type];
|
||||
break;
|
||||
case $type instanceof \ReflectionIntersectionType:
|
||||
$isTypeUnion = false;
|
||||
case $type instanceof \ReflectionUnionType:
|
||||
$types = $type->getTypes();
|
||||
break;
|
||||
default:
|
||||
throw new \LogicException('Unexpected return value of ReflectionParameter::getType');
|
||||
}
|
||||
|
||||
// If there is no type restriction, it matches
|
||||
if (empty($types)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($types as $type) {
|
||||
|
||||
if ($type instanceof \ReflectionIntersectionType) {
|
||||
foreach ($type->getTypes() as $typeToMatch) {
|
||||
assert($typeToMatch instanceof \ReflectionNamedType);
|
||||
$name = $typeToMatch->getName();
|
||||
if (!($matches = (!$typeToMatch->isBuiltin() && $reason instanceof $name))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert(isset($matches));
|
||||
} else {
|
||||
assert($type instanceof \ReflectionNamedType);
|
||||
$name = $type->getName();
|
||||
$matches = !$type->isBuiltin() && $reason instanceof $name;
|
||||
}
|
||||
|
||||
// If we look for a single match (union), we can return early on match
|
||||
// If we look for a full match (intersection), we can return early on mismatch
|
||||
if ($matches) {
|
||||
if ($isTypeUnion) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (!$isTypeUnion) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we look for a single match (union) and did not return early, we matched no type and are false
|
||||
// If we look for a full match (intersection) and did not return early, we matched all types and are true
|
||||
return $isTypeUnion ? false : true;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
if (!\function_exists('React\Promise\resolve')) {
|
||||
require __DIR__.'/functions.php';
|
||||
}
|
||||
Vendored
+793
@@ -0,0 +1,793 @@
|
||||
# Changelog
|
||||
|
||||
## 1.17.0 (2025-11-19)
|
||||
|
||||
* Feature: Improve PHP 8.5+ support by avoiding deprecated `setAccessible()` calls.
|
||||
(#325 by @W0rma and #330 by @WyriHaximus)
|
||||
|
||||
* Improve test suite and update test environment.
|
||||
(#326 and #330 by @WyriHaximus)
|
||||
|
||||
## 1.16.0 (2024-07-26)
|
||||
|
||||
* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable type declarations.
|
||||
(#318 by @clue)
|
||||
|
||||
## 1.15.0 (2023-12-15)
|
||||
|
||||
* Feature: Full PHP 8.3 compatibility.
|
||||
(#310 by @clue)
|
||||
|
||||
* Fix: Fix cancelling during the 50ms resolution delay when DNS is still pending.
|
||||
(#311 by @clue)
|
||||
|
||||
## 1.14.0 (2023-08-25)
|
||||
|
||||
* Feature: Improve Promise v3 support and use template types.
|
||||
(#307 and #309 by @clue)
|
||||
|
||||
* Improve test suite and update to collect all garbage cycles.
|
||||
(#308 by @clue)
|
||||
|
||||
## 1.13.0 (2023-06-07)
|
||||
|
||||
* Feature: Include timeout logic to avoid dependency on reactphp/promise-timer.
|
||||
(#305 by @clue)
|
||||
|
||||
* Feature: Improve errno detection for failed connections without `ext-sockets`.
|
||||
(#304 by @clue)
|
||||
|
||||
* Improve test suite, clean up leftover `.sock` files and report failed assertions.
|
||||
(#299, #300, #301 and #306 by @clue)
|
||||
|
||||
## 1.12.0 (2022-08-25)
|
||||
|
||||
* Feature: Forward compatibility with react/promise 3.
|
||||
(#214 by @WyriHaximus and @clue)
|
||||
|
||||
* Feature: Full support for PHP 8.2 release.
|
||||
(#298 by @WyriHaximus)
|
||||
|
||||
* Feature: Avoid unneeded syscall on socket close.
|
||||
(#292 by @clue)
|
||||
|
||||
* Feature / Fix: Improve error reporting when custom error handler is used.
|
||||
(#290 by @clue)
|
||||
|
||||
* Fix: Fix invalid references in exception stack trace.
|
||||
(#284 by @clue)
|
||||
|
||||
* Minor documentation improvements, update to use new reactphp/async package instead of clue/reactphp-block.
|
||||
(#296 by @clue, #285 by @SimonFrings and #295 by @nhedger)
|
||||
|
||||
* Improve test suite, update macOS and HHVM environment, fix optional tests for `ENETUNREACH`.
|
||||
(#288, #289 and #297 by @clue)
|
||||
|
||||
## 1.11.0 (2022-01-14)
|
||||
|
||||
* Feature: Full support for PHP 8.1 release.
|
||||
(#277 by @clue)
|
||||
|
||||
* Feature: Avoid dependency on `ext-filter`.
|
||||
(#279 by @clue)
|
||||
|
||||
* Improve test suite to skip FD test when hitting memory limit
|
||||
and skip legacy TLS 1.0 tests if disabled by system.
|
||||
(#278 and #281 by @clue and #283 by @SimonFrings)
|
||||
|
||||
## 1.10.0 (2021-11-29)
|
||||
|
||||
* Feature: Support listening on existing file descriptors (FDs) with `SocketServer`.
|
||||
(#269 by @clue)
|
||||
|
||||
```php
|
||||
$socket = new React\Socket\SocketSever('php://fd/3');
|
||||
```
|
||||
|
||||
This is particularly useful when using [systemd socket activation](https://www.freedesktop.org/software/systemd/man/systemd.socket.html) like this:
|
||||
|
||||
```bash
|
||||
$ systemd-socket-activate -l 8000 php examples/03-http-server.php php://fd/3
|
||||
```
|
||||
|
||||
* Feature: Improve error messages for failed connection attempts with `errno` and `errstr`.
|
||||
(#265, #266, #267, #270 and #271 by @clue and #268 by @SimonFrings)
|
||||
|
||||
All error messages now always include the appropriate `errno` and `errstr` to
|
||||
give more details about the error reason when available. Along with these
|
||||
error details exposed by the underlying system functions, it will also
|
||||
include the appropriate error constant name (such as `ECONNREFUSED`) when
|
||||
available. Accordingly, failed TCP/IP connections will now report the actual
|
||||
underlying error condition instead of a generic "Connection refused" error.
|
||||
Higher-level error messages will now consistently report the connection URI
|
||||
scheme and hostname used in all error messages.
|
||||
|
||||
For most common use cases this means that simply reporting the `Exception`
|
||||
message should give the most relevant details for any connection issues:
|
||||
|
||||
```php
|
||||
$connector = new React\Socket\Connector();
|
||||
$connector->connect($uri)->then(function (React\Socket\ConnectionInterface $conn) {
|
||||
// …
|
||||
}, function (Exception $e) {
|
||||
echo 'Error:' . $e->getMessage() . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
* Improve test suite, test against PHP 8.1 release.
|
||||
(#274 by @SimonFrings)
|
||||
|
||||
## 1.9.0 (2021-08-03)
|
||||
|
||||
* Feature: Add new `SocketServer` and deprecate `Server` to avoid class name collisions.
|
||||
(#263 by @clue)
|
||||
|
||||
The new `SocketServer` class has been added with an improved constructor signature
|
||||
as a replacement for the previous `Server` class in order to avoid any ambiguities.
|
||||
The previous name has been deprecated and should not be used anymore.
|
||||
In its most basic form, the deprecated `Server` can now be considered an alias for new `SocketServer`.
|
||||
|
||||
```php
|
||||
// deprecated
|
||||
$socket = new React\Socket\Server(0);
|
||||
$socket = new React\Socket\Server('127.0.0.1:8000');
|
||||
$socket = new React\Socket\Server('127.0.0.1:8000', null, $context);
|
||||
$socket = new React\Socket\Server('127.0.0.1:8000', $loop, $context);
|
||||
|
||||
// new
|
||||
$socket = new React\Socket\SocketServer('127.0.0.1:0');
|
||||
$socket = new React\Socket\SocketServer('127.0.0.1:8000');
|
||||
$socket = new React\Socket\SocketServer('127.0.0.1:8000', $context);
|
||||
$socket = new React\Socket\SocketServer('127.0.0.1:8000', $context, $loop);
|
||||
```
|
||||
|
||||
* Feature: Update `Connector` signature to take optional `$context` as first argument.
|
||||
(#264 by @clue)
|
||||
|
||||
The new signature has been added to match the new `SocketServer` and
|
||||
consistently move the now commonly unneeded loop argument to the last argument.
|
||||
The previous signature has been deprecated and should not be used anymore.
|
||||
In its most basic form, both signatures are compatible.
|
||||
|
||||
```php
|
||||
// deprecated
|
||||
$connector = new React\Socket\Connector(null, $context);
|
||||
$connector = new React\Socket\Connector($loop, $context);
|
||||
|
||||
// new
|
||||
$connector = new React\Socket\Connector($context);
|
||||
$connector = new React\Socket\Connector($context, $loop);
|
||||
```
|
||||
|
||||
## 1.8.0 (2021-07-11)
|
||||
|
||||
A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop).
|
||||
|
||||
* Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop).
|
||||
(#260 by @clue)
|
||||
|
||||
```php
|
||||
// old (still supported)
|
||||
$socket = new React\Socket\Server('127.0.0.1:8080', $loop);
|
||||
$connector = new React\Socket\Connector($loop);
|
||||
|
||||
// new (using default loop)
|
||||
$socket = new React\Socket\Server('127.0.0.1:8080');
|
||||
$connector = new React\Socket\Connector();
|
||||
```
|
||||
|
||||
## 1.7.0 (2021-06-25)
|
||||
|
||||
* Feature: Support falling back to multiple DNS servers from DNS config.
|
||||
(#257 by @clue)
|
||||
|
||||
If you're using the default `Connector`, it will now use all DNS servers
|
||||
configured on your system. If you have multiple DNS servers configured and
|
||||
connectivity to the primary DNS server is broken, it will now fall back to
|
||||
your other DNS servers, thus providing improved connectivity and redundancy
|
||||
for broken DNS configurations.
|
||||
|
||||
* Feature: Use round robin for happy eyeballs DNS responses (load balancing).
|
||||
(#247 by @clue)
|
||||
|
||||
If you're using the default `Connector`, it will now randomize the order of
|
||||
the IP addresses resolved via DNS when connecting. This allows the load to
|
||||
be distributed more evenly across all returned IP addresses. This can be
|
||||
used as a very basic DNS load balancing mechanism.
|
||||
|
||||
* Internal improvement to avoid unhandled rejection for future Promise API.
|
||||
(#258 by @clue)
|
||||
|
||||
* Improve test suite, use GitHub actions for continuous integration (CI).
|
||||
(#254 by @SimonFrings)
|
||||
|
||||
## 1.6.0 (2020-08-28)
|
||||
|
||||
* Feature: Support upcoming PHP 8 release.
|
||||
(#246 by @clue)
|
||||
|
||||
* Feature: Change default socket backlog size to 511.
|
||||
(#242 by @clue)
|
||||
|
||||
* Fix: Fix closing connection when cancelling during TLS handshake.
|
||||
(#241 by @clue)
|
||||
|
||||
* Fix: Fix blocking during possible `accept()` race condition
|
||||
when multiple socket servers listen on same socket address.
|
||||
(#244 by @clue)
|
||||
|
||||
* Improve test suite, update PHPUnit config and add full core team to the license.
|
||||
(#243 by @SimonFrings and #245 by @WyriHaximus)
|
||||
|
||||
## 1.5.0 (2020-07-01)
|
||||
|
||||
* Feature / Fix: Improve error handling and reporting for happy eyeballs and
|
||||
immediately try next connection when one connection attempt fails.
|
||||
(#230, #231, #232 and #233 by @clue)
|
||||
|
||||
Error messages for failed connection attempts now include more details to
|
||||
ease debugging. Additionally, the happy eyeballs algorithm has been improved
|
||||
to avoid having to wait for some timers to expire which significantly
|
||||
improves connection setup times (in particular when IPv6 isn't available).
|
||||
|
||||
* Improve test suite, minor code cleanup and improve code coverage to 100%.
|
||||
Update to PHPUnit 9 and skip legacy TLS 1.0 / TLS 1.1 tests if disabled by
|
||||
system. Run tests on Windows and simplify Travis CI test matrix for Mac OS X
|
||||
setup and skip all TLS tests on legacy HHVM.
|
||||
(#229, #235, #236 and #238 by @clue and #239 by @SimonFrings)
|
||||
|
||||
## 1.4.0 (2020-03-12)
|
||||
|
||||
A major new feature release, see [**release announcement**](https://clue.engineering/2020/introducing-ipv6-for-reactphp).
|
||||
|
||||
* Feature: Add IPv6 support to `Connector` (implement "Happy Eyeballs" algorithm to support IPv6 probing).
|
||||
IPv6 support is turned on by default, use new `happy_eyeballs` option in `Connector` to toggle behavior.
|
||||
(#196, #224 and #225 by @WyriHaximus and @clue)
|
||||
|
||||
* Feature: Default to using DNS cache (with max 256 entries) for `Connector`.
|
||||
(#226 by @clue)
|
||||
|
||||
* Add `.gitattributes` to exclude dev files from exports and some minor code style fixes.
|
||||
(#219 by @reedy and #218 by @mmoreram)
|
||||
|
||||
* Improve test suite to fix failing test cases when using new DNS component,
|
||||
significantly improve test performance by awaiting events instead of sleeping,
|
||||
exclude TLS 1.3 test on PHP 7.3, run tests on PHP 7.4 and simplify test matrix.
|
||||
(#208, #209, #210, #217 and #223 by @clue)
|
||||
|
||||
## 1.3.0 (2019-07-10)
|
||||
|
||||
* Feature: Forward compatibility with upcoming stable DNS component.
|
||||
(#206 by @clue)
|
||||
|
||||
## 1.2.1 (2019-06-03)
|
||||
|
||||
* Avoid uneeded fragmented TLS work around for PHP 7.3.3+ and
|
||||
work around failing test case detecting EOF on TLS 1.3 socket streams.
|
||||
(#201 and #202 by @clue)
|
||||
|
||||
* Improve TLS certificate/passphrase example.
|
||||
(#190 by @jsor)
|
||||
|
||||
## 1.2.0 (2019-01-07)
|
||||
|
||||
* Feature / Fix: Improve TLS 1.3 support.
|
||||
(#186 by @clue)
|
||||
|
||||
TLS 1.3 is now an official standard as of August 2018! :tada:
|
||||
The protocol has major improvements in the areas of security, performance, and privacy.
|
||||
TLS 1.3 is supported by default as of [OpenSSL 1.1.1](https://www.openssl.org/blog/blog/2018/09/11/release111/).
|
||||
For example, this version ships with Ubuntu 18.10 (and newer) by default, meaning that recent installations support TLS 1.3 out of the box :shipit:
|
||||
|
||||
* Fix: Avoid possibility of missing remote address when TLS handshake fails.
|
||||
(#188 by @clue)
|
||||
|
||||
* Improve performance by prefixing all global functions calls with `\` to skip the look up and resolve process and go straight to the global function.
|
||||
(#183 by @WyriHaximus)
|
||||
|
||||
* Update documentation to use full class names with namespaces.
|
||||
(#187 by @clue)
|
||||
|
||||
* Improve test suite to avoid some possible race conditions,
|
||||
test against PHP 7.3 on Travis and
|
||||
use dedicated `assertInstanceOf()` assertions.
|
||||
(#185 by @clue, #178 by @WyriHaximus and #181 by @carusogabriel)
|
||||
|
||||
## 1.1.0 (2018-10-01)
|
||||
|
||||
* Feature: Improve error reporting for failed connection attempts and improve
|
||||
cancellation forwarding during DNS lookup, TCP/IP connection or TLS handshake.
|
||||
(#168, #169, #170, #171, #176 and #177 by @clue)
|
||||
|
||||
All error messages now always contain a reference to the remote URI to give
|
||||
more details which connection actually failed and the reason for this error.
|
||||
Accordingly, failures during DNS lookup will now mention both the remote URI
|
||||
as well as the DNS error reason. TCP/IP connection issues and errors during
|
||||
a secure TLS handshake will both mention the remote URI as well as the
|
||||
underlying socket error. Similarly, lost/dropped connections during a TLS
|
||||
handshake will now report a lost connection instead of an empty error reason.
|
||||
|
||||
For most common use cases this means that simply reporting the `Exception`
|
||||
message should give the most relevant details for any connection issues:
|
||||
|
||||
```php
|
||||
$promise = $connector->connect('tls://example.com:443');
|
||||
$promise->then(function (ConnectionInterface $conn) use ($loop) {
|
||||
// …
|
||||
}, function (Exception $e) {
|
||||
echo $e->getMessage();
|
||||
});
|
||||
```
|
||||
|
||||
## 1.0.0 (2018-07-11)
|
||||
|
||||
* First stable LTS release, now following [SemVer](https://semver.org/).
|
||||
We'd like to emphasize that this component is production ready and battle-tested.
|
||||
We plan to support all long-term support (LTS) releases for at least 24 months,
|
||||
so you have a rock-solid foundation to build on top of.
|
||||
|
||||
> Contains no other changes, so it's actually fully compatible with the v0.8.12 release.
|
||||
|
||||
## 0.8.12 (2018-06-11)
|
||||
|
||||
* Feature: Improve memory consumption for failed and cancelled connection attempts.
|
||||
(#161 by @clue)
|
||||
|
||||
* Improve test suite to fix Travis config to test against legacy PHP 5.3 again.
|
||||
(#162 by @clue)
|
||||
|
||||
## 0.8.11 (2018-04-24)
|
||||
|
||||
* Feature: Improve memory consumption for cancelled connection attempts and
|
||||
simplify skipping DNS lookup when connecting to IP addresses.
|
||||
(#159 and #160 by @clue)
|
||||
|
||||
## 0.8.10 (2018-02-28)
|
||||
|
||||
* Feature: Update DNS dependency to support loading system default DNS
|
||||
nameserver config on all supported platforms
|
||||
(`/etc/resolv.conf` on Unix/Linux/Mac/Docker/WSL and WMIC on Windows)
|
||||
(#152 by @clue)
|
||||
|
||||
This means that connecting to hosts that are managed by a local DNS server,
|
||||
such as a corporate DNS server or when using Docker containers, will now
|
||||
work as expected across all platforms with no changes required:
|
||||
|
||||
```php
|
||||
$connector = new Connector($loop);
|
||||
$connector->connect('intranet.example:80')->then(function ($connection) {
|
||||
// …
|
||||
});
|
||||
```
|
||||
|
||||
## 0.8.9 (2018-01-18)
|
||||
|
||||
* Feature: Support explicitly choosing TLS version to negotiate with remote side
|
||||
by respecting `crypto_method` context parameter for all classes.
|
||||
(#149 by @clue)
|
||||
|
||||
By default, all connector and server classes support TLSv1.0+ and exclude
|
||||
support for legacy SSLv2/SSLv3. As of PHP 5.6+ you can also explicitly
|
||||
choose the TLS version you want to negotiate with the remote side:
|
||||
|
||||
```php
|
||||
// new: now supports 'crypto_method` context parameter for all classes
|
||||
$connector = new Connector($loop, array(
|
||||
'tls' => array(
|
||||
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
|
||||
)
|
||||
));
|
||||
```
|
||||
|
||||
* Minor internal clean up to unify class imports
|
||||
(#148 by @clue)
|
||||
|
||||
## 0.8.8 (2018-01-06)
|
||||
|
||||
* Improve test suite by adding test group to skip integration tests relying on
|
||||
internet connection and fix minor documentation typo.
|
||||
(#146 by @clue and #145 by @cn007b)
|
||||
|
||||
## 0.8.7 (2017-12-24)
|
||||
|
||||
* Fix: Fix closing socket resource before removing from loop
|
||||
(#141 by @clue)
|
||||
|
||||
This fixes the root cause of an uncaught `Exception` that only manifested
|
||||
itself after the recent Stream v0.7.4 component update and only if you're
|
||||
using `ext-event` (`ExtEventLoop`).
|
||||
|
||||
* Improve test suite by testing against PHP 7.2
|
||||
(#140 by @carusogabriel)
|
||||
|
||||
## 0.8.6 (2017-11-18)
|
||||
|
||||
* Feature: Add Unix domain socket (UDS) support to `Server` with `unix://` URI scheme
|
||||
and add advanced `UnixServer` class.
|
||||
(#120 by @andig)
|
||||
|
||||
```php
|
||||
// new: Server now supports "unix://" scheme
|
||||
$server = new Server('unix:///tmp/server.sock', $loop);
|
||||
|
||||
// new: advanced usage
|
||||
$server = new UnixServer('/tmp/server.sock', $loop);
|
||||
```
|
||||
|
||||
* Restructure examples to ease getting started
|
||||
(#136 by @clue)
|
||||
|
||||
* Improve test suite by adding forward compatibility with PHPUnit 6 and
|
||||
ignore Mac OS X test failures for now until Travis tests work again
|
||||
(#133 by @gabriel-caruso and #134 by @clue)
|
||||
|
||||
## 0.8.5 (2017-10-23)
|
||||
|
||||
* Fix: Work around PHP bug with Unix domain socket (UDS) paths for Mac OS X
|
||||
(#123 by @andig)
|
||||
|
||||
* Fix: Fix `SecureServer` to return `null` URI if server socket is already closed
|
||||
(#129 by @clue)
|
||||
|
||||
* Improve test suite by adding forward compatibility with PHPUnit v5 and
|
||||
forward compatibility with upcoming EventLoop releases in tests and
|
||||
test Mac OS X on Travis
|
||||
(#122 by @andig and #125, #127 and #130 by @clue)
|
||||
|
||||
* Readme improvements
|
||||
(#118 by @jsor)
|
||||
|
||||
## 0.8.4 (2017-09-16)
|
||||
|
||||
* Feature: Add `FixedUriConnector` decorator to use fixed, preconfigured URI instead
|
||||
(#117 by @clue)
|
||||
|
||||
This can be useful for consumers that do not support certain URIs, such as
|
||||
when you want to explicitly connect to a Unix domain socket (UDS) path
|
||||
instead of connecting to a default address assumed by an higher-level API:
|
||||
|
||||
```php
|
||||
$connector = new FixedUriConnector(
|
||||
'unix:///var/run/docker.sock',
|
||||
new UnixConnector($loop)
|
||||
);
|
||||
|
||||
// destination will be ignored, actually connects to Unix domain socket
|
||||
$promise = $connector->connect('localhost:80');
|
||||
```
|
||||
|
||||
## 0.8.3 (2017-09-08)
|
||||
|
||||
* Feature: Reduce memory consumption for failed connections
|
||||
(#113 by @valga)
|
||||
|
||||
* Fix: Work around write chunk size for TLS streams for PHP < 7.1.14
|
||||
(#114 by @clue)
|
||||
|
||||
## 0.8.2 (2017-08-25)
|
||||
|
||||
* Feature: Update DNS dependency to support hosts file on all platforms
|
||||
(#112 by @clue)
|
||||
|
||||
This means that connecting to hosts such as `localhost` will now work as
|
||||
expected across all platforms with no changes required:
|
||||
|
||||
```php
|
||||
$connector = new Connector($loop);
|
||||
$connector->connect('localhost:8080')->then(function ($connection) {
|
||||
// …
|
||||
});
|
||||
```
|
||||
|
||||
## 0.8.1 (2017-08-15)
|
||||
|
||||
* Feature: Forward compatibility with upcoming EventLoop v1.0 and v0.5 and
|
||||
target evenement 3.0 a long side 2.0 and 1.0
|
||||
(#104 by @clue and #111 by @WyriHaximus)
|
||||
|
||||
* Improve test suite by locking Travis distro so new defaults will not break the build and
|
||||
fix HHVM build for now again and ignore future HHVM build errors
|
||||
(#109 and #110 by @clue)
|
||||
|
||||
* Minor documentation fixes
|
||||
(#103 by @christiaan and #108 by @hansott)
|
||||
|
||||
## 0.8.0 (2017-05-09)
|
||||
|
||||
* Feature: New `Server` class now acts as a facade for existing server classes
|
||||
and renamed old `Server` to `TcpServer` for advanced usage.
|
||||
(#96 and #97 by @clue)
|
||||
|
||||
The `Server` class is now the main class in this package that implements the
|
||||
`ServerInterface` and allows you to accept incoming streaming connections,
|
||||
such as plaintext TCP/IP or secure TLS connection streams.
|
||||
|
||||
> This is not a BC break and consumer code does not have to be updated.
|
||||
|
||||
* Feature / BC break: All addresses are now URIs that include the URI scheme
|
||||
(#98 by @clue)
|
||||
|
||||
```diff
|
||||
- $parts = parse_url('tcp://' . $conn->getRemoteAddress());
|
||||
+ $parts = parse_url($conn->getRemoteAddress());
|
||||
```
|
||||
|
||||
* Fix: Fix `unix://` addresses for Unix domain socket (UDS) paths
|
||||
(#100 by @clue)
|
||||
|
||||
* Feature: Forward compatibility with Stream v1.0 and v0.7
|
||||
(#99 by @clue)
|
||||
|
||||
## 0.7.2 (2017-04-24)
|
||||
|
||||
* Fix: Work around latest PHP 7.0.18 and 7.1.4 no longer accepting full URIs
|
||||
(#94 by @clue)
|
||||
|
||||
## 0.7.1 (2017-04-10)
|
||||
|
||||
* Fix: Ignore HHVM errors when closing connection that is already closing
|
||||
(#91 by @clue)
|
||||
|
||||
## 0.7.0 (2017-04-10)
|
||||
|
||||
* Feature: Merge SocketClient component into this component
|
||||
(#87 by @clue)
|
||||
|
||||
This means that this package now provides async, streaming plaintext TCP/IP
|
||||
and secure TLS socket server and client connections for ReactPHP.
|
||||
|
||||
```
|
||||
$connector = new React\Socket\Connector($loop);
|
||||
$connector->connect('google.com:80')->then(function (ConnectionInterface $conn) {
|
||||
$connection->write('…');
|
||||
});
|
||||
```
|
||||
|
||||
Accordingly, the `ConnectionInterface` is now used to represent both incoming
|
||||
server side connections as well as outgoing client side connections.
|
||||
|
||||
If you've previously used the SocketClient component to establish outgoing
|
||||
client connections, upgrading should take no longer than a few minutes.
|
||||
All classes have been merged as-is from the latest `v0.7.0` release with no
|
||||
other changes, so you can simply update your code to use the updated namespace
|
||||
like this:
|
||||
|
||||
```php
|
||||
// old from SocketClient component and namespace
|
||||
$connector = new React\SocketClient\Connector($loop);
|
||||
$connector->connect('google.com:80')->then(function (ConnectionInterface $conn) {
|
||||
$connection->write('…');
|
||||
});
|
||||
|
||||
// new
|
||||
$connector = new React\Socket\Connector($loop);
|
||||
$connector->connect('google.com:80')->then(function (ConnectionInterface $conn) {
|
||||
$connection->write('…');
|
||||
});
|
||||
```
|
||||
|
||||
## 0.6.0 (2017-04-04)
|
||||
|
||||
* Feature: Add `LimitingServer` to limit and keep track of open connections
|
||||
(#86 by @clue)
|
||||
|
||||
```php
|
||||
$server = new Server(0, $loop);
|
||||
$server = new LimitingServer($server, 100);
|
||||
|
||||
$server->on('connection', function (ConnectionInterface $connection) {
|
||||
$connection->write('hello there!' . PHP_EOL);
|
||||
…
|
||||
});
|
||||
```
|
||||
|
||||
* Feature / BC break: Add `pause()` and `resume()` methods to limit active
|
||||
connections
|
||||
(#84 by @clue)
|
||||
|
||||
```php
|
||||
$server = new Server(0, $loop);
|
||||
$server->pause();
|
||||
|
||||
$loop->addTimer(1.0, function() use ($server) {
|
||||
$server->resume();
|
||||
});
|
||||
```
|
||||
|
||||
## 0.5.1 (2017-03-09)
|
||||
|
||||
* Feature: Forward compatibility with Stream v0.5 and upcoming v0.6
|
||||
(#79 by @clue)
|
||||
|
||||
## 0.5.0 (2017-02-14)
|
||||
|
||||
* Feature / BC break: Replace `listen()` call with URIs passed to constructor
|
||||
and reject listening on hostnames with `InvalidArgumentException`
|
||||
and replace `ConnectionException` with `RuntimeException` for consistency
|
||||
(#61, #66 and #72 by @clue)
|
||||
|
||||
```php
|
||||
// old
|
||||
$server = new Server($loop);
|
||||
$server->listen(8080);
|
||||
|
||||
// new
|
||||
$server = new Server(8080, $loop);
|
||||
```
|
||||
|
||||
Similarly, you can now pass a full listening URI to the constructor to change
|
||||
the listening host:
|
||||
|
||||
```php
|
||||
// old
|
||||
$server = new Server($loop);
|
||||
$server->listen(8080, '127.0.0.1');
|
||||
|
||||
// new
|
||||
$server = new Server('127.0.0.1:8080', $loop);
|
||||
```
|
||||
|
||||
Trying to start listening on (DNS) host names will now throw an
|
||||
`InvalidArgumentException`, use IP addresses instead:
|
||||
|
||||
```php
|
||||
// old
|
||||
$server = new Server($loop);
|
||||
$server->listen(8080, 'localhost');
|
||||
|
||||
// new
|
||||
$server = new Server('127.0.0.1:8080', $loop);
|
||||
```
|
||||
|
||||
If trying to listen fails (such as if port is already in use or port below
|
||||
1024 may require root access etc.), it will now throw a `RuntimeException`,
|
||||
the `ConnectionException` class has been removed:
|
||||
|
||||
```php
|
||||
// old: throws React\Socket\ConnectionException
|
||||
$server = new Server($loop);
|
||||
$server->listen(80);
|
||||
|
||||
// new: throws RuntimeException
|
||||
$server = new Server(80, $loop);
|
||||
```
|
||||
|
||||
* Feature / BC break: Rename `shutdown()` to `close()` for consistency throughout React
|
||||
(#62 by @clue)
|
||||
|
||||
```php
|
||||
// old
|
||||
$server->shutdown();
|
||||
|
||||
// new
|
||||
$server->close();
|
||||
```
|
||||
|
||||
* Feature / BC break: Replace `getPort()` with `getAddress()`
|
||||
(#67 by @clue)
|
||||
|
||||
```php
|
||||
// old
|
||||
echo $server->getPort(); // 8080
|
||||
|
||||
// new
|
||||
echo $server->getAddress(); // 127.0.0.1:8080
|
||||
```
|
||||
|
||||
* Feature / BC break: `getRemoteAddress()` returns full address instead of only IP
|
||||
(#65 by @clue)
|
||||
|
||||
```php
|
||||
// old
|
||||
echo $connection->getRemoteAddress(); // 192.168.0.1
|
||||
|
||||
// new
|
||||
echo $connection->getRemoteAddress(); // 192.168.0.1:51743
|
||||
```
|
||||
|
||||
* Feature / BC break: Add `getLocalAddress()` method
|
||||
(#68 by @clue)
|
||||
|
||||
```php
|
||||
echo $connection->getLocalAddress(); // 127.0.0.1:8080
|
||||
```
|
||||
|
||||
* BC break: The `Server` and `SecureServer` class are now marked `final`
|
||||
and you can no longer `extend` them
|
||||
(which was never documented or recommended anyway).
|
||||
Public properties and event handlers are now internal only.
|
||||
Please use composition instead of extension.
|
||||
(#71, #70 and #69 by @clue)
|
||||
|
||||
## 0.4.6 (2017-01-26)
|
||||
|
||||
* Feature: Support socket context options passed to `Server`
|
||||
(#64 by @clue)
|
||||
|
||||
* Fix: Properly return `null` for unknown addresses
|
||||
(#63 by @clue)
|
||||
|
||||
* Improve documentation for `ServerInterface` and lock test suite requirements
|
||||
(#60 by @clue, #57 by @shaunbramley)
|
||||
|
||||
## 0.4.5 (2017-01-08)
|
||||
|
||||
* Feature: Add `SecureServer` for secure TLS connections
|
||||
(#55 by @clue)
|
||||
|
||||
* Add functional integration tests
|
||||
(#54 by @clue)
|
||||
|
||||
## 0.4.4 (2016-12-19)
|
||||
|
||||
* Feature / Fix: `ConnectionInterface` should extend `DuplexStreamInterface` + documentation
|
||||
(#50 by @clue)
|
||||
|
||||
* Feature / Fix: Improve test suite and switch to normal stream handler
|
||||
(#51 by @clue)
|
||||
|
||||
* Feature: Add examples
|
||||
(#49 by @clue)
|
||||
|
||||
## 0.4.3 (2016-03-01)
|
||||
|
||||
* Bug fix: Suppress errors on stream_socket_accept to prevent PHP from crashing
|
||||
* Support for PHP7 and HHVM
|
||||
* Support PHP 5.3 again
|
||||
|
||||
## 0.4.2 (2014-05-25)
|
||||
|
||||
* Verify stream is a valid resource in Connection
|
||||
|
||||
## 0.4.1 (2014-04-13)
|
||||
|
||||
* Bug fix: Check read buffer for data before shutdown signal and end emit (@ArtyDev)
|
||||
* Bug fix: v0.3.4 changes merged for v0.4.1
|
||||
|
||||
## 0.3.4 (2014-03-30)
|
||||
|
||||
* Bug fix: Reset socket to non-blocking after shutting down (PHP bug)
|
||||
|
||||
## 0.4.0 (2014-02-02)
|
||||
|
||||
* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks
|
||||
* BC break: Update to React/Promise 2.0
|
||||
* BC break: Update to Evenement 2.0
|
||||
* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0
|
||||
* Bump React dependencies to v0.4
|
||||
|
||||
## 0.3.3 (2013-07-08)
|
||||
|
||||
* Version bump
|
||||
|
||||
## 0.3.2 (2013-05-10)
|
||||
|
||||
* Version bump
|
||||
|
||||
## 0.3.1 (2013-04-21)
|
||||
|
||||
* Feature: Support binding to IPv6 addresses (@clue)
|
||||
|
||||
## 0.3.0 (2013-04-14)
|
||||
|
||||
* Bump React dependencies to v0.3
|
||||
|
||||
## 0.2.6 (2012-12-26)
|
||||
|
||||
* Version bump
|
||||
|
||||
## 0.2.3 (2012-11-14)
|
||||
|
||||
* Version bump
|
||||
|
||||
## 0.2.0 (2012-09-10)
|
||||
|
||||
* Bump React dependencies to v0.2
|
||||
|
||||
## 0.1.1 (2012-07-12)
|
||||
|
||||
* Version bump
|
||||
|
||||
## 0.1.0 (2012-07-11)
|
||||
|
||||
* First tagged release
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Vendored
+1564
File diff suppressed because it is too large
Load Diff
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "react/socket",
|
||||
"description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP",
|
||||
"keywords": ["async", "socket", "stream", "connection", "ReactPHP"],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"homepage": "https://clue.engineering/",
|
||||
"email": "christian@clue.engineering"
|
||||
},
|
||||
{
|
||||
"name": "Cees-Jan Kiewiet",
|
||||
"homepage": "https://wyrihaximus.net/",
|
||||
"email": "reactphp@ceesjankiewiet.nl"
|
||||
},
|
||||
{
|
||||
"name": "Jan Sorgalla",
|
||||
"homepage": "https://sorgalla.com/",
|
||||
"email": "jsorgalla@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Chris Boden",
|
||||
"homepage": "https://cboden.dev/",
|
||||
"email": "cboden@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3.0",
|
||||
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
|
||||
"react/dns": "^1.13",
|
||||
"react/event-loop": "^1.2",
|
||||
"react/promise": "^3.2 || ^2.6 || ^1.2.1",
|
||||
"react/stream": "^1.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
|
||||
"react/async": "^4.3 || ^3.3 || ^2",
|
||||
"react/promise-stream": "^1.4",
|
||||
"react/promise-timer": "^1.11"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"React\\Socket\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"React\\Tests\\Socket\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use Evenement\EventEmitter;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Stream\DuplexResourceStream;
|
||||
use React\Stream\Util;
|
||||
use React\Stream\WritableResourceStream;
|
||||
use React\Stream\WritableStreamInterface;
|
||||
|
||||
/**
|
||||
* The actual connection implementation for ConnectionInterface
|
||||
*
|
||||
* This class should only be used internally, see ConnectionInterface instead.
|
||||
*
|
||||
* @see ConnectionInterface
|
||||
* @internal
|
||||
*/
|
||||
class Connection extends EventEmitter implements ConnectionInterface
|
||||
{
|
||||
/**
|
||||
* Internal flag whether this is a Unix domain socket (UDS) connection
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public $unix = false;
|
||||
|
||||
/**
|
||||
* Internal flag whether encryption has been enabled on this connection
|
||||
*
|
||||
* Mostly used by internal StreamEncryption so that connection returns
|
||||
* `tls://` scheme for encrypted connections instead of `tcp://`.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public $encryptionEnabled = false;
|
||||
|
||||
/** @internal */
|
||||
public $stream;
|
||||
|
||||
private $input;
|
||||
|
||||
public function __construct($resource, LoopInterface $loop)
|
||||
{
|
||||
// PHP < 7.3.3 (and PHP < 7.2.15) suffers from a bug where feof() might
|
||||
// block with 100% CPU usage on fragmented TLS records.
|
||||
// We try to work around this by always consuming the complete receive
|
||||
// buffer at once to avoid stale data in TLS buffers. This is known to
|
||||
// work around high CPU usage for well-behaving peers, but this may
|
||||
// cause very large data chunks for high throughput scenarios. The buggy
|
||||
// behavior can still be triggered due to network I/O buffers or
|
||||
// malicious peers on affected versions, upgrading is highly recommended.
|
||||
// @link https://bugs.php.net/bug.php?id=77390
|
||||
$clearCompleteBuffer = \PHP_VERSION_ID < 70215 || (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70303);
|
||||
|
||||
// PHP < 7.1.4 (and PHP < 7.0.18) suffers from a bug when writing big
|
||||
// chunks of data over TLS streams at once.
|
||||
// We try to work around this by limiting the write chunk size to 8192
|
||||
// bytes for older PHP versions only.
|
||||
// This is only a work-around and has a noticable performance penalty on
|
||||
// affected versions. Please update your PHP version.
|
||||
// This applies to all streams because TLS may be enabled later on.
|
||||
// See https://github.com/reactphp/socket/issues/105
|
||||
$limitWriteChunks = (\PHP_VERSION_ID < 70018 || (\PHP_VERSION_ID >= 70100 && \PHP_VERSION_ID < 70104));
|
||||
|
||||
$this->input = new DuplexResourceStream(
|
||||
$resource,
|
||||
$loop,
|
||||
$clearCompleteBuffer ? -1 : null,
|
||||
new WritableResourceStream($resource, $loop, null, $limitWriteChunks ? 8192 : null)
|
||||
);
|
||||
|
||||
$this->stream = $resource;
|
||||
|
||||
Util::forwardEvents($this->input, $this, array('data', 'end', 'error', 'close', 'pipe', 'drain'));
|
||||
|
||||
$this->input->on('close', array($this, 'close'));
|
||||
}
|
||||
|
||||
public function isReadable()
|
||||
{
|
||||
return $this->input->isReadable();
|
||||
}
|
||||
|
||||
public function isWritable()
|
||||
{
|
||||
return $this->input->isWritable();
|
||||
}
|
||||
|
||||
public function pause()
|
||||
{
|
||||
$this->input->pause();
|
||||
}
|
||||
|
||||
public function resume()
|
||||
{
|
||||
$this->input->resume();
|
||||
}
|
||||
|
||||
public function pipe(WritableStreamInterface $dest, array $options = array())
|
||||
{
|
||||
return $this->input->pipe($dest, $options);
|
||||
}
|
||||
|
||||
public function write($data)
|
||||
{
|
||||
return $this->input->write($data);
|
||||
}
|
||||
|
||||
public function end($data = null)
|
||||
{
|
||||
$this->input->end($data);
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
$this->input->close();
|
||||
$this->handleClose();
|
||||
$this->removeAllListeners();
|
||||
}
|
||||
|
||||
public function handleClose()
|
||||
{
|
||||
if (!\is_resource($this->stream)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to cleanly shut down socket and ignore any errors in case other
|
||||
// side already closed. Underlying Stream implementation will take care
|
||||
// of closing stream resource, so we otherwise keep this open here.
|
||||
@\stream_socket_shutdown($this->stream, \STREAM_SHUT_RDWR);
|
||||
}
|
||||
|
||||
public function getRemoteAddress()
|
||||
{
|
||||
if (!\is_resource($this->stream)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->parseAddress(\stream_socket_get_name($this->stream, true));
|
||||
}
|
||||
|
||||
public function getLocalAddress()
|
||||
{
|
||||
if (!\is_resource($this->stream)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->parseAddress(\stream_socket_get_name($this->stream, false));
|
||||
}
|
||||
|
||||
private function parseAddress($address)
|
||||
{
|
||||
if ($address === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->unix) {
|
||||
// remove trailing colon from address for HHVM < 3.19: https://3v4l.org/5C1lo
|
||||
// note that technically ":" is a valid address, so keep this in place otherwise
|
||||
if (\substr($address, -1) === ':' && \defined('HHVM_VERSION_ID') && \HHVM_VERSION_ID < 31900) {
|
||||
$address = (string)\substr($address, 0, -1); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
// work around unknown addresses should return null value: https://3v4l.org/5C1lo and https://bugs.php.net/bug.php?id=74556
|
||||
// PHP uses "\0" string and HHVM uses empty string (colon removed above)
|
||||
if ($address === '' || $address[0] === "\x00" ) {
|
||||
return null; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return 'unix://' . $address;
|
||||
}
|
||||
|
||||
// check if this is an IPv6 address which includes multiple colons but no square brackets
|
||||
$pos = \strrpos($address, ':');
|
||||
if ($pos !== false && \strpos($address, ':') < $pos && \substr($address, 0, 1) !== '[') {
|
||||
$address = '[' . \substr($address, 0, $pos) . ']:' . \substr($address, $pos + 1); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return ($this->encryptionEnabled ? 'tls' : 'tcp') . '://' . $address;
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use React\Stream\DuplexStreamInterface;
|
||||
|
||||
/**
|
||||
* Any incoming and outgoing connection is represented by this interface,
|
||||
* such as a normal TCP/IP connection.
|
||||
*
|
||||
* An incoming or outgoing connection is a duplex stream (both readable and
|
||||
* writable) that implements React's
|
||||
* [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface).
|
||||
* It contains additional properties for the local and remote address (client IP)
|
||||
* where this connection has been established to/from.
|
||||
*
|
||||
* Most commonly, instances implementing this `ConnectionInterface` are emitted
|
||||
* by all classes implementing the [`ServerInterface`](#serverinterface) and
|
||||
* used by all classes implementing the [`ConnectorInterface`](#connectorinterface).
|
||||
*
|
||||
* Because the `ConnectionInterface` implements the underlying
|
||||
* [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface)
|
||||
* you can use any of its events and methods as usual:
|
||||
*
|
||||
* ```php
|
||||
* $connection->on('data', function ($chunk) {
|
||||
* echo $chunk;
|
||||
* });
|
||||
*
|
||||
* $connection->on('end', function () {
|
||||
* echo 'ended';
|
||||
* });
|
||||
*
|
||||
* $connection->on('error', function (Exception $e) {
|
||||
* echo 'error: ' . $e->getMessage();
|
||||
* });
|
||||
*
|
||||
* $connection->on('close', function () {
|
||||
* echo 'closed';
|
||||
* });
|
||||
*
|
||||
* $connection->write($data);
|
||||
* $connection->end($data = null);
|
||||
* $connection->close();
|
||||
* // …
|
||||
* ```
|
||||
*
|
||||
* For more details, see the
|
||||
* [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface).
|
||||
*
|
||||
* @see DuplexStreamInterface
|
||||
* @see ServerInterface
|
||||
* @see ConnectorInterface
|
||||
*/
|
||||
interface ConnectionInterface extends DuplexStreamInterface
|
||||
{
|
||||
/**
|
||||
* Returns the full remote address (URI) where this connection has been established with
|
||||
*
|
||||
* ```php
|
||||
* $address = $connection->getRemoteAddress();
|
||||
* echo 'Connection with ' . $address . PHP_EOL;
|
||||
* ```
|
||||
*
|
||||
* If the remote address can not be determined or is unknown at this time (such as
|
||||
* after the connection has been closed), it MAY return a `NULL` value instead.
|
||||
*
|
||||
* Otherwise, it will return the full address (URI) as a string value, such
|
||||
* as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`,
|
||||
* `unix://example.sock` or `unix:///path/to/example.sock`.
|
||||
* Note that individual URI components are application specific and depend
|
||||
* on the underlying transport protocol.
|
||||
*
|
||||
* If this is a TCP/IP based connection and you only want the remote IP, you may
|
||||
* use something like this:
|
||||
*
|
||||
* ```php
|
||||
* $address = $connection->getRemoteAddress();
|
||||
* $ip = trim(parse_url($address, PHP_URL_HOST), '[]');
|
||||
* echo 'Connection with ' . $ip . PHP_EOL;
|
||||
* ```
|
||||
*
|
||||
* @return ?string remote address (URI) or null if unknown
|
||||
*/
|
||||
public function getRemoteAddress();
|
||||
|
||||
/**
|
||||
* Returns the full local address (full URI with scheme, IP and port) where this connection has been established with
|
||||
*
|
||||
* ```php
|
||||
* $address = $connection->getLocalAddress();
|
||||
* echo 'Connection with ' . $address . PHP_EOL;
|
||||
* ```
|
||||
*
|
||||
* If the local address can not be determined or is unknown at this time (such as
|
||||
* after the connection has been closed), it MAY return a `NULL` value instead.
|
||||
*
|
||||
* Otherwise, it will return the full address (URI) as a string value, such
|
||||
* as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`,
|
||||
* `unix://example.sock` or `unix:///path/to/example.sock`.
|
||||
* Note that individual URI components are application specific and depend
|
||||
* on the underlying transport protocol.
|
||||
*
|
||||
* This method complements the [`getRemoteAddress()`](#getremoteaddress) method,
|
||||
* so they should not be confused.
|
||||
*
|
||||
* If your `TcpServer` instance is listening on multiple interfaces (e.g. using
|
||||
* the address `0.0.0.0`), you can use this method to find out which interface
|
||||
* actually accepted this connection (such as a public or local interface).
|
||||
*
|
||||
* If your system has multiple interfaces (e.g. a WAN and a LAN interface),
|
||||
* you can use this method to find out which interface was actually
|
||||
* used for this connection.
|
||||
*
|
||||
* @return ?string local address (URI) or null if unknown
|
||||
* @see self::getRemoteAddress()
|
||||
*/
|
||||
public function getLocalAddress();
|
||||
}
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use React\Dns\Config\Config as DnsConfig;
|
||||
use React\Dns\Resolver\Factory as DnsFactory;
|
||||
use React\Dns\Resolver\ResolverInterface;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
/**
|
||||
* The `Connector` class is the main class in this package that implements the
|
||||
* `ConnectorInterface` and allows you to create streaming connections.
|
||||
*
|
||||
* You can use this connector to create any kind of streaming connections, such
|
||||
* as plaintext TCP/IP, secure TLS or local Unix connection streams.
|
||||
*
|
||||
* Under the hood, the `Connector` is implemented as a *higher-level facade*
|
||||
* for the lower-level connectors implemented in this package. This means it
|
||||
* also shares all of their features and implementation details.
|
||||
* If you want to typehint in your higher-level protocol implementation, you SHOULD
|
||||
* use the generic [`ConnectorInterface`](#connectorinterface) instead.
|
||||
*
|
||||
* @see ConnectorInterface for the base interface
|
||||
*/
|
||||
final class Connector implements ConnectorInterface
|
||||
{
|
||||
private $connectors = array();
|
||||
|
||||
/**
|
||||
* Instantiate new `Connector`
|
||||
*
|
||||
* ```php
|
||||
* $connector = new React\Socket\Connector();
|
||||
* ```
|
||||
*
|
||||
* This class takes two optional arguments for more advanced usage:
|
||||
*
|
||||
* ```php
|
||||
* // constructor signature as of v1.9.0
|
||||
* $connector = new React\Socket\Connector(array $context = [], ?LoopInterface $loop = null);
|
||||
*
|
||||
* // legacy constructor signature before v1.9.0
|
||||
* $connector = new React\Socket\Connector(?LoopInterface $loop = null, array $context = []);
|
||||
* ```
|
||||
*
|
||||
* This class takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
* pass the event loop instance to use for this object. You can use a `null` value
|
||||
* here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
|
||||
* This value SHOULD NOT be given unless you're sure you want to explicitly use a
|
||||
* given event loop instance.
|
||||
*
|
||||
* @param array|LoopInterface|null $context
|
||||
* @param null|LoopInterface|array $loop
|
||||
* @throws \InvalidArgumentException for invalid arguments
|
||||
*/
|
||||
public function __construct($context = array(), $loop = null)
|
||||
{
|
||||
// swap arguments for legacy constructor signature
|
||||
if (($context instanceof LoopInterface || $context === null) && (\func_num_args() <= 1 || \is_array($loop))) {
|
||||
$swap = $loop === null ? array(): $loop;
|
||||
$loop = $context;
|
||||
$context = $swap;
|
||||
}
|
||||
|
||||
if (!\is_array($context) || ($loop !== null && !$loop instanceof LoopInterface)) {
|
||||
throw new \InvalidArgumentException('Expected "array $context" and "?LoopInterface $loop" arguments');
|
||||
}
|
||||
|
||||
// apply default options if not explicitly given
|
||||
$context += array(
|
||||
'tcp' => true,
|
||||
'tls' => true,
|
||||
'unix' => true,
|
||||
|
||||
'dns' => true,
|
||||
'timeout' => true,
|
||||
'happy_eyeballs' => true,
|
||||
);
|
||||
|
||||
if ($context['timeout'] === true) {
|
||||
$context['timeout'] = (float)\ini_get("default_socket_timeout");
|
||||
}
|
||||
|
||||
if ($context['tcp'] instanceof ConnectorInterface) {
|
||||
$tcp = $context['tcp'];
|
||||
} else {
|
||||
$tcp = new TcpConnector(
|
||||
$loop,
|
||||
\is_array($context['tcp']) ? $context['tcp'] : array()
|
||||
);
|
||||
}
|
||||
|
||||
if ($context['dns'] !== false) {
|
||||
if ($context['dns'] instanceof ResolverInterface) {
|
||||
$resolver = $context['dns'];
|
||||
} else {
|
||||
if ($context['dns'] !== true) {
|
||||
$config = $context['dns'];
|
||||
} else {
|
||||
// try to load nameservers from system config or default to Google's public DNS
|
||||
$config = DnsConfig::loadSystemConfigBlocking();
|
||||
if (!$config->nameservers) {
|
||||
$config->nameservers[] = '8.8.8.8'; // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
|
||||
$factory = new DnsFactory();
|
||||
$resolver = $factory->createCached(
|
||||
$config,
|
||||
$loop
|
||||
);
|
||||
}
|
||||
|
||||
if ($context['happy_eyeballs'] === true) {
|
||||
$tcp = new HappyEyeBallsConnector($loop, $tcp, $resolver);
|
||||
} else {
|
||||
$tcp = new DnsConnector($tcp, $resolver);
|
||||
}
|
||||
}
|
||||
|
||||
if ($context['tcp'] !== false) {
|
||||
$context['tcp'] = $tcp;
|
||||
|
||||
if ($context['timeout'] !== false) {
|
||||
$context['tcp'] = new TimeoutConnector(
|
||||
$context['tcp'],
|
||||
$context['timeout'],
|
||||
$loop
|
||||
);
|
||||
}
|
||||
|
||||
$this->connectors['tcp'] = $context['tcp'];
|
||||
}
|
||||
|
||||
if ($context['tls'] !== false) {
|
||||
if (!$context['tls'] instanceof ConnectorInterface) {
|
||||
$context['tls'] = new SecureConnector(
|
||||
$tcp,
|
||||
$loop,
|
||||
\is_array($context['tls']) ? $context['tls'] : array()
|
||||
);
|
||||
}
|
||||
|
||||
if ($context['timeout'] !== false) {
|
||||
$context['tls'] = new TimeoutConnector(
|
||||
$context['tls'],
|
||||
$context['timeout'],
|
||||
$loop
|
||||
);
|
||||
}
|
||||
|
||||
$this->connectors['tls'] = $context['tls'];
|
||||
}
|
||||
|
||||
if ($context['unix'] !== false) {
|
||||
if (!$context['unix'] instanceof ConnectorInterface) {
|
||||
$context['unix'] = new UnixConnector($loop);
|
||||
}
|
||||
$this->connectors['unix'] = $context['unix'];
|
||||
}
|
||||
}
|
||||
|
||||
public function connect($uri)
|
||||
{
|
||||
$scheme = 'tcp';
|
||||
if (\strpos($uri, '://') !== false) {
|
||||
$scheme = (string)\substr($uri, 0, \strpos($uri, '://'));
|
||||
}
|
||||
|
||||
if (!isset($this->connectors[$scheme])) {
|
||||
return \React\Promise\reject(new \RuntimeException(
|
||||
'No connector available for URI scheme "' . $scheme . '" (EINVAL)',
|
||||
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
|
||||
));
|
||||
}
|
||||
|
||||
return $this->connectors[$scheme]->connect($uri);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* [internal] Builds on URI from the given URI parts and ip address with original hostname as query
|
||||
*
|
||||
* @param array $parts
|
||||
* @param string $host
|
||||
* @param string $ip
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public static function uri(array $parts, $host, $ip)
|
||||
{
|
||||
$uri = '';
|
||||
|
||||
// prepend original scheme if known
|
||||
if (isset($parts['scheme'])) {
|
||||
$uri .= $parts['scheme'] . '://';
|
||||
}
|
||||
|
||||
if (\strpos($ip, ':') !== false) {
|
||||
// enclose IPv6 addresses in square brackets before appending port
|
||||
$uri .= '[' . $ip . ']';
|
||||
} else {
|
||||
$uri .= $ip;
|
||||
}
|
||||
|
||||
// append original port if known
|
||||
if (isset($parts['port'])) {
|
||||
$uri .= ':' . $parts['port'];
|
||||
}
|
||||
|
||||
// append orignal path if known
|
||||
if (isset($parts['path'])) {
|
||||
$uri .= $parts['path'];
|
||||
}
|
||||
|
||||
// append original query if known
|
||||
if (isset($parts['query'])) {
|
||||
$uri .= '?' . $parts['query'];
|
||||
}
|
||||
|
||||
// append original hostname as query if resolved via DNS and if
|
||||
// destination URI does not contain "hostname" query param already
|
||||
$args = array();
|
||||
\parse_str(isset($parts['query']) ? $parts['query'] : '', $args);
|
||||
if ($host !== $ip && !isset($args['hostname'])) {
|
||||
$uri .= (isset($parts['query']) ? '&' : '?') . 'hostname=' . \rawurlencode($host);
|
||||
}
|
||||
|
||||
// append original fragment if known
|
||||
if (isset($parts['fragment'])) {
|
||||
$uri .= '#' . $parts['fragment'];
|
||||
}
|
||||
|
||||
return $uri;
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
/**
|
||||
* The `ConnectorInterface` is responsible for providing an interface for
|
||||
* establishing streaming connections, such as a normal TCP/IP connection.
|
||||
*
|
||||
* This is the main interface defined in this package and it is used throughout
|
||||
* React's vast ecosystem.
|
||||
*
|
||||
* Most higher-level components (such as HTTP, database or other networking
|
||||
* service clients) accept an instance implementing this interface to create their
|
||||
* TCP/IP connection to the underlying networking service.
|
||||
* This is usually done via dependency injection, so it's fairly simple to actually
|
||||
* swap this implementation against any other implementation of this interface.
|
||||
*
|
||||
* The interface only offers a single `connect()` method.
|
||||
*
|
||||
* @see ConnectionInterface
|
||||
*/
|
||||
interface ConnectorInterface
|
||||
{
|
||||
/**
|
||||
* Creates a streaming connection to the given remote address
|
||||
*
|
||||
* If returns a Promise which either fulfills with a stream implementing
|
||||
* `ConnectionInterface` on success or rejects with an `Exception` if the
|
||||
* connection is not successful.
|
||||
*
|
||||
* ```php
|
||||
* $connector->connect('google.com:443')->then(
|
||||
* function (React\Socket\ConnectionInterface $connection) {
|
||||
* // connection successfully established
|
||||
* },
|
||||
* function (Exception $error) {
|
||||
* // failed to connect due to $error
|
||||
* }
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* The returned Promise MUST be implemented in such a way that it can be
|
||||
* cancelled when it is still pending. Cancelling a pending promise MUST
|
||||
* reject its value with an Exception. It SHOULD clean up any underlying
|
||||
* resources and references as applicable.
|
||||
*
|
||||
* ```php
|
||||
* $promise = $connector->connect($uri);
|
||||
*
|
||||
* $promise->cancel();
|
||||
* ```
|
||||
*
|
||||
* @param string $uri
|
||||
* @return \React\Promise\PromiseInterface<ConnectionInterface>
|
||||
* Resolves with a `ConnectionInterface` on success or rejects with an `Exception` on error.
|
||||
* @see ConnectionInterface
|
||||
*/
|
||||
public function connect($uri);
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use React\Dns\Resolver\ResolverInterface;
|
||||
use React\Promise;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
final class DnsConnector implements ConnectorInterface
|
||||
{
|
||||
private $connector;
|
||||
private $resolver;
|
||||
|
||||
public function __construct(ConnectorInterface $connector, ResolverInterface $resolver)
|
||||
{
|
||||
$this->connector = $connector;
|
||||
$this->resolver = $resolver;
|
||||
}
|
||||
|
||||
public function connect($uri)
|
||||
{
|
||||
$original = $uri;
|
||||
if (\strpos($uri, '://') === false) {
|
||||
$uri = 'tcp://' . $uri;
|
||||
$parts = \parse_url($uri);
|
||||
if (isset($parts['scheme'])) {
|
||||
unset($parts['scheme']);
|
||||
}
|
||||
} else {
|
||||
$parts = \parse_url($uri);
|
||||
}
|
||||
|
||||
if (!$parts || !isset($parts['host'])) {
|
||||
return Promise\reject(new \InvalidArgumentException(
|
||||
'Given URI "' . $original . '" is invalid (EINVAL)',
|
||||
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
|
||||
));
|
||||
}
|
||||
|
||||
$host = \trim($parts['host'], '[]');
|
||||
$connector = $this->connector;
|
||||
|
||||
// skip DNS lookup / URI manipulation if this URI already contains an IP
|
||||
if (@\inet_pton($host) !== false) {
|
||||
return $connector->connect($original);
|
||||
}
|
||||
|
||||
$promise = $this->resolver->resolve($host);
|
||||
$resolved = null;
|
||||
|
||||
return new Promise\Promise(
|
||||
function ($resolve, $reject) use (&$promise, &$resolved, $uri, $connector, $host, $parts) {
|
||||
// resolve/reject with result of DNS lookup
|
||||
$promise->then(function ($ip) use (&$promise, &$resolved, $uri, $connector, $host, $parts) {
|
||||
$resolved = $ip;
|
||||
|
||||
return $promise = $connector->connect(
|
||||
Connector::uri($parts, $host, $ip)
|
||||
)->then(null, function (\Exception $e) use ($uri) {
|
||||
if ($e instanceof \RuntimeException) {
|
||||
$message = \preg_replace('/^(Connection to [^ ]+)[&?]hostname=[^ &]+/', '$1', $e->getMessage());
|
||||
$e = new \RuntimeException(
|
||||
'Connection to ' . $uri . ' failed: ' . $message,
|
||||
$e->getCode(),
|
||||
$e
|
||||
);
|
||||
|
||||
// avoid garbage references by replacing all closures in call stack.
|
||||
// what a lovely piece of code!
|
||||
$r = new \ReflectionProperty('Exception', 'trace');
|
||||
if (\PHP_VERSION_ID < 80100) {
|
||||
$r->setAccessible(true);
|
||||
}
|
||||
$trace = $r->getValue($e);
|
||||
|
||||
// Exception trace arguments are not available on some PHP 7.4 installs
|
||||
// @codeCoverageIgnoreStart
|
||||
foreach ($trace as $ti => $one) {
|
||||
if (isset($one['args'])) {
|
||||
foreach ($one['args'] as $ai => $arg) {
|
||||
if ($arg instanceof \Closure) {
|
||||
$trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
$r->setValue($e, $trace);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
});
|
||||
}, function ($e) use ($uri, $reject) {
|
||||
$reject(new \RuntimeException('Connection to ' . $uri .' failed during DNS lookup: ' . $e->getMessage(), 0, $e));
|
||||
})->then($resolve, $reject);
|
||||
},
|
||||
function ($_, $reject) use (&$promise, &$resolved, $uri) {
|
||||
// cancellation should reject connection attempt
|
||||
// reject DNS resolution with custom reason, otherwise rely on connection cancellation below
|
||||
if ($resolved === null) {
|
||||
$reject(new \RuntimeException(
|
||||
'Connection to ' . $uri . ' cancelled during DNS lookup (ECONNABORTED)',
|
||||
\defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103
|
||||
));
|
||||
}
|
||||
|
||||
// (try to) cancel pending DNS lookup / connection attempt
|
||||
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
|
||||
// overwrite callback arguments for PHP7+ only, so they do not show
|
||||
// up in the Exception trace and do not cause a possible cyclic reference.
|
||||
$_ = $reject = null;
|
||||
|
||||
$promise->cancel();
|
||||
$promise = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+222
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use Evenement\EventEmitter;
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
/**
|
||||
* [Internal] The `FdServer` class implements the `ServerInterface` and
|
||||
* is responsible for accepting connections from an existing file descriptor.
|
||||
*
|
||||
* ```php
|
||||
* $socket = new React\Socket\FdServer(3);
|
||||
* ```
|
||||
*
|
||||
* Whenever a client connects, it will emit a `connection` event with a connection
|
||||
* instance implementing `ConnectionInterface`:
|
||||
*
|
||||
* ```php
|
||||
* $socket->on('connection', function (ConnectionInterface $connection) {
|
||||
* echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL;
|
||||
* $connection->write('hello there!' . PHP_EOL);
|
||||
* …
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* See also the `ServerInterface` for more details.
|
||||
*
|
||||
* @see ServerInterface
|
||||
* @see ConnectionInterface
|
||||
* @internal
|
||||
*/
|
||||
final class FdServer extends EventEmitter implements ServerInterface
|
||||
{
|
||||
private $master;
|
||||
private $loop;
|
||||
private $unix = false;
|
||||
private $listening = false;
|
||||
|
||||
/**
|
||||
* Creates a socket server and starts listening on the given file descriptor
|
||||
*
|
||||
* This starts accepting new incoming connections on the given file descriptor.
|
||||
* See also the `connection event` documented in the `ServerInterface`
|
||||
* for more details.
|
||||
*
|
||||
* ```php
|
||||
* $socket = new React\Socket\FdServer(3);
|
||||
* ```
|
||||
*
|
||||
* If the given FD is invalid or out of range, it will throw an `InvalidArgumentException`:
|
||||
*
|
||||
* ```php
|
||||
* // throws InvalidArgumentException
|
||||
* $socket = new React\Socket\FdServer(-1);
|
||||
* ```
|
||||
*
|
||||
* If the given FD appears to be valid, but listening on it fails (such as
|
||||
* if the FD does not exist or does not refer to a socket server), it will
|
||||
* throw a `RuntimeException`:
|
||||
*
|
||||
* ```php
|
||||
* // throws RuntimeException because FD does not reference a socket server
|
||||
* $socket = new React\Socket\FdServer(0, $loop);
|
||||
* ```
|
||||
*
|
||||
* Note that these error conditions may vary depending on your system and/or
|
||||
* configuration.
|
||||
* See the exception message and code for more details about the actual error
|
||||
* condition.
|
||||
*
|
||||
* @param int|string $fd FD number such as `3` or as URL in the form of `php://fd/3`
|
||||
* @param ?LoopInterface $loop
|
||||
* @throws \InvalidArgumentException if the listening address is invalid
|
||||
* @throws \RuntimeException if listening on this address fails (already in use etc.)
|
||||
*/
|
||||
public function __construct($fd, $loop = null)
|
||||
{
|
||||
if (\preg_match('#^php://fd/(\d+)$#', $fd, $m)) {
|
||||
$fd = (int) $m[1];
|
||||
}
|
||||
if (!\is_int($fd) || $fd < 0 || $fd >= \PHP_INT_MAX) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Invalid FD number given (EINVAL)',
|
||||
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
|
||||
);
|
||||
}
|
||||
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
|
||||
$this->loop = $loop ?: Loop::get();
|
||||
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
\set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
|
||||
// Match errstr from PHP's warning message.
|
||||
// fopen(php://fd/3): Failed to open stream: Error duping file descriptor 3; possibly it doesn't exist: [9]: Bad file descriptor
|
||||
\preg_match('/\[(\d+)\]: (.*)/', $error, $m);
|
||||
$errno = isset($m[1]) ? (int) $m[1] : 0;
|
||||
$errstr = isset($m[2]) ? $m[2] : $error;
|
||||
});
|
||||
|
||||
$this->master = \fopen('php://fd/' . $fd, 'r+');
|
||||
|
||||
\restore_error_handler();
|
||||
|
||||
if (false === $this->master) {
|
||||
throw new \RuntimeException(
|
||||
'Failed to listen on FD ' . $fd . ': ' . $errstr . SocketServer::errconst($errno),
|
||||
$errno
|
||||
);
|
||||
}
|
||||
|
||||
$meta = \stream_get_meta_data($this->master);
|
||||
if (!isset($meta['stream_type']) || $meta['stream_type'] !== 'tcp_socket') {
|
||||
\fclose($this->master);
|
||||
|
||||
$errno = \defined('SOCKET_ENOTSOCK') ? \SOCKET_ENOTSOCK : 88;
|
||||
$errstr = \function_exists('socket_strerror') ? \socket_strerror($errno) : 'Not a socket';
|
||||
|
||||
throw new \RuntimeException(
|
||||
'Failed to listen on FD ' . $fd . ': ' . $errstr . ' (ENOTSOCK)',
|
||||
$errno
|
||||
);
|
||||
}
|
||||
|
||||
// Socket should not have a peer address if this is a listening socket.
|
||||
// Looks like this work-around is the closest we can get because PHP doesn't expose SO_ACCEPTCONN even with ext-sockets.
|
||||
if (\stream_socket_get_name($this->master, true) !== false) {
|
||||
\fclose($this->master);
|
||||
|
||||
$errno = \defined('SOCKET_EISCONN') ? \SOCKET_EISCONN : 106;
|
||||
$errstr = \function_exists('socket_strerror') ? \socket_strerror($errno) : 'Socket is connected';
|
||||
|
||||
throw new \RuntimeException(
|
||||
'Failed to listen on FD ' . $fd . ': ' . $errstr . ' (EISCONN)',
|
||||
$errno
|
||||
);
|
||||
}
|
||||
|
||||
// Assume this is a Unix domain socket (UDS) when its listening address doesn't parse as a valid URL with a port.
|
||||
// Looks like this work-around is the closest we can get because PHP doesn't expose SO_DOMAIN even with ext-sockets.
|
||||
$this->unix = \parse_url($this->getAddress(), \PHP_URL_PORT) === false;
|
||||
|
||||
\stream_set_blocking($this->master, false);
|
||||
|
||||
$this->resume();
|
||||
}
|
||||
|
||||
public function getAddress()
|
||||
{
|
||||
if (!\is_resource($this->master)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$address = \stream_socket_get_name($this->master, false);
|
||||
|
||||
if ($this->unix === true) {
|
||||
return 'unix://' . $address;
|
||||
}
|
||||
|
||||
// check if this is an IPv6 address which includes multiple colons but no square brackets
|
||||
$pos = \strrpos($address, ':');
|
||||
if ($pos !== false && \strpos($address, ':') < $pos && \substr($address, 0, 1) !== '[') {
|
||||
$address = '[' . \substr($address, 0, $pos) . ']:' . \substr($address, $pos + 1); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return 'tcp://' . $address;
|
||||
}
|
||||
|
||||
public function pause()
|
||||
{
|
||||
if (!$this->listening) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->loop->removeReadStream($this->master);
|
||||
$this->listening = false;
|
||||
}
|
||||
|
||||
public function resume()
|
||||
{
|
||||
if ($this->listening || !\is_resource($this->master)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$that = $this;
|
||||
$this->loop->addReadStream($this->master, function ($master) use ($that) {
|
||||
try {
|
||||
$newSocket = SocketServer::accept($master);
|
||||
} catch (\RuntimeException $e) {
|
||||
$that->emit('error', array($e));
|
||||
return;
|
||||
}
|
||||
$that->handleConnection($newSocket);
|
||||
});
|
||||
$this->listening = true;
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
if (!\is_resource($this->master)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->pause();
|
||||
\fclose($this->master);
|
||||
$this->removeAllListeners();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleConnection($socket)
|
||||
{
|
||||
$connection = new Connection($socket, $this->loop);
|
||||
$connection->unix = $this->unix;
|
||||
|
||||
$this->emit('connection', array($connection));
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
/**
|
||||
* Decorates an existing Connector to always use a fixed, preconfigured URI
|
||||
*
|
||||
* This can be useful for consumers that do not support certain URIs, such as
|
||||
* when you want to explicitly connect to a Unix domain socket (UDS) path
|
||||
* instead of connecting to a default address assumed by an higher-level API:
|
||||
*
|
||||
* ```php
|
||||
* $connector = new React\Socket\FixedUriConnector(
|
||||
* 'unix:///var/run/docker.sock',
|
||||
* new React\Socket\UnixConnector()
|
||||
* );
|
||||
*
|
||||
* // destination will be ignored, actually connects to Unix domain socket
|
||||
* $promise = $connector->connect('localhost:80');
|
||||
* ```
|
||||
*/
|
||||
class FixedUriConnector implements ConnectorInterface
|
||||
{
|
||||
private $uri;
|
||||
private $connector;
|
||||
|
||||
/**
|
||||
* @param string $uri
|
||||
* @param ConnectorInterface $connector
|
||||
*/
|
||||
public function __construct($uri, ConnectorInterface $connector)
|
||||
{
|
||||
$this->uri = $uri;
|
||||
$this->connector = $connector;
|
||||
}
|
||||
|
||||
public function connect($_)
|
||||
{
|
||||
return $this->connector->connect($this->uri);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use React\Dns\Model\Message;
|
||||
use React\Dns\Resolver\ResolverInterface;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\EventLoop\TimerInterface;
|
||||
use React\Promise;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class HappyEyeBallsConnectionBuilder
|
||||
{
|
||||
/**
|
||||
* As long as we haven't connected yet keep popping an IP address of the connect queue until one of them
|
||||
* succeeds or they all fail. We will wait 100ms between connection attempts as per RFC.
|
||||
*
|
||||
* @link https://tools.ietf.org/html/rfc8305#section-5
|
||||
*/
|
||||
const CONNECTION_ATTEMPT_DELAY = 0.1;
|
||||
|
||||
/**
|
||||
* Delay `A` lookup by 50ms sending out connection to IPv4 addresses when IPv6 records haven't
|
||||
* resolved yet as per RFC.
|
||||
*
|
||||
* @link https://tools.ietf.org/html/rfc8305#section-3
|
||||
*/
|
||||
const RESOLUTION_DELAY = 0.05;
|
||||
|
||||
public $loop;
|
||||
public $connector;
|
||||
public $resolver;
|
||||
public $uri;
|
||||
public $host;
|
||||
public $resolved = array(
|
||||
Message::TYPE_A => false,
|
||||
Message::TYPE_AAAA => false,
|
||||
);
|
||||
public $resolverPromises = array();
|
||||
public $connectionPromises = array();
|
||||
public $connectQueue = array();
|
||||
public $nextAttemptTimer;
|
||||
public $parts;
|
||||
public $ipsCount = 0;
|
||||
public $failureCount = 0;
|
||||
public $resolve;
|
||||
public $reject;
|
||||
|
||||
public $lastErrorFamily;
|
||||
public $lastError6;
|
||||
public $lastError4;
|
||||
|
||||
public function __construct(LoopInterface $loop, ConnectorInterface $connector, ResolverInterface $resolver, $uri, $host, $parts)
|
||||
{
|
||||
$this->loop = $loop;
|
||||
$this->connector = $connector;
|
||||
$this->resolver = $resolver;
|
||||
$this->uri = $uri;
|
||||
$this->host = $host;
|
||||
$this->parts = $parts;
|
||||
}
|
||||
|
||||
public function connect()
|
||||
{
|
||||
$that = $this;
|
||||
return new Promise\Promise(function ($resolve, $reject) use ($that) {
|
||||
$lookupResolve = function ($type) use ($that, $resolve, $reject) {
|
||||
return function (array $ips) use ($that, $type, $resolve, $reject) {
|
||||
unset($that->resolverPromises[$type]);
|
||||
$that->resolved[$type] = true;
|
||||
|
||||
$that->mixIpsIntoConnectQueue($ips);
|
||||
|
||||
// start next connection attempt if not already awaiting next
|
||||
if ($that->nextAttemptTimer === null && $that->connectQueue) {
|
||||
$that->check($resolve, $reject);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
$that->resolverPromises[Message::TYPE_AAAA] = $that->resolve(Message::TYPE_AAAA, $reject)->then($lookupResolve(Message::TYPE_AAAA));
|
||||
$that->resolverPromises[Message::TYPE_A] = $that->resolve(Message::TYPE_A, $reject)->then(function (array $ips) use ($that) {
|
||||
// happy path: IPv6 has resolved already (or could not resolve), continue with IPv4 addresses
|
||||
if ($that->resolved[Message::TYPE_AAAA] === true || !$ips) {
|
||||
return $ips;
|
||||
}
|
||||
|
||||
// Otherwise delay processing IPv4 lookup until short timer passes or IPv6 resolves in the meantime
|
||||
$deferred = new Promise\Deferred(function () use (&$ips) {
|
||||
// discard all IPv4 addresses if cancelled
|
||||
$ips = array();
|
||||
});
|
||||
$timer = $that->loop->addTimer($that::RESOLUTION_DELAY, function () use ($deferred, $ips) {
|
||||
$deferred->resolve($ips);
|
||||
});
|
||||
|
||||
$that->resolverPromises[Message::TYPE_AAAA]->then(function () use ($that, $timer, $deferred, &$ips) {
|
||||
$that->loop->cancelTimer($timer);
|
||||
$deferred->resolve($ips);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
})->then($lookupResolve(Message::TYPE_A));
|
||||
}, function ($_, $reject) use ($that) {
|
||||
$reject(new \RuntimeException(
|
||||
'Connection to ' . $that->uri . ' cancelled' . (!$that->connectionPromises ? ' during DNS lookup' : '') . ' (ECONNABORTED)',
|
||||
\defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103
|
||||
));
|
||||
$_ = $reject = null;
|
||||
|
||||
$that->cleanUp();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param int $type DNS query type
|
||||
* @param callable $reject
|
||||
* @return \React\Promise\PromiseInterface<string[]> Returns a promise that
|
||||
* always resolves with a list of IP addresses on success or an empty
|
||||
* list on error.
|
||||
*/
|
||||
public function resolve($type, $reject)
|
||||
{
|
||||
$that = $this;
|
||||
return $that->resolver->resolveAll($that->host, $type)->then(null, function (\Exception $e) use ($type, $reject, $that) {
|
||||
unset($that->resolverPromises[$type]);
|
||||
$that->resolved[$type] = true;
|
||||
|
||||
if ($type === Message::TYPE_A) {
|
||||
$that->lastError4 = $e->getMessage();
|
||||
$that->lastErrorFamily = 4;
|
||||
} else {
|
||||
$that->lastError6 = $e->getMessage();
|
||||
$that->lastErrorFamily = 6;
|
||||
}
|
||||
|
||||
// cancel next attempt timer when there are no more IPs to connect to anymore
|
||||
if ($that->nextAttemptTimer !== null && !$that->connectQueue) {
|
||||
$that->loop->cancelTimer($that->nextAttemptTimer);
|
||||
$that->nextAttemptTimer = null;
|
||||
}
|
||||
|
||||
if ($that->hasBeenResolved() && $that->ipsCount === 0) {
|
||||
$reject(new \RuntimeException(
|
||||
$that->error(),
|
||||
0,
|
||||
$e
|
||||
));
|
||||
}
|
||||
|
||||
// Exception already handled above, so don't throw an unhandled rejection here
|
||||
return array();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function check($resolve, $reject)
|
||||
{
|
||||
$ip = \array_shift($this->connectQueue);
|
||||
|
||||
// start connection attempt and remember array position to later unset again
|
||||
$this->connectionPromises[] = $this->attemptConnection($ip);
|
||||
\end($this->connectionPromises);
|
||||
$index = \key($this->connectionPromises);
|
||||
|
||||
$that = $this;
|
||||
$that->connectionPromises[$index]->then(function ($connection) use ($that, $index, $resolve) {
|
||||
unset($that->connectionPromises[$index]);
|
||||
|
||||
$that->cleanUp();
|
||||
|
||||
$resolve($connection);
|
||||
}, function (\Exception $e) use ($that, $index, $ip, $resolve, $reject) {
|
||||
unset($that->connectionPromises[$index]);
|
||||
|
||||
$that->failureCount++;
|
||||
|
||||
$message = \preg_replace('/^(Connection to [^ ]+)[&?]hostname=[^ &]+/', '$1', $e->getMessage());
|
||||
if (\strpos($ip, ':') === false) {
|
||||
$that->lastError4 = $message;
|
||||
$that->lastErrorFamily = 4;
|
||||
} else {
|
||||
$that->lastError6 = $message;
|
||||
$that->lastErrorFamily = 6;
|
||||
}
|
||||
|
||||
// start next connection attempt immediately on error
|
||||
if ($that->connectQueue) {
|
||||
if ($that->nextAttemptTimer !== null) {
|
||||
$that->loop->cancelTimer($that->nextAttemptTimer);
|
||||
$that->nextAttemptTimer = null;
|
||||
}
|
||||
|
||||
$that->check($resolve, $reject);
|
||||
}
|
||||
|
||||
if ($that->hasBeenResolved() === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($that->ipsCount === $that->failureCount) {
|
||||
$that->cleanUp();
|
||||
|
||||
$reject(new \RuntimeException(
|
||||
$that->error(),
|
||||
$e->getCode(),
|
||||
$e
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
// Allow next connection attempt in 100ms: https://tools.ietf.org/html/rfc8305#section-5
|
||||
// Only start timer when more IPs are queued or when DNS query is still pending (might add more IPs)
|
||||
if ($this->nextAttemptTimer === null && (\count($this->connectQueue) > 0 || $this->resolved[Message::TYPE_A] === false || $this->resolved[Message::TYPE_AAAA] === false)) {
|
||||
$this->nextAttemptTimer = $this->loop->addTimer(self::CONNECTION_ATTEMPT_DELAY, function () use ($that, $resolve, $reject) {
|
||||
$that->nextAttemptTimer = null;
|
||||
|
||||
if ($that->connectQueue) {
|
||||
$that->check($resolve, $reject);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function attemptConnection($ip)
|
||||
{
|
||||
$uri = Connector::uri($this->parts, $this->host, $ip);
|
||||
|
||||
return $this->connector->connect($uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function cleanUp()
|
||||
{
|
||||
// clear list of outstanding IPs to avoid creating new connections
|
||||
$this->connectQueue = array();
|
||||
|
||||
// cancel pending connection attempts
|
||||
foreach ($this->connectionPromises as $connectionPromise) {
|
||||
if ($connectionPromise instanceof PromiseInterface && \method_exists($connectionPromise, 'cancel')) {
|
||||
$connectionPromise->cancel();
|
||||
}
|
||||
}
|
||||
|
||||
// cancel pending DNS resolution (cancel IPv4 first in case it is awaiting IPv6 resolution delay)
|
||||
foreach (\array_reverse($this->resolverPromises) as $resolverPromise) {
|
||||
if ($resolverPromise instanceof PromiseInterface && \method_exists($resolverPromise, 'cancel')) {
|
||||
$resolverPromise->cancel();
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->nextAttemptTimer instanceof TimerInterface) {
|
||||
$this->loop->cancelTimer($this->nextAttemptTimer);
|
||||
$this->nextAttemptTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function hasBeenResolved()
|
||||
{
|
||||
foreach ($this->resolved as $typeHasBeenResolved) {
|
||||
if ($typeHasBeenResolved === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixes an array of IP addresses into the connect queue in such a way they alternate when attempting to connect.
|
||||
* The goal behind it is first attempt to connect to IPv6, then to IPv4, then to IPv6 again until one of those
|
||||
* attempts succeeds.
|
||||
*
|
||||
* @link https://tools.ietf.org/html/rfc8305#section-4
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function mixIpsIntoConnectQueue(array $ips)
|
||||
{
|
||||
\shuffle($ips);
|
||||
$this->ipsCount += \count($ips);
|
||||
$connectQueueStash = $this->connectQueue;
|
||||
$this->connectQueue = array();
|
||||
while (\count($connectQueueStash) > 0 || \count($ips) > 0) {
|
||||
if (\count($ips) > 0) {
|
||||
$this->connectQueue[] = \array_shift($ips);
|
||||
}
|
||||
if (\count($connectQueueStash) > 0) {
|
||||
$this->connectQueue[] = \array_shift($connectQueueStash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function error()
|
||||
{
|
||||
if ($this->lastError4 === $this->lastError6) {
|
||||
$message = $this->lastError6;
|
||||
} elseif ($this->lastErrorFamily === 6) {
|
||||
$message = 'Last error for IPv6: ' . $this->lastError6 . '. Previous error for IPv4: ' . $this->lastError4;
|
||||
} else {
|
||||
$message = 'Last error for IPv4: ' . $this->lastError4 . '. Previous error for IPv6: ' . $this->lastError6;
|
||||
}
|
||||
|
||||
if ($this->hasBeenResolved() && $this->ipsCount === 0) {
|
||||
if ($this->lastError6 === $this->lastError4) {
|
||||
$message = ' during DNS lookup: ' . $this->lastError6;
|
||||
} else {
|
||||
$message = ' during DNS lookup. ' . $message;
|
||||
}
|
||||
} else {
|
||||
$message = ': ' . $message;
|
||||
}
|
||||
|
||||
return 'Connection to ' . $this->uri . ' failed' . $message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use React\Dns\Resolver\ResolverInterface;
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise;
|
||||
|
||||
final class HappyEyeBallsConnector implements ConnectorInterface
|
||||
{
|
||||
private $loop;
|
||||
private $connector;
|
||||
private $resolver;
|
||||
|
||||
/**
|
||||
* @param ?LoopInterface $loop
|
||||
* @param ConnectorInterface $connector
|
||||
* @param ResolverInterface $resolver
|
||||
*/
|
||||
public function __construct($loop = null, $connector = null, $resolver = null)
|
||||
{
|
||||
// $connector and $resolver arguments are actually required, marked
|
||||
// optional for technical reasons only. Nullable $loop without default
|
||||
// requires PHP 7.1, null default is also supported in legacy PHP
|
||||
// versions, but required parameters are not allowed after arguments
|
||||
// with null default. Mark all parameters optional and check accordingly.
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #1 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
if (!$connector instanceof ConnectorInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #2 ($connector) expected React\Socket\ConnectorInterface');
|
||||
}
|
||||
if (!$resolver instanceof ResolverInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #3 ($resolver) expected React\Dns\Resolver\ResolverInterface');
|
||||
}
|
||||
|
||||
$this->loop = $loop ?: Loop::get();
|
||||
$this->connector = $connector;
|
||||
$this->resolver = $resolver;
|
||||
}
|
||||
|
||||
public function connect($uri)
|
||||
{
|
||||
$original = $uri;
|
||||
if (\strpos($uri, '://') === false) {
|
||||
$uri = 'tcp://' . $uri;
|
||||
$parts = \parse_url($uri);
|
||||
if (isset($parts['scheme'])) {
|
||||
unset($parts['scheme']);
|
||||
}
|
||||
} else {
|
||||
$parts = \parse_url($uri);
|
||||
}
|
||||
|
||||
if (!$parts || !isset($parts['host'])) {
|
||||
return Promise\reject(new \InvalidArgumentException(
|
||||
'Given URI "' . $original . '" is invalid (EINVAL)',
|
||||
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
|
||||
));
|
||||
}
|
||||
|
||||
$host = \trim($parts['host'], '[]');
|
||||
|
||||
// skip DNS lookup / URI manipulation if this URI already contains an IP
|
||||
if (@\inet_pton($host) !== false) {
|
||||
return $this->connector->connect($original);
|
||||
}
|
||||
|
||||
$builder = new HappyEyeBallsConnectionBuilder(
|
||||
$this->loop,
|
||||
$this->connector,
|
||||
$this->resolver,
|
||||
$uri,
|
||||
$host,
|
||||
$parts
|
||||
);
|
||||
return $builder->connect();
|
||||
}
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use Evenement\EventEmitter;
|
||||
use Exception;
|
||||
use OverflowException;
|
||||
|
||||
/**
|
||||
* The `LimitingServer` decorator wraps a given `ServerInterface` and is responsible
|
||||
* for limiting and keeping track of open connections to this server instance.
|
||||
*
|
||||
* Whenever the underlying server emits a `connection` event, it will check its
|
||||
* limits and then either
|
||||
* - keep track of this connection by adding it to the list of
|
||||
* open connections and then forward the `connection` event
|
||||
* - or reject (close) the connection when its limits are exceeded and will
|
||||
* forward an `error` event instead.
|
||||
*
|
||||
* Whenever a connection closes, it will remove this connection from the list of
|
||||
* open connections.
|
||||
*
|
||||
* ```php
|
||||
* $server = new React\Socket\LimitingServer($server, 100);
|
||||
* $server->on('connection', function (React\Socket\ConnectionInterface $connection) {
|
||||
* $connection->write('hello there!' . PHP_EOL);
|
||||
* …
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* See also the `ServerInterface` for more details.
|
||||
*
|
||||
* @see ServerInterface
|
||||
* @see ConnectionInterface
|
||||
*/
|
||||
class LimitingServer extends EventEmitter implements ServerInterface
|
||||
{
|
||||
private $connections = array();
|
||||
private $server;
|
||||
private $limit;
|
||||
|
||||
private $pauseOnLimit = false;
|
||||
private $autoPaused = false;
|
||||
private $manuPaused = false;
|
||||
|
||||
/**
|
||||
* Instantiates a new LimitingServer.
|
||||
*
|
||||
* You have to pass a maximum number of open connections to ensure
|
||||
* the server will automatically reject (close) connections once this limit
|
||||
* is exceeded. In this case, it will emit an `error` event to inform about
|
||||
* this and no `connection` event will be emitted.
|
||||
*
|
||||
* ```php
|
||||
* $server = new React\Socket\LimitingServer($server, 100);
|
||||
* $server->on('connection', function (React\Socket\ConnectionInterface $connection) {
|
||||
* $connection->write('hello there!' . PHP_EOL);
|
||||
* …
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* You MAY pass a `null` limit in order to put no limit on the number of
|
||||
* open connections and keep accepting new connection until you run out of
|
||||
* operating system resources (such as open file handles). This may be
|
||||
* useful if you do not want to take care of applying a limit but still want
|
||||
* to use the `getConnections()` method.
|
||||
*
|
||||
* You can optionally configure the server to pause accepting new
|
||||
* connections once the connection limit is reached. In this case, it will
|
||||
* pause the underlying server and no longer process any new connections at
|
||||
* all, thus also no longer closing any excessive connections.
|
||||
* The underlying operating system is responsible for keeping a backlog of
|
||||
* pending connections until its limit is reached, at which point it will
|
||||
* start rejecting further connections.
|
||||
* Once the server is below the connection limit, it will continue consuming
|
||||
* connections from the backlog and will process any outstanding data on
|
||||
* each connection.
|
||||
* This mode may be useful for some protocols that are designed to wait for
|
||||
* a response message (such as HTTP), but may be less useful for other
|
||||
* protocols that demand immediate responses (such as a "welcome" message in
|
||||
* an interactive chat).
|
||||
*
|
||||
* ```php
|
||||
* $server = new React\Socket\LimitingServer($server, 100, true);
|
||||
* $server->on('connection', function (React\Socket\ConnectionInterface $connection) {
|
||||
* $connection->write('hello there!' . PHP_EOL);
|
||||
* …
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param ServerInterface $server
|
||||
* @param int|null $connectionLimit
|
||||
* @param bool $pauseOnLimit
|
||||
*/
|
||||
public function __construct(ServerInterface $server, $connectionLimit, $pauseOnLimit = false)
|
||||
{
|
||||
$this->server = $server;
|
||||
$this->limit = $connectionLimit;
|
||||
if ($connectionLimit !== null) {
|
||||
$this->pauseOnLimit = $pauseOnLimit;
|
||||
}
|
||||
|
||||
$this->server->on('connection', array($this, 'handleConnection'));
|
||||
$this->server->on('error', array($this, 'handleError'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with all currently active connections
|
||||
*
|
||||
* ```php
|
||||
* foreach ($server->getConnection() as $connection) {
|
||||
* $connection->write('Hi!');
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @return ConnectionInterface[]
|
||||
*/
|
||||
public function getConnections()
|
||||
{
|
||||
return $this->connections;
|
||||
}
|
||||
|
||||
public function getAddress()
|
||||
{
|
||||
return $this->server->getAddress();
|
||||
}
|
||||
|
||||
public function pause()
|
||||
{
|
||||
if (!$this->manuPaused) {
|
||||
$this->manuPaused = true;
|
||||
|
||||
if (!$this->autoPaused) {
|
||||
$this->server->pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function resume()
|
||||
{
|
||||
if ($this->manuPaused) {
|
||||
$this->manuPaused = false;
|
||||
|
||||
if (!$this->autoPaused) {
|
||||
$this->server->resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
$this->server->close();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleConnection(ConnectionInterface $connection)
|
||||
{
|
||||
// close connection if limit exceeded
|
||||
if ($this->limit !== null && \count($this->connections) >= $this->limit) {
|
||||
$this->handleError(new \OverflowException('Connection closed because server reached connection limit'));
|
||||
$connection->close();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->connections[] = $connection;
|
||||
$that = $this;
|
||||
$connection->on('close', function () use ($that, $connection) {
|
||||
$that->handleDisconnection($connection);
|
||||
});
|
||||
|
||||
// pause accepting new connections if limit exceeded
|
||||
if ($this->pauseOnLimit && !$this->autoPaused && \count($this->connections) >= $this->limit) {
|
||||
$this->autoPaused = true;
|
||||
|
||||
if (!$this->manuPaused) {
|
||||
$this->server->pause();
|
||||
}
|
||||
}
|
||||
|
||||
$this->emit('connection', array($connection));
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleDisconnection(ConnectionInterface $connection)
|
||||
{
|
||||
unset($this->connections[\array_search($connection, $this->connections)]);
|
||||
|
||||
// continue accepting new connection if below limit
|
||||
if ($this->autoPaused && \count($this->connections) < $this->limit) {
|
||||
$this->autoPaused = false;
|
||||
|
||||
if (!$this->manuPaused) {
|
||||
$this->server->resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleError(\Exception $error)
|
||||
{
|
||||
$this->emit('error', array($error));
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise;
|
||||
use BadMethodCallException;
|
||||
use InvalidArgumentException;
|
||||
use UnexpectedValueException;
|
||||
|
||||
final class SecureConnector implements ConnectorInterface
|
||||
{
|
||||
private $connector;
|
||||
private $streamEncryption;
|
||||
private $context;
|
||||
|
||||
/**
|
||||
* @param ConnectorInterface $connector
|
||||
* @param ?LoopInterface $loop
|
||||
* @param array $context
|
||||
*/
|
||||
public function __construct(ConnectorInterface $connector, $loop = null, array $context = array())
|
||||
{
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
|
||||
$this->connector = $connector;
|
||||
$this->streamEncryption = new StreamEncryption($loop ?: Loop::get(), false);
|
||||
$this->context = $context;
|
||||
}
|
||||
|
||||
public function connect($uri)
|
||||
{
|
||||
if (!\function_exists('stream_socket_enable_crypto')) {
|
||||
return Promise\reject(new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)')); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
if (\strpos($uri, '://') === false) {
|
||||
$uri = 'tls://' . $uri;
|
||||
}
|
||||
|
||||
$parts = \parse_url($uri);
|
||||
if (!$parts || !isset($parts['scheme']) || $parts['scheme'] !== 'tls') {
|
||||
return Promise\reject(new \InvalidArgumentException(
|
||||
'Given URI "' . $uri . '" is invalid (EINVAL)',
|
||||
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
|
||||
));
|
||||
}
|
||||
|
||||
$context = $this->context;
|
||||
$encryption = $this->streamEncryption;
|
||||
$connected = false;
|
||||
/** @var \React\Promise\PromiseInterface<ConnectionInterface> $promise */
|
||||
$promise = $this->connector->connect(
|
||||
\str_replace('tls://', '', $uri)
|
||||
)->then(function (ConnectionInterface $connection) use ($context, $encryption, $uri, &$promise, &$connected) {
|
||||
// (unencrypted) TCP/IP connection succeeded
|
||||
$connected = true;
|
||||
|
||||
if (!$connection instanceof Connection) {
|
||||
$connection->close();
|
||||
throw new \UnexpectedValueException('Base connector does not use internal Connection class exposing stream resource');
|
||||
}
|
||||
|
||||
// set required SSL/TLS context options
|
||||
foreach ($context as $name => $value) {
|
||||
\stream_context_set_option($connection->stream, 'ssl', $name, $value);
|
||||
}
|
||||
|
||||
// try to enable encryption
|
||||
return $promise = $encryption->enable($connection)->then(null, function ($error) use ($connection, $uri) {
|
||||
// establishing encryption failed => close invalid connection and return error
|
||||
$connection->close();
|
||||
|
||||
throw new \RuntimeException(
|
||||
'Connection to ' . $uri . ' failed during TLS handshake: ' . $error->getMessage(),
|
||||
$error->getCode()
|
||||
);
|
||||
});
|
||||
}, function (\Exception $e) use ($uri) {
|
||||
if ($e instanceof \RuntimeException) {
|
||||
$message = \preg_replace('/^Connection to [^ ]+/', '', $e->getMessage());
|
||||
$e = new \RuntimeException(
|
||||
'Connection to ' . $uri . $message,
|
||||
$e->getCode(),
|
||||
$e
|
||||
);
|
||||
|
||||
// avoid garbage references by replacing all closures in call stack.
|
||||
// what a lovely piece of code!
|
||||
$r = new \ReflectionProperty('Exception', 'trace');
|
||||
if (\PHP_VERSION_ID < 80100) {
|
||||
$r->setAccessible(true);
|
||||
}
|
||||
$trace = $r->getValue($e);
|
||||
|
||||
// Exception trace arguments are not available on some PHP 7.4 installs
|
||||
// @codeCoverageIgnoreStart
|
||||
foreach ($trace as $ti => $one) {
|
||||
if (isset($one['args'])) {
|
||||
foreach ($one['args'] as $ai => $arg) {
|
||||
if ($arg instanceof \Closure) {
|
||||
$trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
$r->setValue($e, $trace);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
});
|
||||
|
||||
return new \React\Promise\Promise(
|
||||
function ($resolve, $reject) use ($promise) {
|
||||
$promise->then($resolve, $reject);
|
||||
},
|
||||
function ($_, $reject) use (&$promise, $uri, &$connected) {
|
||||
if ($connected) {
|
||||
$reject(new \RuntimeException(
|
||||
'Connection to ' . $uri . ' cancelled during TLS handshake (ECONNABORTED)',
|
||||
\defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103
|
||||
));
|
||||
}
|
||||
|
||||
$promise->cancel();
|
||||
$promise = null;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use Evenement\EventEmitter;
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use BadMethodCallException;
|
||||
use UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* The `SecureServer` class implements the `ServerInterface` and is responsible
|
||||
* for providing a secure TLS (formerly known as SSL) server.
|
||||
*
|
||||
* It does so by wrapping a `TcpServer` instance which waits for plaintext
|
||||
* TCP/IP connections and then performs a TLS handshake for each connection.
|
||||
*
|
||||
* ```php
|
||||
* $server = new React\Socket\TcpServer(8000);
|
||||
* $server = new React\Socket\SecureServer($server, null, array(
|
||||
* // tls context options here…
|
||||
* ));
|
||||
* ```
|
||||
*
|
||||
* Whenever a client completes the TLS handshake, it will emit a `connection` event
|
||||
* with a connection instance implementing [`ConnectionInterface`](#connectioninterface):
|
||||
*
|
||||
* ```php
|
||||
* $server->on('connection', function (React\Socket\ConnectionInterface $connection) {
|
||||
* echo 'Secure connection from' . $connection->getRemoteAddress() . PHP_EOL;
|
||||
*
|
||||
* $connection->write('hello there!' . PHP_EOL);
|
||||
* …
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Whenever a client fails to perform a successful TLS handshake, it will emit an
|
||||
* `error` event and then close the underlying TCP/IP connection:
|
||||
*
|
||||
* ```php
|
||||
* $server->on('error', function (Exception $e) {
|
||||
* echo 'Error' . $e->getMessage() . PHP_EOL;
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* See also the `ServerInterface` for more details.
|
||||
*
|
||||
* Note that the `SecureServer` class is a concrete implementation for TLS sockets.
|
||||
* If you want to typehint in your higher-level protocol implementation, you SHOULD
|
||||
* use the generic `ServerInterface` instead.
|
||||
*
|
||||
* @see ServerInterface
|
||||
* @see ConnectionInterface
|
||||
*/
|
||||
final class SecureServer extends EventEmitter implements ServerInterface
|
||||
{
|
||||
private $tcp;
|
||||
private $encryption;
|
||||
private $context;
|
||||
|
||||
/**
|
||||
* Creates a secure TLS server and starts waiting for incoming connections
|
||||
*
|
||||
* It does so by wrapping a `TcpServer` instance which waits for plaintext
|
||||
* TCP/IP connections and then performs a TLS handshake for each connection.
|
||||
* It thus requires valid [TLS context options],
|
||||
* which in its most basic form may look something like this if you're using a
|
||||
* PEM encoded certificate file:
|
||||
*
|
||||
* ```php
|
||||
* $server = new React\Socket\TcpServer(8000);
|
||||
* $server = new React\Socket\SecureServer($server, null, array(
|
||||
* 'local_cert' => 'server.pem'
|
||||
* ));
|
||||
* ```
|
||||
*
|
||||
* Note that the certificate file will not be loaded on instantiation but when an
|
||||
* incoming connection initializes its TLS context.
|
||||
* This implies that any invalid certificate file paths or contents will only cause
|
||||
* an `error` event at a later time.
|
||||
*
|
||||
* If your private key is encrypted with a passphrase, you have to specify it
|
||||
* like this:
|
||||
*
|
||||
* ```php
|
||||
* $server = new React\Socket\TcpServer(8000);
|
||||
* $server = new React\Socket\SecureServer($server, null, array(
|
||||
* 'local_cert' => 'server.pem',
|
||||
* 'passphrase' => 'secret'
|
||||
* ));
|
||||
* ```
|
||||
*
|
||||
* Note that available [TLS context options],
|
||||
* their defaults and effects of changing these may vary depending on your system
|
||||
* and/or PHP version.
|
||||
* Passing unknown context options has no effect.
|
||||
*
|
||||
* This class takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
* pass the event loop instance to use for this object. You can use a `null` value
|
||||
* here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
|
||||
* This value SHOULD NOT be given unless you're sure you want to explicitly use a
|
||||
* given event loop instance.
|
||||
*
|
||||
* Advanced usage: Despite allowing any `ServerInterface` as first parameter,
|
||||
* you SHOULD pass a `TcpServer` instance as first parameter, unless you
|
||||
* know what you're doing.
|
||||
* Internally, the `SecureServer` has to set the required TLS context options on
|
||||
* the underlying stream resources.
|
||||
* These resources are not exposed through any of the interfaces defined in this
|
||||
* package, but only through the internal `Connection` class.
|
||||
* The `TcpServer` class is guaranteed to emit connections that implement
|
||||
* the `ConnectionInterface` and uses the internal `Connection` class in order to
|
||||
* expose these underlying resources.
|
||||
* If you use a custom `ServerInterface` and its `connection` event does not
|
||||
* meet this requirement, the `SecureServer` will emit an `error` event and
|
||||
* then close the underlying connection.
|
||||
*
|
||||
* @param ServerInterface|TcpServer $tcp
|
||||
* @param ?LoopInterface $loop
|
||||
* @param array $context
|
||||
* @throws BadMethodCallException for legacy HHVM < 3.8 due to lack of support
|
||||
* @see TcpServer
|
||||
* @link https://www.php.net/manual/en/context.ssl.php for TLS context options
|
||||
*/
|
||||
public function __construct(ServerInterface $tcp, $loop = null, array $context = array())
|
||||
{
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
|
||||
if (!\function_exists('stream_socket_enable_crypto')) {
|
||||
throw new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
// default to empty passphrase to suppress blocking passphrase prompt
|
||||
$context += array(
|
||||
'passphrase' => ''
|
||||
);
|
||||
|
||||
$this->tcp = $tcp;
|
||||
$this->encryption = new StreamEncryption($loop ?: Loop::get());
|
||||
$this->context = $context;
|
||||
|
||||
$that = $this;
|
||||
$this->tcp->on('connection', function ($connection) use ($that) {
|
||||
$that->handleConnection($connection);
|
||||
});
|
||||
$this->tcp->on('error', function ($error) use ($that) {
|
||||
$that->emit('error', array($error));
|
||||
});
|
||||
}
|
||||
|
||||
public function getAddress()
|
||||
{
|
||||
$address = $this->tcp->getAddress();
|
||||
if ($address === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return \str_replace('tcp://' , 'tls://', $address);
|
||||
}
|
||||
|
||||
public function pause()
|
||||
{
|
||||
$this->tcp->pause();
|
||||
}
|
||||
|
||||
public function resume()
|
||||
{
|
||||
$this->tcp->resume();
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
return $this->tcp->close();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleConnection(ConnectionInterface $connection)
|
||||
{
|
||||
if (!$connection instanceof Connection) {
|
||||
$this->emit('error', array(new \UnexpectedValueException('Base server does not use internal Connection class exposing stream resource')));
|
||||
$connection->close();
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->context as $name => $value) {
|
||||
\stream_context_set_option($connection->stream, 'ssl', $name, $value);
|
||||
}
|
||||
|
||||
// get remote address before starting TLS handshake in case connection closes during handshake
|
||||
$remote = $connection->getRemoteAddress();
|
||||
$that = $this;
|
||||
|
||||
$this->encryption->enable($connection)->then(
|
||||
function ($conn) use ($that) {
|
||||
$that->emit('connection', array($conn));
|
||||
},
|
||||
function ($error) use ($that, $connection, $remote) {
|
||||
$error = new \RuntimeException(
|
||||
'Connection from ' . $remote . ' failed during TLS handshake: ' . $error->getMessage(),
|
||||
$error->getCode()
|
||||
);
|
||||
|
||||
$that->emit('error', array($error));
|
||||
$connection->close();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+118
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use Evenement\EventEmitter;
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* @deprecated 1.9.0 See `SocketServer` instead
|
||||
* @see SocketServer
|
||||
*/
|
||||
final class Server extends EventEmitter implements ServerInterface
|
||||
{
|
||||
private $server;
|
||||
|
||||
/**
|
||||
* [Deprecated] `Server`
|
||||
*
|
||||
* This class exists for BC reasons only and should not be used anymore.
|
||||
*
|
||||
* ```php
|
||||
* // deprecated
|
||||
* $socket = new React\Socket\Server(0);
|
||||
* $socket = new React\Socket\Server('127.0.0.1:8000');
|
||||
* $socket = new React\Socket\Server('127.0.0.1:8000', null, $context);
|
||||
* $socket = new React\Socket\Server('127.0.0.1:8000', $loop, $context);
|
||||
*
|
||||
* // new
|
||||
* $socket = new React\Socket\SocketServer('127.0.0.1:0');
|
||||
* $socket = new React\Socket\SocketServer('127.0.0.1:8000');
|
||||
* $socket = new React\Socket\SocketServer('127.0.0.1:8000', $context);
|
||||
* $socket = new React\Socket\SocketServer('127.0.0.1:8000', $context, $loop);
|
||||
* ```
|
||||
*
|
||||
* This class takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
* pass the event loop instance to use for this object. You can use a `null` value
|
||||
* here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
|
||||
* This value SHOULD NOT be given unless you're sure you want to explicitly use a
|
||||
* given event loop instance.
|
||||
*
|
||||
* For BC reasons, you can also pass the TCP socket context options as a simple
|
||||
* array without wrapping this in another array under the `tcp` key.
|
||||
*
|
||||
* @param string|int $uri
|
||||
* @param ?LoopInterface $loop
|
||||
* @param array $context
|
||||
* @deprecated 1.9.0 See `SocketServer` instead
|
||||
* @see SocketServer
|
||||
*/
|
||||
public function __construct($uri, $loop = null, array $context = array())
|
||||
{
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
|
||||
$loop = $loop ?: Loop::get();
|
||||
|
||||
// sanitize TCP context options if not properly wrapped
|
||||
if ($context && (!isset($context['tcp']) && !isset($context['tls']) && !isset($context['unix']))) {
|
||||
$context = array('tcp' => $context);
|
||||
}
|
||||
|
||||
// apply default options if not explicitly given
|
||||
$context += array(
|
||||
'tcp' => array(),
|
||||
'tls' => array(),
|
||||
'unix' => array()
|
||||
);
|
||||
|
||||
$scheme = 'tcp';
|
||||
$pos = \strpos($uri, '://');
|
||||
if ($pos !== false) {
|
||||
$scheme = \substr($uri, 0, $pos);
|
||||
}
|
||||
|
||||
if ($scheme === 'unix') {
|
||||
$server = new UnixServer($uri, $loop, $context['unix']);
|
||||
} else {
|
||||
$server = new TcpServer(str_replace('tls://', '', $uri), $loop, $context['tcp']);
|
||||
|
||||
if ($scheme === 'tls') {
|
||||
$server = new SecureServer($server, $loop, $context['tls']);
|
||||
}
|
||||
}
|
||||
|
||||
$this->server = $server;
|
||||
|
||||
$that = $this;
|
||||
$server->on('connection', function (ConnectionInterface $conn) use ($that) {
|
||||
$that->emit('connection', array($conn));
|
||||
});
|
||||
$server->on('error', function (Exception $error) use ($that) {
|
||||
$that->emit('error', array($error));
|
||||
});
|
||||
}
|
||||
|
||||
public function getAddress()
|
||||
{
|
||||
return $this->server->getAddress();
|
||||
}
|
||||
|
||||
public function pause()
|
||||
{
|
||||
$this->server->pause();
|
||||
}
|
||||
|
||||
public function resume()
|
||||
{
|
||||
$this->server->resume();
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
$this->server->close();
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use Evenement\EventEmitterInterface;
|
||||
|
||||
/**
|
||||
* The `ServerInterface` is responsible for providing an interface for accepting
|
||||
* incoming streaming connections, such as a normal TCP/IP connection.
|
||||
*
|
||||
* Most higher-level components (such as a HTTP server) accept an instance
|
||||
* implementing this interface to accept incoming streaming connections.
|
||||
* This is usually done via dependency injection, so it's fairly simple to actually
|
||||
* swap this implementation against any other implementation of this interface.
|
||||
* This means that you SHOULD typehint against this interface instead of a concrete
|
||||
* implementation of this interface.
|
||||
*
|
||||
* Besides defining a few methods, this interface also implements the
|
||||
* `EventEmitterInterface` which allows you to react to certain events:
|
||||
*
|
||||
* connection event:
|
||||
* The `connection` event will be emitted whenever a new connection has been
|
||||
* established, i.e. a new client connects to this server socket:
|
||||
*
|
||||
* ```php
|
||||
* $socket->on('connection', function (React\Socket\ConnectionInterface $connection) {
|
||||
* echo 'new connection' . PHP_EOL;
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* See also the `ConnectionInterface` for more details about handling the
|
||||
* incoming connection.
|
||||
*
|
||||
* error event:
|
||||
* The `error` event will be emitted whenever there's an error accepting a new
|
||||
* connection from a client.
|
||||
*
|
||||
* ```php
|
||||
* $socket->on('error', function (Exception $e) {
|
||||
* echo 'error: ' . $e->getMessage() . PHP_EOL;
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Note that this is not a fatal error event, i.e. the server keeps listening for
|
||||
* new connections even after this event.
|
||||
*
|
||||
* @see ConnectionInterface
|
||||
*/
|
||||
interface ServerInterface extends EventEmitterInterface
|
||||
{
|
||||
/**
|
||||
* Returns the full address (URI) this server is currently listening on
|
||||
*
|
||||
* ```php
|
||||
* $address = $socket->getAddress();
|
||||
* echo 'Server listening on ' . $address . PHP_EOL;
|
||||
* ```
|
||||
*
|
||||
* If the address can not be determined or is unknown at this time (such as
|
||||
* after the socket has been closed), it MAY return a `NULL` value instead.
|
||||
*
|
||||
* Otherwise, it will return the full address (URI) as a string value, such
|
||||
* as `tcp://127.0.0.1:8080`, `tcp://[::1]:80` or `tls://127.0.0.1:443`.
|
||||
* Note that individual URI components are application specific and depend
|
||||
* on the underlying transport protocol.
|
||||
*
|
||||
* If this is a TCP/IP based server and you only want the local port, you may
|
||||
* use something like this:
|
||||
*
|
||||
* ```php
|
||||
* $address = $socket->getAddress();
|
||||
* $port = parse_url($address, PHP_URL_PORT);
|
||||
* echo 'Server listening on port ' . $port . PHP_EOL;
|
||||
* ```
|
||||
*
|
||||
* @return ?string the full listening address (URI) or NULL if it is unknown (not applicable to this server socket or already closed)
|
||||
*/
|
||||
public function getAddress();
|
||||
|
||||
/**
|
||||
* Pauses accepting new incoming connections.
|
||||
*
|
||||
* Removes the socket resource from the EventLoop and thus stop accepting
|
||||
* new connections. Note that the listening socket stays active and is not
|
||||
* closed.
|
||||
*
|
||||
* This means that new incoming connections will stay pending in the
|
||||
* operating system backlog until its configurable backlog is filled.
|
||||
* Once the backlog is filled, the operating system may reject further
|
||||
* incoming connections until the backlog is drained again by resuming
|
||||
* to accept new connections.
|
||||
*
|
||||
* Once the server is paused, no futher `connection` events SHOULD
|
||||
* be emitted.
|
||||
*
|
||||
* ```php
|
||||
* $socket->pause();
|
||||
*
|
||||
* $socket->on('connection', assertShouldNeverCalled());
|
||||
* ```
|
||||
*
|
||||
* This method is advisory-only, though generally not recommended, the
|
||||
* server MAY continue emitting `connection` events.
|
||||
*
|
||||
* Unless otherwise noted, a successfully opened server SHOULD NOT start
|
||||
* in paused state.
|
||||
*
|
||||
* You can continue processing events by calling `resume()` again.
|
||||
*
|
||||
* Note that both methods can be called any number of times, in particular
|
||||
* calling `pause()` more than once SHOULD NOT have any effect.
|
||||
* Similarly, calling this after `close()` is a NO-OP.
|
||||
*
|
||||
* @see self::resume()
|
||||
* @return void
|
||||
*/
|
||||
public function pause();
|
||||
|
||||
/**
|
||||
* Resumes accepting new incoming connections.
|
||||
*
|
||||
* Re-attach the socket resource to the EventLoop after a previous `pause()`.
|
||||
*
|
||||
* ```php
|
||||
* $socket->pause();
|
||||
*
|
||||
* Loop::addTimer(1.0, function () use ($socket) {
|
||||
* $socket->resume();
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Note that both methods can be called any number of times, in particular
|
||||
* calling `resume()` without a prior `pause()` SHOULD NOT have any effect.
|
||||
* Similarly, calling this after `close()` is a NO-OP.
|
||||
*
|
||||
* @see self::pause()
|
||||
* @return void
|
||||
*/
|
||||
public function resume();
|
||||
|
||||
/**
|
||||
* Shuts down this listening socket
|
||||
*
|
||||
* This will stop listening for new incoming connections on this socket.
|
||||
*
|
||||
* Calling this method more than once on the same instance is a NO-OP.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function close();
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use Evenement\EventEmitter;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
final class SocketServer extends EventEmitter implements ServerInterface
|
||||
{
|
||||
private $server;
|
||||
|
||||
/**
|
||||
* The `SocketServer` class is the main class in this package that implements the `ServerInterface` and
|
||||
* allows you to accept incoming streaming connections, such as plaintext TCP/IP or secure TLS connection streams.
|
||||
*
|
||||
* ```php
|
||||
* $socket = new React\Socket\SocketServer('127.0.0.1:0');
|
||||
* $socket = new React\Socket\SocketServer('127.0.0.1:8000');
|
||||
* $socket = new React\Socket\SocketServer('127.0.0.1:8000', $context);
|
||||
* ```
|
||||
*
|
||||
* This class takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
* pass the event loop instance to use for this object. You can use a `null` value
|
||||
* here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
|
||||
* This value SHOULD NOT be given unless you're sure you want to explicitly use a
|
||||
* given event loop instance.
|
||||
*
|
||||
* @param string $uri
|
||||
* @param array $context
|
||||
* @param ?LoopInterface $loop
|
||||
* @throws \InvalidArgumentException if the listening address is invalid
|
||||
* @throws \RuntimeException if listening on this address fails (already in use etc.)
|
||||
*/
|
||||
public function __construct($uri, array $context = array(), $loop = null)
|
||||
{
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #3 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
|
||||
// apply default options if not explicitly given
|
||||
$context += array(
|
||||
'tcp' => array(),
|
||||
'tls' => array(),
|
||||
'unix' => array()
|
||||
);
|
||||
|
||||
$scheme = 'tcp';
|
||||
$pos = \strpos($uri, '://');
|
||||
if ($pos !== false) {
|
||||
$scheme = \substr($uri, 0, $pos);
|
||||
}
|
||||
|
||||
if ($scheme === 'unix') {
|
||||
$server = new UnixServer($uri, $loop, $context['unix']);
|
||||
} elseif ($scheme === 'php') {
|
||||
$server = new FdServer($uri, $loop);
|
||||
} else {
|
||||
if (preg_match('#^(?:\w+://)?\d+$#', $uri)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Invalid URI given (EINVAL)',
|
||||
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
|
||||
);
|
||||
}
|
||||
|
||||
$server = new TcpServer(str_replace('tls://', '', $uri), $loop, $context['tcp']);
|
||||
|
||||
if ($scheme === 'tls') {
|
||||
$server = new SecureServer($server, $loop, $context['tls']);
|
||||
}
|
||||
}
|
||||
|
||||
$this->server = $server;
|
||||
|
||||
$that = $this;
|
||||
$server->on('connection', function (ConnectionInterface $conn) use ($that) {
|
||||
$that->emit('connection', array($conn));
|
||||
});
|
||||
$server->on('error', function (\Exception $error) use ($that) {
|
||||
$that->emit('error', array($error));
|
||||
});
|
||||
}
|
||||
|
||||
public function getAddress()
|
||||
{
|
||||
return $this->server->getAddress();
|
||||
}
|
||||
|
||||
public function pause()
|
||||
{
|
||||
$this->server->pause();
|
||||
}
|
||||
|
||||
public function resume()
|
||||
{
|
||||
$this->server->resume();
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
$this->server->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* [internal] Internal helper method to accept new connection from given server socket
|
||||
*
|
||||
* @param resource $socket server socket to accept connection from
|
||||
* @return resource new client socket if any
|
||||
* @throws \RuntimeException if accepting fails
|
||||
* @internal
|
||||
*/
|
||||
public static function accept($socket)
|
||||
{
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
\set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
|
||||
// Match errstr from PHP's warning message.
|
||||
// stream_socket_accept(): accept failed: Connection timed out
|
||||
$errstr = \preg_replace('#.*: #', '', $error);
|
||||
$errno = SocketServer::errno($errstr);
|
||||
});
|
||||
|
||||
$newSocket = \stream_socket_accept($socket, 0);
|
||||
|
||||
\restore_error_handler();
|
||||
|
||||
if (false === $newSocket) {
|
||||
throw new \RuntimeException(
|
||||
'Unable to accept new connection: ' . $errstr . self::errconst($errno),
|
||||
$errno
|
||||
);
|
||||
}
|
||||
|
||||
return $newSocket;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Internal] Returns errno value for given errstr
|
||||
*
|
||||
* The errno and errstr values describes the type of error that has been
|
||||
* encountered. This method tries to look up the given errstr and find a
|
||||
* matching errno value which can be useful to provide more context to error
|
||||
* messages. It goes through the list of known errno constants when either
|
||||
* `ext-sockets`, `ext-posix` or `ext-pcntl` is available to find an errno
|
||||
* matching the given errstr.
|
||||
*
|
||||
* @param string $errstr
|
||||
* @return int errno value (e.g. value of `SOCKET_ECONNREFUSED`) or 0 if not found
|
||||
* @internal
|
||||
* @copyright Copyright (c) 2023 Christian Lück, taken from https://github.com/clue/errno with permission
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function errno($errstr)
|
||||
{
|
||||
// PHP defines the required `strerror()` function through either `ext-sockets`, `ext-posix` or `ext-pcntl`
|
||||
$strerror = \function_exists('socket_strerror') ? 'socket_strerror' : (\function_exists('posix_strerror') ? 'posix_strerror' : (\function_exists('pcntl_strerror') ? 'pcntl_strerror' : null));
|
||||
if ($strerror !== null) {
|
||||
assert(\is_string($strerror) && \is_callable($strerror));
|
||||
|
||||
// PHP defines most useful errno constants like `ECONNREFUSED` through constants in `ext-sockets` like `SOCKET_ECONNREFUSED`
|
||||
// PHP also defines a hand full of errno constants like `EMFILE` through constants in `ext-pcntl` like `PCNTL_EMFILE`
|
||||
// go through list of all defined constants like `SOCKET_E*` and `PCNTL_E*` and see if they match the given `$errstr`
|
||||
foreach (\get_defined_constants(false) as $name => $value) {
|
||||
if (\is_int($value) && (\strpos($name, 'SOCKET_E') === 0 || \strpos($name, 'PCNTL_E') === 0) && $strerror($value) === $errstr) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
// if we reach this, no matching errno constant could be found (unlikely when `ext-sockets` is available)
|
||||
// go through list of all possible errno values from 1 to `MAX_ERRNO` and see if they match the given `$errstr`
|
||||
for ($errno = 1, $max = \defined('MAX_ERRNO') ? \MAX_ERRNO : 4095; $errno <= $max; ++$errno) {
|
||||
if ($strerror($errno) === $errstr) {
|
||||
return $errno;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we reach this, no matching errno value could be found (unlikely when either `ext-sockets`, `ext-posix` or `ext-pcntl` is available)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Internal] Returns errno constant name for given errno value
|
||||
*
|
||||
* The errno value describes the type of error that has been encountered.
|
||||
* This method tries to look up the given errno value and find a matching
|
||||
* errno constant name which can be useful to provide more context and more
|
||||
* descriptive error messages. It goes through the list of known errno
|
||||
* constants when either `ext-sockets` or `ext-pcntl` is available to find
|
||||
* the matching errno constant name.
|
||||
*
|
||||
* Because this method is used to append more context to error messages, the
|
||||
* constant name will be prefixed with a space and put between parenthesis
|
||||
* when found.
|
||||
*
|
||||
* @param int $errno
|
||||
* @return string e.g. ` (ECONNREFUSED)` or empty string if no matching const for the given errno could be found
|
||||
* @internal
|
||||
* @copyright Copyright (c) 2023 Christian Lück, taken from https://github.com/clue/errno with permission
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function errconst($errno)
|
||||
{
|
||||
// PHP defines most useful errno constants like `ECONNREFUSED` through constants in `ext-sockets` like `SOCKET_ECONNREFUSED`
|
||||
// PHP also defines a hand full of errno constants like `EMFILE` through constants in `ext-pcntl` like `PCNTL_EMFILE`
|
||||
// go through list of all defined constants like `SOCKET_E*` and `PCNTL_E*` and see if they match the given `$errno`
|
||||
foreach (\get_defined_constants(false) as $name => $value) {
|
||||
if ($value === $errno && (\strpos($name, 'SOCKET_E') === 0 || \strpos($name, 'PCNTL_E') === 0)) {
|
||||
return ' (' . \substr($name, \strpos($name, '_') + 1) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
// if we reach this, no matching errno constant could be found (unlikely when `ext-sockets` is available)
|
||||
return '';
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise\Deferred;
|
||||
use RuntimeException;
|
||||
use UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* This class is considered internal and its API should not be relied upon
|
||||
* outside of Socket.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class StreamEncryption
|
||||
{
|
||||
private $loop;
|
||||
private $method;
|
||||
private $server;
|
||||
|
||||
public function __construct(LoopInterface $loop, $server = true)
|
||||
{
|
||||
$this->loop = $loop;
|
||||
$this->server = $server;
|
||||
|
||||
// support TLSv1.0+ by default and exclude legacy SSLv2/SSLv3.
|
||||
// As of PHP 7.2+ the main crypto method constant includes all TLS versions.
|
||||
// As of PHP 5.6+ the crypto method is a bitmask, so we explicitly include all TLS versions.
|
||||
// For legacy PHP < 5.6 the crypto method is a single value only and this constant includes all TLS versions.
|
||||
// @link https://3v4l.org/9PSST
|
||||
if ($server) {
|
||||
$this->method = \STREAM_CRYPTO_METHOD_TLS_SERVER;
|
||||
|
||||
if (\PHP_VERSION_ID < 70200 && \PHP_VERSION_ID >= 50600) {
|
||||
$this->method |= \STREAM_CRYPTO_METHOD_TLSv1_0_SERVER | \STREAM_CRYPTO_METHOD_TLSv1_1_SERVER | \STREAM_CRYPTO_METHOD_TLSv1_2_SERVER; // @codeCoverageIgnore
|
||||
}
|
||||
} else {
|
||||
$this->method = \STREAM_CRYPTO_METHOD_TLS_CLIENT;
|
||||
|
||||
if (\PHP_VERSION_ID < 70200 && \PHP_VERSION_ID >= 50600) {
|
||||
$this->method |= \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Connection $stream
|
||||
* @return \React\Promise\PromiseInterface<Connection>
|
||||
*/
|
||||
public function enable(Connection $stream)
|
||||
{
|
||||
return $this->toggle($stream, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Connection $stream
|
||||
* @param bool $toggle
|
||||
* @return \React\Promise\PromiseInterface<Connection>
|
||||
*/
|
||||
public function toggle(Connection $stream, $toggle)
|
||||
{
|
||||
// pause actual stream instance to continue operation on raw stream socket
|
||||
$stream->pause();
|
||||
|
||||
// TODO: add write() event to make sure we're not sending any excessive data
|
||||
|
||||
// cancelling this leaves this stream in an inconsistent state…
|
||||
$deferred = new Deferred(function () {
|
||||
throw new \RuntimeException();
|
||||
});
|
||||
|
||||
// get actual stream socket from stream instance
|
||||
$socket = $stream->stream;
|
||||
|
||||
// get crypto method from context options or use global setting from constructor
|
||||
$method = $this->method;
|
||||
$context = \stream_context_get_options($socket);
|
||||
if (isset($context['ssl']['crypto_method'])) {
|
||||
$method = $context['ssl']['crypto_method'];
|
||||
}
|
||||
|
||||
$that = $this;
|
||||
$toggleCrypto = function () use ($socket, $deferred, $toggle, $method, $that) {
|
||||
$that->toggleCrypto($socket, $deferred, $toggle, $method);
|
||||
};
|
||||
|
||||
$this->loop->addReadStream($socket, $toggleCrypto);
|
||||
|
||||
if (!$this->server) {
|
||||
$toggleCrypto();
|
||||
}
|
||||
|
||||
$loop = $this->loop;
|
||||
|
||||
return $deferred->promise()->then(function () use ($stream, $socket, $loop, $toggle) {
|
||||
$loop->removeReadStream($socket);
|
||||
|
||||
$stream->encryptionEnabled = $toggle;
|
||||
$stream->resume();
|
||||
|
||||
return $stream;
|
||||
}, function($error) use ($stream, $socket, $loop) {
|
||||
$loop->removeReadStream($socket);
|
||||
$stream->resume();
|
||||
throw $error;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param resource $socket
|
||||
* @param Deferred<null> $deferred
|
||||
* @param bool $toggle
|
||||
* @param int $method
|
||||
* @return void
|
||||
*/
|
||||
public function toggleCrypto($socket, Deferred $deferred, $toggle, $method)
|
||||
{
|
||||
$error = null;
|
||||
\set_error_handler(function ($_, $errstr) use (&$error) {
|
||||
$error = \str_replace(array("\r", "\n"), ' ', $errstr);
|
||||
|
||||
// remove useless function name from error message
|
||||
if (($pos = \strpos($error, "): ")) !== false) {
|
||||
$error = \substr($error, $pos + 3);
|
||||
}
|
||||
});
|
||||
|
||||
$result = \stream_socket_enable_crypto($socket, $toggle, $method);
|
||||
|
||||
\restore_error_handler();
|
||||
|
||||
if (true === $result) {
|
||||
$deferred->resolve(null);
|
||||
} else if (false === $result) {
|
||||
// overwrite callback arguments for PHP7+ only, so they do not show
|
||||
// up in the Exception trace and do not cause a possible cyclic reference.
|
||||
$d = $deferred;
|
||||
$deferred = null;
|
||||
|
||||
if (\feof($socket) || $error === null) {
|
||||
// EOF or failed without error => connection closed during handshake
|
||||
$d->reject(new \UnexpectedValueException(
|
||||
'Connection lost during TLS handshake (ECONNRESET)',
|
||||
\defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 104
|
||||
));
|
||||
} else {
|
||||
// handshake failed with error message
|
||||
$d->reject(new \UnexpectedValueException(
|
||||
$error
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// need more data, will retry
|
||||
}
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
final class TcpConnector implements ConnectorInterface
|
||||
{
|
||||
private $loop;
|
||||
private $context;
|
||||
|
||||
/**
|
||||
* @param ?LoopInterface $loop
|
||||
* @param array $context
|
||||
*/
|
||||
public function __construct($loop = null, array $context = array())
|
||||
{
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #1 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
|
||||
$this->loop = $loop ?: Loop::get();
|
||||
$this->context = $context;
|
||||
}
|
||||
|
||||
public function connect($uri)
|
||||
{
|
||||
if (\strpos($uri, '://') === false) {
|
||||
$uri = 'tcp://' . $uri;
|
||||
}
|
||||
|
||||
$parts = \parse_url($uri);
|
||||
if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') {
|
||||
return Promise\reject(new \InvalidArgumentException(
|
||||
'Given URI "' . $uri . '" is invalid (EINVAL)',
|
||||
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
|
||||
));
|
||||
}
|
||||
|
||||
$ip = \trim($parts['host'], '[]');
|
||||
if (@\inet_pton($ip) === false) {
|
||||
return Promise\reject(new \InvalidArgumentException(
|
||||
'Given URI "' . $uri . '" does not contain a valid host IP (EINVAL)',
|
||||
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
|
||||
));
|
||||
}
|
||||
|
||||
// use context given in constructor
|
||||
$context = array(
|
||||
'socket' => $this->context
|
||||
);
|
||||
|
||||
// parse arguments from query component of URI
|
||||
$args = array();
|
||||
if (isset($parts['query'])) {
|
||||
\parse_str($parts['query'], $args);
|
||||
}
|
||||
|
||||
// If an original hostname has been given, use this for TLS setup.
|
||||
// This can happen due to layers of nested connectors, such as a
|
||||
// DnsConnector reporting its original hostname.
|
||||
// These context options are here in case TLS is enabled later on this stream.
|
||||
// If TLS is not enabled later, this doesn't hurt either.
|
||||
if (isset($args['hostname'])) {
|
||||
$context['ssl'] = array(
|
||||
'SNI_enabled' => true,
|
||||
'peer_name' => $args['hostname']
|
||||
);
|
||||
|
||||
// Legacy PHP < 5.6 ignores peer_name and requires legacy context options instead.
|
||||
// The SNI_server_name context option has to be set here during construction,
|
||||
// as legacy PHP ignores any values set later.
|
||||
// @codeCoverageIgnoreStart
|
||||
if (\PHP_VERSION_ID < 50600) {
|
||||
$context['ssl'] += array(
|
||||
'SNI_server_name' => $args['hostname'],
|
||||
'CN_match' => $args['hostname']
|
||||
);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
// latest versions of PHP no longer accept any other URI components and
|
||||
// HHVM fails to parse URIs with a query but no path, so let's simplify our URI here
|
||||
$remote = 'tcp://' . $parts['host'] . ':' . $parts['port'];
|
||||
|
||||
$stream = @\stream_socket_client(
|
||||
$remote,
|
||||
$errno,
|
||||
$errstr,
|
||||
0,
|
||||
\STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT,
|
||||
\stream_context_create($context)
|
||||
);
|
||||
|
||||
if (false === $stream) {
|
||||
return Promise\reject(new \RuntimeException(
|
||||
'Connection to ' . $uri . ' failed: ' . $errstr . SocketServer::errconst($errno),
|
||||
$errno
|
||||
));
|
||||
}
|
||||
|
||||
// wait for connection
|
||||
$loop = $this->loop;
|
||||
return new Promise\Promise(function ($resolve, $reject) use ($loop, $stream, $uri) {
|
||||
$loop->addWriteStream($stream, function ($stream) use ($loop, $resolve, $reject, $uri) {
|
||||
$loop->removeWriteStream($stream);
|
||||
|
||||
// The following hack looks like the only way to
|
||||
// detect connection refused errors with PHP's stream sockets.
|
||||
if (false === \stream_socket_get_name($stream, true)) {
|
||||
// If we reach this point, we know the connection is dead, but we don't know the underlying error condition.
|
||||
// @codeCoverageIgnoreStart
|
||||
if (\function_exists('socket_import_stream')) {
|
||||
// actual socket errno and errstr can be retrieved with ext-sockets on PHP 5.4+
|
||||
$socket = \socket_import_stream($stream);
|
||||
$errno = \socket_get_option($socket, \SOL_SOCKET, \SO_ERROR);
|
||||
$errstr = \socket_strerror($errno);
|
||||
} elseif (\PHP_OS === 'Linux') {
|
||||
// Linux reports socket errno and errstr again when trying to write to the dead socket.
|
||||
// Suppress error reporting to get error message below and close dead socket before rejecting.
|
||||
// This is only known to work on Linux, Mac and Windows are known to not support this.
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
\set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
|
||||
// Match errstr from PHP's warning message.
|
||||
// fwrite(): send of 1 bytes failed with errno=111 Connection refused
|
||||
\preg_match('/errno=(\d+) (.+)/', $error, $m);
|
||||
$errno = isset($m[1]) ? (int) $m[1] : 0;
|
||||
$errstr = isset($m[2]) ? $m[2] : $error;
|
||||
});
|
||||
|
||||
\fwrite($stream, \PHP_EOL);
|
||||
|
||||
\restore_error_handler();
|
||||
} else {
|
||||
// Not on Linux and ext-sockets not available? Too bad.
|
||||
$errno = \defined('SOCKET_ECONNREFUSED') ? \SOCKET_ECONNREFUSED : 111;
|
||||
$errstr = 'Connection refused?';
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
\fclose($stream);
|
||||
$reject(new \RuntimeException(
|
||||
'Connection to ' . $uri . ' failed: ' . $errstr . SocketServer::errconst($errno),
|
||||
$errno
|
||||
));
|
||||
} else {
|
||||
$resolve(new Connection($stream, $loop));
|
||||
}
|
||||
});
|
||||
}, function () use ($loop, $stream, $uri) {
|
||||
$loop->removeWriteStream($stream);
|
||||
\fclose($stream);
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
// legacy PHP 5.3 sometimes requires a second close call (see tests)
|
||||
if (\PHP_VERSION_ID < 50400 && \is_resource($stream)) {
|
||||
\fclose($stream);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
throw new \RuntimeException(
|
||||
'Connection to ' . $uri . ' cancelled during TCP/IP handshake (ECONNABORTED)',
|
||||
\defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use Evenement\EventEmitter;
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* The `TcpServer` class implements the `ServerInterface` and
|
||||
* is responsible for accepting plaintext TCP/IP connections.
|
||||
*
|
||||
* ```php
|
||||
* $server = new React\Socket\TcpServer(8080);
|
||||
* ```
|
||||
*
|
||||
* Whenever a client connects, it will emit a `connection` event with a connection
|
||||
* instance implementing `ConnectionInterface`:
|
||||
*
|
||||
* ```php
|
||||
* $server->on('connection', function (React\Socket\ConnectionInterface $connection) {
|
||||
* echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL;
|
||||
* $connection->write('hello there!' . PHP_EOL);
|
||||
* …
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* See also the `ServerInterface` for more details.
|
||||
*
|
||||
* @see ServerInterface
|
||||
* @see ConnectionInterface
|
||||
*/
|
||||
final class TcpServer extends EventEmitter implements ServerInterface
|
||||
{
|
||||
private $master;
|
||||
private $loop;
|
||||
private $listening = false;
|
||||
|
||||
/**
|
||||
* Creates a plaintext TCP/IP socket server and starts listening on the given address
|
||||
*
|
||||
* This starts accepting new incoming connections on the given address.
|
||||
* See also the `connection event` documented in the `ServerInterface`
|
||||
* for more details.
|
||||
*
|
||||
* ```php
|
||||
* $server = new React\Socket\TcpServer(8080);
|
||||
* ```
|
||||
*
|
||||
* As above, the `$uri` parameter can consist of only a port, in which case the
|
||||
* server will default to listening on the localhost address `127.0.0.1`,
|
||||
* which means it will not be reachable from outside of this system.
|
||||
*
|
||||
* In order to use a random port assignment, you can use the port `0`:
|
||||
*
|
||||
* ```php
|
||||
* $server = new React\Socket\TcpServer(0);
|
||||
* $address = $server->getAddress();
|
||||
* ```
|
||||
*
|
||||
* In order to change the host the socket is listening on, you can provide an IP
|
||||
* address through the first parameter provided to the constructor, optionally
|
||||
* preceded by the `tcp://` scheme:
|
||||
*
|
||||
* ```php
|
||||
* $server = new React\Socket\TcpServer('192.168.0.1:8080');
|
||||
* ```
|
||||
*
|
||||
* If you want to listen on an IPv6 address, you MUST enclose the host in square
|
||||
* brackets:
|
||||
*
|
||||
* ```php
|
||||
* $server = new React\Socket\TcpServer('[::1]:8080');
|
||||
* ```
|
||||
*
|
||||
* If the given URI is invalid, does not contain a port, any other scheme or if it
|
||||
* contains a hostname, it will throw an `InvalidArgumentException`:
|
||||
*
|
||||
* ```php
|
||||
* // throws InvalidArgumentException due to missing port
|
||||
* $server = new React\Socket\TcpServer('127.0.0.1');
|
||||
* ```
|
||||
*
|
||||
* If the given URI appears to be valid, but listening on it fails (such as if port
|
||||
* is already in use or port below 1024 may require root access etc.), it will
|
||||
* throw a `RuntimeException`:
|
||||
*
|
||||
* ```php
|
||||
* $first = new React\Socket\TcpServer(8080);
|
||||
*
|
||||
* // throws RuntimeException because port is already in use
|
||||
* $second = new React\Socket\TcpServer(8080);
|
||||
* ```
|
||||
*
|
||||
* Note that these error conditions may vary depending on your system and/or
|
||||
* configuration.
|
||||
* See the exception message and code for more details about the actual error
|
||||
* condition.
|
||||
*
|
||||
* This class takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
* pass the event loop instance to use for this object. You can use a `null` value
|
||||
* here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
|
||||
* This value SHOULD NOT be given unless you're sure you want to explicitly use a
|
||||
* given event loop instance.
|
||||
*
|
||||
* Optionally, you can specify [socket context options](https://www.php.net/manual/en/context.socket.php)
|
||||
* for the underlying stream socket resource like this:
|
||||
*
|
||||
* ```php
|
||||
* $server = new React\Socket\TcpServer('[::1]:8080', null, array(
|
||||
* 'backlog' => 200,
|
||||
* 'so_reuseport' => true,
|
||||
* 'ipv6_v6only' => true
|
||||
* ));
|
||||
* ```
|
||||
*
|
||||
* Note that available [socket context options](https://www.php.net/manual/en/context.socket.php),
|
||||
* their defaults and effects of changing these may vary depending on your system
|
||||
* and/or PHP version.
|
||||
* Passing unknown context options has no effect.
|
||||
* The `backlog` context option defaults to `511` unless given explicitly.
|
||||
*
|
||||
* @param string|int $uri
|
||||
* @param ?LoopInterface $loop
|
||||
* @param array $context
|
||||
* @throws InvalidArgumentException if the listening address is invalid
|
||||
* @throws RuntimeException if listening on this address fails (already in use etc.)
|
||||
*/
|
||||
public function __construct($uri, $loop = null, array $context = array())
|
||||
{
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
|
||||
$this->loop = $loop ?: Loop::get();
|
||||
|
||||
// a single port has been given => assume localhost
|
||||
if ((string)(int)$uri === (string)$uri) {
|
||||
$uri = '127.0.0.1:' . $uri;
|
||||
}
|
||||
|
||||
// assume default scheme if none has been given
|
||||
if (\strpos($uri, '://') === false) {
|
||||
$uri = 'tcp://' . $uri;
|
||||
}
|
||||
|
||||
// parse_url() does not accept null ports (random port assignment) => manually remove
|
||||
if (\substr($uri, -2) === ':0') {
|
||||
$parts = \parse_url(\substr($uri, 0, -2));
|
||||
if ($parts) {
|
||||
$parts['port'] = 0;
|
||||
}
|
||||
} else {
|
||||
$parts = \parse_url($uri);
|
||||
}
|
||||
|
||||
// ensure URI contains TCP scheme, host and port
|
||||
if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') {
|
||||
throw new \InvalidArgumentException(
|
||||
'Invalid URI "' . $uri . '" given (EINVAL)',
|
||||
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
|
||||
);
|
||||
}
|
||||
|
||||
if (@\inet_pton(\trim($parts['host'], '[]')) === false) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Given URI "' . $uri . '" does not contain a valid host IP (EINVAL)',
|
||||
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
|
||||
);
|
||||
}
|
||||
|
||||
$this->master = @\stream_socket_server(
|
||||
$uri,
|
||||
$errno,
|
||||
$errstr,
|
||||
\STREAM_SERVER_BIND | \STREAM_SERVER_LISTEN,
|
||||
\stream_context_create(array('socket' => $context + array('backlog' => 511)))
|
||||
);
|
||||
if (false === $this->master) {
|
||||
if ($errno === 0) {
|
||||
// PHP does not seem to report errno, so match errno from errstr
|
||||
// @link https://3v4l.org/3qOBl
|
||||
$errno = SocketServer::errno($errstr);
|
||||
}
|
||||
|
||||
throw new \RuntimeException(
|
||||
'Failed to listen on "' . $uri . '": ' . $errstr . SocketServer::errconst($errno),
|
||||
$errno
|
||||
);
|
||||
}
|
||||
\stream_set_blocking($this->master, false);
|
||||
|
||||
$this->resume();
|
||||
}
|
||||
|
||||
public function getAddress()
|
||||
{
|
||||
if (!\is_resource($this->master)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$address = \stream_socket_get_name($this->master, false);
|
||||
|
||||
// check if this is an IPv6 address which includes multiple colons but no square brackets
|
||||
$pos = \strrpos($address, ':');
|
||||
if ($pos !== false && \strpos($address, ':') < $pos && \substr($address, 0, 1) !== '[') {
|
||||
$address = '[' . \substr($address, 0, $pos) . ']:' . \substr($address, $pos + 1); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return 'tcp://' . $address;
|
||||
}
|
||||
|
||||
public function pause()
|
||||
{
|
||||
if (!$this->listening) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->loop->removeReadStream($this->master);
|
||||
$this->listening = false;
|
||||
}
|
||||
|
||||
public function resume()
|
||||
{
|
||||
if ($this->listening || !\is_resource($this->master)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$that = $this;
|
||||
$this->loop->addReadStream($this->master, function ($master) use ($that) {
|
||||
try {
|
||||
$newSocket = SocketServer::accept($master);
|
||||
} catch (\RuntimeException $e) {
|
||||
$that->emit('error', array($e));
|
||||
return;
|
||||
}
|
||||
$that->handleConnection($newSocket);
|
||||
});
|
||||
$this->listening = true;
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
if (!\is_resource($this->master)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->pause();
|
||||
\fclose($this->master);
|
||||
$this->removeAllListeners();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleConnection($socket)
|
||||
{
|
||||
$this->emit('connection', array(
|
||||
new Connection($socket, $this->loop)
|
||||
));
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise\Promise;
|
||||
|
||||
final class TimeoutConnector implements ConnectorInterface
|
||||
{
|
||||
private $connector;
|
||||
private $timeout;
|
||||
private $loop;
|
||||
|
||||
/**
|
||||
* @param ConnectorInterface $connector
|
||||
* @param float $timeout
|
||||
* @param ?LoopInterface $loop
|
||||
*/
|
||||
public function __construct(ConnectorInterface $connector, $timeout, $loop = null)
|
||||
{
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #3 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
|
||||
$this->connector = $connector;
|
||||
$this->timeout = $timeout;
|
||||
$this->loop = $loop ?: Loop::get();
|
||||
}
|
||||
|
||||
public function connect($uri)
|
||||
{
|
||||
$promise = $this->connector->connect($uri);
|
||||
|
||||
$loop = $this->loop;
|
||||
$time = $this->timeout;
|
||||
return new Promise(function ($resolve, $reject) use ($loop, $time, $promise, $uri) {
|
||||
$timer = null;
|
||||
$promise = $promise->then(function ($v) use (&$timer, $loop, $resolve) {
|
||||
if ($timer) {
|
||||
$loop->cancelTimer($timer);
|
||||
}
|
||||
$timer = false;
|
||||
$resolve($v);
|
||||
}, function ($v) use (&$timer, $loop, $reject) {
|
||||
if ($timer) {
|
||||
$loop->cancelTimer($timer);
|
||||
}
|
||||
$timer = false;
|
||||
$reject($v);
|
||||
});
|
||||
|
||||
// promise already resolved => no need to start timer
|
||||
if ($timer === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// start timeout timer which will cancel the pending promise
|
||||
$timer = $loop->addTimer($time, function () use ($time, &$promise, $reject, $uri) {
|
||||
$reject(new \RuntimeException(
|
||||
'Connection to ' . $uri . ' timed out after ' . $time . ' seconds (ETIMEDOUT)',
|
||||
\defined('SOCKET_ETIMEDOUT') ? \SOCKET_ETIMEDOUT : 110
|
||||
));
|
||||
|
||||
// Cancel pending connection to clean up any underlying resources and references.
|
||||
// Avoid garbage references in call stack by passing pending promise by reference.
|
||||
assert(\method_exists($promise, 'cancel'));
|
||||
$promise->cancel();
|
||||
$promise = null;
|
||||
});
|
||||
}, function () use (&$promise) {
|
||||
// Cancelling this promise will cancel the pending connection, thus triggering the rejection logic above.
|
||||
// Avoid garbage references in call stack by passing pending promise by reference.
|
||||
assert(\method_exists($promise, 'cancel'));
|
||||
$promise->cancel();
|
||||
$promise = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Unix domain socket connector
|
||||
*
|
||||
* Unix domain sockets use atomic operations, so we can as well emulate
|
||||
* async behavior.
|
||||
*/
|
||||
final class UnixConnector implements ConnectorInterface
|
||||
{
|
||||
private $loop;
|
||||
|
||||
/**
|
||||
* @param ?LoopInterface $loop
|
||||
*/
|
||||
public function __construct($loop = null)
|
||||
{
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #1 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
|
||||
$this->loop = $loop ?: Loop::get();
|
||||
}
|
||||
|
||||
public function connect($path)
|
||||
{
|
||||
if (\strpos($path, '://') === false) {
|
||||
$path = 'unix://' . $path;
|
||||
} elseif (\substr($path, 0, 7) !== 'unix://') {
|
||||
return Promise\reject(new \InvalidArgumentException(
|
||||
'Given URI "' . $path . '" is invalid (EINVAL)',
|
||||
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
|
||||
));
|
||||
}
|
||||
|
||||
$resource = @\stream_socket_client($path, $errno, $errstr, 1.0);
|
||||
|
||||
if (!$resource) {
|
||||
return Promise\reject(new \RuntimeException(
|
||||
'Unable to connect to unix domain socket "' . $path . '": ' . $errstr . SocketServer::errconst($errno),
|
||||
$errno
|
||||
));
|
||||
}
|
||||
|
||||
$connection = new Connection($resource, $this->loop);
|
||||
$connection->unix = true;
|
||||
|
||||
return Promise\resolve($connection);
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace React\Socket;
|
||||
|
||||
use Evenement\EventEmitter;
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* The `UnixServer` class implements the `ServerInterface` and
|
||||
* is responsible for accepting plaintext connections on unix domain sockets.
|
||||
*
|
||||
* ```php
|
||||
* $server = new React\Socket\UnixServer('unix:///tmp/app.sock');
|
||||
* ```
|
||||
*
|
||||
* See also the `ServerInterface` for more details.
|
||||
*
|
||||
* @see ServerInterface
|
||||
* @see ConnectionInterface
|
||||
*/
|
||||
final class UnixServer extends EventEmitter implements ServerInterface
|
||||
{
|
||||
private $master;
|
||||
private $loop;
|
||||
private $listening = false;
|
||||
|
||||
/**
|
||||
* Creates a plaintext socket server and starts listening on the given unix socket
|
||||
*
|
||||
* This starts accepting new incoming connections on the given address.
|
||||
* See also the `connection event` documented in the `ServerInterface`
|
||||
* for more details.
|
||||
*
|
||||
* ```php
|
||||
* $server = new React\Socket\UnixServer('unix:///tmp/app.sock');
|
||||
* ```
|
||||
*
|
||||
* This class takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
* pass the event loop instance to use for this object. You can use a `null` value
|
||||
* here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
|
||||
* This value SHOULD NOT be given unless you're sure you want to explicitly use a
|
||||
* given event loop instance.
|
||||
*
|
||||
* @param string $path
|
||||
* @param ?LoopInterface $loop
|
||||
* @param array $context
|
||||
* @throws InvalidArgumentException if the listening address is invalid
|
||||
* @throws RuntimeException if listening on this address fails (already in use etc.)
|
||||
*/
|
||||
public function __construct($path, $loop = null, array $context = array())
|
||||
{
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
|
||||
$this->loop = $loop ?: Loop::get();
|
||||
|
||||
if (\strpos($path, '://') === false) {
|
||||
$path = 'unix://' . $path;
|
||||
} elseif (\substr($path, 0, 7) !== 'unix://') {
|
||||
throw new \InvalidArgumentException(
|
||||
'Given URI "' . $path . '" is invalid (EINVAL)',
|
||||
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
|
||||
);
|
||||
}
|
||||
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
\set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
|
||||
// PHP does not seem to report errno/errstr for Unix domain sockets (UDS) right now.
|
||||
// This only applies to UDS server sockets, see also https://3v4l.org/NAhpr.
|
||||
// Parse PHP warning message containing unknown error, HHVM reports proper info at least.
|
||||
if (\preg_match('/\(([^\)]+)\)|\[(\d+)\]: (.*)/', $error, $match)) {
|
||||
$errstr = isset($match[3]) ? $match['3'] : $match[1];
|
||||
$errno = isset($match[2]) ? (int)$match[2] : 0;
|
||||
}
|
||||
});
|
||||
|
||||
$this->master = \stream_socket_server(
|
||||
$path,
|
||||
$errno,
|
||||
$errstr,
|
||||
\STREAM_SERVER_BIND | \STREAM_SERVER_LISTEN,
|
||||
\stream_context_create(array('socket' => $context))
|
||||
);
|
||||
|
||||
\restore_error_handler();
|
||||
|
||||
if (false === $this->master) {
|
||||
throw new \RuntimeException(
|
||||
'Failed to listen on Unix domain socket "' . $path . '": ' . $errstr . SocketServer::errconst($errno),
|
||||
$errno
|
||||
);
|
||||
}
|
||||
\stream_set_blocking($this->master, 0);
|
||||
|
||||
$this->resume();
|
||||
}
|
||||
|
||||
public function getAddress()
|
||||
{
|
||||
if (!\is_resource($this->master)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'unix://' . \stream_socket_get_name($this->master, false);
|
||||
}
|
||||
|
||||
public function pause()
|
||||
{
|
||||
if (!$this->listening) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->loop->removeReadStream($this->master);
|
||||
$this->listening = false;
|
||||
}
|
||||
|
||||
public function resume()
|
||||
{
|
||||
if ($this->listening || !is_resource($this->master)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$that = $this;
|
||||
$this->loop->addReadStream($this->master, function ($master) use ($that) {
|
||||
try {
|
||||
$newSocket = SocketServer::accept($master);
|
||||
} catch (\RuntimeException $e) {
|
||||
$that->emit('error', array($e));
|
||||
return;
|
||||
}
|
||||
$that->handleConnection($newSocket);
|
||||
});
|
||||
$this->listening = true;
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
if (!\is_resource($this->master)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->pause();
|
||||
\fclose($this->master);
|
||||
$this->removeAllListeners();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleConnection($socket)
|
||||
{
|
||||
$connection = new Connection($socket, $this->loop);
|
||||
$connection->unix = true;
|
||||
|
||||
$this->emit('connection', array(
|
||||
$connection
|
||||
));
|
||||
}
|
||||
}
|
||||
Vendored
+460
@@ -0,0 +1,460 @@
|
||||
# Changelog
|
||||
|
||||
## 1.4.0 (2024-06-11)
|
||||
|
||||
* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable type declarations.
|
||||
(#179 by @clue)
|
||||
|
||||
* Feature: Full PHP 8.3 compatibility.
|
||||
(#172 by @clue)
|
||||
|
||||
* Fix: Fix `drain` event of `ThroughStream` to handle potential race condition.
|
||||
(#171 by @clue)
|
||||
|
||||
## 1.3.0 (2023-06-16)
|
||||
|
||||
* Feature: Full PHP 8.1 and PHP 8.2 compatibility.
|
||||
(#160 by @SimonFrings, #165 by @clue and #169 by @WyriHaximus)
|
||||
|
||||
* Feature: Avoid unneeded syscall when creating non-blocking `DuplexResourceStream`.
|
||||
(#164 by @clue)
|
||||
|
||||
* Minor documentation improvements.
|
||||
(#161 by @mrsimonbennett, #162 by @SimonFrings and #166 by @nhedger)
|
||||
|
||||
* Improve test suite and project setup and report failed assertions.
|
||||
(#168 and #170 by @clue and #163 by @SimonFrings)
|
||||
|
||||
## 1.2.0 (2021-07-11)
|
||||
|
||||
A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop).
|
||||
|
||||
* Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop).
|
||||
(#159 by @clue)
|
||||
|
||||
```php
|
||||
// old (still supported)
|
||||
$stream = new ReadableResourceStream($resource, $loop);
|
||||
$stream = new WritabeResourceStream($resource, $loop);
|
||||
$stream = new DuplexResourceStream($resource, $loop);
|
||||
|
||||
// new (using default loop)
|
||||
$stream = new ReadableResourceStream($resource);
|
||||
$stream = new WritabeResourceStream($resource);
|
||||
$stream = new DuplexResourceStream($resource);
|
||||
```
|
||||
|
||||
* Improve test suite, use GitHub actions for continuous integration (CI),
|
||||
update PHPUnit config, run tests on PHP 8 and add full core team to the license.
|
||||
(#153, #156 and #157 by @SimonFrings and #154 by @WyriHaximus)
|
||||
|
||||
## 1.1.1 (2020-05-04)
|
||||
|
||||
* Fix: Fix faulty write buffer behavior when sending large data chunks over TLS (Mac OS X only).
|
||||
(#150 by @clue)
|
||||
|
||||
* Minor code style improvements to fix phpstan analysis warnings and
|
||||
add `.gitattributes` to exclude dev files from exports.
|
||||
(#140 by @flow-control and #144 by @reedy)
|
||||
|
||||
* Improve test suite to run tests on PHP 7.4 and simplify test matrix.
|
||||
(#147 by @clue)
|
||||
|
||||
## 1.1.0 (2019-01-01)
|
||||
|
||||
* Improvement: Increase performance by optimizing global function and constant look ups.
|
||||
(#137 by @WyriHaximus)
|
||||
|
||||
* Travis: Test against PHP 7.3.
|
||||
(#138 by @WyriHaximus)
|
||||
|
||||
* Fix: Ignore empty reads.
|
||||
(#139 by @WyriHaximus)
|
||||
|
||||
## 1.0.0 (2018-07-11)
|
||||
|
||||
* First stable LTS release, now following [SemVer](https://semver.org/).
|
||||
We'd like to emphasize that this component is production ready and battle-tested.
|
||||
We plan to support all long-term support (LTS) releases for at least 24 months,
|
||||
so you have a rock-solid foundation to build on top of.
|
||||
|
||||
> Contains no other changes, so it's actually fully compatible with the v0.7.7 release.
|
||||
|
||||
## 0.7.7 (2018-01-19)
|
||||
|
||||
* Improve test suite by fixing forward compatibility with upcoming EventLoop
|
||||
releases, avoid risky tests and add test group to skip integration tests
|
||||
relying on internet connection and apply appropriate test timeouts.
|
||||
(#128, #131 and #132 by @clue)
|
||||
|
||||
## 0.7.6 (2017-12-21)
|
||||
|
||||
* Fix: Work around reading from unbuffered pipe stream in legacy PHP < 5.4.28 and PHP < 5.5.12
|
||||
(#126 by @clue)
|
||||
|
||||
* Improve test suite by simplifying test bootstrapping logic via Composer and
|
||||
test against PHP 7.2
|
||||
(#127 by @clue and #124 by @carusogabriel)
|
||||
|
||||
## 0.7.5 (2017-11-20)
|
||||
|
||||
* Fix: Igore excessive `fopen()` mode flags for `WritableResourceStream`
|
||||
(#119 by @clue)
|
||||
|
||||
* Fix: Fix forward compatibility with upcoming EventLoop releases
|
||||
(#121 by @clue)
|
||||
|
||||
* Restructure examples to ease getting started
|
||||
(#123 by @clue)
|
||||
|
||||
* Improve test suite by adding forward compatibility with PHPUnit 6 and
|
||||
ignore Mac OS X test failures for now until Travis tests work again
|
||||
(#122 by @gabriel-caruso and #120 by @clue)
|
||||
|
||||
## 0.7.4 (2017-10-11)
|
||||
|
||||
* Fix: Remove event listeners from `CompositeStream` once closed and
|
||||
remove undocumented left-over `close` event argument
|
||||
(#116 by @clue)
|
||||
|
||||
* Minor documentation improvements: Fix wrong class name in example,
|
||||
fix typos in README and
|
||||
fix forward compatibility with upcoming EventLoop releases in example
|
||||
(#113 by @docteurklein and #114 and #115 by @clue)
|
||||
|
||||
* Improve test suite by running against Mac OS X on Travis
|
||||
(#112 by @clue)
|
||||
|
||||
## 0.7.3 (2017-08-05)
|
||||
|
||||
* Improvement: Support Événement 3.0 a long side 2.0 and 1.0
|
||||
(#108 by @WyriHaximus)
|
||||
|
||||
* Readme: Corrected loop initialization in usage example
|
||||
(#109 by @pulyavin)
|
||||
|
||||
* Travis: Lock linux distribution preventing future builds from breaking
|
||||
(#110 by @clue)
|
||||
|
||||
## 0.7.2 (2017-06-15)
|
||||
|
||||
* Bug fix: WritableResourceStream: Close the underlying stream when closing the stream.
|
||||
(#107 by @WyriHaximus)
|
||||
|
||||
## 0.7.1 (2017-05-20)
|
||||
|
||||
* Feature: Add optional `$writeChunkSize` parameter to limit maximum number of
|
||||
bytes to write at once.
|
||||
(#105 by @clue)
|
||||
|
||||
```php
|
||||
$stream = new WritableResourceStream(STDOUT, $loop, null, 8192);
|
||||
```
|
||||
|
||||
* Ignore HHVM test failures for now until Travis tests work again
|
||||
(#106 by @clue)
|
||||
|
||||
## 0.7.0 (2017-05-04)
|
||||
|
||||
* Removed / BC break: Remove deprecated and unneeded functionality
|
||||
(#45, #87, #90, #91 and #93 by @clue)
|
||||
|
||||
* Remove deprecated `Stream` class, use `DuplexResourceStream` instead
|
||||
(#87 by @clue)
|
||||
|
||||
* Remove public `$buffer` property, use new constructor parameters instead
|
||||
(#91 by @clue)
|
||||
|
||||
* Remove public `$stream` property from all resource streams
|
||||
(#90 by @clue)
|
||||
|
||||
* Remove undocumented and now unused `ReadableStream` and `WritableStream`
|
||||
(#93 by @clue)
|
||||
|
||||
* Remove `BufferedSink`
|
||||
(#45 by @clue)
|
||||
|
||||
* Feature / BC break: Simplify `ThroughStream` by using data callback instead of
|
||||
inheritance. It is now a direct implementation of `DuplexStreamInterface`.
|
||||
(#88 and #89 by @clue)
|
||||
|
||||
```php
|
||||
$through = new ThroughStream(function ($data) {
|
||||
return json_encode($data) . PHP_EOL;
|
||||
});
|
||||
$through->on('data', $this->expectCallableOnceWith("[2, true]\n"));
|
||||
|
||||
$through->write(array(2, true));
|
||||
```
|
||||
|
||||
* Feature / BC break: The `CompositeStream` starts closed if either side is
|
||||
already closed and forwards pause to pipe source on first write attempt.
|
||||
(#96 and #103 by @clue)
|
||||
|
||||
If either side of the composite stream closes, it will also close the other
|
||||
side. We now also ensure that if either side is already closed during
|
||||
instantiation, it will also close the other side.
|
||||
|
||||
* BC break: Mark all classes as `final` and
|
||||
mark internal API as `private` to discourage inheritance
|
||||
(#95 and #99 by @clue)
|
||||
|
||||
* Feature / BC break: Only emit `error` event for fatal errors
|
||||
(#92 by @clue)
|
||||
|
||||
> The `error` event was previously also allowed to be emitted for non-fatal
|
||||
errors, but our implementations actually only ever emitted this as a fatal
|
||||
error and then closed the stream.
|
||||
|
||||
* Feature: Explicitly allow custom events and exclude any semantics
|
||||
(#97 by @clue)
|
||||
|
||||
* Strict definition for event callback functions
|
||||
(#101 by @clue)
|
||||
|
||||
* Support legacy PHP 5.3 through PHP 7.1 and HHVM and improve usage documentation
|
||||
(#100 and #102 by @clue)
|
||||
|
||||
* Actually require all dependencies so this is self-contained and improve
|
||||
forward compatibility with EventLoop v1.0 and v0.5
|
||||
(#94 and #98 by @clue)
|
||||
|
||||
## 0.6.0 (2017-03-26)
|
||||
|
||||
* Feature / Fix / BC break: Add `DuplexResourceStream` and deprecate `Stream`
|
||||
(#85 by @clue)
|
||||
|
||||
```php
|
||||
// old (does still work for BC reasons)
|
||||
$stream = new Stream($connection, $loop);
|
||||
|
||||
// new
|
||||
$stream = new DuplexResourceStream($connection, $loop);
|
||||
```
|
||||
|
||||
Note that the `DuplexResourceStream` now rejects read-only or write-only
|
||||
streams, so this may affect BC. If you want a read-only or write-only
|
||||
resource, use `ReadableResourceStream` or `WritableResourceStream` instead of
|
||||
`DuplexResourceStream`.
|
||||
|
||||
> BC note: This class was previously called `Stream`. The `Stream` class still
|
||||
exists for BC reasons and will be removed in future versions of this package.
|
||||
|
||||
* Feature / BC break: Add `WritableResourceStream` (previously called `Buffer`)
|
||||
(#84 by @clue)
|
||||
|
||||
```php
|
||||
// old
|
||||
$stream = new Buffer(STDOUT, $loop);
|
||||
|
||||
// new
|
||||
$stream = new WritableResourceStream(STDOUT, $loop);
|
||||
```
|
||||
|
||||
* Feature: Add `ReadableResourceStream`
|
||||
(#83 by @clue)
|
||||
|
||||
```php
|
||||
$stream = new ReadableResourceStream(STDIN, $loop);
|
||||
```
|
||||
|
||||
* Fix / BC Break: Enforce using non-blocking I/O
|
||||
(#46 by @clue)
|
||||
|
||||
> BC note: This is known to affect process pipes on Windows which do not
|
||||
support non-blocking I/O and could thus block the whole EventLoop previously.
|
||||
|
||||
* Feature / Fix / BC break: Consistent semantics for
|
||||
`DuplexStreamInterface::end()` to ensure it SHOULD also end readable side
|
||||
(#86 by @clue)
|
||||
|
||||
* Fix: Do not use unbuffered reads on pipe streams for legacy PHP < 5.4
|
||||
(#80 by @clue)
|
||||
|
||||
## 0.5.0 (2017-03-08)
|
||||
|
||||
* Feature / BC break: Consistent `end` event semantics (EOF)
|
||||
(#70 by @clue)
|
||||
|
||||
The `end` event will now only be emitted for a *successful* end, not if the
|
||||
stream closes due to an unrecoverable `error` event or if you call `close()`
|
||||
explicitly.
|
||||
If you want to detect when the stream closes (terminates), use the `close`
|
||||
event instead.
|
||||
|
||||
* BC break: Remove custom (undocumented) `full-drain` event from `Buffer`
|
||||
(#63 and #68 by @clue)
|
||||
|
||||
> The `full-drain` event was undocumented and mostly used internally.
|
||||
Relying on this event has attracted some low-quality code in the past, so
|
||||
we've removed this from the public API in order to work out a better
|
||||
solution instead.
|
||||
If you want to detect when the buffer finishes flushing data to the stream,
|
||||
you may want to look into its `end()` method or the `close` event instead.
|
||||
|
||||
* Feature / BC break: Consistent event semantics and documentation,
|
||||
explicitly state *when* events will be emitted and *which* arguments they
|
||||
receive.
|
||||
(#73 and #69 by @clue)
|
||||
|
||||
The documentation now explicitly defines each event and its arguments.
|
||||
Custom events and event arguments are still supported.
|
||||
Most notably, all defined events only receive inherently required event
|
||||
arguments and no longer transmit the instance they are emitted on for
|
||||
consistency and performance reasons.
|
||||
|
||||
```php
|
||||
// old (inconsistent and not supported by all implementations)
|
||||
$stream->on('data', function ($data, $stream) {
|
||||
// process $data
|
||||
});
|
||||
|
||||
// new (consistent throughout the whole ecosystem)
|
||||
$stream->on('data', function ($data) use ($stream) {
|
||||
// process $data
|
||||
});
|
||||
```
|
||||
|
||||
> This mostly adds documentation (and thus some stricter, consistent
|
||||
definitions) for the existing behavior, it does NOT define any major
|
||||
changes otherwise.
|
||||
Most existing code should be compatible with these changes, unless
|
||||
it relied on some undocumented/unintended semantics.
|
||||
|
||||
* Feature / BC break: Consistent method semantics and documentation
|
||||
(#72 by @clue)
|
||||
|
||||
> This mostly adds documentation (and thus some stricter, consistent
|
||||
definitions) for the existing behavior, it does NOT define any major
|
||||
changes otherwise.
|
||||
Most existing code should be compatible with these changes, unless
|
||||
it relied on some undocumented/unintended semantics.
|
||||
|
||||
* Feature: Consistent `pipe()` semantics for closed and closing streams
|
||||
(#71 from @clue)
|
||||
|
||||
The source stream will now always be paused via `pause()` when the
|
||||
destination stream closes. Also, properly stop piping if the source
|
||||
stream closes and remove all event forwarding.
|
||||
|
||||
* Improve test suite by adding PHPUnit to `require-dev` and improving coverage.
|
||||
(#74 and #75 by @clue, #66 by @nawarian)
|
||||
|
||||
## 0.4.6 (2017-01-25)
|
||||
|
||||
* Feature: The `Buffer` can now be injected into the `Stream` (or be used standalone)
|
||||
(#62 by @clue)
|
||||
|
||||
* Fix: Forward `close` event only once for `CompositeStream` and `ThroughStream`
|
||||
(#60 by @clue)
|
||||
|
||||
* Fix: Consistent `close` event behavior for `Buffer`
|
||||
(#61 by @clue)
|
||||
|
||||
## 0.4.5 (2016-11-13)
|
||||
|
||||
* Feature: Support setting read buffer size to `null` (infinite)
|
||||
(#42 by @clue)
|
||||
|
||||
* Fix: Do not emit `full-drain` event if `Buffer` is closed during `drain` event
|
||||
(#55 by @clue)
|
||||
|
||||
* Vastly improved performance by factor of 10x to 20x.
|
||||
Raise default buffer sizes to 64 KiB and simplify and improve error handling
|
||||
and unneeded function calls.
|
||||
(#53, #55, #56 by @clue)
|
||||
|
||||
## 0.4.4 (2016-08-22)
|
||||
|
||||
* Bug fix: Emit `error` event and close `Stream` when accessing the underlying
|
||||
stream resource fails with a permanent error.
|
||||
(#52 and #40 by @clue, #25 by @lysenkobv)
|
||||
|
||||
* Bug fix: Do not emit empty `data` event if nothing has been read (stream reached EOF)
|
||||
(#39 by @clue)
|
||||
|
||||
* Bug fix: Ignore empty writes to `Buffer`
|
||||
(#51 by @clue)
|
||||
|
||||
* Add benchmarking script to measure throughput in CI
|
||||
(#41 by @clue)
|
||||
|
||||
## 0.4.3 (2015-10-07)
|
||||
|
||||
* Bug fix: Read buffer to 0 fixes error with libevent and large quantity of I/O (@mbonneau)
|
||||
* Bug fix: No double-write during drain call (@arnaud-lb)
|
||||
* Bug fix: Support HHVM (@clue)
|
||||
* Adjust compatibility to 5.3 (@clue)
|
||||
|
||||
## 0.4.2 (2014-09-09)
|
||||
|
||||
* Added DuplexStreamInterface
|
||||
* Stream sets stream resources to non-blocking
|
||||
* Fixed potential race condition in pipe
|
||||
|
||||
## 0.4.1 (2014-04-13)
|
||||
|
||||
* Bug fix: v0.3.4 changes merged for v0.4.1
|
||||
|
||||
## 0.3.4 (2014-03-30)
|
||||
|
||||
* Bug fix: [Stream] Fixed 100% CPU spike from non-empty write buffer on closed stream
|
||||
|
||||
## 0.4.0 (2014-02-02)
|
||||
|
||||
* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks
|
||||
* BC break: Update to Evenement 2.0
|
||||
* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0
|
||||
|
||||
## 0.3.3 (2013-07-08)
|
||||
|
||||
* Bug fix: [Stream] Correctly detect closed connections
|
||||
|
||||
## 0.3.2 (2013-05-10)
|
||||
|
||||
* Bug fix: [Stream] Make sure CompositeStream is closed properly
|
||||
|
||||
## 0.3.1 (2013-04-21)
|
||||
|
||||
* Bug fix: [Stream] Allow any `ReadableStreamInterface` on `BufferedSink::createPromise()`
|
||||
|
||||
## 0.3.0 (2013-04-14)
|
||||
|
||||
* Feature: [Stream] Factory method for BufferedSink
|
||||
|
||||
## 0.2.6 (2012-12-26)
|
||||
|
||||
* Version bump
|
||||
|
||||
## 0.2.5 (2012-11-26)
|
||||
|
||||
* Feature: Make BufferedSink trigger progress events on the promise (@jsor)
|
||||
|
||||
## 0.2.4 (2012-11-18)
|
||||
|
||||
* Feature: Added ThroughStream, CompositeStream, ReadableStream and WritableStream
|
||||
* Feature: Added BufferedSink
|
||||
|
||||
## 0.2.3 (2012-11-14)
|
||||
|
||||
* Version bump
|
||||
|
||||
## 0.2.2 (2012-10-28)
|
||||
|
||||
* Version bump
|
||||
|
||||
## 0.2.1 (2012-10-14)
|
||||
|
||||
* Bug fix: Check for EOF in `Buffer::write()`
|
||||
|
||||
## 0.2.0 (2012-09-10)
|
||||
|
||||
* Version bump
|
||||
|
||||
## 0.1.1 (2012-07-12)
|
||||
|
||||
* Bug fix: Testing and functional against PHP >= 5.3.3 and <= 5.3.8
|
||||
|
||||
## 0.1.0 (2012-07-11)
|
||||
|
||||
* First tagged release
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user