Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
51 changes: 50 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Table of Contents
* [When::reject()](#whenreject)
* [When::lazy()](#whenlazy)
* [Promisor](#promisor)
* [CancellablePromiseInterface](#cancellablepromiseinterface)
* [CancellablePromiseInterface::cancel()](#cancellablepromiseinterfacecancel)
4. [Examples](#examples)
* [How to use Deferred](#how-to-use-deferred)
* [How Promise forwarding works](#how-promise-forwarding-works)
Expand Down Expand Up @@ -111,6 +113,18 @@ $deferred->reject(mixed $reason = null);
$deferred->progress(mixed $update = null);
```

The constructor of the `Deferred` accepts an optional `$canceller` argument.
See [Promise](#promise-1) for more information.


``` php
$deferred = new React\Promise\Deferred(function ($resolve, $reject, $progress) {
throw new \Exception('Promise cancelled');
});

$deferred->cancel();
```

### Promise

The Promise represents the eventual outcome, which is either fulfillment
Expand All @@ -133,7 +147,13 @@ $resolver = function (callable $resolve, callable $reject, callable $notify) {
// or $notify($progressNotification);
};

$promise = new React\Promise\Promise($resolver);
$canceller = function (callable $resolve, callable $reject, callable $progress) {
// Cancel/abort any running operations like network connections, streams etc.

$reject(new \Exception('Promise cancelled'));
};

$promise = new React\Promise\Promise($resolver, $canceller);
```

The promise constructor receives a resolver function which will be called
Expand All @@ -150,6 +170,9 @@ immediately with 3 arguments:
If the resolver throws an exception, the promise will be rejected with that
thrown exception as the rejection reason.

The resolver function will be called immediately, the canceller function only
once all consumers called the `cancel()` method of the promise.

A Promise has a single method `then()` which registers new fulfilled, error and
progress handlers with this Promise (all parameters are optional):

Expand Down Expand Up @@ -484,6 +507,32 @@ The `React\Promise\PromisorInterface` provides a common interface for objects
that provide a promise. `React\Promise\Deferred` implements it, but since it
is part of the public API anyone can implement it.

### CancellablePromiseInterface

A cancellable promise provides a mechanism for consumers to notify the creator
of the promise that they are not longer interested in the result of an
operation.

#### CancellablePromiseInterface::cancel()

``` php
$promise->cancel();
```

The `cancel()` method notifies the creator of the promise that there is no
further interest in the results of the operation.

Once a promise is settled (either resolved or rejected), calling `cancel()` on
a promise has no effect.

#### Implementations

* [Deferred](#deferred-1)
* [Promise](#promise-1)
* [FulfilledPromise](#fulfilledpromise)
* [RejectedPromise](#rejectedpromise)
* [LazyPromise](#lazypromise)

Examples
--------

Expand Down
11 changes: 11 additions & 0 deletions src/React/Promise/CancellablePromiseInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace React\Promise;

interface CancellablePromiseInterface extends PromiseInterface
{
/**
* @return void
*/
public function cancel();
}
68 changes: 66 additions & 2 deletions src/React/Promise/Deferred.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,56 @@

namespace React\Promise;

class Deferred implements PromiseInterface, ResolverInterface, PromisorInterface
class Deferred implements PromiseInterface, ResolverInterface, PromisorInterface, CancellablePromiseInterface
{
private $completed;
private $promise;
private $resolver;
private $handlers = array();
private $progressHandlers = array();
private $canceller;

private $requiredCancelRequests = 0;
private $cancelRequests = 0;

public function __construct($canceller = null)
{
if ($canceller !== null && !is_callable($canceller)) {
throw new \InvalidArgumentException(
sprintf(
'The canceller argument must be null or of type callable, %s given.',
gettype($canceller)
)
);
}

$this->canceller = $canceller;
}

public function then($fulfilledHandler = null, $errorHandler = null, $progressHandler = null)
{
if (null !== $this->completed) {
return $this->completed->then($fulfilledHandler, $errorHandler, $progressHandler);
}

$deferred = new static();
$canceller = null;
if ($this->canceller !== null) {
$this->requiredCancelRequests++;

$that = $this;
$current =& $this->cancelRequests;
$required =& $this->requiredCancelRequests;

$canceller = function () use ($that, &$current, &$required) {
if (++$current < $required) {
return;
}

$that->cancel();
};
}

$deferred = new static($canceller);

if (is_callable($progressHandler)) {
$progHandler = function ($update) use ($deferred, $progressHandler) {
Expand Down Expand Up @@ -96,6 +131,35 @@ public function resolver()
return $this->resolver;
}

public function cancel()
{
if (null === $this->canceller || null !== $this->completed) {
return;
}

$canceller = $this->canceller;
$this->canceller = null;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, you should check here for is_callabe($canceller) and trigger a notice when false like done here, here and here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, you could throw in the constructor, similar to here.

try {
$that = $this;

call_user_func(
$canceller,
function ($value = null) use ($that) {
$that->resolve($value);
},
function ($reason = null) use ($that) {
$that->reject($reason);
},
function ($update = null) use ($that) {
$that->progress($update);
}
);
} catch (\Exception $e) {
$this->reject($e);
}
}

protected function processQueue($queue, $value)
{
foreach ($queue as $handler) {
Expand Down
5 changes: 5 additions & 0 deletions src/React/Promise/DeferredPromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,9 @@ public function then($fulfilledHandler = null, $errorHandler = null, $progressHa
{
return $this->deferred->then($fulfilledHandler, $errorHandler, $progressHandler);
}

public function cancel()
{
$this->deferred->cancel();
}
}
6 changes: 5 additions & 1 deletion src/React/Promise/FulfilledPromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace React\Promise;

class FulfilledPromise implements PromiseInterface
class FulfilledPromise implements PromiseInterface, CancellablePromiseInterface
{
private $result;

Expand All @@ -27,4 +27,8 @@ public function then($fulfilledHandler = null, $errorHandler = null, $progressHa
return new RejectedPromise($exception);
}
}

public function cancel()
{
}
}
18 changes: 15 additions & 3 deletions src/React/Promise/LazyPromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace React\Promise;

class LazyPromise implements PromiseInterface
class LazyPromise implements PromiseInterface, CancellablePromiseInterface
{
private $factory;
private $promise;
Expand All @@ -13,6 +13,19 @@ public function __construct($factory)
}

public function then($fulfilledHandler = null, $errorHandler = null, $progressHandler = null)
{
return $this->promise()->then($fulfilledHandler, $errorHandler, $progressHandler);
}

public function cancel()
{
$promise = $this->promise();
if ($promise instanceof CancellablePromiseInterface) {
$promise->cancel();
}
}

private function promise()
{
if (null === $this->promise) {
try {
Expand All @@ -21,7 +34,6 @@ public function then($fulfilledHandler = null, $errorHandler = null, $progressHa
$this->promise = new RejectedPromise($exception);
}
}

return $this->promise->then($fulfilledHandler, $errorHandler, $progressHandler);
return $this->promise;
}
}
11 changes: 8 additions & 3 deletions src/React/Promise/Promise.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

namespace React\Promise;

class Promise implements PromiseInterface
class Promise implements PromiseInterface, CancellablePromiseInterface
{
private $deferred;

public function __construct($resolver)
public function __construct($resolver, $canceller = null)
{
if (!is_callable($resolver)) {
throw new \InvalidArgumentException(
Expand All @@ -17,7 +17,7 @@ public function __construct($resolver)
);
}

$this->deferred = new Deferred();
$this->deferred = new Deferred($canceller);
$this->call($resolver);
}

Expand All @@ -26,6 +26,11 @@ public function then($fulfilledHandler = null, $errorHandler = null, $progressHa
return $this->deferred->then($fulfilledHandler, $errorHandler, $progressHandler);
}

public function cancel()
{
$this->deferred->cancel();
}

private function call($callback)
{
$deferred = $this->deferred;
Expand Down
6 changes: 5 additions & 1 deletion src/React/Promise/RejectedPromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace React\Promise;

class RejectedPromise implements PromiseInterface
class RejectedPromise implements PromiseInterface, CancellablePromiseInterface
{
private $reason;

Expand All @@ -27,4 +27,8 @@ public function then($fulfilledHandler = null, $errorHandler = null, $progressHa
return new RejectedPromise($exception);
}
}

public function cancel()
{
}
}
12 changes: 12 additions & 0 deletions tests/React/Promise/DeferredPromiseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,16 @@ public function shouldForwardToDeferred()
$p = new DeferredPromise($mock);
$p->then(1, 2, 3);
}

/** @test */
public function shouldForwardCancelToDeferred()
{
$mock = $this->getMock('React\\Promise\\Deferred');
$mock
->expects($this->once())
->method('cancel');

$p = new DeferredPromise($mock);
$p->cancel();
}
}
Loading