Skip to content

Commit a8481f9

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: ```yaml 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 a8481f9

13 files changed

+212
-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): mixed` 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 $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: 38 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,10 @@ function delay(float $seconds): void
592607
* });
593608
* ```
594609
*
595-
* @param callable(mixed ...$args):(\Generator<mixed,PromiseInterface,mixed,mixed>|mixed) $function
610+
* @template T
611+
* @param callable(mixed ...$args):(\Generator<mixed,PromiseInterface<T>,mixed,mixed>|T) $function
596612
* @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is
597-
* @return PromiseInterface<mixed>
613+
* @return PromiseInterface<T>
598614
* @since 3.0.0
599615
*/
600616
function coroutine(callable $function, mixed ...$args): PromiseInterface
@@ -609,9 +625,9 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
609625
return resolve($generator);
610626
}
611627

628+
/** @var ?PromiseInterface<T> $promise */
612629
$promise = null;
613630
$deferred = new Deferred(function () use (&$promise) {
614-
/** @var ?PromiseInterface $promise */
615631
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
616632
$promise->cancel();
617633
}
@@ -632,7 +648,7 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
632648
return;
633649
}
634650

635-
/** @var mixed $promise */
651+
/** @var mixed|PromiseInterface<T> $promise */
636652
$promise = $generator->current();
637653
if (!$promise instanceof PromiseInterface) {
638654
$next = null;
@@ -660,12 +676,13 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
660676
}
661677

662678
/**
663-
* @param iterable<callable():PromiseInterface<mixed>> $tasks
664-
* @return PromiseInterface<array<mixed>>
679+
* @template T
680+
* @param iterable<callable():PromiseInterface<T>> $tasks
681+
* @return PromiseInterface<array<T>>
665682
*/
666683
function parallel(iterable $tasks): PromiseInterface
667684
{
668-
/** @var array<int,PromiseInterface> $pending */
685+
/** @var array<int,PromiseInterface<T>> $pending */
669686
$pending = [];
670687
$deferred = new Deferred(function () use (&$pending) {
671688
foreach ($pending as $promise) {
@@ -720,14 +737,15 @@ function parallel(iterable $tasks): PromiseInterface
720737
}
721738

722739
/**
723-
* @param iterable<callable():PromiseInterface<mixed>> $tasks
724-
* @return PromiseInterface<array<mixed>>
740+
* @template T
741+
* @param iterable<callable():PromiseInterface<T>> $tasks
742+
* @return PromiseInterface<array<T>>
725743
*/
726744
function series(iterable $tasks): PromiseInterface
727745
{
728746
$pending = null;
729747
$deferred = new Deferred(function () use (&$pending) {
730-
/** @var ?PromiseInterface $pending */
748+
/** @var ?PromiseInterface<T> $pending */
731749
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
732750
$pending->cancel();
733751
}
@@ -774,14 +792,15 @@ function series(iterable $tasks): PromiseInterface
774792
}
775793

776794
/**
777-
* @param iterable<(callable():PromiseInterface<mixed>)|(callable(mixed):PromiseInterface<mixed>)> $tasks
778-
* @return PromiseInterface<mixed>
795+
* @template T
796+
* @param iterable<(callable():PromiseInterface<T>)|(callable(mixed):PromiseInterface<T>)> $tasks
797+
* @return PromiseInterface<T>
779798
*/
780799
function waterfall(iterable $tasks): PromiseInterface
781800
{
782801
$pending = null;
783802
$deferred = new Deferred(function () use (&$pending) {
784-
/** @var ?PromiseInterface $pending */
803+
/** @var ?PromiseInterface<T> $pending */
785804
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
786805
$pending->cancel();
787806
}

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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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 (): PromiseInterface => resolve(true))());
10+
assertType('React\Promise\PromiseInterface<bool>', async(static fn (): bool => await(resolve(true)))());

tests/types/await.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
assertType('bool', await(coroutine(static function () {
18+
return (yield resolve(true));
19+
})));
20+
assertType('array<bool|float|int>', await(parallel([
21+
static fn (): PromiseInterface => resolve(true),
22+
static fn (): PromiseInterface => resolve(time()),
23+
static fn (): PromiseInterface => resolve(microtime(true)),
24+
])));
25+
assertType('array<bool|float|int>', await(series([
26+
static fn (): PromiseInterface => resolve(true),
27+
static fn (): PromiseInterface => resolve(time()),
28+
static fn (): PromiseInterface => resolve(microtime(true)),
29+
])));
30+
assertType('float', await(waterfall([
31+
static fn (): PromiseInterface => resolve(microtime(true)),
32+
])));
33+
34+
// Desired, but currently unsupported with the current set of templates
35+
//assertType('float', await(waterfall([
36+
// static fn (): PromiseInterface => resolve(true),
37+
// static fn (bool $bool): PromiseInterface => resolve(time()),
38+
// static fn (int $int): PromiseInterface => resolve(microtime(true)),
39+
//])));
40+
41+
final class AwaitExampleUser
42+
{
43+
public string $name;
44+
45+
public function __construct(string $name) {
46+
$this->name = $name;
47+
}
48+
}
49+
50+
assertType('string', await(resolve(new AwaitExampleUser('WyriHaximus')))->name);

0 commit comments

Comments
 (0)