Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement AsyncInterop\Promise #78

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
{"name": "Jan Sorgalla", "email": "[email protected]"}
],
"require": {
"php": ">=5.4.0"
"php": ">=5.4.0",
"async-interop/promise": "^0.4.0"
},
"require-dev": {
"async-interop/promise-test": "^0.4.1",
"phpunit/phpunit": "~4.8"
},
"autoload": {
Expand Down
15 changes: 14 additions & 1 deletion src/FulfilledPromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

namespace React\Promise;

use AsyncInterop\Promise as AsyncInteropPromise;

final class FulfilledPromise implements PromiseInterface
{
private $value;

public function __construct($value = null)
{
if ($value instanceof PromiseInterface) {
if ($value instanceof AsyncInteropPromise) {
throw new \InvalidArgumentException('You cannot create React\Promise\FulfilledPromise with a promise. Use React\Promise\resolve($promiseOrValue) instead.');
}

Expand Down Expand Up @@ -66,4 +68,15 @@ public function always(callable $onFulfilledOrRejected)
public function cancel()
{
}

public function when(callable $onResolved)
{
try {
$onResolved(null, $this->value);
} catch (\Throwable $exception) {
AsyncInteropPromise\ErrorHandler::notify($exception);
} catch (\Exception $exception) {
AsyncInteropPromise\ErrorHandler::notify($exception);
}
}
}
5 changes: 5 additions & 0 deletions src/LazyPromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ public function cancel()
return $this->promise()->cancel();
}

public function when(callable $onResolved)
{
return $this->promise()->when($onResolved);
}

/**
* @internal
* @see Promise::settle()
Expand Down
11 changes: 11 additions & 0 deletions src/Promise.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ public function cancel()
$this->call($canceller);
}

public function when(callable $onResolved)
{
if (null !== $this->result) {
return $this->result()->when($onResolved);
}

$this->handlers[] = function (PromiseInterface $promise) use ($onResolved) {
$promise->when($onResolved);
};
}

private function resolver(callable $onFulfilled = null, callable $onRejected = null)
{
return function ($resolve, $reject) use ($onFulfilled, $onRejected) {
Expand Down
4 changes: 3 additions & 1 deletion src/PromiseInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace React\Promise;

interface PromiseInterface
use AsyncInterop\Promise as AsyncInteropPromise;

interface PromiseInterface extends AsyncInteropPromise
{
/**
* @return PromiseInterface
Expand Down
18 changes: 17 additions & 1 deletion src/RejectedPromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

namespace React\Promise;

use AsyncInterop\Promise as AsyncInteropPromise;

final class RejectedPromise implements PromiseInterface
{
private $reason;

public function __construct($reason = null)
{
if ($reason instanceof PromiseInterface) {
if ($reason instanceof AsyncInteropPromise) {
throw new \InvalidArgumentException('You cannot create React\Promise\RejectedPromise with a promise. Use React\Promise\reject($promiseOrValue) instead.');
}

Expand Down Expand Up @@ -74,4 +76,18 @@ public function always(callable $onFulfilledOrRejected)
public function cancel()
{
}

public function when(callable $onResolved)
{
try {
$onResolved(
UnhandledRejectionException::resolve($this->reason),
null
);
} catch (\Throwable $exception) {
AsyncInteropPromise\ErrorHandler::notify($exception);
} catch (\Exception $exception) {
AsyncInteropPromise\ErrorHandler::notify($exception);
}
}
}
23 changes: 23 additions & 0 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,27 @@

namespace React\Promise;

use AsyncInterop\Promise as AsyncInteropPromise;

function resolve($promiseOrValue = null)
{
if ($promiseOrValue instanceof PromiseInterface) {
return $promiseOrValue;
}

if ($promiseOrValue instanceof AsyncInteropPromise) {
return new Promise(function ($resolve, $reject) use ($promiseOrValue) {
$promiseOrValue->when(function ($reason = null, $value = null) use ($resolve, $reject) {
if ($reason) {
$reject($reason);
return;
}

$resolve($value);
});
});
}

if (method_exists($promiseOrValue, 'then')) {
$canceller = null;

Expand All @@ -31,6 +46,14 @@ function reject($promiseOrValue = null)
});
}

if ($promiseOrValue instanceof AsyncInteropPromise) {
return new Promise(function ($resolve, $reject) use ($promiseOrValue) {
$promiseOrValue->when(function ($reason = null, $value = null) use ($resolve, $reject) {
$reject($reason ? $reason : $value);
});
});
}

return new RejectedPromise($promiseOrValue);
}

Expand Down
20 changes: 20 additions & 0 deletions tests/AsyncInterop/DeferredTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace React\Promise\AsyncInterop;

use AsyncInterop\Promise\Test;
use React\Promise\Deferred;

class DeferredTest extends Test
{
public function promise(callable $canceller = null)
{
$d = new Deferred($canceller);

return [
$d->promise(),
[$d, 'resolve'],
[$d, 'reject']
];
}
}
25 changes: 25 additions & 0 deletions tests/AsyncInterop/LazyPromiseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace React\Promise\AsyncInterop;

use AsyncInterop\Promise\Test;
use React\Promise\Deferred;
use React\Promise\LazyPromise;

class LazyPromiseTest extends Test
{
public function promise(callable $canceller = null)
{
$d = new Deferred($canceller);

$factory = function () use ($d) {
return $d->promise();
};

return [
new LazyPromise($factory),
[$d, 'resolve'],
[$d, 'reject']
];
}
}
25 changes: 25 additions & 0 deletions tests/AsyncInterop/PromiseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace React\Promise\AsyncInterop;

use AsyncInterop\Promise\Test;
use React\Promise\Promise;

class PromiseTest extends Test
{
public function promise(callable $canceller = null)
{
$resolveCallback = $rejectCallback = null;

$promise = new Promise(function ($resolve, $reject) use (&$resolveCallback, &$rejectCallback) {
$resolveCallback = $resolve;
$rejectCallback = $reject;
}, $canceller);

return [
$promise,
$resolveCallback,
$rejectCallback
];
}
}
8 changes: 8 additions & 0 deletions tests/FulfilledPromiseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,12 @@ public function shouldThrowExceptionIfConstructedWithAPromise()

return new FulfilledPromise(new FulfilledPromise());
}

/** @test */
public function shouldThrowExceptionIfConstructedWithAAsyncInteropPromise()
{
$this->setExpectedException('\InvalidArgumentException');

return new FulfilledPromise(new SimpleFulfilledAsyncInteropTestPromise());
}
}
37 changes: 37 additions & 0 deletions tests/FunctionRejectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ public function shouldRejectAFulfilledPromise()
);
}

