diff --git a/docs/class-reference.md b/docs/class-reference.md index fb0cf6bd1..ebdba9ebc 100644 --- a/docs/class-reference.md +++ b/docs/class-reference.md @@ -1805,6 +1805,8 @@ function toArray(int $debug = 'GraphQL\\Error\\DebugFlag::NONE'): array Provides a means for integration of async PHP platforms ([related docs](data-fetching.md#async-php)). +@template TAdopted = mixed + ### GraphQL\Executor\Promise\PromiseAdapter Methods ```php @@ -1824,6 +1826,8 @@ function isThenable($value): bool * * @param mixed $thenable * + * @phpstan-return Promise + * * @api */ function convertThenable($thenable): GraphQL\Executor\Promise\Promise @@ -1834,6 +1838,10 @@ function convertThenable($thenable): GraphQL\Executor\Promise\Promise * Accepts our Promise wrapper, extracts adopted promise out of it and executes actual `then` logic described * in Promises/A+ specs. Then returns new wrapped instance of GraphQL\Executor\Promise\Promise. * + * @phpstan-param Promise $promise + * + * @phpstan-return Promise + * * @api */ function then( @@ -1849,6 +1857,8 @@ function then( * * @param callable(callable $resolve, callable $reject): void $resolver * + * @phpstan-return Promise + * * @api */ function create(callable $resolver): GraphQL\Executor\Promise\Promise @@ -1860,6 +1870,8 @@ function create(callable $resolver): GraphQL\Executor\Promise\Promise * * @param mixed $value * + * @phpstan-return Promise + * * @api */ function createFulfilled($value = null): GraphQL\Executor\Promise\Promise @@ -1867,9 +1879,9 @@ function createFulfilled($value = null): GraphQL\Executor\Promise\Promise ```php /** - * Creates a rejected promise for a reason if the reason is not a promise. + * Return a promise rejected with the given reason. * - * If the provided reason is a promise, then it is returned as-is. + * @phpstan-return Promise * * @api */ @@ -1883,6 +1895,8 @@ function createRejected(Throwable $reason): GraphQL\Executor\Promise\Promise * * @param iterable $promisesOrValues * + * @phpstan-return Promise + * * @api */ function all(iterable $promisesOrValues): GraphQL\Executor\Promise\Promise diff --git a/phpstan.neon.dist b/phpstan.neon.dist index bb358463a..ec13cceaf 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -61,6 +61,22 @@ parameters: - src/Executor/Promise - examples/04-async-php + # The v2 AmpPromiseAdapter wraps concrete Amp\Success/Amp\Failure; against the + # invariant Amp\Promise binding PHPStan reports a return mismatch (as + # Amp\Success on a v2 install, or bare Amp\Success when v2 is absent on a + # v3 install). Tolerate either form / either state. + - message: "~Method GraphQL\\\\Executor\\\\Promise\\\\Adapter\\\\AmpPromiseAdapter::create(Fulfilled|Rejected)\\(\\) should return GraphQL\\\\Executor\\\\Promise\\\\Promise> but returns GraphQL\\\\Executor\\\\Promise\\\\Promise<(Amp\\\\Promise\\|)?Amp\\\\(Success|Failure)()?(\\|Amp\\\\Promise)?>~" + reportUnmatched: false + path: src/Executor/Promise/Adapter/AmpPromiseAdapter.php + + # AmpFutureAdapter::createFulfilled()/createRejected() build a concrete Amp\Future + # whose generic arg PHPStan infers narrower than the invariant Amp\Future + # binding. Only reported when amphp/amp v3 is installed (absent under v2), so + # tolerate either state. + - message: "~Method GraphQL\\\\Executor\\\\Promise\\\\Adapter\\\\AmpFutureAdapter::create(Fulfilled|Rejected)\\(\\) should return GraphQL\\\\Executor\\\\Promise\\\\Promise> but returns GraphQL\\\\Executor\\\\Promise\\\\Promise)?>~" + reportUnmatched: false + path: src/Executor/Promise/Adapter/AmpFutureAdapter.php + includes: - phpstan-baseline.neon - phpstan/include-by-php-version.php diff --git a/src/Executor/Promise/Adapter/AmpFutureAdapter.php b/src/Executor/Promise/Adapter/AmpFutureAdapter.php index 556ceae48..0a75e7cce 100644 --- a/src/Executor/Promise/Adapter/AmpFutureAdapter.php +++ b/src/Executor/Promise/Adapter/AmpFutureAdapter.php @@ -15,6 +15,8 @@ * Allows integration with amphp/amp v3 (fiber-based futures). * * @see https://amphp.org/amp + * + * @implements PromiseAdapter> */ class AmpFutureAdapter implements PromiseAdapter { @@ -23,17 +25,26 @@ public function isThenable($value): bool return $value instanceof Future; } - /** @throws InvariantViolation */ + /** + * @throws InvariantViolation + * + * @phpstan-return Promise> + */ public function convertThenable($thenable): Promise { return new Promise($thenable, $this); } - /** @throws InvariantViolation */ + /** + * @phpstan-param Promise> $promise + * + * @throws InvariantViolation + * + * @phpstan-return Promise> + */ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise { $future = $promise->adoptedPromise; - assert($future instanceof Future); $next = async(static function () use ($future, $onFulfilled, $onRejected) { try { @@ -80,6 +91,8 @@ static function (\Throwable $exception) use ($deferred): void { /** * @throws \Error * @throws InvariantViolation + * + * @phpstan-return Promise> */ public function createFulfilled($value = null): Promise { @@ -94,7 +107,11 @@ public function createFulfilled($value = null): Promise return new Promise(Future::complete($value), $this); } - /** @throws InvariantViolation */ + /** + * @throws InvariantViolation + * + * @phpstan-return Promise> + */ public function createRejected(\Throwable $reason): Promise { return new Promise(Future::error($reason), $this); diff --git a/src/Executor/Promise/Adapter/AmpPromiseAdapter.php b/src/Executor/Promise/Adapter/AmpPromiseAdapter.php index 25e9fc84b..bc7bad890 100644 --- a/src/Executor/Promise/Adapter/AmpPromiseAdapter.php +++ b/src/Executor/Promise/Adapter/AmpPromiseAdapter.php @@ -12,6 +12,9 @@ use function Amp\Promise\all; +/** + * @implements PromiseAdapter> + */ class AmpPromiseAdapter implements PromiseAdapter { public function isThenable($value): bool @@ -25,7 +28,13 @@ public function convertThenable($thenable): Promise return new Promise($thenable, $this); } - /** @throws InvariantViolation */ + /** + * @phpstan-param Promise> $promise + * + * @throws InvariantViolation + * + * @phpstan-return Promise> + */ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise { $deferred = new Deferred(); @@ -42,7 +51,6 @@ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable }; $ampPromise = $promise->adoptedPromise; - assert($ampPromise instanceof AmpPromise); $ampPromise->onResolve($onResolve); return new Promise($deferred->promise(), $this); @@ -68,6 +76,8 @@ static function (\Throwable $exception) use ($deferred): void { /** * @throws \Error * @throws InvariantViolation + * + * @phpstan-return Promise> */ public function createFulfilled($value = null): Promise { @@ -76,7 +86,11 @@ public function createFulfilled($value = null): Promise return new Promise($promise, $this); } - /** @throws InvariantViolation */ + /** + * @throws InvariantViolation + * + * @phpstan-return Promise> + */ public function createRejected(\Throwable $reason): Promise { $promise = new Failure($reason); diff --git a/src/Executor/Promise/Adapter/ReactPromiseAdapter.php b/src/Executor/Promise/Adapter/ReactPromiseAdapter.php index 7eb153c13..683bedb65 100644 --- a/src/Executor/Promise/Adapter/ReactPromiseAdapter.php +++ b/src/Executor/Promise/Adapter/ReactPromiseAdapter.php @@ -12,6 +12,9 @@ use function React\Promise\reject; use function React\Promise\resolve; +/** + * @implements PromiseAdapter> + */ class ReactPromiseAdapter implements PromiseAdapter { public function isThenable($value): bool @@ -25,16 +28,25 @@ public function convertThenable($thenable): Promise return new Promise($thenable, $this); } - /** @throws InvariantViolation */ + /** + * @phpstan-param Promise> $promise + * + * @throws InvariantViolation + * + * @phpstan-return Promise> + */ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise { $reactPromise = $promise->adoptedPromise; - assert($reactPromise instanceof ReactPromiseInterface); return new Promise($reactPromise->then($onFulfilled, $onRejected), $this); } - /** @throws InvariantViolation */ + /** + * @throws InvariantViolation + * + * @phpstan-return Promise> + */ public function create(callable $resolver): Promise { $reactPromise = new ReactPromise($resolver); @@ -42,7 +54,11 @@ public function create(callable $resolver): Promise return new Promise($reactPromise, $this); } - /** @throws InvariantViolation */ + /** + * @throws InvariantViolation + * + * @phpstan-return Promise> + */ public function createFulfilled($value = null): Promise { $reactPromise = resolve($value); @@ -50,15 +66,24 @@ public function createFulfilled($value = null): Promise return new Promise($reactPromise, $this); } - /** @throws InvariantViolation */ + /** + * @throws InvariantViolation + * + * @phpstan-return Promise> + */ public function createRejected(\Throwable $reason): Promise { + /** @var ReactPromiseInterface $reactPromise */ $reactPromise = reject($reason); return new Promise($reactPromise, $this); } - /** @throws InvariantViolation */ + /** + * @throws InvariantViolation + * + * @phpstan-return Promise> + */ public function all(iterable $promisesOrValues): Promise { foreach ($promisesOrValues as &$promiseOrValue) { @@ -70,6 +95,7 @@ public function all(iterable $promisesOrValues): Promise $promisesOrValuesArray = is_array($promisesOrValues) ? $promisesOrValues : iterator_to_array($promisesOrValues); + /** @var ReactPromiseInterface $reactPromise */ $reactPromise = all($promisesOrValuesArray)->then(static fn (array $values): array => array_map( static fn ($key) => $values[$key], array_keys($promisesOrValuesArray), diff --git a/src/Executor/Promise/Adapter/SyncPromiseAdapter.php b/src/Executor/Promise/Adapter/SyncPromiseAdapter.php index d59151c8c..67c262e4b 100644 --- a/src/Executor/Promise/Adapter/SyncPromiseAdapter.php +++ b/src/Executor/Promise/Adapter/SyncPromiseAdapter.php @@ -11,6 +11,8 @@ /** * Allows changing order of field resolution even in sync environments * (by leveraging queue of deferreds and promises). + * + * @implements PromiseAdapter */ class SyncPromiseAdapter implements PromiseAdapter { @@ -32,11 +34,16 @@ public function convertThenable($thenable): Promise return new Promise($thenable, $this); } - /** @throws InvariantViolation */ + /** + * @phpstan-param Promise $promise + * + * @throws InvariantViolation + * + * @phpstan-return Promise + */ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise { $syncPromise = $promise->adoptedPromise; - assert($syncPromise instanceof SyncPromise); return new Promise($syncPromise->then($onFulfilled, $onRejected), $this); } diff --git a/src/Executor/Promise/Promise.php b/src/Executor/Promise/Promise.php index 42c307b28..0eb420b69 100644 --- a/src/Executor/Promise/Promise.php +++ b/src/Executor/Promise/Promise.php @@ -2,24 +2,34 @@ namespace GraphQL\Executor\Promise; -use Amp\Future as AmpFuture; -use Amp\Promise as AmpPromise; use GraphQL\Error\InvariantViolation; -use GraphQL\Executor\Promise\Adapter\SyncPromise; -use React\Promise\PromiseInterface as ReactPromise; /** * Convenience wrapper for promises represented by Promise Adapter. + * + * The adopted promise is whatever the configured adapter produces (e.g. + * {@see \GraphQL\Executor\Promise\Adapter\SyncPromise}, a ReactPHP promise or an + * amphp Future). It is kept as a generic so the concrete platform type never has + * to be named in this class, which would otherwise require importing a class + * that may not exist for the installed platform. + * + * @template TAdopted = mixed */ class Promise { - /** @var SyncPromise|ReactPromise|AmpFuture|AmpPromise */ + /** + * @phpstan-var TAdopted + * + * @readonly + */ public $adoptedPromise; + /** @phpstan-var PromiseAdapter */ private PromiseAdapter $adapter; /** - * @param mixed $adoptedPromise + * @phpstan-param TAdopted $adoptedPromise + * @phpstan-param PromiseAdapter $adapter * * @throws InvariantViolation */ @@ -34,6 +44,7 @@ public function __construct($adoptedPromise, PromiseAdapter $adapter) $this->adapter = $adapter; } + /** @phpstan-return Promise */ public function then(?callable $onFulfilled = null, ?callable $onRejected = null): Promise { return $this->adapter->then($this, $onFulfilled, $onRejected); diff --git a/src/Executor/Promise/PromiseAdapter.php b/src/Executor/Promise/PromiseAdapter.php index a91ee243d..89e1f8e63 100644 --- a/src/Executor/Promise/PromiseAdapter.php +++ b/src/Executor/Promise/PromiseAdapter.php @@ -4,6 +4,8 @@ /** * Provides a means for integration of async PHP platforms ([related docs](data-fetching.md#async-php)). + * + * @template TAdopted = mixed */ interface PromiseAdapter { @@ -21,6 +23,8 @@ public function isThenable($value): bool; * * @param mixed $thenable * + * @phpstan-return Promise + * * @api */ public function convertThenable($thenable): Promise; @@ -29,6 +33,10 @@ public function convertThenable($thenable): Promise; * Accepts our Promise wrapper, extracts adopted promise out of it and executes actual `then` logic described * in Promises/A+ specs. Then returns new wrapped instance of GraphQL\Executor\Promise\Promise. * + * @phpstan-param Promise $promise + * + * @phpstan-return Promise + * * @api */ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise; @@ -38,6 +46,8 @@ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable * * @param callable(callable $resolve, callable $reject): void $resolver * + * @phpstan-return Promise + * * @api */ public function create(callable $resolver): Promise; @@ -47,14 +57,16 @@ public function create(callable $resolver): Promise; * * @param mixed $value * + * @phpstan-return Promise + * * @api */ public function createFulfilled($value = null): Promise; /** - * Creates a rejected promise for a reason if the reason is not a promise. + * Return a promise rejected with the given reason. * - * If the provided reason is a promise, then it is returned as-is. + * @phpstan-return Promise * * @api */ @@ -66,6 +78,8 @@ public function createRejected(\Throwable $reason): Promise; * * @param iterable $promisesOrValues * + * @phpstan-return Promise + * * @api */ public function all(iterable $promisesOrValues): Promise; diff --git a/tests/Executor/Promise/AmpFutureAdapterTest.php b/tests/Executor/Promise/AmpFutureAdapterTest.php index 5f4b0539e..a8bbb4663 100644 --- a/tests/Executor/Promise/AmpFutureAdapterTest.php +++ b/tests/Executor/Promise/AmpFutureAdapterTest.php @@ -49,7 +49,7 @@ public function testConvertsAmpFuturesToGraphQLOnes(): void $promise = $ampAdapter->convertThenable($future); - self::assertInstanceOf(Future::class, $promise->adoptedPromise); + self::assertSame($future, $promise->adoptedPromise); } public function testThen(): void @@ -67,8 +67,6 @@ static function ($value) use (&$result): void { } ); - self::assertInstanceOf(Future::class, $resultPromise->adoptedPromise); - $resultPromise->adoptedPromise->await(); self::assertSame(1, $result); @@ -81,17 +79,13 @@ public function testCreate(): void $resolve(1); }); - self::assertInstanceOf(Future::class, $resolvedPromise->adoptedPromise); - $result = null; $resultPromise = $resolvedPromise->then(static function ($value) use (&$result): void { $result = $value; }); - $resultFuture = $resultPromise->adoptedPromise; - self::assertInstanceOf(Future::class, $resultFuture); - $resultFuture->await(); + $resultPromise->adoptedPromise->await(); self::assertSame(1, $result); } @@ -101,17 +95,13 @@ public function testCreateFulfilled(): void $ampAdapter = new AmpFutureAdapter(); $fulfilledPromise = $ampAdapter->createFulfilled(1); - self::assertInstanceOf(Future::class, $fulfilledPromise->adoptedPromise); - $result = null; $resultPromise = $fulfilledPromise->then(static function ($value) use (&$result): void { $result = $value; }); - $resultFuture = $resultPromise->adoptedPromise; - self::assertInstanceOf(Future::class, $resultFuture); - $resultFuture->await(); + $resultPromise->adoptedPromise->await(); self::assertSame(1, $result); } @@ -121,8 +111,6 @@ public function testCreateRejected(): void $ampAdapter = new AmpFutureAdapter(); $rejectedPromise = $ampAdapter->createRejected(new \Exception('I am a bad promise')); - self::assertInstanceOf(Future::class, $rejectedPromise->adoptedPromise); - $exception = null; $resultPromise = $rejectedPromise->then( @@ -132,9 +120,7 @@ static function ($error) use (&$exception): void { } ); - $resultFuture = $resultPromise->adoptedPromise; - self::assertInstanceOf(Future::class, $resultFuture); - $resultFuture->await(); + $resultPromise->adoptedPromise->await(); self::assertInstanceOf(\Throwable::class, $exception); self::assertSame('I am a bad promise', $exception->getMessage()); @@ -147,8 +133,6 @@ public function testAll(): void $allPromise = $ampAdapter->all($promises); - self::assertInstanceOf(Future::class, $allPromise->adoptedPromise); - $result = $allPromise->adoptedPromise->await(); self::assertSame([1, 2, 3], $result); @@ -164,9 +148,7 @@ public function testAllShouldPreserveTheOrderOfTheArrayWhenResolvingAsyncPromise $deferred->complete(3); - $allFuture = $allPromise->adoptedPromise; - self::assertInstanceOf(Future::class, $allFuture); - $result = $allFuture->await(); + $result = $allPromise->adoptedPromise->await(); self::assertSame([1, 2, 3, 4], $result); } diff --git a/tests/Executor/Promise/AmpPromiseAdapterTest.php b/tests/Executor/Promise/AmpPromiseAdapterTest.php index b416a7d3b..616805cda 100644 --- a/tests/Executor/Promise/AmpPromiseAdapterTest.php +++ b/tests/Executor/Promise/AmpPromiseAdapterTest.php @@ -76,7 +76,6 @@ static function ($value) use (&$result): void { ); self::assertSame(1, $result); - self::assertInstanceOf(Promise::class, $resultPromise->adoptedPromise); } public function testCreate(): void @@ -86,8 +85,6 @@ public function testCreate(): void $resolve(1); }); - self::assertInstanceOf(Promise::class, $resolvedPromise->adoptedPromise); - $result = null; $resolvedPromise->then(static function ($value) use (&$result): void { @@ -140,8 +137,6 @@ public function testAll(): void $allPromise = $ampAdapter->all($promises); - self::assertInstanceOf(Promise::class, $allPromise->adoptedPromise); - $result = null; $allPromise->then(static function ($values) use (&$result): void { diff --git a/tests/Executor/Promise/SyncPromiseAdapterTest.php b/tests/Executor/Promise/SyncPromiseAdapterTest.php index 4c5465f9d..3348fcebf 100644 --- a/tests/Executor/Promise/SyncPromiseAdapterTest.php +++ b/tests/Executor/Promise/SyncPromiseAdapterTest.php @@ -37,7 +37,7 @@ public function testConvert(): void $dfd = new Deferred(static function (): void {}); $result = $this->promises->convertThenable($dfd); - self::assertInstanceOf(SyncPromise::class, $result->adoptedPromise); + self::assertSame($dfd, $result->adoptedPromise); $this->expectException(InvariantViolation::class); $this->expectExceptionMessage('Expected instance of GraphQL\Deferred, got (empty string)'); @@ -51,14 +51,12 @@ public function testThen(): void $result = $this->promises->then($promise); - self::assertInstanceOf(SyncPromise::class, $result->adoptedPromise); + self::assertNotSame($promise, $result); } public function testCreatePromise(): void { - $promise = $this->promises->create(static function ($resolve, $reject): void {}); - - self::assertInstanceOf(SyncPromise::class, $promise->adoptedPromise); + $this->promises->create(static function ($resolve, $reject): void {}); $promise = $this->promises->create(static function ($resolve, $reject): void { $resolve('A'); @@ -200,13 +198,9 @@ public function testWait(): void // until all pending promises are resolved self::assertSame(2, $result); - $p3AdoptedPromise = $p3->adoptedPromise; - self::assertInstanceOf(SyncPromise::class, $p3AdoptedPromise); - self::assertSame(SyncPromise::FULFILLED, $p3AdoptedPromise->state); + self::assertSame(SyncPromise::FULFILLED, $p3->adoptedPromise->state); - $allAdoptedPromise = $all->adoptedPromise; - self::assertInstanceOf(SyncPromise::class, $allAdoptedPromise); - self::assertSame(SyncPromise::FULFILLED, $allAdoptedPromise->state); + self::assertSame(SyncPromise::FULFILLED, $all->adoptedPromise->state); self::assertSame([1, 2, 3, 4], $called);