Skip to content

Commit 52f67c2

Browse files
committed
Throw when end of chain has been reached
1 parent 6019855 commit 52f67c2

File tree

9 files changed

+103
-29
lines changed

9 files changed

+103
-29
lines changed

src/Internal/RejectedPromise.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,24 @@
1212
final class RejectedPromise implements PromiseInterface
1313
{
1414
private $reason;
15+
private $endOfChain = true;
1516

1617
public function __construct(\Throwable $reason)
1718
{
1819
$this->reason = $reason;
1920
}
2021

22+
public function __destruct()
23+
{
24+
if ($this->endOfChain === true) {
25+
throw $this->reason;
26+
}
27+
}
28+
2129
public function then(callable $onFulfilled = null, callable $onRejected = null): PromiseInterface
2230
{
31+
$this->endOfChain = false;
32+
2333
if (null === $onRejected) {
2434
return $this;
2535
}
@@ -33,6 +43,8 @@ public function then(callable $onFulfilled = null, callable $onRejected = null):
3343

3444
public function catch(callable $onRejected): PromiseInterface
3545
{
46+
$this->endOfChain = false;
47+
3648
if (!_checkTypehint($onRejected, $this->reason)) {
3749
return $this;
3850
}
@@ -42,6 +54,8 @@ public function catch(callable $onRejected): PromiseInterface
4254

4355
public function finally(callable $onFulfilledOrRejected): PromiseInterface
4456
{
57+
$this->endOfChain = false;
58+
4559
return $this->then(null, function (\Throwable $reason) use ($onFulfilledOrRejected): PromiseInterface {
4660
return resolve($onFulfilledOrRejected())->then(function () use ($reason): PromiseInterface {
4761
return new RejectedPromise($reason);

src/Promise.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ final class Promise implements PromiseInterface
1212
private $handlers = [];
1313

1414
private $requiredCancelRequests = 0;
15+
private $endOfChain = true;
16+
private $cancelled = false;
1517

1618
public function __construct(callable $resolver, callable $canceller = null)
1719
{
@@ -25,8 +27,17 @@ public function __construct(callable $resolver, callable $canceller = null)
2527
$this->call($cb);
2628
}
2729

30+
public function __destruct()
31+
{
32+
if ($this->endOfChain === true && $this->cancelled === false && $this->result instanceof \Throwable) {
33+
throw $this->result;
34+
}
35+
}
36+
2837
public function then(callable $onFulfilled = null, callable $onRejected = null): PromiseInterface
2938
{
39+
$this->endOfChain = false;
40+
3041
if (null !== $this->result) {
3142
return $this->result->then($onFulfilled, $onRejected);
3243
}
@@ -59,6 +70,7 @@ static function () use (&$parent) {
5970

6071
public function catch(callable $onRejected): PromiseInterface
6172
{
73+
$this->endOfChain = false;
6274
return $this->then(null, static function ($reason) use ($onRejected) {
6375
if (!_checkTypehint($onRejected, $reason)) {
6476
return new RejectedPromise($reason);
@@ -70,6 +82,7 @@ public function catch(callable $onRejected): PromiseInterface
7082

7183
public function finally(callable $onFulfilledOrRejected): PromiseInterface
7284
{
85+
$this->endOfChain = false;
7386
return $this->then(static function ($value) use ($onFulfilledOrRejected) {
7487
return resolve($onFulfilledOrRejected())->then(function () use ($value) {
7588
return $value;
@@ -83,6 +96,7 @@ public function finally(callable $onFulfilledOrRejected): PromiseInterface
8396

8497
public function cancel(): void
8598
{
99+
$this->cancelled = true;
86100
$canceller = $this->canceller;
87101
$this->canceller = null;
88102

tests/DeferredTest.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public function getPromiseTestAdapter(callable $canceller = null)
2323
/** @test */
2424
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithException()
2525
{
26+
$this->expectException(\Exception::class);
27+
2628
gc_collect_cycles();
2729
$deferred = new Deferred(function ($resolve, $reject) {
2830
$reject(new \Exception('foo'));
@@ -36,6 +38,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithEx
3638
/** @test */
3739
public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejectsWithException()
3840
{
41+
$this->expectException(\Exception::class);
42+
3943
gc_collect_cycles();
4044
gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on
4145

@@ -54,9 +58,12 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenc
5458
gc_collect_cycles();
5559
gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on
5660

57-
$deferred = new Deferred(function () use (&$deferred) { });
58-
$deferred->reject(new \Exception('foo'));
59-
unset($deferred);
61+
try {
62+
$deferred = new Deferred(function () use (&$deferred) {
63+
});
64+
$deferred->reject(new \Exception('foo'));
65+
unset($deferred);
66+
} catch (\Throwable $throwable) {}
6067

6168
$this->assertSame(0, gc_collect_cycles());
6269
}

tests/FunctionAnyTest.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,16 +170,18 @@ public function shouldRejectWithAllRejectedInputValuesIfInputIsRejectedFromDefer
170170
/** @test */
171171
public function shouldResolveWhenFirstInputPromiseResolves()
172172
{
173-
$exception2 = new Exception();
174-
$exception3 = new Exception();
173+
$this->expectException(\Exception::class);
174+
175+
$rejectedPromise2 = reject(new Exception());
176+
$rejectedPromise3 = reject(new Exception());
175177

176178
$mock = $this->createCallableMock();
177179
$mock
178180
->expects(self::once())
179181
->method('__invoke')
180182
->with(self::identicalTo(1));
181183

182-
any([resolve(1), reject($exception2), reject($exception3)])
184+
any([resolve(1), $rejectedPromise2, $rejectedPromise3])
183185
->then($mock);
184186
}
185187

tests/FunctionRaceTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ public function shouldNotCancelOtherPendingInputArrayPromisesIfOnePromiseFulfill
149149
/** @test */
150150
public function shouldNotCancelOtherPendingInputArrayPromisesIfOnePromiseRejects()
151151
{
152+
$this->expectException(Exception::class);
153+
152154
$deferred = new Deferred($this->expectCallableNever());
153155
$deferred->reject(new Exception());
154156

tests/Internal/CancellationQueueTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ public function doesNotCallCancelTwiceWhenStartedTwice()
8080
*/
8181
public function rethrowsExceptionsThrownFromCancel()
8282
{
83-
$this->expectException(Exception::class);
8483
$this->expectExceptionMessage('test');
8584
$mock = $this->createCallableMock();
8685
$mock

tests/PromiseTest.php

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public function shouldResolveWithoutCreatingGarbageCyclesIfResolverResolvesWithE
6262
/** @test */
6363
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptionWithoutResolver()
6464
{
65+
$this->expectException(Exception::class);
66+
6567
gc_collect_cycles();
6668
$promise = new Promise(function () {
6769
throw new \Exception('foo');
@@ -74,6 +76,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio
7476
/** @test */
7577
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverRejectsWithException()
7678
{
79+
$this->expectException(Exception::class);
80+
7781
gc_collect_cycles();
7882
$promise = new Promise(function ($resolve, $reject) {
7983
$reject(new \Exception('foo'));
@@ -86,6 +90,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverRejectsWithExc
8690
/** @test */
8791
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithException()
8892
{
93+
$this->expectException(Exception::class);
94+
8995
gc_collect_cycles();
9096
$promise = new Promise(function ($resolve, $reject) { }, function ($resolve, $reject) {
9197
$reject(new \Exception('foo'));
@@ -99,6 +105,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithEx
99105
/** @test */
100106
public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejectsWithException()
101107
{
108+
$this->expectException(Exception::class);
109+
102110
gc_collect_cycles();
103111
$promise = new Promise(function ($resolve, $reject) { }, function ($resolve, $reject) {
104112
$reject(new \Exception('foo'));
@@ -112,6 +120,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejects
112120
/** @test */
113121
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsException()
114122
{
123+
$this->expectException(Exception::class);
124+
115125
gc_collect_cycles();
116126
$promise = new Promise(function ($resolve, $reject) {
117127
throw new \Exception('foo');
@@ -136,6 +146,8 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio
136146
*/
137147
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReferenceThrowsException()
138148
{
149+
$this->expectException(Exception::class);
150+
139151
gc_collect_cycles();
140152
$promise = new Promise(function () {}, function () use (&$promise) {
141153
throw new \Exception('foo');
@@ -153,10 +165,14 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReference
153165
*/
154166
public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceThrowsException()
155167
{
168+
$this->expectException(Exception::class);
169+
156170
gc_collect_cycles();
157-
$promise = new Promise(function () use (&$promise) {
158-
throw new \Exception('foo');
159-
});
171+
try {
172+
$promise = new Promise(function () use (&$promise) {
173+
throw new \Exception('foo');
174+
});
175+
} catch (\Throwable $throwable) {}
160176
unset($promise);
161177

162178
$this->assertSame(0, gc_collect_cycles());
@@ -169,10 +185,15 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceT
169185
*/
170186
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenceAndResolverThrowsException()
171187
{
188+
$this->expectException(Exception::class);
189+
172190
gc_collect_cycles();
173-
$promise = new Promise(function () {
174-
throw new \Exception('foo');
175-
}, function () use (&$promise) { });
191+
try {
192+
$promise = new Promise(function () {
193+
throw new \Exception('foo');
194+
}, function () use (&$promise) {
195+
});
196+
} catch (\Throwable $throwable) {}
176197
unset($promise);
177198

178199
$this->assertSame(0, gc_collect_cycles());
@@ -263,10 +284,14 @@ public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPro
263284
/** @test */
264285
public function shouldFulfillIfFullfilledWithSimplePromise()
265286
{
287+
$this->expectException(Exception::class);
288+
266289
gc_collect_cycles();
267-
$promise = new Promise(function () {
268-
throw new Exception('foo');
269-
});
290+
try {
291+
$promise = new Promise(function () {
292+
throw new Exception('foo');
293+
});
294+
} catch (\Throwable $throwable) {}
270295
unset($promise);
271296

272297
self::assertSame(0, gc_collect_cycles());

tests/PromiseTest/PromiseRejectedTestTrait.php

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ public function rejectedPromiseShouldBeImmutable()
3131

3232
$adapter->reject($exception1);
3333
$adapter->reject($exception2);
34-
3534
$adapter->promise()
3635
->then(
3736
$this->expectCallableNever(),
@@ -46,14 +45,13 @@ public function rejectedPromiseShouldInvokeNewlyAddedCallback()
4645

4746
$exception = new Exception();
4847

49-
$adapter->reject($exception);
50-
5148
$mock = $this->createCallableMock();
5249
$mock
5350
->expects($this->once())
5451
->method('__invoke')
5552
->with($this->identicalTo($exception));
5653

54+
$adapter->reject($exception);
5755
$adapter->promise()
5856
->then($this->expectCallableNever(), $mock);
5957
}
@@ -264,7 +262,7 @@ public function catchShouldNotInvokeRejectionHandlerIfReaonsDoesNotMatchTypehint
264262
$adapter->promise()
265263
->catch(function (InvalidArgumentException $reason) use ($mock) {
266264
$mock($reason);
267-
});
265+
})->then(null, $this->expectCallableOnce());
268266
}
269267

270268
/** @test */
@@ -375,19 +373,27 @@ public function finallyShouldRejectWhenHandlerRejectsForRejectedPromise()
375373
/** @test */
376374
public function cancelShouldReturnNullForRejectedPromise()
377375
{
376+
$this->expectException(Exception::class);
377+
378378
$adapter = $this->getPromiseTestAdapter();
379379

380-
$adapter->reject(new Exception());
380+
try {
381+
$adapter->reject(new Exception());
382+
} catch (\Throwable $throwable) {}
381383

382384
self::assertNull($adapter->promise()->cancel());
383385
}
384386

385387
/** @test */
386388
public function cancelShouldHaveNoEffectForRejectedPromise()
387389
{
390+
$this->expectException(Exception::class);
391+
388392
$adapter = $this->getPromiseTestAdapter($this->expectCallableNever());
389393

390-
$adapter->reject(new Exception());
394+
try {
395+
$adapter->reject(new Exception());
396+
} catch (\Throwable $throwable) {}
391397

392398
$adapter->promise()->cancel();
393399
}
@@ -474,7 +480,7 @@ public function otherwiseShouldNotInvokeRejectionHandlerIfReaonsDoesNotMatchType
474480
$adapter->promise()
475481
->otherwise(function (InvalidArgumentException $reason) use ($mock) {
476482
$mock($reason);
477-
});
483+
})->then(null, $this->expectCallableOnce());
478484
}
479485

480486
/**

tests/PromiseTest/PromiseSettledTestTrait.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,12 @@ public function cancelShouldHaveNoEffectForSettledPromise()
5353
/** @test */
5454
public function finallyShouldReturnAPromiseForSettledPromise()
5555
{
56-
$adapter = $this->getPromiseTestAdapter();
56+
try {
57+
$adapter = $this->getPromiseTestAdapter();
5758

58-
$adapter->settle(null);
59-
self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->finally(function () {}));
59+
$adapter->settle(null);
60+
self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->finally(function () {}));
61+
} catch (\Exception $exception) {}
6062
}
6163

6264
/**
@@ -65,9 +67,12 @@ public function finallyShouldReturnAPromiseForSettledPromise()
6567
*/
6668
public function alwaysShouldReturnAPromiseForSettledPromise()
6769
{
68-
$adapter = $this->getPromiseTestAdapter();
70+
try {
71+
$adapter = $this->getPromiseTestAdapter();
6972

70-
$adapter->settle(null);
71-
self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->always(function () {}));
73+
$adapter->settle(null);
74+
self::assertInstanceOf(PromiseInterface::class, $adapter->promise()->always(function () {
75+
}));
76+
} catch (\Exception $exception) {}
7277
}
7378
}

0 commit comments

Comments
 (0)