diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..20d4cdf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.yaml] +indent_size = 2 + +[*.yml] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.gitattributes b/.gitattributes index 5d5606d..c466442 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,10 @@ -/.gitattributes export-ignore -/.github/ export-ignore -/.gitignore export-ignore -/phpstan.neon.dist export-ignore -/phpunit.xml.dist export-ignore -/phpunit.xml.legacy export-ignore -/tests/ export-ignore +* text=auto eol=lf + +/.* export-ignore +/tests export-ignore +/phpunit.xml* export-ignore +/psalm.* export-ignore +/psalm-baseline.xml export-ignore +/infection.* export-ignore +/rector.php export-ignore +/phpstan.* export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7761a8..d495381 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,17 +9,13 @@ jobs: name: PHPUnit (PHP ${{ matrix.php }}) runs-on: ubuntu-24.04 strategy: + fail-fast: false matrix: php: - 8.4 - 8.3 - 8.2 - 8.1 - - 8.0 - - 7.4 - - 7.3 - - 7.2 - - 7.1 steps: - uses: actions/checkout@v5 - uses: shivammathur/setup-php@v2 @@ -28,10 +24,9 @@ jobs: coverage: xdebug ini-file: development - run: composer install - - run: vendor/bin/phpunit --coverage-text - if: ${{ matrix.php >= 7.3 }} - - run: vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy - if: ${{ matrix.php < 7.3 }} + - run: composer test:arch + - run: composer test:unit + - run: composer test:feat PHPStan: name: PHPStan (PHP ${{ matrix.php }}) @@ -43,10 +38,6 @@ jobs: - 8.3 - 8.2 - 8.1 - - 8.0 - - 7.4 - - 7.3 - - 7.2 steps: - uses: actions/checkout@v5 - uses: shivammathur/setup-php@v2 @@ -54,4 +45,4 @@ jobs: php-version: ${{ matrix.php }} coverage: none - run: composer install - - run: vendor/bin/phpstan + - run: composer stan diff --git a/.github/workflows/cs-fix.yml b/.github/workflows/cs-fix.yml new file mode 100644 index 0000000..0395b27 --- /dev/null +++ b/.github/workflows/cs-fix.yml @@ -0,0 +1,12 @@ +on: + push: + branches: + - '*' + +name: Fix Code Style + +jobs: + cs-fix: + permissions: + contents: write + uses: spiral/gh-actions/.github/workflows/cs-fix.yml@master diff --git a/.gitignore b/.gitignore index c8153b5..7c97d91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ -/composer.lock +/.* +!/.github/ +!/.php-cs-fixer.dist.php +!/.editorconfig +/runtime/ /vendor/ +/.env +/composer.lock +*.log diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..78b194f --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,13 @@ +include(__DIR__ . '/src') + ->include(__DIR__ . '/tests') + ->include(__DIR__ . '/rector.php') + ->include(__FILE__) + ->allowRisky(false) + ->build(); diff --git a/README.md b/README.md index 2108d98..2b04986 100644 --- a/README.md +++ b/README.md @@ -1,722 +1,57 @@ -Promise -======= +# Promise -A lightweight implementation of -[CommonJS Promises/A](http://wiki.commonjs.org/wiki/Promises/A) for PHP. +A lightweight implementation of [CommonJS Promises/A][CommonJS Promises/A] for PHP. -[![CI status](https://github.com/reactphp/promise/workflows/CI/badge.svg)](https://github.com/reactphp/promise/actions) -[![installs on Packagist](https://img.shields.io/packagist/dt/react/promise?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/promise) +> [!NOTE] +> This is a fork of [reactphp/promise][reactphp/promise] with the following improvements: +> - PHP 8.1+ +> - `declare(strict_types=1);` in all the PHP files +> - `@yield` annotation in the PromiseInterface +> - Replaces `react/promise` v3 +> - Make rejection handler reusable. `error_log()` is still used by default. +> - Removed `exit(255)` from RejectionPromise. -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: +## Install ```bash -composer require react/promise:^3.2 +composer require internal/promise ``` -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" -``` +[![PHP](https://img.shields.io/packagist/php-v/internal/promise.svg?style=flat-square&logo=php)](https://packagist.org/packages/internal/promise) +[![Latest Version on Packagist](https://img.shields.io/packagist/v/internal/promise.svg?style=flat-square&logo=packagist)](https://packagist.org/packages/internal/promise) +[![License](https://img.shields.io/packagist/l/internal/promise.svg?style=flat-square)](LICENSE.md) +[![Total Downloads](https://img.shields.io/packagist/dt/internal/promise.svg?style=flat-square)](https://packagist.org/packages/buggregator/trap) ## 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 +composer test ``` On top of this, we use PHPStan on max level to ensure type safety across the project: ```bash -vendor/bin/phpstan +composer stan ``` -Credits -------- - -Promise is a port of [when.js](https://github.com/cujojs/when) -by [Brian Cavalier](https://github.com/briancavalier). +## Credits -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). +This fork is based on [reactphp/promise][reactphp/promise], which is a port of [when.js][when.js] +by [Brian Cavalier][Brian Cavalier]. -License -------- +Also, large parts of the [documentation][documentation] have been ported from the when.js +[Wiki][Wiki] and the +[API docs][API docs]. -Released under the [MIT](LICENSE) license. +[documentation]: documentation.md +[CommonJS Promises/A]: http://wiki.commonjs.org/wiki/Promises/A +[CI status]: https://img.shields.io/github/actions/workflow/status/internal/promise/ci.yml?branch=2.x +[CI status link]: https://github.com/internal/promise/actions +[installs]: https://img.shields.io/packagist/dt/internal/promise?color=blue&label=installs%20on%20Packagist +[packagist link]: https://packagist.org/packages/internal/promise +[Composer]: https://getcomposer.org +[when.js]: https://github.com/cujojs/when +[Brian Cavalier]: https://github.com/briancavalier +[reactphp/promise]: https://github.com/reactphp/promise diff --git a/composer.json b/composer.json index 6bf687d..ac4769b 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,11 @@ { - "name": "react/promise", + "name": "internal/promise", "description": "A lightweight implementation of CommonJS Promises/A for PHP", "license": "MIT", + "keywords": [ + "promise", + "promises" + ], "authors": [ { "name": "Jan Sorgalla", @@ -25,11 +29,17 @@ } ], "require": { - "php": ">=7.1.0" + "php": ">=8.1.0" + }, + "replace": { + "react/promise": "^3.0" }, "require-dev": { - "phpstan/phpstan": "1.12.28 || 1.4.10", - "phpunit/phpunit": "^9.6 || ^7.5" + "phpstan/phpstan": "1.12.28", + "phpunit/phpunit": "^10.5", + "rector/rector": "^1.2", + "spiral/code-style": "^2.2", + "ta-tikoma/phpunit-architecture-test": "^0.8.5" }, "autoload": { "psr-4": { @@ -41,17 +51,28 @@ }, "autoload-dev": { "psr-4": { - "React\\Promise\\": [ - "tests/fixtures/", + "React\\Promise\\Tests\\": [ "tests/" ] + } + }, + "config": { + "audit": { + "abandoned": "report" }, - "files": [ - "tests/Fiber.php" - ] + "sort-packages": true }, - "keywords": [ - "promise", - "promises" - ] + "scripts": { + "cs:fix": "php-cs-fixer fix -v", + "stan": "phpstan", + "test": "phpunit --color=always --testdox", + "test:arch": "phpunit --color=always --testdox --testsuite=Arch", + "test:feat": "phpunit --color=always --testdox --testsuite=Feature", + "test:unit": "phpunit --color=always --testdox --testsuite=Unit", + "refactor": "rector process --config=rector.php", + "test:cc": [ + "@putenv XDEBUG_MODE=coverage", + "phpunit --coverage-clover=runtime/phpunit/logs/clover.xml --color=always" + ] + } } diff --git a/documentation.md b/documentation.md new file mode 100644 index 0000000..37bcd04 --- /dev/null +++ b/documentation.md @@ -0,0 +1,641 @@ +# Promise + +A lightweight implementation of [CommonJS Promises/A](http://wiki.commonjs.org/wiki/Promises/A) for PHP. + +## 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) + +## 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" +``` diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 33ea968..7d0eb26 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,25 +2,40 @@ +> - - ./tests/ - ./tests/ + + ./tests/Arch + + + ./tests/Feature + + + ./tests/Unit + + + + + + + + + + - ./src/ + src - ./src/functions_include.php + tests - + diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy deleted file mode 100644 index 0dacab1..0000000 --- a/phpunit.xml.legacy +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - ./tests/ - ./tests/ - - - - - ./src/ - - ./src/functions_include.php - - - - - - - - - - - - diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..acab7ac --- /dev/null +++ b/rector.php @@ -0,0 +1,19 @@ +withPaths([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + // uncomment to reach your current PHP version + // ->withPhpSets(php81: true) + // ->withSets([ + // \Rector\PHPUnit\Set\PHPUnitSetList::PHPUNIT_100, + // ]) + ->withTypeCoverageLevel(0) + ->withDeadCodeLevel(0) + ->withCodeQualityLevel(0); diff --git a/src/Deferred.php b/src/Deferred.php index 80b8fcf..f51a25d 100644 --- a/src/Deferred.php +++ b/src/Deferred.php @@ -1,5 +1,7 @@ resolveCallback)($value); } diff --git a/src/Exception/CompositeException.php b/src/Exception/CompositeException.php index 2e672a0..9426330 100644 --- a/src/Exception/CompositeException.php +++ b/src/Exception/CompositeException.php @@ -1,5 +1,7 @@ throwables = $throwables; } /** diff --git a/src/Exception/LengthException.php b/src/Exception/LengthException.php index 775c48d..cf6c1ed 100644 --- a/src/Exception/LengthException.php +++ b/src/Exception/LengthException.php @@ -1,7 +1,7 @@ started) { + if (!\is_object($cancellable) || !\method_exists($cancellable, 'then') || !\method_exists($cancellable, 'cancel')) { return; } - $this->started = true; - $this->drain(); + $length = \array_push($this->queue, $cancellable); + + if ($this->started && $length === 1) { + $this->drain(); + } } - /** - * @param mixed $cancellable - */ - public function enqueue($cancellable): void + public function __invoke(): void { - if (!\is_object($cancellable) || !\method_exists($cancellable, 'then') || !\method_exists($cancellable, 'cancel')) { + if ($this->started) { return; } - $length = \array_push($this->queue, $cancellable); - - if ($this->started && 1 === $length) { - $this->drain(); - } + $this->started = true; + $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')); + \assert(\method_exists($cancellable, 'cancel')); $exception = null; diff --git a/src/Internal/FulfilledPromise.php b/src/Internal/FulfilledPromise.php index 8a66cff..edbb023 100644 --- a/src/Internal/FulfilledPromise.php +++ b/src/Internal/FulfilledPromise.php @@ -1,8 +1,11 @@ then(function ($value) use ($onFulfilledOrRejected): PromiseInterface { - /** @var T $value */ - return resolve($onFulfilledOrRejected())->then(function () use ($value) { - return $value; - }); - }); + return $this->then( + static fn(mixed $value): PromiseInterface => resolve($onFulfilledOrRejected()) + ->then(static fn(): mixed => $value), + ); } - public function cancel(): void - { - } + public function cancel(): void {} /** * @deprecated 3.0.0 Use `catch()` instead diff --git a/src/Internal/RejectedPromise.php b/src/Internal/RejectedPromise.php index aa1dff3..fbf9927 100644 --- a/src/Internal/RejectedPromise.php +++ b/src/Internal/RejectedPromise.php @@ -1,11 +1,13 @@ 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); - } + self::$rejectionHandler = $handler === null ? null : $handler(...); } /** * @template TRejected - * @param ?callable $onFulfilled * @param ?(callable(\Throwable): (PromiseInterface|TRejected)) $onRejected * @return PromiseInterface<($onRejected is null ? never : TRejected)> */ public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface { - if (null === $onRejected) { + if ($onRejected === null) { return $this; } @@ -96,11 +66,11 @@ public function catch(callable $onRejected): PromiseInterface 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); - }); - }); + return $this->then( + null, + static fn(\Throwable $reason): PromiseInterface => resolve($onFulfilledOrRejected()) + ->then(static fn(): PromiseInterface => new RejectedPromise($reason)), + ); } public function cancel(): void @@ -125,4 +95,24 @@ public function always(callable $onFulfilledOrRejected): PromiseInterface { return $this->finally($onFulfilledOrRejected); } + + /** + * @throws void + */ + public function __destruct() + { + if ($this->handled || self::$rejectionHandler === null) { + return; + } + + try { + (self::$rejectionHandler)($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); + } + } } diff --git a/src/Promise.php b/src/Promise.php index 0828674..f70224b 100644 --- a/src/Promise.php +++ b/src/Promise.php @@ -1,5 +1,7 @@ result) { + if ($this->result !== null) { return $this->result->then($onFulfilled, $onRejected); } - if (null === $this->canceller) { - return new static($this->resolver($onFulfilled, $onRejected)); + if ($this->canceller === null) { + return new self($this->resolver($onFulfilled, $onRejected)); } // This promise has a canceller, so we create a new child promise which @@ -59,10 +61,10 @@ public function then(?callable $onFulfilled = null, ?callable $onRejected = null $parent = $this; ++$parent->requiredCancelRequests; - return new static( + return new self( $this->resolver($onFulfilled, $onRejected), static function () use (&$parent): void { - assert($parent instanceof self); + \assert($parent instanceof self); --$parent->requiredCancelRequests; if ($parent->requiredCancelRequests <= 0) { @@ -70,7 +72,7 @@ static function () use (&$parent): void { } $parent = null; - } + }, ); } @@ -96,16 +98,13 @@ public function catch(callable $onRejected): PromiseInterface 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); - }); - }); + return $this + ->then( + static fn($value): PromiseInterface => resolve($onFulfilledOrRejected()) + ->then(static fn(): mixed => $value), + static fn(\Throwable $reason): PromiseInterface => resolve($onFulfilledOrRejected()) + ->then(static fn(): RejectedPromise => new RejectedPromise($reason)), + ); } public function cancel(): void @@ -116,7 +115,7 @@ public function cancel(): void $parentCanceller = null; - if (null !== $this->result) { + if ($this->result !== null) { // Forward cancellation to rejected promise to avoid reporting unhandled rejection if ($this->result instanceof RejectedPromise) { $this->result->cancel(); @@ -128,18 +127,18 @@ public function cancel(): void // Return if the root promise is already resolved or a // FulfilledPromise or RejectedPromise - if (!$root instanceof self || null !== $root->result) { + if (!$root instanceof self || $root->result !== null) { return; } $root->requiredCancelRequests--; if ($root->requiredCancelRequests <= 0) { - $parentCanceller = [$root, 'cancel']; + $parentCanceller = $root->cancel(...); } } - if (null !== $canceller) { + if ($canceller !== null) { $this->call($canceller); } @@ -170,7 +169,9 @@ public function always(callable $onFulfilledOrRejected): PromiseInterface 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 { + $this->handlers[] = static function ( + PromiseInterface $promise, + ) use ($onFulfilled, $onRejected, $resolve, $reject): void { $promise = $promise->then($onFulfilled, $onRejected); if ($promise instanceof self && $promise->result === null) { @@ -186,7 +187,7 @@ private function resolver(?callable $onFulfilled = null, ?callable $onRejected = private function reject(\Throwable $reason): void { - if (null !== $this->result) { + if ($this->result !== null) { return; } @@ -202,7 +203,7 @@ private function settle(PromiseInterface $result): void if ($result === $this) { $result = new RejectedPromise( - new \LogicException('Cannot resolve a promise with itself.') + new \LogicException('Cannot resolve a promise with itself.'), ); } @@ -234,7 +235,7 @@ private function settle(PromiseInterface $result): void */ private function unwrap(PromiseInterface $promise): PromiseInterface { - while ($promise instanceof self && null !== $promise->result) { + while ($promise instanceof self && $promise->result !== null) { /** @var PromiseInterface $promise */ $promise = $promise->result; } @@ -262,7 +263,7 @@ private function call(callable $cb): void } elseif (\is_object($callback) && !$callback instanceof \Closure) { $ref = new \ReflectionMethod($callback, '__invoke'); } else { - assert($callback instanceof \Closure || \is_string($callback)); + \assert($callback instanceof \Closure || \is_string($callback)); $ref = new \ReflectionFunction($callback); } $args = $ref->getNumberOfParameters(); @@ -279,7 +280,7 @@ private function call(callable $cb): void // 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; + $target = & $this; $callback( static function ($value) use (&$target): void { @@ -293,7 +294,7 @@ static function (\Throwable $reason) use (&$target): void { $target->reject($reason); $target = null; } - } + }, ); } } catch (\Throwable $e) { diff --git a/src/PromiseInterface.php b/src/PromiseInterface.php index 5869f76..888e703 100644 --- a/src/PromiseInterface.php +++ b/src/PromiseInterface.php @@ -1,8 +1,11 @@ |T $promiseOrValue * @return PromiseInterface */ -function resolve($promiseOrValue): PromiseInterface +function resolve(mixed $promiseOrValue): PromiseInterface { if ($promiseOrValue instanceof PromiseInterface) { return $promiseOrValue; @@ -32,11 +34,11 @@ function resolve($promiseOrValue): PromiseInterface if (\method_exists($promiseOrValue, 'cancel')) { $canceller = [$promiseOrValue, 'cancel']; - assert(\is_callable($canceller)); + \assert(\is_callable($canceller)); } /** @var Promise */ - return new Promise(function (callable $resolve, callable $reject) use ($promiseOrValue): void { + return new Promise(static function (callable $resolve, callable $reject) use ($promiseOrValue): void { $promiseOrValue->then($resolve, $reject); }, $canceller); } @@ -79,7 +81,7 @@ function all(iterable $promisesOrValues): PromiseInterface $cancellationQueue = new Internal\CancellationQueue(); /** @var Promise> */ - return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void { + return new Promise(static function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void { $toResolve = 0; /** @var bool */ $continue = true; @@ -91,17 +93,17 @@ function all(iterable $promisesOrValues): PromiseInterface ++$toResolve; resolve($promiseOrValue)->then( - function ($value) use ($i, &$values, &$toResolve, &$continue, $resolve): void { + static function ($value) use ($i, &$values, &$toResolve, &$continue, $resolve): void { $values[$i] = $value; - if (0 === --$toResolve && !$continue) { + if (--$toResolve === 0 && !$continue) { $resolve($values); } }, - function (\Throwable $reason) use (&$continue, $reject): void { + static function (\Throwable $reason) use (&$continue, $reject): void { $continue = false; $reject($reason); - } + }, ); if (!$continue && !\is_array($promisesOrValues)) { @@ -132,13 +134,13 @@ function race(iterable $promisesOrValues): PromiseInterface $cancellationQueue = new Internal\CancellationQueue(); /** @var Promise */ - return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void { + return new Promise(static 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 { + resolve($promiseOrValue)->then($resolve, $reject)->finally(static function () use (&$continue): void { $continue = false; }); @@ -169,7 +171,7 @@ function any(iterable $promisesOrValues): PromiseInterface $cancellationQueue = new Internal\CancellationQueue(); /** @var Promise */ - return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void { + return new Promise(static function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void { $toReject = 0; $continue = true; $reasons = []; @@ -179,20 +181,20 @@ function any(iterable $promisesOrValues): PromiseInterface ++$toReject; resolve($promiseOrValue)->then( - function ($value) use ($resolve, &$continue): void { + static function ($value) use ($resolve, &$continue): void { $continue = false; $resolve($value); }, - function (\Throwable $reason) use ($i, &$reasons, &$toReject, $reject, &$continue): void { + static function (\Throwable $reason) use ($i, &$reasons, &$toReject, $reject, &$continue): void { $reasons[$i] = $reason; - if (0 === --$toReject && !$continue) { + if (--$toReject === 0 && !$continue) { $reject(new CompositeException( $reasons, - 'All promises rejected.' + 'All promises rejected.', )); } - } + }, ); if (!$continue && !\is_array($promisesOrValues)) { @@ -203,12 +205,12 @@ function (\Throwable $reason) use ($i, &$reasons, &$toReject, $reject, &$continu $continue = false; if ($toReject === 0 && !$reasons) { $reject(new Exception\LengthException( - 'Must contain at least 1 item but contains only 0 items.' + 'Must contain at least 1 item but contains only 0 items.', )); } elseif ($toReject === 0) { $reject(new CompositeException( $reasons, - 'All promises rejected.' + 'All promises rejected.', )); } }, $cancellationQueue); @@ -249,18 +251,21 @@ function (\Throwable $reason) use ($i, &$reasons, &$toReject, $reject, &$continu * [`finally()` method](#promiseinterfacefinally). * See also the [`reject()` function](#reject) for more details. * - * @param callable(\Throwable):void|null $callback - * @return callable(\Throwable):void|null + * @param callable(\Throwable):mixed|null $callback + * @return callable(\Throwable):mixed|null */ function set_rejection_handler(?callable $callback): ?callable { static $current = null; $previous = $current; $current = $callback; + RejectedPromise::setRejectionHandler($current); return $previous; } +set_rejection_handler(static fn(\Throwable $reason) => \error_log('Unhandled promise rejection with ' . $reason)); + /** * @internal */ @@ -271,7 +276,7 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool } elseif (\is_object($callback) && !$callback instanceof \Closure) { $callbackReflection = new \ReflectionMethod($callback, '__invoke'); } else { - assert($callback instanceof \Closure || \is_string($callback)); + \assert($callback instanceof \Closure || \is_string($callback)); $callbackReflection = new \ReflectionFunction($callback); } @@ -297,6 +302,7 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool break; case $type instanceof \ReflectionIntersectionType: $isTypeUnion = false; + // no break case $type instanceof \ReflectionUnionType: $types = $type->getTypes(); break; @@ -313,15 +319,15 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool if ($type instanceof \ReflectionIntersectionType) { foreach ($type->getTypes() as $typeToMatch) { - assert($typeToMatch instanceof \ReflectionNamedType); + \assert($typeToMatch instanceof \ReflectionNamedType); $name = $typeToMatch->getName(); if (!($matches = (!$typeToMatch->isBuiltin() && $reason instanceof $name))) { break; } } - assert(isset($matches)); + \assert(isset($matches)); } else { - assert($type instanceof \ReflectionNamedType); + \assert($type instanceof \ReflectionNamedType); $name = $type->getName(); $matches = !$type->isBuiltin() && $reason instanceof $name; } diff --git a/src/functions_include.php b/src/functions_include.php index bd0c54f..54b0141 100644 --- a/src/functions_include.php +++ b/src/functions_include.php @@ -1,5 +1,7 @@ layer(); + + foreach ($layer as $object) { + foreach ($object->uses as $use) { + foreach ($functions as $function) { + $function === $use and throw new \Exception( + \sprintf( + 'Function `%s()` is used in %s.', + $function, + $object->name, + ), + ); + } + } + } + + $this->assertTrue(true); + } +} diff --git a/tests/DeferredTestCancelNoopThenRejectShouldNotReportUnhandled.phpt b/tests/Feature/DeferredTestCancelNoopThenRejectShouldNotReportUnhandled.phpt similarity index 100% rename from tests/DeferredTestCancelNoopThenRejectShouldNotReportUnhandled.phpt rename to tests/Feature/DeferredTestCancelNoopThenRejectShouldNotReportUnhandled.phpt diff --git a/tests/DeferredTestCancelThatRejectsShouldNotReportUnhandled.phpt b/tests/Feature/DeferredTestCancelThatRejectsShouldNotReportUnhandled.phpt similarity index 100% rename from tests/DeferredTestCancelThatRejectsShouldNotReportUnhandled.phpt rename to tests/Feature/DeferredTestCancelThatRejectsShouldNotReportUnhandled.phpt diff --git a/tests/DeferredTestRejectShouldReportUnhandled.phpt b/tests/Feature/DeferredTestRejectShouldReportUnhandled.phpt similarity index 100% rename from tests/DeferredTestRejectShouldReportUnhandled.phpt rename to tests/Feature/DeferredTestRejectShouldReportUnhandled.phpt diff --git a/tests/DeferredTestRejectThenCancelShouldNotReportUnhandled.phpt b/tests/Feature/DeferredTestRejectThenCancelShouldNotReportUnhandled.phpt similarity index 100% rename from tests/DeferredTestRejectThenCancelShouldNotReportUnhandled.phpt rename to tests/Feature/DeferredTestRejectThenCancelShouldNotReportUnhandled.phpt diff --git a/tests/FunctionAllTestRejectedShouldReportUnhandled.phpt b/tests/Feature/FunctionAllTestRejectedShouldReportUnhandled.phpt similarity index 100% rename from tests/FunctionAllTestRejectedShouldReportUnhandled.phpt rename to tests/Feature/FunctionAllTestRejectedShouldReportUnhandled.phpt diff --git a/tests/FunctionAllTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt b/tests/Feature/FunctionAllTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt similarity index 100% rename from tests/FunctionAllTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt rename to tests/Feature/FunctionAllTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt diff --git a/tests/FunctionAnyTestRejectedShouldReportUnhandled.phpt b/tests/Feature/FunctionAnyTestRejectedShouldReportUnhandled.phpt similarity index 100% rename from tests/FunctionAnyTestRejectedShouldReportUnhandled.phpt rename to tests/Feature/FunctionAnyTestRejectedShouldReportUnhandled.phpt diff --git a/tests/FunctionAnyTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt b/tests/Feature/FunctionAnyTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt similarity index 100% rename from tests/FunctionAnyTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt rename to tests/Feature/FunctionAnyTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt diff --git a/tests/FunctionRaceTestRejectedShouldReportUnhandled.phpt b/tests/Feature/FunctionRaceTestRejectedShouldReportUnhandled.phpt similarity index 100% rename from tests/FunctionRaceTestRejectedShouldReportUnhandled.phpt rename to tests/Feature/FunctionRaceTestRejectedShouldReportUnhandled.phpt diff --git a/tests/FunctionRaceTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt b/tests/Feature/FunctionRaceTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt similarity index 100% rename from tests/FunctionRaceTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt rename to tests/Feature/FunctionRaceTestRejectedThenMatchingThatReturnsShouldNotReportUnhandled.phpt diff --git a/tests/FunctionRejectTestCancelShouldNotReportUnhandled.phpt b/tests/Feature/FunctionRejectTestCancelShouldNotReportUnhandled.phpt similarity index 100% rename from tests/FunctionRejectTestCancelShouldNotReportUnhandled.phpt rename to tests/Feature/FunctionRejectTestCancelShouldNotReportUnhandled.phpt diff --git a/tests/FunctionRejectTestCatchMatchingShouldNotReportUnhandled.phpt b/tests/Feature/FunctionRejectTestCatchMatchingShouldNotReportUnhandled.phpt similarity index 100% rename from tests/FunctionRejectTestCatchMatchingShouldNotReportUnhandled.phpt rename to tests/Feature/FunctionRejectTestCatchMatchingShouldNotReportUnhandled.phpt diff --git a/tests/FunctionRejectTestCatchMismatchShouldReportUnhandled.phpt b/tests/Feature/FunctionRejectTestCatchMismatchShouldReportUnhandled.phpt similarity index 100% rename from tests/FunctionRejectTestCatchMismatchShouldReportUnhandled.phpt rename to tests/Feature/FunctionRejectTestCatchMismatchShouldReportUnhandled.phpt diff --git a/tests/FunctionRejectTestFinallyThatReturnsShouldReportUnhandled.phpt b/tests/Feature/FunctionRejectTestFinallyThatReturnsShouldReportUnhandled.phpt similarity index 100% rename from tests/FunctionRejectTestFinallyThatReturnsShouldReportUnhandled.phpt rename to tests/Feature/FunctionRejectTestFinallyThatReturnsShouldReportUnhandled.phpt diff --git a/tests/FunctionRejectTestFinallyThatThrowsNewExceptionShouldReportUnhandledForNewExceptionOnly.phpt b/tests/Feature/FunctionRejectTestFinallyThatThrowsNewExceptionShouldReportUnhandledForNewExceptionOnly.phpt similarity index 100% rename from tests/FunctionRejectTestFinallyThatThrowsNewExceptionShouldReportUnhandledForNewExceptionOnly.phpt rename to tests/Feature/FunctionRejectTestFinallyThatThrowsNewExceptionShouldReportUnhandledForNewExceptionOnly.phpt diff --git a/tests/FunctionRejectTestShouldReportUnhandled.phpt b/tests/Feature/FunctionRejectTestShouldReportUnhandled.phpt similarity index 100% rename from tests/FunctionRejectTestShouldReportUnhandled.phpt rename to tests/Feature/FunctionRejectTestShouldReportUnhandled.phpt diff --git a/tests/FunctionRejectTestShouldReportUnhandledWithPreviousExceptions.phpt b/tests/Feature/FunctionRejectTestShouldReportUnhandledWithPreviousExceptions.phpt similarity index 100% rename from tests/FunctionRejectTestShouldReportUnhandledWithPreviousExceptions.phpt rename to tests/Feature/FunctionRejectTestShouldReportUnhandledWithPreviousExceptions.phpt diff --git a/tests/FunctionRejectTestThenMatchingThatReturnsShouldNotReportUnhandled.phpt b/tests/Feature/FunctionRejectTestThenMatchingThatReturnsShouldNotReportUnhandled.phpt similarity index 100% rename from tests/FunctionRejectTestThenMatchingThatReturnsShouldNotReportUnhandled.phpt rename to tests/Feature/FunctionRejectTestThenMatchingThatReturnsShouldNotReportUnhandled.phpt diff --git a/tests/FunctionRejectTestThenMatchingThatThrowsNewExceptionShouldReportUnhandledRejectionForNewExceptionOnly.phpt b/tests/Feature/FunctionRejectTestThenMatchingThatThrowsNewExceptionShouldReportUnhandledRejectionForNewExceptionOnly.phpt similarity index 100% rename from tests/FunctionRejectTestThenMatchingThatThrowsNewExceptionShouldReportUnhandledRejectionForNewExceptionOnly.phpt rename to tests/Feature/FunctionRejectTestThenMatchingThatThrowsNewExceptionShouldReportUnhandledRejectionForNewExceptionOnly.phpt diff --git a/tests/FunctionResolveTestThenShouldNotReportUnhandled.phpt b/tests/Feature/FunctionResolveTestThenShouldNotReportUnhandled.phpt similarity index 100% rename from tests/FunctionResolveTestThenShouldNotReportUnhandled.phpt rename to tests/Feature/FunctionResolveTestThenShouldNotReportUnhandled.phpt diff --git a/tests/FunctionSetRejectionHandlerShouldBeInvokedForUnhandled.phpt b/tests/Feature/FunctionSetRejectionHandlerShouldBeInvokedForUnhandled.phpt similarity index 100% rename from tests/FunctionSetRejectionHandlerShouldBeInvokedForUnhandled.phpt rename to tests/Feature/FunctionSetRejectionHandlerShouldBeInvokedForUnhandled.phpt diff --git a/tests/FunctionSetRejectionHandlerShouldInvokeLastHandlerForUnhandled.phpt b/tests/Feature/FunctionSetRejectionHandlerShouldInvokeLastHandlerForUnhandled.phpt similarity index 100% rename from tests/FunctionSetRejectionHandlerShouldInvokeLastHandlerForUnhandled.phpt rename to tests/Feature/FunctionSetRejectionHandlerShouldInvokeLastHandlerForUnhandled.phpt diff --git a/tests/FunctionSetRejectionHandlerThatHasUnhandledShouldReportUnhandled.phpt b/tests/Feature/FunctionSetRejectionHandlerThatHasUnhandledShouldReportUnhandled.phpt similarity index 100% rename from tests/FunctionSetRejectionHandlerThatHasUnhandledShouldReportUnhandled.phpt rename to tests/Feature/FunctionSetRejectionHandlerThatHasUnhandledShouldReportUnhandled.phpt diff --git a/tests/FunctionSetRejectionHandlerThatThrowsShouldTerminateProgramForUnhandled.phpt b/tests/Feature/FunctionSetRejectionHandlerThatThrowsShouldTerminateProgramForUnhandled.phpt similarity index 100% rename from tests/FunctionSetRejectionHandlerThatThrowsShouldTerminateProgramForUnhandled.phpt rename to tests/Feature/FunctionSetRejectionHandlerThatThrowsShouldTerminateProgramForUnhandled.phpt diff --git a/tests/FunctionSetRejectionHandlerThatThrowsShouldTerminateProgramForUnhandledWithPreviousExceptions.phpt b/tests/Feature/FunctionSetRejectionHandlerThatThrowsShouldTerminateProgramForUnhandledWithPreviousExceptions.phpt similarity index 100% rename from tests/FunctionSetRejectionHandlerThatThrowsShouldTerminateProgramForUnhandledWithPreviousExceptions.phpt rename to tests/Feature/FunctionSetRejectionHandlerThatThrowsShouldTerminateProgramForUnhandledWithPreviousExceptions.phpt diff --git a/tests/FunctionSetRejectionHandlerThatTriggersDefaultHandlerShouldTerminateProgramForUnhandled.phpt b/tests/Feature/FunctionSetRejectionHandlerThatTriggersDefaultHandlerShouldTerminateProgramForUnhandled.phpt similarity index 100% rename from tests/FunctionSetRejectionHandlerThatTriggersDefaultHandlerShouldTerminateProgramForUnhandled.phpt rename to tests/Feature/FunctionSetRejectionHandlerThatTriggersDefaultHandlerShouldTerminateProgramForUnhandled.phpt diff --git a/tests/FunctionSetRejectionHandlerThatTriggersErrorHandlerThatThrowsShouldTerminateProgramForUnhandled.phpt b/tests/Feature/FunctionSetRejectionHandlerThatTriggersErrorHandlerThatThrowsShouldTerminateProgramForUnhandled.phpt similarity index 100% rename from tests/FunctionSetRejectionHandlerThatTriggersErrorHandlerThatThrowsShouldTerminateProgramForUnhandled.phpt rename to tests/Feature/FunctionSetRejectionHandlerThatTriggersErrorHandlerThatThrowsShouldTerminateProgramForUnhandled.phpt diff --git a/tests/FunctionSetRejectionHandlerThatUsesNestedSetRejectionHandlerShouldInvokeInnerHandlerForUnhandled.phpt b/tests/Feature/FunctionSetRejectionHandlerThatUsesNestedSetRejectionHandlerShouldInvokeInnerHandlerForUnhandled.phpt similarity index 100% rename from tests/FunctionSetRejectionHandlerThatUsesNestedSetRejectionHandlerShouldInvokeInnerHandlerForUnhandled.phpt rename to tests/Feature/FunctionSetRejectionHandlerThatUsesNestedSetRejectionHandlerShouldInvokeInnerHandlerForUnhandled.phpt diff --git a/tests/PromiseTestCancelThatRejectsAfterwardsShouldNotReportUnhandled.phpt b/tests/Feature/PromiseTestCancelThatRejectsAfterwardsShouldNotReportUnhandled.phpt similarity index 100% rename from tests/PromiseTestCancelThatRejectsAfterwardsShouldNotReportUnhandled.phpt rename to tests/Feature/PromiseTestCancelThatRejectsAfterwardsShouldNotReportUnhandled.phpt diff --git a/tests/PromiseTestCancelThatRejectsShouldNotReportUnhandled.phpt b/tests/Feature/PromiseTestCancelThatRejectsShouldNotReportUnhandled.phpt similarity index 100% rename from tests/PromiseTestCancelThatRejectsShouldNotReportUnhandled.phpt rename to tests/Feature/PromiseTestCancelThatRejectsShouldNotReportUnhandled.phpt diff --git a/tests/Fiber.php b/tests/Fiber.php deleted file mode 100644 index a1b196c..0000000 --- a/tests/Fiber.php +++ /dev/null @@ -1,27 +0,0 @@ -= 80000) die("Skipped: PHP 7 only."); ?> ---INI-- -# suppress legacy PHPUnit 7 warning for Xdebug 3 -xdebug.default_enable= ---FILE-- -then(null, function (UnexpectedValueException $unexpected): void { // @phpstan-ignore-line - echo 'This will never be shown because the types do not match' . PHP_EOL; -}); - -?> ---EXPECTF-- -Unhandled promise rejection with TypeError: Argument 1 passed to {closure}() must be an instance of UnexpectedValueException, instance of RuntimeException given, called in %s/src/Internal/RejectedPromise.php on line %d and defined in %s:%d -Stack trace: -#0 %s/src/Internal/RejectedPromise.php(%d): {closure}(%S) -#1 %s(%d): React\Promise\Internal\RejectedPromise->then(%S) -#2 %A{main} diff --git a/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp8.phpt b/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp8.phpt deleted file mode 100644 index 698f7ec..0000000 --- a/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp8.phpt +++ /dev/null @@ -1,25 +0,0 @@ ---TEST-- -Calling reject() and then then() with invalid type should report unhandled rejection for TypeError ---SKIPIF-- - ---INI-- -# suppress legacy PHPUnit 7 warning for Xdebug 3 -xdebug.default_enable= ---FILE-- -then(null, function (UnexpectedValueException $unexpected): void { // @phpstan-ignore-line - echo 'This will never be shown because the types do not match' . PHP_EOL; -}); - -?> ---EXPECTF-- -Unhandled promise rejection with TypeError: {closure%S}(): Argument #1 ($unexpected) must be of type UnexpectedValueException, RuntimeException given, called in %s/src/Internal/RejectedPromise.php on line %d and defined in %s:%d -Stack trace: -#0 %s/src/Internal/RejectedPromise.php(%d): {closure%S}(%S) -#1 %s(%d): React\Promise\Internal\RejectedPromise->then(%S) -#2 %A{main} diff --git a/tests/Internal/FulfilledPromiseTest.php b/tests/Internal/FulfilledPromiseTest.php deleted file mode 100644 index b216e1d..0000000 --- a/tests/Internal/FulfilledPromiseTest.php +++ /dev/null @@ -1,60 +0,0 @@ - - */ - public function getPromiseTestAdapter(?callable $canceller = null): CallbackPromiseAdapter - { - /** @var ?FulfilledPromise */ - $promise = null; - - return new CallbackPromiseAdapter([ - 'promise' => function () use (&$promise) { - if (!$promise) { - throw new LogicException('FulfilledPromise must be resolved before obtaining the promise'); - } - - return $promise; - }, - 'resolve' => function ($value = null) use (&$promise) { - if (!$promise) { - $promise = new FulfilledPromise($value); - } - }, - 'reject' => function () { - throw new LogicException('You cannot call reject() for React\Promise\FulfilledPromise'); - }, - 'settle' => function ($value = null) use (&$promise) { - if (!$promise) { - $promise = new FulfilledPromise($value); - } - }, - ]); - } - - /** - * @test - */ - public function shouldThrowExceptionIfConstructedWithAPromise(): void - { - $this->expectException(InvalidArgumentException::class); - new FulfilledPromise(new FulfilledPromise()); - } -} diff --git a/tests/Internal/RejectedPromiseTest.php b/tests/Internal/RejectedPromiseTest.php deleted file mode 100644 index 2338b4f..0000000 --- a/tests/Internal/RejectedPromiseTest.php +++ /dev/null @@ -1,52 +0,0 @@ - - */ - public function getPromiseTestAdapter(?callable $canceller = null): CallbackPromiseAdapter - { - /** @var ?RejectedPromise */ - $promise = null; - - return new CallbackPromiseAdapter([ - 'promise' => function () use (&$promise) { - if (!$promise) { - throw new LogicException('RejectedPromise must be rejected before obtaining the promise'); - } - - return $promise; - }, - 'resolve' => function () { - throw new LogicException('You cannot call resolve() for React\Promise\RejectedPromise'); - }, - 'reject' => function (\Throwable $reason) use (&$promise) { - if (!$promise) { - $promise = new RejectedPromise($reason); - } - }, - 'settle' => function ($reason = '') use (&$promise) { - if (!$promise) { - if (!$reason instanceof Exception) { - $reason = new Exception((string) $reason); - } - - $promise = new RejectedPromise($reason); - } - }, - ]); - } -} diff --git a/tests/PHP8.php b/tests/PHP8.php deleted file mode 100644 index 4e2b7d4..0000000 --- a/tests/PHP8.php +++ /dev/null @@ -1,17 +0,0 @@ -= 80000); - } -} diff --git a/tests/PromiseTest.php b/tests/PromiseTest.php deleted file mode 100644 index a0e9ace..0000000 --- a/tests/PromiseTest.php +++ /dev/null @@ -1,297 +0,0 @@ - - */ - public function getPromiseTestAdapter(?callable $canceller = null): CallbackPromiseAdapter - { - $resolveCallback = $rejectCallback = null; - - $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; - }, - 'resolve' => $resolveCallback, - 'reject' => $rejectCallback, - 'settle' => $resolveCallback, - ]); - } - - /** @test */ - public function shouldRejectIfResolverThrowsException(): void - { - $exception = new Exception('foo'); - - $promise = new Promise(function () use ($exception) { - throw $exception; - }); - - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo($exception)); - - $promise - ->then($this->expectCallableNever(), $mock); - } - - /** @test */ - public function shouldResolveWithoutCreatingGarbageCyclesIfResolverResolvesWithException(): void - { - gc_collect_cycles(); - $promise = new Promise(function ($resolve) { - $resolve(new \Exception('foo')); - }); - unset($promise); - - $this->assertSame(0, gc_collect_cycles()); - } - - /** @test */ - public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptionWithoutResolver(): void - { - gc_collect_cycles(); - $promise = new Promise(function () { - throw new \Exception('foo'); - }); - - $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection - - unset($promise); - - $this->assertSame(0, gc_collect_cycles()); - } - - /** @test */ - public function shouldRejectWithoutCreatingGarbageCyclesIfResolverRejectsWithException(): void - { - gc_collect_cycles(); - $promise = new Promise(function ($resolve, $reject) { - $reject(new \Exception('foo')); - }); - - $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection - - unset($promise); - - $this->assertSame(0, gc_collect_cycles()); - } - - /** @test */ - public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithException(): void - { - gc_collect_cycles(); - $promise = new Promise(function ($resolve, $reject) { }, function ($resolve, $reject) { - $reject(new \Exception('foo')); - }); - $promise->cancel(); - unset($promise); - - $this->assertSame(0, gc_collect_cycles()); - } - - /** @test */ - public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejectsWithException(): void - { - gc_collect_cycles(); - $promise = new Promise(function ($resolve, $reject) { }, function ($resolve, $reject) { - $reject(new \Exception('foo')); - }); - $promise->then()->then()->then()->cancel(); - unset($promise); - - $this->assertSame(0, gc_collect_cycles()); - } - - /** @test */ - public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsException(): void - { - gc_collect_cycles(); - $promise = new Promise(function ($resolve, $reject) { - throw new \Exception('foo'); - }); - - $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection - - unset($promise); - - $this->assertSame(0, gc_collect_cycles()); - } - - /** - * Test that checks number of garbage cycles after throwing from a canceller - * that explicitly uses a reference to the promise. This is rather synthetic, - * actual use cases often have implicit (hidden) references which ought not - * to be stored in the stack trace. - * - * Reassigned arguments only show up in the stack trace in PHP 7, so we can't - * avoid this on legacy PHP. As an alternative, consider explicitly unsetting - * any references before throwing. - * - * @test - * @requires PHP 7 - */ - public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReferenceThrowsException(): void - { - gc_collect_cycles(); - /** @var Promise $promise */ - $promise = new Promise(function () {}, function () use (&$promise) { - assert($promise instanceof Promise); - throw new \Exception('foo'); - }); - $promise->cancel(); - unset($promise); - - $this->assertSame(0, gc_collect_cycles()); - } - - /** - * @test - * @requires PHP 7 - * @see self::shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReferenceThrowsException - */ - public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceThrowsException(): void - { - gc_collect_cycles(); - /** @var Promise $promise */ - $promise = new Promise(function () use (&$promise) { - assert($promise instanceof Promise); - throw new \Exception('foo'); - }); - - $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection - - unset($promise); - - $this->assertSame(0, gc_collect_cycles()); - } - - /** - * @test - * @requires PHP 7 - * @see self::shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReferenceThrowsException - */ - public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenceAndResolverThrowsException(): void - { - gc_collect_cycles(); - /** @var Promise $promise */ - $promise = new Promise(function () { - throw new \Exception('foo'); - }, function () use (&$promise) { - assert($promise instanceof Promise); - }); - - $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection - - unset($promise); - - $this->assertSame(0, gc_collect_cycles()); - } - - /** @test */ - public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromise(): void - { - gc_collect_cycles(); - $promise = new Promise(function () { }); - unset($promise); - - $this->assertSame(0, gc_collect_cycles()); - } - - /** @test */ - public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithThenFollowers(): void - { - gc_collect_cycles(); - $promise = new Promise(function () { }); - $promise->then()->then()->then(); - unset($promise); - - $this->assertSame(0, gc_collect_cycles()); - } - - /** @test */ - public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithCatchFollowers(): void - { - gc_collect_cycles(); - $promise = new Promise(function () { }); - $promise->catch(function () { }); - unset($promise); - - $this->assertSame(0, gc_collect_cycles()); - } - - /** @test */ - public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithFinallyFollowers(): void - { - gc_collect_cycles(); - $promise = new Promise(function () { }); - $promise->finally(function () { }); - unset($promise); - - $this->assertSame(0, gc_collect_cycles()); - } - - /** - * @test - * @deprecated - */ - public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithOtherwiseFollowers(): void - { - gc_collect_cycles(); - $promise = new Promise(function () { }); - $promise->otherwise(function () { }); - unset($promise); - - $this->assertSame(0, gc_collect_cycles()); - } - - /** - * @test - * @deprecated - */ - public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithAlwaysFollowers(): void - { - gc_collect_cycles(); - $promise = new Promise(function () { }); - $promise->always(function () { }); - unset($promise); - - $this->assertSame(0, gc_collect_cycles()); - } - - /** @test */ - public function shouldFulfillIfFullfilledWithSimplePromise(): void - { - gc_collect_cycles(); - $promise = new Promise(function () { - throw new Exception('foo'); - }); - - $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection - - unset($promise); - - self::assertSame(0, gc_collect_cycles()); - } -} diff --git a/tests/PromiseTest/FullTestTrait.php b/tests/PromiseTest/FullTestTrait.php deleted file mode 100644 index 5a331dd..0000000 --- a/tests/PromiseTest/FullTestTrait.php +++ /dev/null @@ -1,14 +0,0 @@ ->', all([resolve(true), resolve(false)])); assertType('React\Promise\PromiseInterface>', all([resolve(true), false])); -assertType('React\Promise\PromiseInterface>', all([true, time()])); -assertType('React\Promise\PromiseInterface>', all([resolve(true), resolve(time())])); +assertType('React\Promise\PromiseInterface>', all([true, \time()])); +assertType('React\Promise\PromiseInterface>', all([resolve(true), resolve(\time())])); diff --git a/tests/types/any.php b/tests/Types/any.php similarity index 85% rename from tests/types/any.php rename to tests/Types/any.php index 2182dd3..3fca483 100644 --- a/tests/types/any.php +++ b/tests/Types/any.php @@ -1,10 +1,12 @@ ', any([resolve(true), resolve(false)])); assertType('React\Promise\PromiseInterface', any([resolve(true), false])); -assertType('React\Promise\PromiseInterface', any([true, time()])); -assertType('React\Promise\PromiseInterface', any([resolve(true), resolve(time())])); +assertType('React\Promise\PromiseInterface', any([true, \time()])); +assertType('React\Promise\PromiseInterface', any([resolve(true), resolve(\time())])); diff --git a/tests/types/deferred.php b/tests/Types/deferred.php similarity index 80% rename from tests/types/deferred.php rename to tests/Types/deferred.php index d468849..e39a9e9 100644 --- a/tests/types/deferred.php +++ b/tests/Types/deferred.php @@ -1,6 +1,9 @@ ', $deferred); // invalid types for arguments of $canceller /** @phpstan-ignore-next-line */ -$deferred = new Deferred(function (int $a, string $b) { }); +$deferred = new Deferred(static function (int $a, string $b): void {}); assertType('React\Promise\Deferred', $deferred); // invalid number of arguments passed to $resolve -$deferred = new Deferred(function (callable $resolve) { +$deferred = new Deferred(static function (callable $resolve): void { /** @phpstan-ignore-next-line */ $resolve(); }); assertType('React\Promise\Deferred', $deferred); // invalid type passed to $reject -$deferred = new Deferred(function (callable $resolve, callable $reject) { +$deferred = new Deferred(static function (callable $resolve, callable $reject): void { /** @phpstan-ignore-next-line */ $reject(2); }); diff --git a/tests/types/promise.php b/tests/Types/promise.php similarity index 72% rename from tests/types/promise.php rename to tests/Types/promise.php index fa5b15c..95733fc 100644 --- a/tests/types/promise.php +++ b/tests/Types/promise.php @@ -1,6 +1,9 @@ ', $promise); // invalid types for arguments of $resolver /** @phpstan-ignore-next-line */ -$promise = new Promise(function (int $a, string $b) { }); +$promise = new Promise(static function (int $a, string $b): void {}); // assertType('React\Promise\PromiseInterface', $promise); // invalid number of arguments passed to $resolve -$promise = new Promise(function (callable $resolve) { +$promise = new Promise(static function (callable $resolve): void { /** @phpstan-ignore-next-line */ $resolve(); }); // assertType('React\Promise\PromiseInterface', $promise); // invalid number of arguments passed to $reject -$promise = new Promise(function (callable $resolve, callable $reject) { +$promise = new Promise(static function (callable $resolve, callable $reject): void { /** @phpstan-ignore-next-line */ $reject(); }); // assertType('React\Promise\PromiseInterface', $promise); // invalid type passed to $reject -$promise = new Promise(function (callable $resolve, callable $reject) { +$promise = new Promise(static function (callable $resolve, callable $reject): void { /** @phpstan-ignore-next-line */ $reject(2); }); @@ -61,30 +64,30 @@ // invalid number of arguments for $canceller /** @phpstan-ignore-next-line */ -$promise = new Promise(function () { }, function ($a, $b, $c) { }); +$promise = new Promise(static function (): void {}, static function ($a, $b, $c): void {}); // assertType('React\Promise\PromiseInterface', $promise); // invalid types for arguments of $canceller /** @phpstan-ignore-next-line */ -$promise = new Promise(function () { }, function (int $a, string $b) { }); +$promise = new Promise(static function (): void {}, static function (int $a, string $b): void {}); // assertType('React\Promise\PromiseInterface', $promise); // invalid number of arguments passed to $resolve -$promise = new Promise(function () { }, function (callable $resolve) { +$promise = new Promise(static function (): void {}, static function (callable $resolve): void { /** @phpstan-ignore-next-line */ $resolve(); }); // assertType('React\Promise\PromiseInterface', $promise); // invalid number of arguments passed to $reject -$promise = new Promise(function () { }, function (callable $resolve, callable $reject) { +$promise = new Promise(static function (): void {}, static function (callable $resolve, callable $reject): void { /** @phpstan-ignore-next-line */ $reject(); }); // assertType('React\Promise\PromiseInterface', $promise); // invalid type passed to $reject -$promise = new Promise(function() { }, function (callable $resolve, callable $reject) { +$promise = new Promise(static function (): void {}, static function (callable $resolve, callable $reject): void { /** @phpstan-ignore-next-line */ $reject(2); }); diff --git a/tests/types/race.php b/tests/Types/race.php similarity index 84% rename from tests/types/race.php rename to tests/Types/race.php index fa1a861..eaad1c6 100644 --- a/tests/types/race.php +++ b/tests/Types/race.php @@ -1,10 +1,12 @@ ', race([resolve(true), resolve(false)])); assertType('React\Promise\PromiseInterface', race([resolve(true), false])); -assertType('React\Promise\PromiseInterface', race([true, time()])); -assertType('React\Promise\PromiseInterface', race([resolve(true), resolve(time())])); +assertType('React\Promise\PromiseInterface', race([true, \time()])); +assertType('React\Promise\PromiseInterface', race([resolve(true), resolve(\time())])); diff --git a/tests/types/reject.php b/tests/Types/reject.php similarity index 60% rename from tests/types/reject.php rename to tests/Types/reject.php index fd8802e..6bbf067 100644 --- a/tests/types/reject.php +++ b/tests/Types/reject.php @@ -1,6 +1,9 @@ ', reject(new RuntimeException())->then(function (): int { // return 42; // })); -assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->then(null, function (): int { - return 42; -})); -assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->then(null, function (): PromiseInterface { - return resolve(42); -})); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->then(null, static fn(): int => 42)); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->then(null, static fn(): PromiseInterface => resolve(42))); // assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->then(function (): bool { // return true; // }, function (): int { // return 42; // })); -assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->catch(function (): int { - return 42; -})); -assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->catch(function (\UnexpectedValueException $e): int { - return 42; -})); -assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->catch(function (): PromiseInterface { - return resolve(42); -})); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->catch(static fn(): int => 42)); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->catch(static fn(\UnexpectedValueException $e): int => 42)); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->catch(static fn(): PromiseInterface => resolve(42))); -assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->finally(function (): void { })); -assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->finally(function (): never { +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->finally(static function (): void {})); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->finally(static function (): never { throw new \UnexpectedValueException(); })); -assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->finally(function (): PromiseInterface { - return reject(new \UnexpectedValueException()); -})); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->finally(static fn(): PromiseInterface => reject(new \UnexpectedValueException()))); -assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->otherwise(function (): int { - return 42; -})); -assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->otherwise(function (\UnexpectedValueException $e): int { - return 42; -})); -assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->otherwise(function (): PromiseInterface { - return resolve(42); -})); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->otherwise(static fn(): int => 42)); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->otherwise(static fn(\UnexpectedValueException $e): int => 42)); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->otherwise(static fn(): PromiseInterface => resolve(42))); -assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->always(function (): void { })); -assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->always(function (): never { +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->always(static function (): void {})); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->always(static function (): never { throw new \UnexpectedValueException(); })); -assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->always(function (): PromiseInterface { - return reject(new \UnexpectedValueException()); -})); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->always(static fn(): PromiseInterface => reject(new \UnexpectedValueException()))); diff --git a/tests/types/resolve.php b/tests/Types/resolve.php similarity index 70% rename from tests/types/resolve.php rename to tests/Types/resolve.php index 7206a23..069e97d 100644 --- a/tests/types/resolve.php +++ b/tests/Types/resolve.php @@ -1,6 +1,9 @@ */ -function stringOrIntPromise(): PromiseInterface { - return resolve(time() % 2 ? 'string' : time()); +function stringOrIntPromise(): PromiseInterface +{ + return resolve(\time() % 2 ? 'string' : \time()); }; assertType('React\Promise\PromiseInterface', resolve(true)); @@ -25,24 +30,16 @@ function stringOrIntPromise(): PromiseInterface { assertType('React\Promise\PromiseInterface', resolve(resolve(true))); assertType('React\Promise\PromiseInterface', resolve(true)->then(null, null)); -assertType('React\Promise\PromiseInterface', resolve(true)->then(function (bool $bool): bool { - return $bool; -})); -assertType('React\Promise\PromiseInterface', resolve(true)->then(function (bool $value): int { - return 42; -})); -assertType('React\Promise\PromiseInterface', resolve(true)->then(function (bool $value): PromiseInterface { - return resolve(42); -})); -assertType('React\Promise\PromiseInterface', resolve(true)->then(function (bool $value): never { +assertType('React\Promise\PromiseInterface', resolve(true)->then(static fn(bool $bool): bool => $bool)); +assertType('React\Promise\PromiseInterface', resolve(true)->then(static fn(bool $value): int => 42)); +assertType('React\Promise\PromiseInterface', resolve(true)->then(static fn(bool $value): PromiseInterface => resolve(42))); +assertType('React\Promise\PromiseInterface', resolve(true)->then(static function (bool $value): never { throw new \RuntimeException(); })); -assertType('React\Promise\PromiseInterface', resolve(true)->then(null, function (\Throwable $e): int { - return 42; -})); +assertType('React\Promise\PromiseInterface', resolve(true)->then(null, static fn(\Throwable $e): int => 42)); -assertType('React\Promise\PromiseInterface', resolve(true)->then(function (bool $bool): void { })); -assertType('React\Promise\PromiseInterface', resolve(false)->then(function (bool $bool): void { })->then(function (null $value) { })); +assertType('React\Promise\PromiseInterface', resolve(true)->then(static function (bool $bool): void {})); +assertType('React\Promise\PromiseInterface', resolve(false)->then(static function (bool $bool): void {})->then(static function (null $value): void {})); $value = null; assertType('React\Promise\PromiseInterface', resolve(true)->then(static function (bool $v) use (&$value): void { @@ -50,17 +47,13 @@ function stringOrIntPromise(): PromiseInterface { })); assertType('bool|null', $value); -assertType('React\Promise\PromiseInterface', resolve(true)->catch(function (\Throwable $e): never { +assertType('React\Promise\PromiseInterface', resolve(true)->catch(static function (\Throwable $e): never { throw $e; })); -assertType('React\Promise\PromiseInterface', resolve(true)->catch(function (\Throwable $e): int { - return 42; -})); -assertType('React\Promise\PromiseInterface', resolve(true)->catch(function (\Throwable $e): PromiseInterface { - return resolve(42); -})); +assertType('React\Promise\PromiseInterface', resolve(true)->catch(static fn(\Throwable $e): int => 42)); +assertType('React\Promise\PromiseInterface', resolve(true)->catch(static fn(\Throwable $e): PromiseInterface => resolve(42))); -assertType('React\Promise\PromiseInterface', resolve(true)->finally(function (): void { })); +assertType('React\Promise\PromiseInterface', resolve(true)->finally(static function (): void {})); // assertType('React\Promise\PromiseInterface', resolve(true)->finally(function (): never { // throw new \RuntimeException(); // })); @@ -68,17 +61,13 @@ function stringOrIntPromise(): PromiseInterface { // return reject(new \RuntimeException()); // })); -assertType('React\Promise\PromiseInterface', resolve(true)->otherwise(function (\Throwable $e): never { +assertType('React\Promise\PromiseInterface', resolve(true)->otherwise(static function (\Throwable $e): never { throw $e; })); -assertType('React\Promise\PromiseInterface', resolve(true)->otherwise(function (\Throwable $e): int { - return 42; -})); -assertType('React\Promise\PromiseInterface', resolve(true)->otherwise(function (\Throwable $e): PromiseInterface { - return resolve(42); -})); +assertType('React\Promise\PromiseInterface', resolve(true)->otherwise(static fn(\Throwable $e): int => 42)); +assertType('React\Promise\PromiseInterface', resolve(true)->otherwise(static fn(\Throwable $e): PromiseInterface => resolve(42))); -assertType('React\Promise\PromiseInterface', resolve(true)->always(function (): void { })); +assertType('React\Promise\PromiseInterface', resolve(true)->always(static function (): void {})); // assertType('React\Promise\PromiseInterface', resolve(true)->always(function (): never { // throw new \RuntimeException(); // })); diff --git a/tests/DeferredTest.php b/tests/Unit/DeferredTest.php similarity index 58% rename from tests/DeferredTest.php rename to tests/Unit/DeferredTest.php index f63c0fd..065e5b4 100644 --- a/tests/DeferredTest.php +++ b/tests/Unit/DeferredTest.php @@ -1,8 +1,12 @@ promise()->cancel(); unset($deferred); - $this->assertSame(0, gc_collect_cycles()); + $this->assertSame(0, \gc_collect_cycles()); } - /** @test */ + #[Test] public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejectsWithException(): void { - gc_collect_cycles(); - gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on + \gc_collect_cycles(); + \gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on - $deferred = new Deferred(function ($resolve, $reject) { + $deferred = new Deferred(static function ($resolve, $reject): void { $reject(new \Exception('foo')); }); $deferred->promise()->then()->cancel(); unset($deferred); - $this->assertSame(0, gc_collect_cycles()); + $this->assertSame(0, \gc_collect_cycles()); } - /** @test */ + #[Test] public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenceAndExplicitlyRejectWithException(): void { - gc_collect_cycles(); - gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on + \gc_collect_cycles(); + \gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on /** @var Deferred $deferred */ - $deferred = new Deferred(function () use (&$deferred) { - assert($deferred instanceof Deferred); + $deferred = new Deferred(static function () use (&$deferred): void { + \assert($deferred instanceof Deferred); }); $deferred->promise()->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection @@ -70,6 +74,6 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenc $deferred->reject(new \Exception('foo')); unset($deferred); - $this->assertSame(0, gc_collect_cycles()); + $this->assertSame(0, \gc_collect_cycles()); } } diff --git a/tests/Unit/Fixture/CallbackWithDNFTypehintClass.php b/tests/Unit/Fixture/CallbackWithDNFTypehintClass.php new file mode 100644 index 0000000..597dd3c --- /dev/null +++ b/tests/Unit/Fixture/CallbackWithDNFTypehintClass.php @@ -0,0 +1,12 @@ + */ class IterableException extends \RuntimeException implements \IteratorAggregate diff --git a/tests/fixtures/SimpleFulfilledTestThenable.php b/tests/Unit/Fixture/SimpleFulfilledTestThenable.php similarity index 78% rename from tests/fixtures/SimpleFulfilledTestThenable.php rename to tests/Unit/Fixture/SimpleFulfilledTestThenable.php index c435bcc..821d349 100644 --- a/tests/fixtures/SimpleFulfilledTestThenable.php +++ b/tests/Unit/Fixture/SimpleFulfilledTestThenable.php @@ -1,6 +1,8 @@ cancelCalled = true; - if (is_callable($this->onCancel)) { + if (\is_callable($this->onCancel)) { ($this->onCancel)(); } } diff --git a/tests/FunctionAllTest.php b/tests/Unit/FunctionAllTest.php similarity index 86% rename from tests/FunctionAllTest.php rename to tests/Unit/FunctionAllTest.php index 10cfa2f..9735684 100644 --- a/tests/FunctionAllTest.php +++ b/tests/Unit/FunctionAllTest.php @@ -1,12 +1,19 @@ createCallableMock(); @@ -19,7 +26,7 @@ public function shouldResolveEmptyInput(): void ->then($mock); } - /** @test */ + #[Test] public function shouldResolveValuesArray(): void { $mock = $this->createCallableMock(); @@ -32,7 +39,7 @@ public function shouldResolveValuesArray(): void ->then($mock); } - /** @test */ + #[Test] public function shouldResolvePromisesArray(): void { $mock = $this->createCallableMock(); @@ -45,7 +52,7 @@ public function shouldResolvePromisesArray(): void ->then($mock); } - /** @test */ + #[Test] public function shouldResolveSparseArrayInput(): void { $mock = $this->createCallableMock(); @@ -58,7 +65,7 @@ public function shouldResolveSparseArrayInput(): void ->then($mock); } - /** @test */ + #[Test] public function shouldResolveValuesGenerator(): void { $mock = $this->createCallableMock(); @@ -67,7 +74,7 @@ public function shouldResolveValuesGenerator(): void ->method('__invoke') ->with(self::identicalTo([1, 2, 3])); - $gen = (function () { + $gen = (static function () { for ($i = 1; $i <= 3; ++$i) { yield $i; } @@ -76,7 +83,7 @@ public function shouldResolveValuesGenerator(): void all($gen)->then($mock); } - /** @test */ + #[Test] public function shouldResolveValuesGeneratorEmpty(): void { $mock = $this->createCallableMock(); @@ -85,7 +92,7 @@ public function shouldResolveValuesGeneratorEmpty(): void ->method('__invoke') ->with(self::identicalTo([])); - $gen = (function () { + $gen = (static function () { if (false) { // @phpstan-ignore-line yield; } @@ -94,11 +101,11 @@ public function shouldResolveValuesGeneratorEmpty(): void all($gen)->then($mock); } - /** @test */ + #[Test] public function shouldRejectIfAnyInputPromiseRejects(): void { - $exception2 = new Exception(); - $exception3 = new Exception(); + $exception2 = new \Exception(); + $exception3 = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -110,7 +117,7 @@ public function shouldRejectIfAnyInputPromiseRejects(): void ->then($this->expectCallableNever(), $mock); } - /** @test */ + #[Test] public function shouldRejectInfiteGeneratorOrRejectedPromises(): void { $mock = $this->createCallableMock(); @@ -119,7 +126,7 @@ public function shouldRejectInfiteGeneratorOrRejectedPromises(): void ->method('__invoke') ->with(new \RuntimeException('Iteration 1')); - $gen = (function () { + $gen = (static function () { for ($i = 1; ; ++$i) { yield reject(new \RuntimeException('Iteration ' . $i)); } @@ -128,7 +135,7 @@ public function shouldRejectInfiteGeneratorOrRejectedPromises(): void all($gen)->then(null, $mock); } - /** @test */ + #[Test] public function shouldPreserveTheOrderOfArrayWhenResolvingAsyncPromises(): void { $mock = $this->createCallableMock(); diff --git a/tests/FunctionAnyTest.php b/tests/Unit/FunctionAnyTest.php similarity index 79% rename from tests/FunctionAnyTest.php rename to tests/Unit/FunctionAnyTest.php index 563f882..99e42f1 100644 --- a/tests/FunctionAnyTest.php +++ b/tests/Unit/FunctionAnyTest.php @@ -1,14 +1,22 @@ createCallableMock(); @@ -16,17 +24,15 @@ public function shouldRejectWithLengthExceptionWithEmptyInputArray(): void ->expects(self::once()) ->method('__invoke') ->with( - self::callback(function ($exception) { - return $exception instanceof LengthException && - 'Must contain at least 1 item but contains only 0 items.' === $exception->getMessage(); - }) + self::callback(static fn($exception) => $exception instanceof LengthException && + $exception->getMessage() === 'Must contain at least 1 item but contains only 0 items.'), ); any([]) ->then($this->expectCallableNever(), $mock); } - /** @test */ + #[Test] public function shouldRejectWithLengthExceptionWithEmptyInputGenerator(): void { $mock = $this->createCallableMock(); @@ -35,7 +41,7 @@ public function shouldRejectWithLengthExceptionWithEmptyInputGenerator(): void ->method('__invoke') ->with(new LengthException('Must contain at least 1 item but contains only 0 items.')); - $gen = (function () { + $gen = (static function () { if (false) { // @phpstan-ignore-line yield; } @@ -44,7 +50,7 @@ public function shouldRejectWithLengthExceptionWithEmptyInputGenerator(): void any($gen)->then($this->expectCallableNever(), $mock); } - /** @test */ + #[Test] public function shouldResolveWithAnInputValue(): void { $mock = $this->createCallableMock(); @@ -57,7 +63,7 @@ public function shouldResolveWithAnInputValue(): void ->then($mock); } - /** @test */ + #[Test] public function shouldResolveWithAPromisedInputValue(): void { $mock = $this->createCallableMock(); @@ -70,7 +76,7 @@ public function shouldResolveWithAPromisedInputValue(): void ->then($mock); } - /** @test */ + #[Test] public function shouldResolveWithAnInputValueFromDeferred(): void { $mock = $this->createCallableMock(); @@ -86,7 +92,7 @@ public function shouldResolveWithAnInputValueFromDeferred(): void $deferred->resolve(1); } - /** @test */ + #[Test] public function shouldResolveValuesGenerator(): void { $mock = $this->createCallableMock(); @@ -95,7 +101,7 @@ public function shouldResolveValuesGenerator(): void ->method('__invoke') ->with(self::identicalTo(1)); - $gen = (function () { + $gen = (static function () { for ($i = 1; $i <= 3; ++$i) { yield $i; } @@ -104,7 +110,7 @@ public function shouldResolveValuesGenerator(): void any($gen)->then($mock); } - /** @test */ + #[Test] public function shouldResolveValuesInfiniteGenerator(): void { $mock = $this->createCallableMock(); @@ -113,7 +119,7 @@ public function shouldResolveValuesInfiniteGenerator(): void ->method('__invoke') ->with(self::identicalTo(1)); - $gen = (function () { + $gen = (static function () { for ($i = 1; ; ++$i) { yield $i; } @@ -122,16 +128,16 @@ public function shouldResolveValuesInfiniteGenerator(): void any($gen)->then($mock); } - /** @test */ + #[Test] public function shouldRejectWithAllRejectedInputValuesIfAllInputsAreRejected(): void { - $exception1 = new Exception(); - $exception2 = new Exception(); - $exception3 = new Exception(); + $exception1 = new \Exception(); + $exception2 = new \Exception(); + $exception3 = new \Exception(); $compositeException = new CompositeException( [0 => $exception1, 1 => $exception2, 2 => $exception3], - 'All promises rejected.' + 'All promises rejected.', ); $mock = $this->createCallableMock(); @@ -144,14 +150,14 @@ public function shouldRejectWithAllRejectedInputValuesIfAllInputsAreRejected(): ->then($this->expectCallableNever(), $mock); } - /** @test */ + #[Test] public function shouldRejectWithAllRejectedInputValuesIfInputIsRejectedFromDeferred(): void { - $exception = new Exception(); + $exception = new \Exception(); $compositeException = new CompositeException( [2 => $exception], - 'All promises rejected.' + 'All promises rejected.', ); $mock = $this->createCallableMock(); @@ -167,11 +173,11 @@ public function shouldRejectWithAllRejectedInputValuesIfInputIsRejectedFromDefer $deferred->reject($exception); } - /** @test */ + #[Test] public function shouldResolveWhenFirstInputPromiseResolves(): void { - $exception2 = new Exception(); - $exception3 = new Exception(); + $exception2 = new \Exception(); + $exception3 = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -183,7 +189,7 @@ public function shouldResolveWhenFirstInputPromiseResolves(): void ->then($mock); } - /** @test */ + #[Test] public function shouldNotRelyOnArryIndexesWhenUnwrappingToASingleResolutionValue(): void { $mock = $this->createCallableMock(); @@ -202,22 +208,22 @@ public function shouldNotRelyOnArryIndexesWhenUnwrappingToASingleResolutionValue $d1->resolve(1); } - /** @test */ + #[Test] public function shouldCancelInputArrayPromises(): void { - $promise1 = new Promise(function () {}, $this->expectCallableOnce()); - $promise2 = new Promise(function () {}, $this->expectCallableOnce()); + $promise1 = new Promise(static function (): void {}, $this->expectCallableOnce()); + $promise2 = new Promise(static function (): void {}, $this->expectCallableOnce()); any([$promise1, $promise2])->cancel(); } - /** @test */ + #[Test] public function shouldNotCancelOtherPendingInputArrayPromisesIfOnePromiseFulfills(): void { $deferred = new Deferred($this->expectCallableNever()); $deferred->resolve(null); - $promise2 = new Promise(function () {}, $this->expectCallableNever()); + $promise2 = new Promise(static function (): void {}, $this->expectCallableNever()); any([$deferred->promise(), $promise2])->cancel(); } diff --git a/tests/FunctionCheckTypehintTest.php b/tests/Unit/FunctionCheckTypehintTest.php similarity index 70% rename from tests/FunctionCheckTypehintTest.php rename to tests/Unit/FunctionCheckTypehintTest.php index 6f7fc0f..16b653d 100644 --- a/tests/FunctionCheckTypehintTest.php +++ b/tests/Unit/FunctionCheckTypehintTest.php @@ -1,51 +1,61 @@ then($this->expectCallableNever(), $this->expectCallableNever()); } - /** @test */ + #[Test] public function shouldResolveValuesArray(): void { $mock = $this->createCallableMock(); @@ -24,11 +30,11 @@ public function shouldResolveValuesArray(): void ->with(self::identicalTo(1)); race( - [1, 2, 3] + [1, 2, 3], )->then($mock); } - /** @test */ + #[Test] public function shouldResolvePromisesArray(): void { $mock = $this->createCallableMock(); @@ -42,7 +48,7 @@ public function shouldResolvePromisesArray(): void $d3 = new Deferred(); race( - [$d1->promise(), $d2->promise(), $d3->promise()] + [$d1->promise(), $d2->promise(), $d3->promise()], )->then($mock); $d2->resolve(2); @@ -51,7 +57,7 @@ public function shouldResolvePromisesArray(): void $d3->resolve(3); } - /** @test */ + #[Test] public function shouldResolveSparseArrayInput(): void { $mock = $this->createCallableMock(); @@ -61,11 +67,11 @@ public function shouldResolveSparseArrayInput(): void ->with(self::identicalTo(null)); race( - [null, 1, null, 2, 3] + [null, 1, null, 2, 3], )->then($mock); } - /** @test */ + #[Test] public function shouldResolveValuesGenerator(): void { $mock = $this->createCallableMock(); @@ -74,7 +80,7 @@ public function shouldResolveValuesGenerator(): void ->method('__invoke') ->with(self::identicalTo(1)); - $gen = (function () { + $gen = (static function () { for ($i = 1; $i <= 3; ++$i) { yield $i; } @@ -83,7 +89,7 @@ public function shouldResolveValuesGenerator(): void race($gen)->then($mock); } - /** @test */ + #[Test] public function shouldResolveValuesInfiniteGenerator(): void { $mock = $this->createCallableMock(); @@ -92,7 +98,7 @@ public function shouldResolveValuesInfiniteGenerator(): void ->method('__invoke') ->with(self::identicalTo(1)); - $gen = (function () { + $gen = (static function () { for ($i = 1; ; ++$i) { yield $i; } @@ -101,10 +107,10 @@ public function shouldResolveValuesInfiniteGenerator(): void race($gen)->then($mock); } - /** @test */ + #[Test] public function shouldRejectIfFirstSettledPromiseRejects(): void { - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -117,7 +123,7 @@ public function shouldRejectIfFirstSettledPromiseRejects(): void $d3 = new Deferred(); race( - [$d1->promise(), $d2->promise(), $d3->promise()] + [$d1->promise(), $d2->promise(), $d3->promise()], )->then($this->expectCallableNever(), $mock); $d2->reject($exception); @@ -126,33 +132,33 @@ public function shouldRejectIfFirstSettledPromiseRejects(): void $d3->resolve(3); } - /** @test */ + #[Test] public function shouldCancelInputArrayPromises(): void { - $promise1 = new Promise(function () {}, $this->expectCallableOnce()); - $promise2 = new Promise(function () {}, $this->expectCallableOnce()); + $promise1 = new Promise(static function (): void {}, $this->expectCallableOnce()); + $promise2 = new Promise(static function (): void {}, $this->expectCallableOnce()); race([$promise1, $promise2])->cancel(); } - /** @test */ + #[Test] public function shouldNotCancelOtherPendingInputArrayPromisesIfOnePromiseFulfills(): void { $deferred = new Deferred($this->expectCallableNever()); $deferred->resolve(null); - $promise2 = new Promise(function () {}, $this->expectCallableNever()); + $promise2 = new Promise(static function (): void {}, $this->expectCallableNever()); race([$deferred->promise(), $promise2])->cancel(); } - /** @test */ + #[Test] public function shouldNotCancelOtherPendingInputArrayPromisesIfOnePromiseRejects(): void { $deferred = new Deferred($this->expectCallableNever()); - $deferred->reject(new Exception()); + $deferred->reject(new \Exception()); - $promise2 = new Promise(function () {}, $this->expectCallableNever()); + $promise2 = new Promise(static function (): void {}, $this->expectCallableNever()); race([$deferred->promise(), $promise2])->cancel(); } diff --git a/tests/FunctionRejectTest.php b/tests/Unit/FunctionRejectTest.php similarity index 67% rename from tests/FunctionRejectTest.php rename to tests/Unit/FunctionRejectTest.php index 38b7cc6..d29b8b7 100644 --- a/tests/FunctionRejectTest.php +++ b/tests/Unit/FunctionRejectTest.php @@ -1,15 +1,19 @@ createCallableMock(); $mock diff --git a/tests/FunctionResolveTest.php b/tests/Unit/FunctionResolveTest.php similarity index 78% rename from tests/FunctionResolveTest.php rename to tests/Unit/FunctionResolveTest.php index 1eacd34..cda1003 100644 --- a/tests/FunctionResolveTest.php +++ b/tests/Unit/FunctionResolveTest.php @@ -1,14 +1,21 @@ then( $mock, - $this->expectCallableNever() + $this->expectCallableNever(), ); } - /** @test */ + #[Test] public function shouldResolveAFulfilledPromise(): void { $expected = 123; @@ -42,11 +49,11 @@ public function shouldResolveAFulfilledPromise(): void resolve($resolved) ->then( $mock, - $this->expectCallableNever() + $this->expectCallableNever(), ); } - /** @test */ + #[Test] public function shouldResolveAThenable(): void { $thenable = new SimpleFulfilledTestThenable(); @@ -60,11 +67,11 @@ public function shouldResolveAThenable(): void resolve($thenable) ->then( $mock, - $this->expectCallableNever() + $this->expectCallableNever(), ); } - /** @test */ + #[Test] public function shouldResolveACancellableThenable(): void { $thenable = new SimpleTestCancellableThenable(); @@ -75,10 +82,10 @@ public function shouldResolveACancellableThenable(): void self::assertTrue($thenable->cancelCalled); } - /** @test */ + #[Test] public function shouldRejectARejectedPromise(): void { - $exception = new Exception(); + $exception = new \Exception(); $resolved = new RejectedPromise($exception); @@ -91,28 +98,24 @@ public function shouldRejectARejectedPromise(): void resolve($resolved) ->then( $this->expectCallableNever(), - $mock + $mock, ); } - /** @test */ + #[Test] public function shouldSupportDeepNestingInPromiseChains(): void { $d = new Deferred(); $d->resolve(false); - $result = resolve(resolve($d->promise()->then(function ($val) { + $result = resolve(resolve($d->promise()->then(static function ($val) { $d = new Deferred(); $d->resolve($val); - $identity = function ($val) { - return $val; - }; + $identity = static fn($val) => $val; return resolve($d->promise()->then($identity))->then( - function ($val) { - return !$val; - } + static fn($val) => !$val, ); }))); @@ -125,13 +128,9 @@ function ($val) { $result->then($mock); } - /** @test */ + #[Test] public function shouldSupportVeryDeepNestedPromises(): void { - if (PHP_VERSION_ID < 70200 && ini_get('xdebug.max_nesting_level') !== false) { - $this->markTestSkipped('Skip unhandled rejection on legacy PHP 7.1'); - } - $deferreds = []; for ($i = 0; $i < 150; $i++) { @@ -140,9 +139,7 @@ public function shouldSupportVeryDeepNestedPromises(): void $last = $p; for ($j = 0; $j < 150; $j++) { - $last = $last->then(function ($result) { - return $result; - }); + $last = $last->then(static fn($result) => $result); } } diff --git a/tests/Internal/CancellationQueueTest.php b/tests/Unit/Internal/CancellationQueueTest.php similarity index 82% rename from tests/Internal/CancellationQueueTest.php rename to tests/Unit/Internal/CancellationQueueTest.php index e8fd58e..2f9eea8 100644 --- a/tests/Internal/CancellationQueueTest.php +++ b/tests/Unit/Internal/CancellationQueueTest.php @@ -1,16 +1,19 @@ cancelCalled); } - /** @test */ + #[Test] public function ignoresSimpleCancellable(): void { $p = new SimpleTestCancellable(); @@ -36,7 +39,7 @@ public function ignoresSimpleCancellable(): void self::assertFalse($p->cancelCalled); } - /** @test */ + #[Test] public function callsCancelOnPromisesEnqueuedBeforeStart(): void { $d1 = $this->getCancellableDeferred(); @@ -49,7 +52,7 @@ public function callsCancelOnPromisesEnqueuedBeforeStart(): void $cancellationQueue(); } - /** @test */ + #[Test] public function callsCancelOnPromisesEnqueuedAfterStart(): void { $d1 = $this->getCancellableDeferred(); @@ -63,7 +66,7 @@ public function callsCancelOnPromisesEnqueuedAfterStart(): void $cancellationQueue->enqueue($d1->promise()); } - /** @test */ + #[Test] public function doesNotCallCancelTwiceWhenStartedTwice(): void { $d = $this->getCancellableDeferred(); @@ -75,18 +78,16 @@ public function doesNotCallCancelTwiceWhenStartedTwice(): void $cancellationQueue(); } - /** - * @test - */ + #[Test] public function rethrowsExceptionsThrownFromCancel(): void { - $this->expectException(Exception::class); + $this->expectException(\Exception::class); $this->expectExceptionMessage('test'); $mock = $this->createCallableMock(); $mock ->expects(self::once()) ->method('__invoke') - ->will(self::throwException(new Exception('test'))); + ->will(self::throwException(new \Exception('test'))); $promise = new SimpleTestCancellableThenable($mock); diff --git a/tests/Unit/Internal/FulfilledPromiseTest.php b/tests/Unit/Internal/FulfilledPromiseTest.php new file mode 100644 index 0000000..a9282f9 --- /dev/null +++ b/tests/Unit/Internal/FulfilledPromiseTest.php @@ -0,0 +1,60 @@ + + */ + public function getPromiseTestAdapter(?callable $canceller = null): CallbackPromiseAdapter + { + /** @var ?FulfilledPromise */ + $promise = null; + + return new CallbackPromiseAdapter([ + 'promise' => static function () use (&$promise) { + if (!$promise) { + throw new \LogicException('FulfilledPromise must be resolved before obtaining the promise'); + } + + return $promise; + }, + 'resolve' => static function ($value = null) use (&$promise): void { + if (!$promise) { + $promise = new FulfilledPromise($value); + } + }, + 'reject' => static function (): void { + throw new \LogicException('You cannot call reject() for React\Promise\FulfilledPromise'); + }, + 'settle' => static function ($value = null) use (&$promise): void { + if (!$promise) { + $promise = new FulfilledPromise($value); + } + }, + ]); + } + + #[Test] + public function shouldThrowExceptionIfConstructedWithAPromise(): void + { + $this->expectException(\InvalidArgumentException::class); + new FulfilledPromise(new FulfilledPromise()); + } +} diff --git a/tests/Unit/Internal/RejectedPromiseTest.php b/tests/Unit/Internal/RejectedPromiseTest.php new file mode 100644 index 0000000..ab2117f --- /dev/null +++ b/tests/Unit/Internal/RejectedPromiseTest.php @@ -0,0 +1,53 @@ + + */ + public function getPromiseTestAdapter(?callable $canceller = null): CallbackPromiseAdapter + { + /** @var ?RejectedPromise */ + $promise = null; + + return new CallbackPromiseAdapter([ + 'promise' => static function () use (&$promise) { + if (!$promise) { + throw new \LogicException('RejectedPromise must be rejected before obtaining the promise'); + } + + return $promise; + }, + 'resolve' => static function (): void { + throw new \LogicException('You cannot call resolve() for React\Promise\RejectedPromise'); + }, + 'reject' => static function (\Throwable $reason) use (&$promise): void { + if (!$promise) { + $promise = new RejectedPromise($reason); + } + }, + 'settle' => static function ($reason = '') use (&$promise): void { + if (!$promise) { + if (!$reason instanceof \Exception) { + $reason = new \Exception((string) $reason); + } + + $promise = new RejectedPromise($reason); + } + }, + ]); + } +} diff --git a/tests/PromiseAdapter/CallbackPromiseAdapter.php b/tests/Unit/PromiseAdapter/CallbackPromiseAdapter.php similarity index 53% rename from tests/PromiseAdapter/CallbackPromiseAdapter.php rename to tests/Unit/PromiseAdapter/CallbackPromiseAdapter.php index 9b72204..10288d0 100644 --- a/tests/PromiseAdapter/CallbackPromiseAdapter.php +++ b/tests/Unit/PromiseAdapter/CallbackPromiseAdapter.php @@ -1,6 +1,8 @@ callbacks = $callbacks; - } + public function __construct(private array $callbacks) {} /** * @return PromiseInterface */ public function promise(): PromiseInterface { - return ($this->callbacks['promise'])(...func_get_args()); + return ($this->callbacks['promise'])(...\func_get_args()); } public function resolve($value): void { - ($this->callbacks['resolve'])(...func_get_args()); + ($this->callbacks['resolve'])(...\func_get_args()); } public function reject(): void { - ($this->callbacks['reject'])(...func_get_args()); + ($this->callbacks['reject'])(...\func_get_args()); } public function settle(): void { - ($this->callbacks['settle'])(...func_get_args()); + ($this->callbacks['settle'])(...\func_get_args()); } } diff --git a/tests/PromiseAdapter/PromiseAdapterInterface.php b/tests/Unit/PromiseAdapter/PromiseAdapterInterface.php similarity index 68% rename from tests/PromiseAdapter/PromiseAdapterInterface.php rename to tests/Unit/PromiseAdapter/PromiseAdapterInterface.php index 171d03e..0b8ec11 100644 --- a/tests/PromiseAdapter/PromiseAdapterInterface.php +++ b/tests/Unit/PromiseAdapter/PromiseAdapterInterface.php @@ -1,6 +1,8 @@ + */ + public function getPromiseTestAdapter(?callable $canceller = null): CallbackPromiseAdapter + { + $resolveCallback = $rejectCallback = null; + + $promise = new Promise(static 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' => static fn() => $promise, + 'resolve' => $resolveCallback, + 'reject' => $rejectCallback, + 'settle' => $resolveCallback, + ]); + } + + #[Test] + public function shouldRejectIfResolverThrowsException(): void + { + $exception = new \Exception('foo'); + + $promise = new Promise(static function () use ($exception): void { + throw $exception; + }); + + $mock = $this->createCallableMock(); + $mock + ->expects(self::once()) + ->method('__invoke') + ->with(self::identicalTo($exception)); + + $promise + ->then($this->expectCallableNever(), $mock); + } + + #[Test] + public function shouldResolveWithoutCreatingGarbageCyclesIfResolverResolvesWithException(): void + { + \gc_collect_cycles(); + $promise = new Promise(static function ($resolve): void { + $resolve(new \Exception('foo')); + }); + unset($promise); + + $this->assertSame(0, \gc_collect_cycles()); + } + + #[Test] + public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptionWithoutResolver(): void + { + \gc_collect_cycles(); + $promise = new Promise(static function (): void { + throw new \Exception('foo'); + }); + + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection + + unset($promise); + + $this->assertSame(0, \gc_collect_cycles()); + } + + #[Test] + public function shouldRejectWithoutCreatingGarbageCyclesIfResolverRejectsWithException(): void + { + \gc_collect_cycles(); + $promise = new Promise(static function ($resolve, $reject): void { + $reject(new \Exception('foo')); + }); + + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection + + unset($promise); + + $this->assertSame(0, \gc_collect_cycles()); + } + + #[Test] + public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithException(): void + { + \gc_collect_cycles(); + $promise = new Promise(static function ($resolve, $reject): void {}, static function ($resolve, $reject): void { + $reject(new \Exception('foo')); + }); + $promise->cancel(); + unset($promise); + + $this->assertSame(0, \gc_collect_cycles()); + } + + #[Test] + public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejectsWithException(): void + { + \gc_collect_cycles(); + $promise = new Promise(static function ($resolve, $reject): void {}, static function ($resolve, $reject): void { + $reject(new \Exception('foo')); + }); + $promise->then()->then()->then()->cancel(); + unset($promise); + + $this->assertSame(0, \gc_collect_cycles()); + } + + #[Test] + public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsException(): void + { + \gc_collect_cycles(); + $promise = new Promise(static function ($resolve, $reject): void { + throw new \Exception('foo'); + }); + + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection + + unset($promise); + + $this->assertSame(0, \gc_collect_cycles()); + } + + #[Test] + public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromise(): void + { + \gc_collect_cycles(); + $promise = new Promise(static function (): void {}); + unset($promise); + + $this->assertSame(0, \gc_collect_cycles()); + } + + #[Test] + public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithThenFollowers(): void + { + \gc_collect_cycles(); + $promise = new Promise(static function (): void {}); + $promise->then()->then()->then(); + unset($promise); + + $this->assertSame(0, \gc_collect_cycles()); + } + + #[Test] + public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithCatchFollowers(): void + { + \gc_collect_cycles(); + $promise = new Promise(static function (): void {}); + $promise->catch(static function (): void {}); + unset($promise); + + $this->assertSame(0, \gc_collect_cycles()); + } + + #[Test] + public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithFinallyFollowers(): void + { + \gc_collect_cycles(); + $promise = new Promise(static function (): void {}); + $promise->finally(static function (): void {}); + unset($promise); + + $this->assertSame(0, \gc_collect_cycles()); + } + + /** + * @deprecated + */ + #[Test] + public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithOtherwiseFollowers(): void + { + \gc_collect_cycles(); + $promise = new Promise(static function (): void {}); + $promise->otherwise(static function (): void {}); + unset($promise); + + $this->assertSame(0, \gc_collect_cycles()); + } + + /** + * @deprecated + */ + #[Test] + public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithAlwaysFollowers(): void + { + \gc_collect_cycles(); + $promise = new Promise(static function (): void {}); + $promise->always(static function (): void {}); + unset($promise); + + $this->assertSame(0, \gc_collect_cycles()); + } + + #[Test] + public function shouldFulfillIfFullfilledWithSimplePromise(): void + { + \gc_collect_cycles(); + $promise = new Promise(static function (): void { + throw new \Exception('foo'); + }); + + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection + + unset($promise); + + self::assertSame(0, \gc_collect_cycles()); + } +} diff --git a/tests/PromiseTest/CancelTestTrait.php b/tests/Unit/PromiseTest/CancelTestTrait.php similarity index 81% rename from tests/PromiseTest/CancelTestTrait.php rename to tests/Unit/PromiseTest/CancelTestTrait.php index d119c1b..5e12da2 100644 --- a/tests/PromiseTest/CancelTestTrait.php +++ b/tests/Unit/PromiseTest/CancelTestTrait.php @@ -1,36 +1,38 @@ getPromiseTestAdapter(function ($resolve, $reject) use (&$args) { - $args = func_get_args(); + $adapter = $this->getPromiseTestAdapter(static function ($resolve, $reject) use (&$args): void { + $args = \func_get_args(); }); $adapter->promise()->cancel(); self::assertCount(2, $args); - self::assertTrue(is_callable($args[0])); - self::assertTrue(is_callable($args[1])); + self::assertTrue(\is_callable($args[0])); + self::assertTrue(\is_callable($args[1])); } - /** @test */ + #[Test] public function cancelShouldCallCancellerWithoutArgumentsIfNotAccessed(): void { $args = null; - $adapter = $this->getPromiseTestAdapter(function () use (&$args) { - $args = func_num_args(); + $adapter = $this->getPromiseTestAdapter(static function () use (&$args): void { + $args = \func_num_args(); }); $adapter->promise()->cancel(); @@ -38,10 +40,10 @@ public function cancelShouldCallCancellerWithoutArgumentsIfNotAccessed(): void self::assertSame(0, $args); } - /** @test */ + #[Test] public function cancelShouldFulfillPromiseIfCancellerFulfills(): void { - $adapter = $this->getPromiseTestAdapter(function ($resolve) { + $adapter = $this->getPromiseTestAdapter(static function ($resolve): void { $resolve(1); }); @@ -57,12 +59,12 @@ public function cancelShouldFulfillPromiseIfCancellerFulfills(): void $adapter->promise()->cancel(); } - /** @test */ + #[Test] public function cancelShouldRejectPromiseIfCancellerRejects(): void { - $exception = new Exception(); + $exception = new \Exception(); - $adapter = $this->getPromiseTestAdapter(function ($resolve, $reject) use ($exception) { + $adapter = $this->getPromiseTestAdapter(static function ($resolve, $reject) use ($exception): void { $reject($exception); }); @@ -78,12 +80,12 @@ public function cancelShouldRejectPromiseIfCancellerRejects(): void $adapter->promise()->cancel(); } - /** @test */ + #[Test] public function cancelShouldRejectPromiseWithExceptionIfCancellerThrows(): void { - $e = new Exception(); + $e = new \Exception(); - $adapter = $this->getPromiseTestAdapter(function () use ($e) { + $adapter = $this->getPromiseTestAdapter(static function () use ($e): void { throw $e; }); @@ -99,14 +101,14 @@ public function cancelShouldRejectPromiseWithExceptionIfCancellerThrows(): void $adapter->promise()->cancel(); } - /** @test */ + #[Test] public function cancelShouldCallCancellerOnlyOnceIfCancellerResolves(): void { $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->will($this->returnCallback(function ($resolve) { + ->will($this->returnCallback(static function ($resolve): void { $resolve(null); })); @@ -116,10 +118,10 @@ public function cancelShouldCallCancellerOnlyOnceIfCancellerResolves(): void $adapter->promise()->cancel(); } - /** @test */ + #[Test] public function cancelShouldHaveNoEffectIfCancellerDoesNothing(): void { - $adapter = $this->getPromiseTestAdapter(function () {}); + $adapter = $this->getPromiseTestAdapter(static function (): void {}); $adapter->promise() ->then($this->expectCallableNever(), $this->expectCallableNever()); @@ -128,28 +130,24 @@ public function cancelShouldHaveNoEffectIfCancellerDoesNothing(): void $adapter->promise()->cancel(); } - /** @test */ + #[Test] public function cancelShouldCallCancellerFromDeepNestedPromiseChain(): void { $adapter = $this->getPromiseTestAdapter($this->expectCallableOnce()); $promise = $adapter->promise() - ->then(function () { - return new Promise\Promise(function () {}); - }) - ->then(function () { + ->then(static fn() => new Promise\Promise(static function (): void {})) + ->then(static function () { $d = new Promise\Deferred(); return $d->promise(); }) - ->then(function () { - return new Promise\Promise(function () {}); - }); + ->then(static fn() => new Promise\Promise(static function (): void {})); $promise->cancel(); } - /** @test */ + #[Test] public function cancelCalledOnChildrenSouldOnlyCancelWhenAllChildrenCancelled(): void { $adapter = $this->getPromiseTestAdapter($this->expectCallableNever()); @@ -164,7 +162,7 @@ public function cancelCalledOnChildrenSouldOnlyCancelWhenAllChildrenCancelled(): $child1->cancel(); } - /** @test */ + #[Test] public function cancelShouldTriggerCancellerWhenAllChildrenCancel(): void { $adapter = $this->getPromiseTestAdapter($this->expectCallableOnce()); @@ -180,7 +178,7 @@ public function cancelShouldTriggerCancellerWhenAllChildrenCancel(): void $child2->cancel(); } - /** @test */ + #[Test] public function cancelShouldNotTriggerCancellerWhenCancellingOneChildrenMultipleTimes(): void { $adapter = $this->getPromiseTestAdapter($this->expectCallableNever()); @@ -196,7 +194,7 @@ public function cancelShouldNotTriggerCancellerWhenCancellingOneChildrenMultiple $child1->cancel(); } - /** @test */ + #[Test] public function cancelShouldTriggerCancellerOnlyOnceWhenCancellingMultipleTimes(): void { $adapter = $this->getPromiseTestAdapter($this->expectCallableOnce()); @@ -205,7 +203,7 @@ public function cancelShouldTriggerCancellerOnlyOnceWhenCancellingMultipleTimes( $adapter->promise()->cancel(); } - /** @test */ + #[Test] public function cancelShouldAlwaysTriggerCancellerWhenCalledOnRootPromise(): void { $adapter = $this->getPromiseTestAdapter($this->expectCallableOnce()); @@ -220,7 +218,7 @@ public function cancelShouldAlwaysTriggerCancellerWhenCalledOnRootPromise(): voi $adapter->promise()->cancel(); } - /** @test */ + #[Test] public function cancelShouldTriggerCancellerWhenFollowerCancels(): void { $adapter1 = $this->getPromiseTestAdapter($this->expectCallableOnce()); @@ -235,7 +233,7 @@ public function cancelShouldTriggerCancellerWhenFollowerCancels(): void $follower->cancel(); } - /** @test */ + #[Test] public function cancelShouldNotTriggerCancellerWhenCancellingOnlyOneFollower(): void { $adapter1 = $this->getPromiseTestAdapter($this->expectCallableNever()); @@ -253,7 +251,7 @@ public function cancelShouldNotTriggerCancellerWhenCancellingOnlyOneFollower(): $follower1->cancel(); } - /** @test */ + #[Test] public function cancelCalledOnFollowerShouldOnlyCancelWhenAllChildrenAndFollowerCancelled(): void { $adapter1 = $this->getPromiseTestAdapter($this->expectCallableOnce()); @@ -271,7 +269,7 @@ public function cancelCalledOnFollowerShouldOnlyCancelWhenAllChildrenAndFollower $child->cancel(); } - /** @test */ + #[Test] public function cancelShouldNotTriggerCancellerWhenCancellingFollowerButNotChildren(): void { $adapter1 = $this->getPromiseTestAdapter($this->expectCallableNever()); diff --git a/tests/Unit/PromiseTest/FullTestTrait.php b/tests/Unit/PromiseTest/FullTestTrait.php new file mode 100644 index 0000000..bddc733 --- /dev/null +++ b/tests/Unit/PromiseTest/FullTestTrait.php @@ -0,0 +1,16 @@ +getPromiseTestAdapter(); @@ -30,11 +32,11 @@ public function fulfilledPromiseShouldBeImmutable(): void $adapter->promise() ->then( $mock, - $this->expectCallableNever() + $this->expectCallableNever(), ); } - /** @test */ + #[Test] public function fulfilledPromiseShouldInvokeNewlyAddedCallback(): void { $adapter = $this->getPromiseTestAdapter(); @@ -51,7 +53,7 @@ public function fulfilledPromiseShouldInvokeNewlyAddedCallback(): void ->then($mock, $this->expectCallableNever()); } - /** @test */ + #[Test] public function thenShouldForwardResultWhenCallbackIsNull(): void { $adapter = $this->getPromiseTestAdapter(); @@ -66,15 +68,15 @@ public function thenShouldForwardResultWhenCallbackIsNull(): void $adapter->promise() ->then( null, - $this->expectCallableNever() + $this->expectCallableNever(), ) ->then( $mock, - $this->expectCallableNever() + $this->expectCallableNever(), ); } - /** @test */ + #[Test] public function thenShouldForwardCallbackResultToNextCallback(): void { $adapter = $this->getPromiseTestAdapter(); @@ -88,18 +90,16 @@ public function thenShouldForwardCallbackResultToNextCallback(): void $adapter->resolve(1); $adapter->promise() ->then( - function ($val) { - return $val + 1; - }, - $this->expectCallableNever() + static fn($val) => $val + 1, + $this->expectCallableNever(), ) ->then( $mock, - $this->expectCallableNever() + $this->expectCallableNever(), ); } - /** @test */ + #[Test] public function thenShouldForwardPromisedCallbackResultValueToNextCallback(): void { $adapter = $this->getPromiseTestAdapter(); @@ -113,23 +113,21 @@ public function thenShouldForwardPromisedCallbackResultValueToNextCallback(): vo $adapter->resolve(1); $adapter->promise() ->then( - function ($val) { - return resolve($val + 1); - }, - $this->expectCallableNever() + static fn($val) => resolve($val + 1), + $this->expectCallableNever(), ) ->then( $mock, - $this->expectCallableNever() + $this->expectCallableNever(), ); } - /** @test */ + #[Test] public function thenShouldSwitchFromCallbacksToErrbacksWhenCallbackReturnsARejection(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -140,23 +138,21 @@ public function thenShouldSwitchFromCallbacksToErrbacksWhenCallbackReturnsARejec $adapter->resolve(1); $adapter->promise() ->then( - function () use ($exception) { - return reject($exception); - }, - $this->expectCallableNever() + static fn() => reject($exception), + $this->expectCallableNever(), ) ->then( $this->expectCallableNever(), - $mock + $mock, ); } - /** @test */ + #[Test] public function thenShouldSwitchFromCallbacksToErrbacksWhenCallbackThrows(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -174,26 +170,26 @@ public function thenShouldSwitchFromCallbacksToErrbacksWhenCallbackThrows(): voi $adapter->promise() ->then( $mock, - $this->expectCallableNever() + $this->expectCallableNever(), ) ->then( $this->expectCallableNever(), - $mock2 + $mock2, ); } /** - * @test * @requires PHP 8.1 */ + #[Test] public function thenShouldContinueToExecuteCallbacksWhenPriorCallbackSuspendsFiber(): void { /** @var PromiseAdapterInterface $adapter */ $adapter = $this->getPromiseTestAdapter(); $adapter->resolve(42); - $fiber = new \Fiber(function () use ($adapter) { - $adapter->promise()->then(function (int $value) { + $fiber = new \Fiber(static function () use ($adapter): void { + $adapter->promise()->then(static function (int $value): void { \Fiber::suspend($value); }); }); @@ -210,7 +206,7 @@ public function thenShouldContinueToExecuteCallbacksWhenPriorCallbackSuspendsFib $adapter->promise()->then($mock); } - /** @test */ + #[Test] public function cancelShouldHaveNoEffectForFulfilledPromise(): void { $adapter = $this->getPromiseTestAdapter($this->expectCallableNever()); @@ -220,7 +216,7 @@ public function cancelShouldHaveNoEffectForFulfilledPromise(): void $adapter->promise()->cancel(); } - /** @test */ + #[Test] public function catchShouldNotInvokeRejectionHandlerForFulfilledPromise(): void { $adapter = $this->getPromiseTestAdapter(); @@ -229,12 +225,12 @@ public function catchShouldNotInvokeRejectionHandlerForFulfilledPromise(): void $adapter->promise()->catch($this->expectCallableNever()); } - /** @test */ + #[Test] public function finallyShouldNotSuppressValueForFulfilledPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $value = new stdClass(); + $value = new \stdClass(); $mock = $this->createCallableMock(); $mock @@ -244,16 +240,16 @@ public function finallyShouldNotSuppressValueForFulfilledPromise(): void $adapter->resolve($value); $adapter->promise() - ->finally(function () {}) + ->finally(static function (): void {}) ->then($mock); } - /** @test */ + #[Test] public function finallyShouldNotSuppressValueWhenHandlerReturnsANonPromiseForFulfilledPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $value = new stdClass(); + $value = new \stdClass(); $mock = $this->createCallableMock(); $mock @@ -263,18 +259,17 @@ public function finallyShouldNotSuppressValueWhenHandlerReturnsANonPromiseForFul $adapter->resolve($value); $adapter->promise() - ->finally(function (): int { // @phpstan-ignore-line - return 1; - }) + // @phpstan-ignore-line + ->finally(static fn(): int => 1) ->then($mock); } - /** @test */ + #[Test] public function finallyShouldNotSuppressValueWhenHandlerReturnsAPromiseForFulfilledPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $value = new stdClass(); + $value = new \stdClass(); $mock = $this->createCallableMock(); $mock @@ -284,18 +279,17 @@ public function finallyShouldNotSuppressValueWhenHandlerReturnsAPromiseForFulfil $adapter->resolve($value); $adapter->promise() - ->finally(function (): PromiseInterface { // @phpstan-ignore-line - return resolve(1); - }) + // @phpstan-ignore-line + ->finally(static fn(): PromiseInterface => resolve(1)) ->then($mock); } - /** @test */ + #[Test] public function finallyShouldRejectWhenHandlerThrowsForFulfilledPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -305,18 +299,18 @@ public function finallyShouldRejectWhenHandlerThrowsForFulfilledPromise(): void $adapter->resolve(1); $adapter->promise() - ->finally(function () use ($exception) { + ->finally(static function () use ($exception): void { throw $exception; }) ->then(null, $mock); } - /** @test */ + #[Test] public function finallyShouldRejectWhenHandlerRejectsForFulfilledPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -326,16 +320,14 @@ public function finallyShouldRejectWhenHandlerRejectsForFulfilledPromise(): void $adapter->resolve(1); $adapter->promise() - ->finally(function () use ($exception) { - return reject($exception); - }) + ->finally(static fn() => reject($exception)) ->then(null, $mock); } /** - * @test * @deprecated */ + #[Test] public function otherwiseShouldNotInvokeRejectionHandlerForFulfilledPromise(): void { $adapter = $this->getPromiseTestAdapter(); @@ -345,14 +337,14 @@ public function otherwiseShouldNotInvokeRejectionHandlerForFulfilledPromise(): v } /** - * @test * @deprecated */ + #[Test] public function alwaysShouldNotSuppressValueForFulfilledPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $value = new stdClass(); + $value = new \stdClass(); $mock = $this->createCallableMock(); $mock @@ -362,19 +354,19 @@ public function alwaysShouldNotSuppressValueForFulfilledPromise(): void $adapter->resolve($value); $adapter->promise() - ->always(function () {}) + ->always(static function (): void {}) ->then($mock); } /** - * @test * @deprecated */ + #[Test] public function alwaysShouldNotSuppressValueWhenHandlerReturnsANonPromiseForFulfilledPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $value = new stdClass(); + $value = new \stdClass(); $mock = $this->createCallableMock(); $mock @@ -384,21 +376,20 @@ public function alwaysShouldNotSuppressValueWhenHandlerReturnsANonPromiseForFulf $adapter->resolve($value); $adapter->promise() - ->always(function (): int { // @phpstan-ignore-line - return 1; - }) + // @phpstan-ignore-line + ->always(static fn(): int => 1) ->then($mock); } /** - * @test * @deprecated */ + #[Test] public function alwaysShouldNotSuppressValueWhenHandlerReturnsAPromiseForFulfilledPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $value = new stdClass(); + $value = new \stdClass(); $mock = $this->createCallableMock(); $mock @@ -408,21 +399,20 @@ public function alwaysShouldNotSuppressValueWhenHandlerReturnsAPromiseForFulfill $adapter->resolve($value); $adapter->promise() - ->always(function (): PromiseInterface { // @phpstan-ignore-line - return resolve(1); - }) + // @phpstan-ignore-line + ->always(static fn(): PromiseInterface => resolve(1)) ->then($mock); } /** - * @test * @deprecated */ + #[Test] public function alwaysShouldRejectWhenHandlerThrowsForFulfilledPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -432,21 +422,21 @@ public function alwaysShouldRejectWhenHandlerThrowsForFulfilledPromise(): void $adapter->resolve(1); $adapter->promise() - ->always(function () use ($exception) { + ->always(static function () use ($exception): void { throw $exception; }) ->then(null, $mock); } /** - * @test * @deprecated */ + #[Test] public function alwaysShouldRejectWhenHandlerRejectsForFulfilledPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -456,9 +446,7 @@ public function alwaysShouldRejectWhenHandlerRejectsForFulfilledPromise(): void $adapter->resolve(1); $adapter->promise() - ->always(function () use ($exception) { - return reject($exception); - }) + ->always(static fn() => reject($exception)) ->then(null, $mock); } } diff --git a/tests/PromiseTest/PromisePendingTestTrait.php b/tests/Unit/PromiseTest/PromisePendingTestTrait.php similarity index 81% rename from tests/PromiseTest/PromisePendingTestTrait.php rename to tests/Unit/PromiseTest/PromisePendingTestTrait.php index db5e2e4..dfd1680 100644 --- a/tests/PromiseTest/PromisePendingTestTrait.php +++ b/tests/Unit/PromiseTest/PromisePendingTestTrait.php @@ -1,15 +1,18 @@ getPromiseTestAdapter(); @@ -17,7 +20,7 @@ public function thenShouldReturnAPromiseForPendingPromise(): void self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->then()); } - /** @test */ + #[Test] public function thenShouldReturnAllowNullForPendingPromise(): void { $adapter = $this->getPromiseTestAdapter(); @@ -25,7 +28,7 @@ public function thenShouldReturnAllowNullForPendingPromise(): void self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->then(null, null)); } - /** @test */ + #[Test] public function catchShouldNotInvokeRejectionHandlerForPendingPromise(): void { $adapter = $this->getPromiseTestAdapter(); @@ -34,18 +37,18 @@ public function catchShouldNotInvokeRejectionHandlerForPendingPromise(): void $adapter->promise()->catch($this->expectCallableNever()); } - /** @test */ + #[Test] public function finallyShouldReturnAPromiseForPendingPromise(): void { $adapter = $this->getPromiseTestAdapter(); - self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->finally(function () {})); + self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->finally(static function (): void {})); } /** - * @test * @deprecated */ + #[Test] public function otherwiseShouldNotInvokeRejectionHandlerForPendingPromise(): void { $adapter = $this->getPromiseTestAdapter(); @@ -55,13 +58,13 @@ public function otherwiseShouldNotInvokeRejectionHandlerForPendingPromise(): voi } /** - * @test * @deprecated */ + #[Test] public function alwaysShouldReturnAPromiseForPendingPromise(): void { $adapter = $this->getPromiseTestAdapter(); - self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->always(function () {})); + self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->always(static function (): void {})); } } diff --git a/tests/PromiseTest/PromiseRejectedTestTrait.php b/tests/Unit/PromiseTest/PromiseRejectedTestTrait.php similarity index 76% rename from tests/PromiseTest/PromiseRejectedTestTrait.php rename to tests/Unit/PromiseTest/PromiseRejectedTestTrait.php index a125698..ee8f887 100644 --- a/tests/PromiseTest/PromiseRejectedTestTrait.php +++ b/tests/Unit/PromiseTest/PromiseRejectedTestTrait.php @@ -1,11 +1,13 @@ getPromiseTestAdapter(); - $exception1 = new Exception(); - $exception2 = new Exception(); + $exception1 = new \Exception(); + $exception2 = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -33,16 +35,16 @@ public function rejectedPromiseShouldBeImmutable(): void $adapter->promise() ->then( $this->expectCallableNever(), - $mock + $mock, ); } - /** @test */ + #[Test] public function rejectedPromiseShouldInvokeNewlyAddedCallback(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $adapter->reject($exception); @@ -56,7 +58,7 @@ public function rejectedPromiseShouldInvokeNewlyAddedCallback(): void ->then($this->expectCallableNever(), $mock); } - /** @test */ + #[Test] public function shouldForwardUndefinedRejectionValue(): void { $adapter = $this->getPromiseTestAdapter(); @@ -67,24 +69,24 @@ public function shouldForwardUndefinedRejectionValue(): void ->method('__invoke') ->with(null); - $adapter->reject(new Exception()); + $adapter->reject(new \Exception()); $adapter->promise() ->then( $this->expectCallableNever(), - function () { + static function (): void { // Presence of rejection handler is enough to switch back // to resolve mode, even though it returns undefined. // The ONLY way to propagate a rejection is to re-throw or // return a rejected promise; - } + }, ) ->then( $mock, - $this->expectCallableNever() + $this->expectCallableNever(), ); } - /** @test */ + #[Test] public function shouldSwitchFromErrbacksToCallbacksWhenErrbackDoesNotExplicitlyPropagate(): void { $adapter = $this->getPromiseTestAdapter(); @@ -95,21 +97,19 @@ public function shouldSwitchFromErrbacksToCallbacksWhenErrbackDoesNotExplicitlyP ->method('__invoke') ->with($this->identicalTo(2)); - $adapter->reject(new Exception()); + $adapter->reject(new \Exception()); $adapter->promise() ->then( $this->expectCallableNever(), - function () { - return 2; - } + static fn() => 2, ) ->then( $mock, - $this->expectCallableNever() + $this->expectCallableNever(), ); } - /** @test */ + #[Test] public function shouldSwitchFromErrbacksToCallbacksWhenErrbackReturnsAResolution(): void { $adapter = $this->getPromiseTestAdapter(); @@ -120,26 +120,24 @@ public function shouldSwitchFromErrbacksToCallbacksWhenErrbackReturnsAResolution ->method('__invoke') ->with($this->identicalTo(2)); - $adapter->reject(new Exception()); + $adapter->reject(new \Exception()); $adapter->promise() ->then( $this->expectCallableNever(), - function () { - return resolve(2); - } + static fn() => resolve(2), ) ->then( $mock, - $this->expectCallableNever() + $this->expectCallableNever(), ); } - /** @test */ + #[Test] public function shouldPropagateRejectionsWhenErrbackThrows(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -153,24 +151,24 @@ public function shouldPropagateRejectionsWhenErrbackThrows(): void ->method('__invoke') ->with($this->identicalTo($exception)); - $adapter->reject(new Exception()); + $adapter->reject(new \Exception()); $adapter->promise() ->then( $this->expectCallableNever(), - $mock + $mock, ) ->then( $this->expectCallableNever(), - $mock2 + $mock2, ); } - /** @test */ + #[Test] public function shouldPropagateRejectionsWhenErrbackReturnsARejection(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -178,26 +176,24 @@ public function shouldPropagateRejectionsWhenErrbackReturnsARejection(): void ->method('__invoke') ->with($this->identicalTo($exception)); - $adapter->reject(new Exception()); + $adapter->reject(new \Exception()); $adapter->promise() ->then( $this->expectCallableNever(), - function () use ($exception) { - return reject($exception); - } + static fn() => reject($exception), ) ->then( $this->expectCallableNever(), - $mock + $mock, ); } - /** @test */ + #[Test] public function catchShouldInvokeRejectionHandlerForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -209,12 +205,12 @@ public function catchShouldInvokeRejectionHandlerForRejectedPromise(): void $adapter->promise()->catch($mock); } - /** @test */ + #[Test] public function catchShouldInvokeNonTypeHintedRejectionHandlerIfReasonIsAnExceptionForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -224,17 +220,17 @@ public function catchShouldInvokeNonTypeHintedRejectionHandlerIfReasonIsAnExcept $adapter->reject($exception); $adapter->promise() - ->catch(function ($reason) use ($mock) { + ->catch(static function ($reason) use ($mock): void { $mock($reason); }); } - /** @test */ + #[Test] public function catchShouldInvokeRejectionHandlerIfReasonMatchesTypehintForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new InvalidArgumentException(); + $exception = new \InvalidArgumentException(); $mock = $this->createCallableMock(); $mock @@ -244,33 +240,33 @@ public function catchShouldInvokeRejectionHandlerIfReasonMatchesTypehintForRejec $adapter->reject($exception); $adapter->promise() - ->catch(function (InvalidArgumentException $reason) use ($mock) { + ->catch(static function (\InvalidArgumentException $reason) use ($mock): void { $mock($reason); }); } - /** @test */ + #[Test] public function catchShouldNotInvokeRejectionHandlerIfReaonsDoesNotMatchTypehintForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->expectCallableNever(); $adapter->reject($exception); $adapter->promise() - ->catch(function (InvalidArgumentException $reason) use ($mock) { + ->catch(static function (\InvalidArgumentException $reason) use ($mock): void { $mock($reason); })->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection } - /** @test */ + #[Test] public function finallyShouldNotSuppressRejectionForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -280,16 +276,16 @@ public function finallyShouldNotSuppressRejectionForRejectedPromise(): void $adapter->reject($exception); $adapter->promise() - ->finally(function () {}) + ->finally(static function (): void {}) ->then(null, $mock); } - /** @test */ + #[Test] public function finallyShouldNotSuppressRejectionWhenHandlerReturnsANonPromiseForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -299,18 +295,18 @@ public function finallyShouldNotSuppressRejectionWhenHandlerReturnsANonPromiseFo $adapter->reject($exception); $adapter->promise() - ->finally(function (): int { // @phpstan-ignore-line - return 1; - }) + ->finally(static fn(): int => + // @phpstan-ignore-line + 1) ->then(null, $mock); } - /** @test */ + #[Test] public function finallyShouldNotSuppressRejectionWhenHandlerReturnsAPromiseForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -320,19 +316,19 @@ public function finallyShouldNotSuppressRejectionWhenHandlerReturnsAPromiseForRe $adapter->reject($exception); $adapter->promise() - ->finally(function (): PromiseInterface { // @phpstan-ignore-line - return resolve(1); - }) + ->finally(static fn(): PromiseInterface => + // @phpstan-ignore-line + resolve(1)) ->then(null, $mock); } - /** @test */ + #[Test] public function finallyShouldRejectWhenHandlerThrowsForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception1 = new Exception(); - $exception2 = new Exception(); + $exception1 = new \Exception(); + $exception2 = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -342,19 +338,19 @@ public function finallyShouldRejectWhenHandlerThrowsForRejectedPromise(): void $adapter->reject($exception1); $adapter->promise() - ->finally(function () use ($exception2) { + ->finally(static function () use ($exception2): void { throw $exception2; }) ->then(null, $mock); } - /** @test */ + #[Test] public function finallyShouldRejectWhenHandlerRejectsForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception1 = new Exception(); - $exception2 = new Exception(); + $exception1 = new \Exception(); + $exception2 = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -364,31 +360,29 @@ public function finallyShouldRejectWhenHandlerRejectsForRejectedPromise(): void $adapter->reject($exception1); $adapter->promise() - ->finally(function () use ($exception2) { - return reject($exception2); - }) + ->finally(static fn() => reject($exception2)) ->then(null, $mock); } - /** @test */ + #[Test] public function cancelShouldHaveNoEffectForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter($this->expectCallableNever()); - $adapter->reject(new Exception()); + $adapter->reject(new \Exception()); $adapter->promise()->cancel(); } /** - * @test * @deprecated */ + #[Test] public function otherwiseShouldInvokeRejectionHandlerForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -401,14 +395,14 @@ public function otherwiseShouldInvokeRejectionHandlerForRejectedPromise(): void } /** - * @test * @deprecated */ + #[Test] public function otherwiseShouldInvokeNonTypeHintedRejectionHandlerIfReasonIsAnExceptionForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -418,20 +412,20 @@ public function otherwiseShouldInvokeNonTypeHintedRejectionHandlerIfReasonIsAnEx $adapter->reject($exception); $adapter->promise() - ->otherwise(function ($reason) use ($mock) { + ->otherwise(static function ($reason) use ($mock): void { $mock($reason); }); } /** - * @test * @deprecated */ + #[Test] public function otherwiseShouldInvokeRejectionHandlerIfReasonMatchesTypehintForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new InvalidArgumentException(); + $exception = new \InvalidArgumentException(); $mock = $this->createCallableMock(); $mock @@ -441,39 +435,39 @@ public function otherwiseShouldInvokeRejectionHandlerIfReasonMatchesTypehintForR $adapter->reject($exception); $adapter->promise() - ->otherwise(function (InvalidArgumentException $reason) use ($mock) { + ->otherwise(static function (\InvalidArgumentException $reason) use ($mock): void { $mock($reason); }); } /** - * @test * @deprecated */ + #[Test] public function otherwiseShouldNotInvokeRejectionHandlerIfReaonsDoesNotMatchTypehintForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->expectCallableNever(); $adapter->reject($exception); $adapter->promise() - ->otherwise(function (InvalidArgumentException $reason) use ($mock) { + ->otherwise(static function (\InvalidArgumentException $reason) use ($mock): void { $mock($reason); })->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection } /** - * @test * @deprecated */ + #[Test] public function alwaysShouldNotSuppressRejectionForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -483,43 +477,43 @@ public function alwaysShouldNotSuppressRejectionForRejectedPromise(): void $adapter->reject($exception); $adapter->promise() - ->always(function () {}) + ->always(static function (): void {}) ->then(null, $mock); } /** - * @test * @deprecated */ + #[Test] public function alwaysShouldNotSuppressRejectionWhenHandlerReturnsANonPromiseForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock - ->expects($this->once()) - ->method('__invoke') - ->with($this->identicalTo($exception)); + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); $adapter->reject($exception); $adapter->promise() - ->finally(function (): int { // @phpstan-ignore-line - return 1; - }) - ->then(null, $mock); + ->finally(static fn(): int => + // @phpstan-ignore-line + 1) + ->then(null, $mock); } /** - * @test * @deprecated */ + #[Test] public function alwaysShouldNotSuppressRejectionWhenHandlerReturnsAPromiseForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -529,22 +523,22 @@ public function alwaysShouldNotSuppressRejectionWhenHandlerReturnsAPromiseForRej $adapter->reject($exception); $adapter->promise() - ->always(function (): PromiseInterface { // @phpstan-ignore-line - return resolve(1); - }) + ->always(static fn(): PromiseInterface => + // @phpstan-ignore-line + resolve(1)) ->then(null, $mock); } /** - * @test * @deprecated */ + #[Test] public function alwaysShouldRejectWhenHandlerThrowsForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception1 = new Exception(); - $exception2 = new Exception(); + $exception1 = new \Exception(); + $exception2 = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -554,22 +548,22 @@ public function alwaysShouldRejectWhenHandlerThrowsForRejectedPromise(): void $adapter->reject($exception1); $adapter->promise() - ->always(function () use ($exception2) { + ->always(static function () use ($exception2): void { throw $exception2; }) ->then(null, $mock); } /** - * @test * @deprecated */ + #[Test] public function alwaysShouldRejectWhenHandlerRejectsForRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception1 = new Exception(); - $exception2 = new Exception(); + $exception1 = new \Exception(); + $exception2 = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -579,9 +573,7 @@ public function alwaysShouldRejectWhenHandlerRejectsForRejectedPromise(): void $adapter->reject($exception1); $adapter->promise() - ->always(function () use ($exception2) { - return reject($exception2); - }) + ->always(static fn() => reject($exception2)) ->then(null, $mock); } } diff --git a/tests/PromiseTest/PromiseSettledTestTrait.php b/tests/Unit/PromiseTest/PromiseSettledTestTrait.php similarity index 85% rename from tests/PromiseTest/PromiseSettledTestTrait.php rename to tests/Unit/PromiseTest/PromiseSettledTestTrait.php index 093ab8b..8699fdf 100644 --- a/tests/PromiseTest/PromiseSettledTestTrait.php +++ b/tests/Unit/PromiseTest/PromiseSettledTestTrait.php @@ -1,16 +1,19 @@ getPromiseTestAdapter(); @@ -23,7 +26,7 @@ public function thenShouldReturnAPromiseForSettledPromise(): void } } - /** @test */ + #[Test] public function thenShouldReturnAllowNullForSettledPromise(): void { $adapter = $this->getPromiseTestAdapter(); @@ -36,7 +39,7 @@ public function thenShouldReturnAllowNullForSettledPromise(): void } } - /** @test */ + #[Test] public function cancelShouldHaveNoEffectForSettledPromise(): void { $adapter = $this->getPromiseTestAdapter($this->expectCallableNever()); @@ -46,13 +49,13 @@ public function cancelShouldHaveNoEffectForSettledPromise(): void $adapter->promise()->cancel(); } - /** @test */ + #[Test] public function finallyShouldReturnAPromiseForSettledPromise(): void { $adapter = $this->getPromiseTestAdapter(); $adapter->settle(null); - self::assertInstanceOf(PromiseInterface::class, $promise = $adapter->promise()->finally(function () {})); + self::assertInstanceOf(PromiseInterface::class, $promise = $adapter->promise()->finally(static function (): void {})); if ($promise instanceof RejectedPromise) { $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection @@ -60,15 +63,15 @@ public function finallyShouldReturnAPromiseForSettledPromise(): void } /** - * @test * @deprecated */ + #[Test] public function alwaysShouldReturnAPromiseForSettledPromise(): void { $adapter = $this->getPromiseTestAdapter(); $adapter->settle(null); - self::assertInstanceOf(PromiseInterface::class, $promise = $adapter->promise()->always(function () {})); + self::assertInstanceOf(PromiseInterface::class, $promise = $adapter->promise()->always(static function (): void {})); if ($promise instanceof RejectedPromise) { $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection diff --git a/tests/PromiseTest/RejectTestTrait.php b/tests/Unit/PromiseTest/RejectTestTrait.php similarity index 76% rename from tests/PromiseTest/RejectTestTrait.php rename to tests/Unit/PromiseTest/RejectTestTrait.php index eb1d891..4451508 100644 --- a/tests/PromiseTest/RejectTestTrait.php +++ b/tests/Unit/PromiseTest/RejectTestTrait.php @@ -1,12 +1,13 @@ getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -33,12 +34,12 @@ public function rejectShouldRejectWithAnException(): void $adapter->reject($exception); } - /** @test */ + #[Test] public function rejectShouldForwardReasonWhenCallbackIsNull(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -48,24 +49,24 @@ public function rejectShouldForwardReasonWhenCallbackIsNull(): void $adapter->promise() ->then( - $this->expectCallableNever() + $this->expectCallableNever(), ) ->then( $this->expectCallableNever(), - $mock + $mock, ); $adapter->reject($exception); } - /** @test */ + #[Test] public function rejectShouldMakePromiseImmutable(): void { $adapter = $this->getPromiseTestAdapter(); - $exception1 = new Exception(); - $exception2 = new Exception(); - $exception3 = new Exception(); + $exception1 = new \Exception(); + $exception2 = new \Exception(); + $exception3 = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -74,26 +75,26 @@ public function rejectShouldMakePromiseImmutable(): void ->with($this->identicalTo($exception1)); $adapter->promise() - ->then(null, function (\Throwable $value) use ($exception3, $adapter): PromiseInterface { + ->then(null, static function (\Throwable $value) use ($exception3, $adapter): PromiseInterface { $adapter->reject($exception3); return reject($value); }) ->then( $this->expectCallableNever(), - $mock + $mock, ); $adapter->reject($exception1); $adapter->reject($exception2); } - /** @test */ + #[Test] public function rejectShouldInvokeCatchHandler(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -107,12 +108,12 @@ public function rejectShouldInvokeCatchHandler(): void $adapter->reject($exception); } - /** @test */ + #[Test] public function finallyShouldNotSuppressRejection(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -121,18 +122,18 @@ public function finallyShouldNotSuppressRejection(): void ->with($this->identicalTo($exception)); $adapter->promise() - ->finally(function () {}) + ->finally(static function (): void {}) ->then(null, $mock); $adapter->reject($exception); } - /** @test */ + #[Test] public function finallyShouldNotSuppressRejectionWhenHandlerReturnsANonPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -141,20 +142,19 @@ public function finallyShouldNotSuppressRejectionWhenHandlerReturnsANonPromise() ->with($this->identicalTo($exception)); $adapter->promise() - ->finally(function (): int { // @phpstan-ignore-line - return 1; - }) + // @phpstan-ignore-line + ->finally(static fn(): int => 1) ->then(null, $mock); $adapter->reject($exception); } - /** @test */ + #[Test] public function finallyShouldNotSuppressRejectionWhenHandlerReturnsAPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -163,20 +163,19 @@ public function finallyShouldNotSuppressRejectionWhenHandlerReturnsAPromise(): v ->with($this->identicalTo($exception)); $adapter->promise() - ->finally(function (): PromiseInterface { // @phpstan-ignore-line - return resolve(1); - }) + // @phpstan-ignore-line + ->finally(static fn(): PromiseInterface => resolve(1)) ->then(null, $mock); $adapter->reject($exception); } - /** @test */ + #[Test] public function finallyShouldRejectWhenHandlerThrowsForRejection(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -185,7 +184,7 @@ public function finallyShouldRejectWhenHandlerThrowsForRejection(): void ->with($this->identicalTo($exception)); $adapter->promise() - ->finally(function () use ($exception) { + ->finally(static function () use ($exception): void { throw $exception; }) ->then(null, $mock); @@ -193,12 +192,12 @@ public function finallyShouldRejectWhenHandlerThrowsForRejection(): void $adapter->reject($exception); } - /** @test */ + #[Test] public function finallyShouldRejectWhenHandlerRejectsForRejection(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -207,9 +206,7 @@ public function finallyShouldRejectWhenHandlerRejectsForRejection(): void ->with($this->identicalTo($exception)); $adapter->promise() - ->finally(function () use ($exception) { - return reject($exception); - }) + ->finally(static fn() => reject($exception)) ->then(null, $mock); $adapter->reject($exception); diff --git a/tests/PromiseTest/ResolveTestTrait.php b/tests/Unit/PromiseTest/ResolveTestTrait.php similarity index 79% rename from tests/PromiseTest/ResolveTestTrait.php rename to tests/Unit/PromiseTest/ResolveTestTrait.php index 2a23065..6594365 100644 --- a/tests/PromiseTest/ResolveTestTrait.php +++ b/tests/Unit/PromiseTest/ResolveTestTrait.php @@ -1,13 +1,13 @@ getPromiseTestAdapter(); @@ -32,7 +32,7 @@ public function resolveShouldResolve(): void $adapter->resolve(1); } - /** @test */ + #[Test] public function resolveShouldResolveWithPromisedValue(): void { $adapter = $this->getPromiseTestAdapter(); @@ -49,12 +49,12 @@ public function resolveShouldResolveWithPromisedValue(): void $adapter->resolve(resolve(1)); } - /** @test */ + #[Test] public function resolveShouldRejectWhenResolvedWithRejectedPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -68,7 +68,7 @@ public function resolveShouldRejectWhenResolvedWithRejectedPromise(): void $adapter->resolve(reject($exception)); } - /** @test */ + #[Test] public function resolveShouldForwardValueWhenCallbackIsNull(): void { $adapter = $this->getPromiseTestAdapter(); @@ -82,17 +82,17 @@ public function resolveShouldForwardValueWhenCallbackIsNull(): void $adapter->promise() ->then( null, - $this->expectCallableNever() + $this->expectCallableNever(), ) ->then( $mock, - $this->expectCallableNever() + $this->expectCallableNever(), ); $adapter->resolve(1); } - /** @test */ + #[Test] public function resolveShouldMakePromiseImmutable(): void { $adapter = $this->getPromiseTestAdapter(); @@ -104,23 +104,21 @@ public function resolveShouldMakePromiseImmutable(): void ->with($this->identicalTo(1)); $adapter->promise() - ->then(function ($value) use ($adapter) { + ->then(static function ($value) use ($adapter) { $adapter->resolve(3); return $value; }) ->then( $mock, - $this->expectCallableNever() + $this->expectCallableNever(), ); $adapter->resolve(1); $adapter->resolve(2); } - /** - * @test - */ + #[Test] public function resolveShouldRejectWhenResolvedWithItself(): void { $adapter = $this->getPromiseTestAdapter(); @@ -129,20 +127,18 @@ public function resolveShouldRejectWhenResolvedWithItself(): void $mock ->expects($this->once()) ->method('__invoke') - ->with(new LogicException('Cannot resolve a promise with itself.')); + ->with(new \LogicException('Cannot resolve a promise with itself.')); $adapter->promise() ->then( $this->expectCallableNever(), - $mock + $mock, ); $adapter->resolve($adapter->promise()); } - /** - * @test - */ + #[Test] public function resolveShouldRejectWhenResolvedWithAPromiseWhichFollowsItself(): void { $adapter1 = $this->getPromiseTestAdapter(); @@ -152,7 +148,7 @@ public function resolveShouldRejectWhenResolvedWithAPromiseWhichFollowsItself(): $mock ->expects($this->once()) ->method('__invoke') - ->with(new LogicException('Cannot resolve a promise with itself.')); + ->with(new \LogicException('Cannot resolve a promise with itself.')); $promise1 = $adapter1->promise(); @@ -160,19 +156,19 @@ public function resolveShouldRejectWhenResolvedWithAPromiseWhichFollowsItself(): $promise2->then( $this->expectCallableNever(), - $mock + $mock, ); $adapter1->resolve($promise2); $adapter2->resolve($promise1); } - /** @test */ + #[Test] public function finallyShouldNotSuppressValue(): void { $adapter = $this->getPromiseTestAdapter(); - $value = new stdClass(); + $value = new \stdClass(); $mock = $this->createCallableMock(); $mock @@ -181,18 +177,18 @@ public function finallyShouldNotSuppressValue(): void ->with($this->identicalTo($value)); $adapter->promise() - ->finally(function () {}) + ->finally(static function (): void {}) ->then($mock); $adapter->resolve($value); } - /** @test */ + #[Test] public function finallyShouldNotSuppressValueWhenHandlerReturnsANonPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $value = new stdClass(); + $value = new \stdClass(); $mock = $this->createCallableMock(); $mock @@ -201,20 +197,19 @@ public function finallyShouldNotSuppressValueWhenHandlerReturnsANonPromise(): vo ->with($this->identicalTo($value)); $adapter->promise() - ->finally(function (): int { // @phpstan-ignore-line - return 1; - }) + // @phpstan-ignore-line + ->finally(static fn(): int => 1) ->then($mock); $adapter->resolve($value); } - /** @test */ + #[Test] public function finallyShouldNotSuppressValueWhenHandlerReturnsAPromise(): void { $adapter = $this->getPromiseTestAdapter(); - $value = new stdClass(); + $value = new \stdClass(); $mock = $this->createCallableMock(); $mock @@ -223,20 +218,20 @@ public function finallyShouldNotSuppressValueWhenHandlerReturnsAPromise(): void ->with($this->identicalTo($value)); $adapter->promise() - ->finally(function (): PromiseInterface { // @phpstan-ignore-line - return resolve(1); - }) + // @phpstan-ignore-line + ->finally(static fn(): PromiseInterface => + resolve(1)) ->then($mock); $adapter->resolve($value); } - /** @test */ + #[Test] public function finallyShouldRejectWhenHandlerThrowsForFulfillment(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -245,7 +240,7 @@ public function finallyShouldRejectWhenHandlerThrowsForFulfillment(): void ->with($this->identicalTo($exception)); $adapter->promise() - ->finally(function () use ($exception) { + ->finally(static function () use ($exception): void { throw $exception; }) ->then(null, $mock); @@ -253,12 +248,12 @@ public function finallyShouldRejectWhenHandlerThrowsForFulfillment(): void $adapter->resolve(1); } - /** @test */ + #[Test] public function finallyShouldRejectWhenHandlerRejectsForFulfillment(): void { $adapter = $this->getPromiseTestAdapter(); - $exception = new Exception(); + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock @@ -267,9 +262,7 @@ public function finallyShouldRejectWhenHandlerRejectsForFulfillment(): void ->with($this->identicalTo($exception)); $adapter->promise() - ->finally(function () use ($exception) { - return reject($exception); - }) + ->finally(static fn() => reject($exception)) ->then(null, $mock); $adapter->resolve(1); diff --git a/tests/TestCase.php b/tests/Unit/TestCase.php similarity index 72% rename from tests/TestCase.php rename to tests/Unit/TestCase.php index 108e8ef..1542d98 100644 --- a/tests/TestCase.php +++ b/tests/Unit/TestCase.php @@ -1,8 +1,9 @@ createCallableMock(); $mock->expects(self::exactly($amount))->method('__invoke'); - assert(is_callable($mock)); + \assert(\is_callable($mock)); return $mock; } @@ -21,7 +22,7 @@ public function expectCallableOnce(): callable { $mock = $this->createCallableMock(); $mock->expects(self::once())->method('__invoke'); - assert(is_callable($mock)); + \assert(\is_callable($mock)); return $mock; } @@ -30,23 +31,25 @@ public function expectCallableNever(): callable { $mock = $this->createCallableMock(); $mock->expects(self::never())->method('__invoke'); - assert(is_callable($mock)); + \assert(\is_callable($mock)); return $mock; } - /** @return MockObject&callable */ + /** + * @return MockObject&callable + */ protected function createCallableMock(): MockObject { $builder = $this->getMockBuilder(\stdClass::class); - if (method_exists($builder, 'addMethods')) { + if (\method_exists($builder, 'addMethods')) { // PHPUnit 9+ $mock = $builder->addMethods(['__invoke'])->getMock(); } else { // legacy PHPUnit 4 - PHPUnit 9 - $mock = $builder->setMethods(['__invoke'])->getMock(); + $mock = $builder->getMock(); } - assert($mock instanceof MockObject && is_callable($mock)); + \assert($mock instanceof MockObject && \is_callable($mock)); return $mock; } diff --git a/tests/fixtures/CallbackWithDNFTypehintClass.php b/tests/fixtures/CallbackWithDNFTypehintClass.php deleted file mode 100644 index b7d773b..0000000 --- a/tests/fixtures/CallbackWithDNFTypehintClass.php +++ /dev/null @@ -1,18 +0,0 @@ -