Skip to content

Commit

Permalink
Add template annotations
Browse files Browse the repository at this point in the history
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
```
  • Loading branch information
WyriHaximus authored and clue committed Oct 29, 2023
1 parent 037cbf2 commit d1c9606
Show file tree
Hide file tree
Showing 12 changed files with 239 additions and 27 deletions.
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ jobs:
- 7.4
- 7.3
- 7.2
- 7.1
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Async\await(…);

### await()

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

```php
Expand Down Expand Up @@ -94,7 +94,7 @@ try {

### coroutine()

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

```php
Expand Down Expand Up @@ -277,7 +277,7 @@ trigger at the earliest possible time in the future.

### parallel()

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

```php
Expand Down Expand Up @@ -319,7 +319,7 @@ React\Async\parallel([

### series()

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

```php
Expand Down Expand Up @@ -361,7 +361,7 @@ React\Async\series([

### waterfall()

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

```php
Expand Down
44 changes: 28 additions & 16 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@
* }
* ```
*
* @param PromiseInterface $promise
* @return mixed returns whatever the promise resolves to
* @template T
* @param PromiseInterface<T> $promise
* @return T returns whatever the promise resolves to
* @throws \Exception when the promise is rejected with an `Exception`
* @throws \Throwable when the promise is rejected with a `Throwable`
* @throws \UnexpectedValueException when the promise is rejected with an unexpected value (Promise API v1 or v2 only)
Expand Down Expand Up @@ -93,13 +94,14 @@ function ($error) use (&$exception, &$rejected, &$wait, &$loopStarted) {
// promise is rejected with an unexpected value (Promise API v1 or v2 only)
if (!$exception instanceof \Throwable) {
$exception = new \UnexpectedValueException(
'Promise rejected with unexpected value of type ' . (is_object($exception) ? get_class($exception) : gettype($exception))
'Promise rejected with unexpected value of type ' . (is_object($exception) ? get_class($exception) : gettype($exception)) // @phpstan-ignore-line
);
}

throw $exception;
}

/** @var T $resolved */
return $resolved;
}

Expand Down Expand Up @@ -296,9 +298,16 @@ function delay(float $seconds): void
* });
* ```
*
* @param callable(mixed ...$args):(\Generator<mixed,PromiseInterface,mixed,mixed>|mixed) $function
* @template T
* @template TYield
* @template A1 (any number of function arguments, see https://github.com/phpstan/phpstan/issues/8214)
* @template A2
* @template A3
* @template A4
* @template A5
* @param callable(A1, A2, A3, A4, A5):(\Generator<mixed, PromiseInterface<TYield>, TYield, PromiseInterface<T>|T>|PromiseInterface<T>|T) $function
* @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is
* @return PromiseInterface<mixed>
* @return PromiseInterface<T>
* @since 3.0.0
*/
function coroutine(callable $function, ...$args): PromiseInterface
Expand All @@ -315,7 +324,7 @@ function coroutine(callable $function, ...$args): PromiseInterface

$promise = null;
$deferred = new Deferred(function () use (&$promise) {
/** @var ?PromiseInterface $promise */
/** @var ?PromiseInterface<T> $promise */
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
Expand All @@ -336,7 +345,6 @@ function coroutine(callable $function, ...$args): PromiseInterface
return;
}

/** @var mixed $promise */
$promise = $generator->current();
if (!$promise instanceof PromiseInterface) {
$next = null;
Expand All @@ -346,6 +354,7 @@ function coroutine(callable $function, ...$args): PromiseInterface
return;
}

/** @var PromiseInterface<TYield> $promise */
assert($next instanceof \Closure);
$promise->then(function ($value) use ($generator, $next) {
$generator->send($value);
Expand All @@ -364,12 +373,13 @@ function coroutine(callable $function, ...$args): PromiseInterface
}

/**
* @param iterable<callable():PromiseInterface<mixed>> $tasks
* @return PromiseInterface<array<mixed>>
* @template T
* @param iterable<callable():(PromiseInterface<T>|T)> $tasks
* @return PromiseInterface<array<T>>
*/
function parallel(iterable $tasks): PromiseInterface
{
/** @var array<int,PromiseInterface> $pending */
/** @var array<int,PromiseInterface<T>> $pending */
$pending = [];
$deferred = new Deferred(function () use (&$pending) {
foreach ($pending as $promise) {
Expand Down Expand Up @@ -424,14 +434,15 @@ function parallel(iterable $tasks): PromiseInterface
}

/**
* @param iterable<callable():PromiseInterface<mixed>> $tasks
* @return PromiseInterface<array<mixed>>
* @template T
* @param iterable<callable():(PromiseInterface<T>|T)> $tasks
* @return PromiseInterface<array<T>>
*/
function series(iterable $tasks): PromiseInterface
{
$pending = null;
$deferred = new Deferred(function () use (&$pending) {
/** @var ?PromiseInterface $pending */
/** @var ?PromiseInterface<T> $pending */
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
$pending->cancel();
}
Expand Down Expand Up @@ -478,14 +489,15 @@ function series(iterable $tasks): PromiseInterface
}

/**
* @param iterable<(callable():PromiseInterface<mixed>)|(callable(mixed):PromiseInterface<mixed>)> $tasks
* @return PromiseInterface<mixed>
* @template T
* @param iterable<(callable():(PromiseInterface<T>|T))|(callable(mixed):(PromiseInterface<T>|T))> $tasks
* @return PromiseInterface<($tasks is non-empty-array|\Traversable ? T : null)>
*/
function waterfall(iterable $tasks): PromiseInterface
{
$pending = null;
$deferred = new Deferred(function () use (&$pending) {
/** @var ?PromiseInterface $pending */
/** @var ?PromiseInterface<T> $pending */
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
$pending->cancel();
}
Expand Down
10 changes: 5 additions & 5 deletions tests/CoroutineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsImmediately
{
$promise = coroutine(function () {
if (false) { // @phpstan-ignore-line
yield;
yield resolve(null);
}
return 42;
});
Expand Down Expand Up @@ -53,7 +53,7 @@ public function testCoroutineReturnsRejectedPromiseIfFunctionThrowsImmediately()
{
$promise = coroutine(function () {
if (false) { // @phpstan-ignore-line
yield;
yield resolve(null);
}
throw new \RuntimeException('Foo');
});
Expand Down Expand Up @@ -99,7 +99,7 @@ public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsAfterYieldi

public function testCoroutineReturnsRejectedPromiseIfFunctionYieldsInvalidValue(): void
{
$promise = coroutine(function () {
$promise = coroutine(function () { // @phpstan-ignore-line
yield 42;
});

Expand Down Expand Up @@ -169,7 +169,7 @@ public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorRet

$promise = coroutine(function () {
if (false) { // @phpstan-ignore-line
yield;
yield resolve(null);
}
return 42;
});
Expand Down Expand Up @@ -249,7 +249,7 @@ public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorYie

gc_collect_cycles();

$promise = coroutine(function () {
$promise = coroutine(function () { // @phpstan-ignore-line
yield 42;
});

Expand Down
3 changes: 3 additions & 0 deletions tests/ParallelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class ParallelTest extends TestCase
{
public function testParallelWithoutTasks(): void
{
/**
* @var array<callable(): React\Promise\PromiseInterface<mixed>> $tasks
*/
$tasks = array();

$promise = React\Async\parallel($tasks);
Expand Down
6 changes: 6 additions & 0 deletions tests/SeriesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class SeriesTest extends TestCase
{
public function testSeriesWithoutTasks(): void
{
/**
* @var array<callable(): React\Promise\PromiseInterface<mixed>> $tasks
*/
$tasks = array();

$promise = React\Async\series($tasks);
Expand Down Expand Up @@ -152,6 +155,9 @@ public function testSeriesWithErrorFromInfiniteIteratorAggregateReturnsPromiseRe
/** @var int */
public $called = 0;

/**
* @return \Iterator<callable(): React\Promise\PromiseInterface<mixed>>
*/
public function getIterator(): \Iterator
{
while (true) { // @phpstan-ignore-line
Expand Down
6 changes: 6 additions & 0 deletions tests/WaterfallTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class WaterfallTest extends TestCase
{
public function testWaterfallWithoutTasks(): void
{
/**
* @var array<callable(): React\Promise\PromiseInterface<mixed>> $tasks
*/
$tasks = array();

$promise = React\Async\waterfall($tasks);
Expand Down Expand Up @@ -166,6 +169,9 @@ public function testWaterfallWithErrorFromInfiniteIteratorAggregateReturnsPromis
/** @var int */
public $called = 0;

/**
* @return \Iterator<callable(): React\Promise\PromiseInterface<mixed>>
*/
public function getIterator(): \Iterator
{
while (true) { // @phpstan-ignore-line
Expand Down
18 changes: 18 additions & 0 deletions tests/types/await.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

use function PHPStan\Testing\assertType;
use function React\Async\await;
use function React\Promise\resolve;

assertType('bool', await(resolve(true)));

final class AwaitExampleUser
{
public string $name;

public function __construct(string $name) {
$this->name = $name;
}
}

assertType('string', await(resolve(new AwaitExampleUser('WyriHaximus')))->name);
60 changes: 60 additions & 0 deletions tests/types/coroutine.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

use function PHPStan\Testing\assertType;
use function React\Async\await;
use function React\Async\coroutine;
use function React\Promise\resolve;

assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
return true;
}));

assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
return resolve(true);
}));

// assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
// return (yield resolve(true));
// }));

assertType('React\Promise\PromiseInterface<int>', coroutine(static function () {
// $bool = yield resolve(true);
// assertType('bool', $bool);

return time();
}));

// assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
// $bool = yield resolve(true);
// assertType('bool', $bool);

// return $bool;
// }));

assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
yield resolve(time());

return true;
}));

assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
for ($i = 0; $i <= 10; $i++) {
yield resolve($i);
}

return true;
}));

assertType('React\Promise\PromiseInterface<int>', coroutine(static function (int $a): int { return $a; }, 42));
assertType('React\Promise\PromiseInterface<int>', coroutine(static function (int $a, int $b): int { return $a + $b; }, 10, 32));
assertType('React\Promise\PromiseInterface<int>', coroutine(static function (int $a, int $b, int $c): int { return $a + $b + $c; }, 10, 22, 10));
assertType('React\Promise\PromiseInterface<int>', coroutine(static function (int $a, int $b, int $c, int $d): int { return $a + $b + $c + $d; }, 10, 22, 5, 5));
assertType('React\Promise\PromiseInterface<int>', coroutine(static function (int $a, int $b, int $c, int $d, int $e): int { return $a + $b + $c + $d + $e; }, 10, 12, 10, 5, 5));

assertType('bool', await(coroutine(static function () {
return true;
})));

// assertType('bool', await(coroutine(static function () {
// return (yield resolve(true));
// })));
33 changes: 33 additions & 0 deletions tests/types/parallel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

use React\Promise\PromiseInterface;
use function PHPStan\Testing\assertType;
use function React\Async\await;
use function React\Async\parallel;
use function React\Promise\resolve;

assertType('React\Promise\PromiseInterface<array>', parallel([]));

assertType('React\Promise\PromiseInterface<array<bool|float|int>>', parallel([
static function (): PromiseInterface { return resolve(true); },
static function (): PromiseInterface { return resolve(time()); },
static function (): PromiseInterface { return resolve(microtime(true)); },
]));

assertType('React\Promise\PromiseInterface<array<bool|float|int>>', parallel([
static function (): bool { return true; },
static function (): int { return time(); },
static function (): float { return microtime(true); },
]));

assertType('array<bool|float|int>', await(parallel([
static function (): PromiseInterface { return resolve(true); },
static function (): PromiseInterface { return resolve(time()); },
static function (): PromiseInterface { return resolve(microtime(true)); },
])));

assertType('array<bool|float|int>', await(parallel([
static function (): bool { return true; },
static function (): int { return time(); },
static function (): float { return microtime(true); },
])));
Loading

0 comments on commit d1c9606

Please sign in to comment.