diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40fdcf9458849c7a8be0cbed9e379ff520a974f2..a398ff94a34f33863bfcf7c6e5596b757f003efe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: php: + - 8.3 - 8.2 - 8.1 - 8.0 @@ -19,7 +20,7 @@ jobs: - 7.2 - 7.1 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} @@ -37,6 +38,7 @@ jobs: strategy: matrix: php: + - 8.3 - 8.2 - 8.1 - 8.0 @@ -44,7 +46,7 @@ jobs: - 7.3 - 7.2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 14434944a2504a9a81e61f5a9f0575e7cb4fc032..ae5bbebe17b131c428c24b23d7af31af5c0cae49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 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). diff --git a/README.md b/README.md index 6b8f9722009c699b5581efef5b96d379e3721be2..5cb6f7df1c9233834717b6222635af97c7fed0cd 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ 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/3889970). +[read this first](https://gist.github.com/domenic/3889970). Concepts -------- @@ -318,7 +318,7 @@ $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 3 arguments: +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. @@ -664,7 +664,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version from this branch: ```bash -composer require react/promise:^3 +composer require react/promise:^3.1 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. diff --git a/composer.json b/composer.json index 33eb2a1bf0e4a659b7621a38c48b54787514555b..5d1e277109e9ae8c3129a9cad4ad21d868256406 100644 --- a/composer.json +++ b/composer.json @@ -28,8 +28,8 @@ "php": ">=7.1.0" }, "require-dev": { - "phpstan/phpstan": "1.10.20 || 1.4.10", - "phpunit/phpunit": "^9.5 || ^7.5" + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, "autoload": { "psr-4": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index bba7c9008c38efd9f5b5b907df61d780125d4f2c..33ea968c9de8c5b22aab99d5b501f21673f3f06d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- PHPUnit configuration file with new format for PHPUnit 9.5+ --> +<!-- PHPUnit configuration file with new format for PHPUnit 9.6+ --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd" bootstrap="vendor/autoload.php" cacheResult="false" colors="true" @@ -24,7 +24,7 @@ <php> <ini name="error_reporting" value="-1" /> <!-- Evaluate assertions, requires running with "php -d zend.assertions=1 vendor/bin/phpunit" --> - <!-- <ini name="zend.assertions=1" value="1" /> --> + <!-- <ini name="zend.assertions" value="1" /> --> <ini name="assert.active" value="1" /> <ini name="assert.exception" value="1" /> <ini name="assert.bail" value="0" /> diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index 4bdf7718ec7516a6e7d7b637d112735e63f7bb37..0dacab1c18999d68bab669fd6f1a3958ec548cfe 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- PHPUnit configuration file with old format before PHPUnit 9 --> +<!-- PHPUnit configuration file with old format for legacy PHPUnit --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/7.5/phpunit.xsd" bootstrap="vendor/autoload.php" @@ -22,7 +22,7 @@ <php> <ini name="error_reporting" value="-1" /> <!-- Evaluate assertions, requires running with "php -d zend.assertions=1 vendor/bin/phpunit" --> - <!-- <ini name="zend.assertions=1" value="1" /> --> + <!-- <ini name="zend.assertions" value="1" /> --> <ini name="assert.active" value="1" /> <ini name="assert.exception" value="1" /> <ini name="assert.bail" value="0" /> diff --git a/src/Deferred.php b/src/Deferred.php index 53d945e4d4ed4f6c16774b52ce8bf3f09543cf54..dac6b2b8f70dbb751e469449a292bb981956fd6a 100644 --- a/src/Deferred.php +++ b/src/Deferred.php @@ -12,12 +12,15 @@ final class Deferred */ private $promise; - /** @var callable */ + /** @var callable(T):void */ private $resolveCallback; - /** @var callable */ + /** @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 { diff --git a/src/Promise.php b/src/Promise.php index 1613db51044044367e3e9c6a67a965e80bfd559c..c46b41f061de0913879157a3fa8b9a845f08f769 100644 --- a/src/Promise.php +++ b/src/Promise.php @@ -10,13 +10,13 @@ use React\Promise\Internal\RejectedPromise; */ final class Promise implements PromiseInterface { - /** @var ?callable */ + /** @var (callable(callable(T):void,callable(\Throwable):void):void)|null */ private $canceller; /** @var ?PromiseInterface<T> */ private $result; - /** @var callable[] */ + /** @var list<callable(PromiseInterface<T>):void> */ private $handlers = []; /** @var int */ @@ -25,6 +25,10 @@ final class Promise implements PromiseInterface /** @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; @@ -57,7 +61,7 @@ final class Promise implements PromiseInterface return new static( $this->resolver($onFulfilled, $onRejected), - static function () use (&$parent) { + static function () use (&$parent): void { assert($parent instanceof self); --$parent->requiredCancelRequests; @@ -78,7 +82,7 @@ final class Promise implements PromiseInterface */ public function catch(callable $onRejected): PromiseInterface { - return $this->then(null, static function ($reason) use ($onRejected) { + return $this->then(null, static function (\Throwable $reason) use ($onRejected) { if (!_checkTypehint($onRejected, $reason)) { return new RejectedPromise($reason); } @@ -92,12 +96,12 @@ final class Promise implements PromiseInterface public function finally(callable $onFulfilledOrRejected): PromiseInterface { - return $this->then(static function ($value) use ($onFulfilledOrRejected) { + return $this->then(static function ($value) use ($onFulfilledOrRejected): PromiseInterface { return resolve($onFulfilledOrRejected())->then(function () use ($value) { return $value; }); - }, static function ($reason) use ($onFulfilledOrRejected) { - return resolve($onFulfilledOrRejected())->then(function () use ($reason) { + }, static function (\Throwable $reason) use ($onFulfilledOrRejected): PromiseInterface { + return resolve($onFulfilledOrRejected())->then(function () use ($reason): RejectedPromise { return new RejectedPromise($reason); }); }); @@ -164,12 +168,12 @@ final class Promise implements PromiseInterface private function resolver(callable $onFulfilled = null, callable $onRejected = null): callable { - return function ($resolve, $reject) use ($onFulfilled, $onRejected) { - $this->handlers[] = static function (PromiseInterface $promise) use ($onFulfilled, $onRejected, $resolve, $reject) { + 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) { + $promise->handlers[] = static function (PromiseInterface $promise) use ($resolve, $reject): void { $promise->then($resolve, $reject); }; } else { @@ -237,6 +241,9 @@ final class Promise implements PromiseInterface 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 @@ -274,13 +281,13 @@ final class Promise implements PromiseInterface $target =& $this; $callback( - static function ($value) use (&$target) { + static function ($value) use (&$target): void { if ($target !== null) { $target->settle(resolve($value)); $target = null; } }, - static function (\Throwable $reason) use (&$target) { + static function (\Throwable $reason) use (&$target): void { if ($target !== null) { $target->reject($reason); $target = null; diff --git a/src/functions.php b/src/functions.php index ac3a66c1e723c2060bde32865a53ce5eda6341ac..2aab877e31b4a59898aa1bc7f90066defe73a73d 100644 --- a/src/functions.php +++ b/src/functions.php @@ -35,7 +35,8 @@ function resolve($promiseOrValue): PromiseInterface assert(\is_callable($canceller)); } - return new Promise(function ($resolve, $reject) use ($promiseOrValue): void { + /** @var Promise<T> */ + return new Promise(function (callable $resolve, callable $reject) use ($promiseOrValue): void { $promiseOrValue->then($resolve, $reject); }, $canceller); } @@ -77,7 +78,8 @@ function all(iterable $promisesOrValues): PromiseInterface { $cancellationQueue = new Internal\CancellationQueue(); - return new Promise(function ($resolve, $reject) use ($promisesOrValues, $cancellationQueue): void { + /** @var Promise<array<T>> */ + return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void { $toResolve = 0; /** @var bool */ $continue = true; @@ -129,6 +131,7 @@ 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; @@ -165,7 +168,8 @@ function any(iterable $promisesOrValues): PromiseInterface { $cancellationQueue = new Internal\CancellationQueue(); - return new Promise(function ($resolve, $reject) use ($promisesOrValues, $cancellationQueue): void { + /** @var Promise<T> */ + return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void { $toReject = 0; $continue = true; $reasons = []; diff --git a/tests/Internal/CancellationQueueTest.php b/tests/Internal/CancellationQueueTest.php index fea5696faccdd79c670af6615b243922dca4c786..e8fd58e95f31ab5d397152e2bc06404b48ec8584 100644 --- a/tests/Internal/CancellationQueueTest.php +++ b/tests/Internal/CancellationQueueTest.php @@ -101,6 +101,7 @@ class CancellationQueueTest extends TestCase */ private function getCancellableDeferred(): Deferred { + /** @var Deferred<never> */ return new Deferred($this->expectCallableOnce()); } } diff --git a/tests/PromiseTest.php b/tests/PromiseTest.php index 74a1446cdb831b91361ed6f63198eb01425c774a..34411dcc3b599b7e0a8a77cd12bab389e64c32d6 100644 --- a/tests/PromiseTest.php +++ b/tests/PromiseTest.php @@ -19,11 +19,14 @@ class PromiseTest extends TestCase { $resolveCallback = $rejectCallback = null; - $promise = new Promise(function ($resolve, $reject) use (&$resolveCallback, &$rejectCallback) { + $promise = new Promise(function (callable $resolve, callable $reject) use (&$resolveCallback, &$rejectCallback): void { $resolveCallback = $resolve; $rejectCallback = $reject; }, $canceller); + assert(is_callable($resolveCallback)); + assert(is_callable($rejectCallback)); + return new CallbackPromiseAdapter([ 'promise' => function () use ($promise) { return $promise; diff --git a/tests/types/deferred.php b/tests/types/deferred.php index ffdea720f89cfc36670b0eb4ba1dcaca57506c30..d468849f716dd4fcc416890efd1868edd97128ea 100644 --- a/tests/types/deferred.php +++ b/tests/types/deferred.php @@ -10,3 +10,40 @@ assertType('React\Promise\PromiseInterface<mixed>', $deferredA->promise()); $deferredB = new Deferred(); $deferredB->resolve(42); assertType('React\Promise\PromiseInterface<int>', $deferredB->promise()); + +// $deferred = new Deferred(); +// $deferred->resolve(42); +// assertType('React\Promise\Deferred<int>', $deferred); + +// $deferred = new Deferred(); +// $deferred->resolve(true); +// $deferred->resolve('ignored'); +// assertType('React\Promise\Deferred<bool>', $deferred); + +// $deferred = new Deferred(); +// $deferred->reject(new \RuntimeException()); +// assertType('React\Promise\Deferred<never>', $deferred); + +// invalid number of arguments passed to $canceller +/** @phpstan-ignore-next-line */ +$deferred = new Deferred(function ($a, $b, $c) { }); +assertType('React\Promise\Deferred<mixed>', $deferred); + +// invalid types for arguments of $canceller +/** @phpstan-ignore-next-line */ +$deferred = new Deferred(function (int $a, string $b) { }); +assertType('React\Promise\Deferred<mixed>', $deferred); + +// invalid number of arguments passed to $resolve +$deferred = new Deferred(function (callable $resolve) { + /** @phpstan-ignore-next-line */ + $resolve(); +}); +assertType('React\Promise\Deferred<mixed>', $deferred); + +// invalid type passed to $reject +$deferred = new Deferred(function (callable $resolve, callable $reject) { + /** @phpstan-ignore-next-line */ + $reject(2); +}); +assertType('React\Promise\Deferred<mixed>', $deferred); diff --git a/tests/types/promise.php b/tests/types/promise.php new file mode 100644 index 0000000000000000000000000000000000000000..fa5b15ca743aaff59d7a5d13c0c7ffc77b5e46c3 --- /dev/null +++ b/tests/types/promise.php @@ -0,0 +1,91 @@ +<?php + +use React\Promise\Promise; +use function PHPStan\Testing\assertType; + +// $promise = new Promise(function (): void { }); +// assertType('React\Promise\PromiseInterface<never>', $promise); + +// $promise = new Promise(function (callable $resolve): void { +// $resolve(42); +// }); +// assertType('React\Promise\PromiseInterface<int>', $promise); + +// $promise = new Promise(function (callable $resolve): void { +// $resolve(true); +// $resolve('ignored'); +// }); +// assertType('React\Promise\PromiseInterface<bool>', $promise); + +// $promise = new Promise(function (callable $resolve, callable $reject): void { +// $reject(new \RuntimeException()); +// }); +// assertType('React\Promise\PromiseInterface<never>', $promise); + +// $promise = new Promise(function (): never { +// throw new \RuntimeException(); +// }); +// assertType('React\Promise\PromiseInterface<never>', $promise); + +// invalid number of arguments for $resolver +/** @phpstan-ignore-next-line */ +$promise = new Promise(function ($a, $b, $c) { }); +assert($promise instanceof Promise); +// assertType('React\Promise\PromiseInterface<never>', $promise); + +// invalid types for arguments of $resolver +/** @phpstan-ignore-next-line */ +$promise = new Promise(function (int $a, string $b) { }); +// assertType('React\Promise\PromiseInterface<never>', $promise); + +// invalid number of arguments passed to $resolve +$promise = new Promise(function (callable $resolve) { + /** @phpstan-ignore-next-line */ + $resolve(); +}); +// assertType('React\Promise\PromiseInterface<never>', $promise); + +// invalid number of arguments passed to $reject +$promise = new Promise(function (callable $resolve, callable $reject) { + /** @phpstan-ignore-next-line */ + $reject(); +}); +// assertType('React\Promise\PromiseInterface<never>', $promise); + +// invalid type passed to $reject +$promise = new Promise(function (callable $resolve, callable $reject) { + /** @phpstan-ignore-next-line */ + $reject(2); +}); +// assertType('React\Promise\PromiseInterface<never>', $promise); + +// invalid number of arguments for $canceller +/** @phpstan-ignore-next-line */ +$promise = new Promise(function () { }, function ($a, $b, $c) { }); +// assertType('React\Promise\PromiseInterface<never>', $promise); + +// invalid types for arguments of $canceller +/** @phpstan-ignore-next-line */ +$promise = new Promise(function () { }, function (int $a, string $b) { }); +// assertType('React\Promise\PromiseInterface<never>', $promise); + +// invalid number of arguments passed to $resolve +$promise = new Promise(function () { }, function (callable $resolve) { + /** @phpstan-ignore-next-line */ + $resolve(); +}); +// assertType('React\Promise\PromiseInterface<never>', $promise); + +// invalid number of arguments passed to $reject +$promise = new Promise(function () { }, function (callable $resolve, callable $reject) { + /** @phpstan-ignore-next-line */ + $reject(); +}); +// assertType('React\Promise\PromiseInterface<never>', $promise); + +// invalid type passed to $reject +$promise = new Promise(function() { }, function (callable $resolve, callable $reject) { + /** @phpstan-ignore-next-line */ + $reject(2); +}); +// assertType('React\Promise\PromiseInterface<never>', $promise); diff --git a/tests/types/reject.php b/tests/types/reject.php index cc443de4ba249ea9398bcad1541c1445d8cbc51e..fd8802e0156cfb7e498ec3adc21d81f1a5344002 100644 --- a/tests/types/reject.php +++ b/tests/types/reject.php @@ -5,9 +5,9 @@ use function PHPStan\Testing\assertType; use function React\Promise\reject; use function React\Promise\resolve; -assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new RuntimeException())); -assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new RuntimeException())->then(null, null)); -// assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new RuntimeException())->then(function (): int { +assertType('React\Promise\PromiseInterface<never>', reject(new RuntimeException())); +assertType('React\Promise\PromiseInterface<never>', reject(new RuntimeException())->then(null, null)); +// assertType('React\Promise\PromiseInterface<never>', reject(new RuntimeException())->then(function (): int { // return 42; // })); assertType('React\Promise\PromiseInterface<int>', reject(new RuntimeException())->then(null, function (): int { @@ -32,11 +32,11 @@ assertType('React\Promise\PromiseInterface<int>', reject(new RuntimeException()) return resolve(42); })); -assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new RuntimeException())->finally(function (): void { })); -assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new RuntimeException())->finally(function (): never { +assertType('React\Promise\PromiseInterface<never>', reject(new RuntimeException())->finally(function (): void { })); +assertType('React\Promise\PromiseInterface<never>', reject(new RuntimeException())->finally(function (): never { throw new \UnexpectedValueException(); })); -assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new RuntimeException())->finally(function (): PromiseInterface { +assertType('React\Promise\PromiseInterface<never>', reject(new RuntimeException())->finally(function (): PromiseInterface { return reject(new \UnexpectedValueException()); })); @@ -50,10 +50,10 @@ assertType('React\Promise\PromiseInterface<int>', reject(new RuntimeException()) return resolve(42); })); -assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new RuntimeException())->always(function (): void { })); -assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new RuntimeException())->always(function (): never { +assertType('React\Promise\PromiseInterface<never>', reject(new RuntimeException())->always(function (): void { })); +assertType('React\Promise\PromiseInterface<never>', reject(new RuntimeException())->always(function (): never { throw new \UnexpectedValueException(); })); -assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new RuntimeException())->always(function (): PromiseInterface { +assertType('React\Promise\PromiseInterface<never>', reject(new RuntimeException())->always(function (): PromiseInterface { return reject(new \UnexpectedValueException()); })); diff --git a/tests/types/resolve.php b/tests/types/resolve.php index e50272f0c36787b86de63ab5d2b5e16bd268b675..7206a23a2a89e826ef3158821a5ad020a8b7d4fa 100644 --- a/tests/types/resolve.php +++ b/tests/types/resolve.php @@ -34,7 +34,7 @@ assertType('React\Promise\PromiseInterface<int>', resolve(true)->then(function ( assertType('React\Promise\PromiseInterface<int>', resolve(true)->then(function (bool $value): PromiseInterface { return resolve(42); })); -assertType('React\Promise\PromiseInterface<*NEVER*>', resolve(true)->then(function (bool $value): never { +assertType('React\Promise\PromiseInterface<never>', resolve(true)->then(function (bool $value): never { throw new \RuntimeException(); })); assertType('React\Promise\PromiseInterface<bool|int>', resolve(true)->then(null, function (\Throwable $e): int { @@ -61,10 +61,10 @@ assertType('React\Promise\PromiseInterface<bool|int>', resolve(true)->catch(func })); assertType('React\Promise\PromiseInterface<bool>', resolve(true)->finally(function (): void { })); -// assertType('React\Promise\PromiseInterface<*NEVER*>', resolve(true)->finally(function (): never { +// assertType('React\Promise\PromiseInterface<never>', resolve(true)->finally(function (): never { // throw new \RuntimeException(); // })); -// assertType('React\Promise\PromiseInterface<*NEVER*>', resolve(true)->finally(function (): PromiseInterface { +// assertType('React\Promise\PromiseInterface<never>', resolve(true)->finally(function (): PromiseInterface { // return reject(new \RuntimeException()); // })); @@ -79,9 +79,9 @@ assertType('React\Promise\PromiseInterface<bool|int>', resolve(true)->otherwise( })); assertType('React\Promise\PromiseInterface<bool>', resolve(true)->always(function (): void { })); -// assertType('React\Promise\PromiseInterface<*NEVER*>', resolve(true)->always(function (): never { +// assertType('React\Promise\PromiseInterface<never>', resolve(true)->always(function (): never { // throw new \RuntimeException(); // })); -// assertType('React\Promise\PromiseInterface<*NEVER*>', resolve(true)->always(function (): PromiseInterface { +// assertType('React\Promise\PromiseInterface<never>', resolve(true)->always(function (): PromiseInterface { // return reject(new \RuntimeException()); // }));