/** @test */
public function shouldRejectAFulfilledAsyncInteropPromise()
{
$resolved = new SimpleFulfilledAsyncInteropTestPromise();

$mock = $this->createCallableMock();
$mock
->expects($this->once())
->method('__invoke')
->with($this->identicalTo('foo'));

reject($resolved)
->then(
$this->expectCallableNever(),
$mock
);
}

/** @test */
public function shouldRejectARejectedPromise()
{
Expand All @@ -61,4 +79,23 @@ public function shouldRejectARejectedPromise()
$mock
);
}

/** @test */
public function shouldRejectARejectedAsyncInteropPromise()
{
$exception = new \Exception('foo');
$resolved = new SimpleRejectedAsyncInteropTestPromise($exception);

$mock = $this->createCallableMock();
$mock
->expects($this->once())
->method('__invoke')
->with($this->identicalTo($exception));

reject($resolved)
->then(
$this->expectCallableNever(),
$mock
);
}
}
37 changes: 37 additions & 0 deletions tests/FunctionResolveTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,24 @@ public function shouldResolveACancellableThenable()
$this->assertTrue($thenable->cancelCalled);
}

/** @test */
public function shouldResolveAFulfilledAsyncInteropPromise()
{
$promise = new SimpleFulfilledAsyncInteropTestPromise();

$mock = $this->createCallableMock();
$mock
->expects($this->once())
->method('__invoke')
->with($this->identicalTo('foo'));

resolve($promise)
->then(
$mock,
$this->expectCallableNever()
);
}

/** @test */
public function shouldRejectARejectedPromise()
{
Expand All @@ -91,6 +109,25 @@ public function shouldRejectARejectedPromise()
);
}

/** @test */
public function shouldRejectARejectedAsyncInteropPromise()
{
$exception = new \Exception('foo');
$promise = new SimpleRejectedAsyncInteropTestPromise($exception);

$mock = $this->createCallableMock();
$mock
->expects($this->once())
->method('__invoke')
->with($this->identicalTo($exception));

resolve($promise)
->then(
$this->expectCallableNever(),
$mock
);
}

/** @test */
public function shouldSupportDeepNestingInPromiseChains()
{
Expand Down
8 changes: 8 additions & 0 deletions tests/RejectedPromiseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,12 @@ public function shouldThrowExceptionIfConstructedWithAPromise()

return new RejectedPromise(new RejectedPromise());
}

/** @test */
public function shouldThrowExceptionIfConstructedWithAAsyncInteropPromise()
{
$this->setExpectedException('\InvalidArgumentException');

return new RejectedPromise(new SimpleFulfilledAsyncInteropTestPromise());
}
}
13 changes: 13 additions & 0 deletions tests/fixtures/SimpleFulfilledAsyncInteropTestPromise.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace React\Promise;

use AsyncInterop\Promise as AsyncInteropPromise;

class SimpleFulfilledAsyncInteropTestPromise implements AsyncInteropPromise
{
public function when(callable $onResolved)
{
$onResolved(null, 'foo');
}
}
20 changes: 20 additions & 0 deletions tests/fixtures/SimpleRejectedAsyncInteropTestPromise.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace React\Promise;

use AsyncInterop\Promise as AsyncInteropPromise;

class SimpleRejectedAsyncInteropTestPromise implements AsyncInteropPromise
{
private $exception;

public function __construct(\Exception $exception)
{
$this->exception = $exception;
}

public function when(callable $onResolved)
{
$onResolved($this->exception, null);
}
}