Skip to content

Commit fe528f8

Browse files
committed
Add template annotations
These annotations will aid static analyses like PHPStan and Psalm to enhance type-safety for this project and projects depending on it These changes make the following example understandable by PHPStan: ```php final readonly class User { public function __construct( public string $name, ) } /** * \React\Promise\PromiseInterface<User> */ function getCurrentUserFromDatabase(): \React\Promise\PromiseInterface { // The following line would do the database query and fetch the result from it // but keeping it simple for the sake of the example. return \React\Promise\resolve(new User('WyriHaximus')); } // For the sake of this example we're going to assume the following code runs // in \React\Async\async call echo await(getCurrentUserFromDatabase())->name; // This echos: WyriHaximus ```
1 parent 307684c commit fe528f8

13 files changed

+262
-29
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ await($promise);
226226

227227
### await()
228228

229-
The `await(PromiseInterface $promise): mixed` function can be used to
229+
The `await(PromiseInterface<T> $promise): T` function can be used to
230230
block waiting for the given `$promise` to be fulfilled.
231231

232232
```php
@@ -278,7 +278,7 @@ try {
278278

279279
### coroutine()
280280

281-
The `coroutine(callable $function, mixed ...$args): PromiseInterface<mixed>` function can be used to
281+
The `coroutine(callable(mixed ...$args):(\Generator<mixed,PromiseInterface<T>,mixed,mixed>|T) $function, mixed ...$args): PromiseInterface<T>` function can be used to
282282
execute a Generator-based coroutine to "await" promises.
283283

284284
```php
@@ -498,7 +498,7 @@ Loop::addTimer(2.0, function () use ($promise): void {
498498

499499
### parallel()
500500

501-
The `parallel(iterable<callable():PromiseInterface<mixed>> $tasks): PromiseInterface<array<mixed>>` function can be used
501+
The `parallel(iterable<callable():PromiseInterface<T>> $tasks): PromiseInterface<array<T>>` function can be used
502502
like this:
503503

504504
```php
@@ -540,7 +540,7 @@ React\Async\parallel([
540540

541541
### series()
542542

543-
The `series(iterable<callable():PromiseInterface<mixed>> $tasks): PromiseInterface<array<mixed>>` function can be used
543+
The `series(iterable<callable():PromiseInterface<T>> $tasks): PromiseInterface<array<T>>` function can be used
544544
like this:
545545

546546
```php
@@ -582,7 +582,7 @@ React\Async\series([
582582

583583
### waterfall()
584584

585-
The `waterfall(iterable<callable(mixed=):PromiseInterface<mixed>> $tasks): PromiseInterface<mixed>` function can be used
585+
The `waterfall(iterable<callable(mixed=):PromiseInterface<T>> $tasks): PromiseInterface<T>` function can be used
586586
like this:
587587

588588
```php

src/FiberMap.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66

77
/**
88
* @internal
9+
*
10+
* @template T
911
*/
1012
final class FiberMap
1113
{
1214
/** @var array<int,bool> */
1315
private static array $status = [];
1416

15-
/** @var array<int,PromiseInterface> */
17+
/** @var array<int,PromiseInterface<T>> */
1618
private static array $map = [];
1719

1820
/** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
@@ -27,19 +29,28 @@ public static function cancel(\Fiber $fiber): void
2729
self::$status[\spl_object_id($fiber)] = true;
2830
}
2931

30-
/** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
32+
/**
33+
* @param \Fiber<mixed,mixed,mixed,mixed> $fiber
34+
* @param PromiseInterface<T> $promise
35+
*/
3136
public static function setPromise(\Fiber $fiber, PromiseInterface $promise): void
3237
{
3338
self::$map[\spl_object_id($fiber)] = $promise;
3439
}
3540

36-
/** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
41+
/**
42+
* @param \Fiber<mixed,mixed,mixed,mixed> $fiber
43+
* @param PromiseInterface<T> $promise
44+
*/
3745
public static function unsetPromise(\Fiber $fiber, PromiseInterface $promise): void
3846
{
3947
unset(self::$map[\spl_object_id($fiber)]);
4048
}
4149

42-
/** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
50+
/**
51+
* @param \Fiber<mixed,mixed,mixed,mixed> $fiber
52+
* @return ?PromiseInterface<T>
53+
*/
4354
public static function getPromise(\Fiber $fiber): ?PromiseInterface
4455
{
4556
return self::$map[\spl_object_id($fiber)] ?? null;

src/functions.php

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,11 @@
176176
* await($promise);
177177
* ```
178178
*
179-
* @param callable $function
180-
* @return callable(mixed ...): PromiseInterface<mixed>
179+
* @template T
180+
* @template TFulfilled as PromiseInterface<T>|T
181+
* @template A
182+
* @param (callable(): TFulfilled)|(callable(A): TFulfilled) $function
183+
* @return callable(mixed ...$args): PromiseInterface<T>
181184
* @since 4.0.0
182185
* @see coroutine()
183186
*/
@@ -268,8 +271,9 @@ function async(callable $function): callable
268271
* }
269272
* ```
270273
*
271-
* @param PromiseInterface $promise
272-
* @return mixed returns whatever the promise resolves to
274+
* @template T
275+
* @param PromiseInterface<T> $promise
276+
* @return T
273277
* @throws \Exception when the promise is rejected with an `Exception`
274278
* @throws \Throwable when the promise is rejected with a `Throwable`
275279
* @throws \UnexpectedValueException when the promise is rejected with an unexpected value (Promise API v1 or v2 only)
@@ -279,6 +283,10 @@ function await(PromiseInterface $promise): mixed
279283
$fiber = null;
280284
$resolved = false;
281285
$rejected = false;
286+
287+
/**
288+
* @var T $resolvedValue
289+
*/
282290
$resolvedValue = null;
283291
$rejectedThrowable = null;
284292
$lowLevelFiber = \Fiber::getCurrent();
@@ -292,6 +300,9 @@ function (mixed $value) use (&$resolved, &$resolvedValue, &$fiber, $lowLevelFibe
292300
/** @var ?\Fiber<mixed,mixed,mixed,mixed> $fiber */
293301
if ($fiber === null) {
294302
$resolved = true;
303+
/**
304+
* @var T $resolvedValue
305+
*/
295306
$resolvedValue = $value;
296307
return;
297308
}
@@ -305,7 +316,7 @@ function (mixed $throwable) use (&$rejected, &$rejectedThrowable, &$fiber, $lowL
305316

306317
if (!$throwable instanceof \Throwable) {
307318
$throwable = new \UnexpectedValueException(
308-
'Promise rejected with unexpected value of type ' . (is_object($throwable) ? get_class($throwable) : gettype($throwable))
319+
'Promise rejected with unexpected value of type ' . (is_object($throwable) ? get_class($throwable) : gettype($throwable)) /** @phpstan-ignore-line */
309320
);
310321

311322
// avoid garbage references by replacing all closures in call stack.
@@ -354,7 +365,11 @@ function (mixed $throwable) use (&$rejected, &$rejectedThrowable, &$fiber, $lowL
354365

355366
$fiber = FiberFactory::create();
356367

357-
return $fiber->suspend();
368+
/**
369+
* @var T $result
370+
*/
371+
$result = $fiber->suspend();
372+
return $result;
358373
}
359374

360375
/**
@@ -592,9 +607,11 @@ function delay(float $seconds): void
592607
* });
593608
* ```
594609
*
595-
* @param callable(mixed ...$args):(\Generator<mixed,PromiseInterface,mixed,mixed>|mixed) $function
610+
* @template A
611+
* @template T
612+
* @param callable(mixed ...$args):(\Generator<mixed,PromiseInterface<A>,A,T>|T) $function
596613
* @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is
597-
* @return PromiseInterface<mixed>
614+
* @return PromiseInterface<T>
598615
* @since 3.0.0
599616
*/
600617
function coroutine(callable $function, mixed ...$args): PromiseInterface
@@ -609,9 +626,9 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
609626
return resolve($generator);
610627
}
611628

629+
/** @var ?PromiseInterface<T> $promise */
612630
$promise = null;
613631
$deferred = new Deferred(function () use (&$promise) {
614-
/** @var ?PromiseInterface $promise */
615632
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
616633
$promise->cancel();
617634
}
@@ -632,7 +649,7 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
632649
return;
633650
}
634651

635-
/** @var mixed $promise */
652+
/** @var mixed|PromiseInterface<T> $promise */
636653
$promise = $generator->current();
637654
if (!$promise instanceof PromiseInterface) {
638655
$next = null;
@@ -660,12 +677,13 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
660677
}
661678

662679
/**
663-
* @param iterable<callable():PromiseInterface<mixed>> $tasks
664-
* @return PromiseInterface<array<mixed>>
680+
* @template T
681+
* @param iterable<callable():(PromiseInterface<T>|T)> $tasks
682+
* @return PromiseInterface<array<T>>
665683
*/
666684
function parallel(iterable $tasks): PromiseInterface
667685
{
668-
/** @var array<int,PromiseInterface> $pending */
686+
/** @var array<int,PromiseInterface<T>> $pending */
669687
$pending = [];
670688
$deferred = new Deferred(function () use (&$pending) {
671689
foreach ($pending as $promise) {
@@ -720,14 +738,15 @@ function parallel(iterable $tasks): PromiseInterface
720738
}
721739

722740
/**
723-
* @param iterable<callable():PromiseInterface<mixed>> $tasks
724-
* @return PromiseInterface<array<mixed>>
741+
* @template T
742+
* @param iterable<callable():(PromiseInterface<T>|T)> $tasks
743+
* @return PromiseInterface<array<T>>
725744
*/
726745
function series(iterable $tasks): PromiseInterface
727746
{
728747
$pending = null;
729748
$deferred = new Deferred(function () use (&$pending) {
730-
/** @var ?PromiseInterface $pending */
749+
/** @var ?PromiseInterface<T> $pending */
731750
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
732751
$pending->cancel();
733752
}
@@ -774,14 +793,15 @@ function series(iterable $tasks): PromiseInterface
774793
}
775794

776795
/**
777-
* @param iterable<(callable():PromiseInterface<mixed>)|(callable(mixed):PromiseInterface<mixed>)> $tasks
778-
* @return PromiseInterface<mixed>
796+
* @template T
797+
* @param iterable<(callable():(PromiseInterface<T>|T))|(callable(mixed):(PromiseInterface<T>|T))> $tasks
798+
* @return PromiseInterface<T>
779799
*/
780800
function waterfall(iterable $tasks): PromiseInterface
781801
{
782802
$pending = null;
783803
$deferred = new Deferred(function () use (&$pending) {
784-
/** @var ?PromiseInterface $pending */
804+
/** @var ?PromiseInterface<T> $pending */
785805
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
786806
$pending->cancel();
787807
}

tests/AwaitTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ public function testRejectedPromisesShouldBeDetached(callable $await): void
413413
})());
414414
}
415415

416-
/** @return iterable<string,list<callable(PromiseInterface): mixed>> */
416+
/** @return iterable<string,list<callable(PromiseInterface<mixed>): mixed>> */
417417
public function provideAwaiters(): iterable
418418
{
419419
yield 'await' => [static fn (React\Promise\PromiseInterface $promise): mixed => React\Async\await($promise)];

tests/ParallelTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ class ParallelTest extends TestCase
1212
{
1313
public function testParallelWithoutTasks(): void
1414
{
15+
/**
16+
* @var array<callable(): React\Promise\PromiseInterface<mixed>> $tasks
17+
*/
1518
$tasks = array();
1619

1720
$promise = React\Async\parallel($tasks);

tests/SeriesTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ class SeriesTest extends TestCase
1212
{
1313
public function testSeriesWithoutTasks(): void
1414
{
15+
/**
16+
* @var array<callable(): React\Promise\PromiseInterface<mixed>> $tasks
17+
*/
1518
$tasks = array();
1619

1720
$promise = React\Async\series($tasks);
@@ -151,6 +154,9 @@ public function testSeriesWithErrorFromInfiniteIteratorAggregateReturnsPromiseRe
151154
$tasks = new class() implements \IteratorAggregate {
152155
public int $called = 0;
153156

157+
/**
158+
* @return \Iterator<callable(): React\Promise\PromiseInterface<mixed>>
159+
*/
154160
public function getIterator(): \Iterator
155161
{
156162
while (true) { // @phpstan-ignore-line

tests/WaterfallTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ class WaterfallTest extends TestCase
1212
{
1313
public function testWaterfallWithoutTasks(): void
1414
{
15+
/**
16+
* @var array<callable(): React\Promise\PromiseInterface<mixed>> $tasks
17+
*/
1518
$tasks = array();
1619

1720
$promise = React\Async\waterfall($tasks);
@@ -165,6 +168,9 @@ public function testWaterfallWithErrorFromInfiniteIteratorAggregateReturnsPromis
165168
$tasks = new class() implements \IteratorAggregate {
166169
public int $called = 0;
167170

171+
/**
172+
* @return \Iterator<callable(): React\Promise\PromiseInterface<mixed>>
173+
*/
168174
public function getIterator(): \Iterator
169175
{
170176
while (true) { // @phpstan-ignore-line

tests/types/async.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
use React\Promise\PromiseInterface;
4+
use function PHPStan\Testing\assertType;
5+
use function React\Async\async;
6+
use function React\Async\await;
7+
use function React\Promise\resolve;
8+
9+
assertType('React\Promise\PromiseInterface<bool>', async(static fn (): bool => true)());
10+
assertType('React\Promise\PromiseInterface<bool>', async(static fn (): PromiseInterface => resolve(true))());
11+
assertType('React\Promise\PromiseInterface<bool>', async(static fn (): bool => await(resolve(true)))());

tests/types/await.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
use React\Promise\PromiseInterface;
4+
use function PHPStan\Testing\assertType;
5+
use function React\Async\async;
6+
use function React\Async\await;
7+
use function React\Async\coroutine;
8+
use function React\Async\parallel;
9+
use function React\Async\series;
10+
use function React\Async\waterfall;
11+
use function React\Promise\resolve;
12+
13+
assertType('bool', await(resolve(true)));
14+
assertType('bool', await(async(static fn (): bool => true)()));
15+
assertType('bool', await(async(static fn (): PromiseInterface => resolve(true))()));
16+
assertType('bool', await(async(static fn (): bool => await(resolve(true)))()));
17+
18+
final class AwaitExampleUser
19+
{
20+
public string $name;
21+
22+
public function __construct(string $name) {
23+
$this->name = $name;
24+
}
25+
}
26+
27+
assertType('string', await(resolve(new AwaitExampleUser('WyriHaximus')))->name);

tests/types/coroutine.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
use function PHPStan\Testing\assertType;
4+
use function React\Async\await;
5+
use function React\Async\coroutine;
6+
use function React\Promise\resolve;
7+
8+
assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
9+
return true;
10+
}));
11+
12+
assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
13+
return (yield resolve(true));
14+
}));
15+
16+
assertType('React\Promise\PromiseInterface<int>', coroutine(static function () {
17+
$bool = yield resolve(true);
18+
19+
return time();
20+
}));
21+
22+
assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
23+
$bool = yield resolve(true);
24+
25+
return $bool;
26+
}));
27+
28+
// Unsupported behavior, but left here as example
29+
//assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
30+
// yield resolve(time());
31+
//
32+
// return true;
33+
//}));
34+
//
35+
//assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
36+
// for ($i = 0; $i <= 10; $i++) {
37+
// yield resolve($i);
38+
// }
39+
//
40+
// return true;
41+
//}));
42+
43+
assertType('bool', await(coroutine(static function () {
44+
return (yield resolve(true));
45+
})));
46+

0 commit comments

Comments
 (0)