Skip to content

Commit ddad40d

Browse files
[12.x] Introduce FailOnException job middleware (#56037)
* introduce RetryIf middleware * test * test * test * yoooo this testing setup is more complicated than it needs to be * one more shot * better name * ok, this is it * move namespace * another method * add job test * comments * rename * Update FailOnException.php * formatting --------- Co-authored-by: Taylor Otwell <taylor@laravel.com>
1 parent 8737a65 commit ddad40d

File tree

2 files changed

+193
-0
lines changed

2 files changed

+193
-0
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace Illuminate\Queue\Middleware;
4+
5+
use Closure;
6+
use Throwable;
7+
8+
class FailOnException
9+
{
10+
/**
11+
* The truth-test callback to determine if the job should fail.
12+
*
13+
* @var \Closure(\Throwable, mixed): bool
14+
*/
15+
protected Closure $callback;
16+
17+
/**
18+
* Create a middleware instance.
19+
*
20+
* @param (\Closure(\Throwable, mixed): bool)|array<array-key, class-string<\Throwable>> $callback
21+
*/
22+
public function __construct($callback)
23+
{
24+
if (is_array($callback)) {
25+
$callback = $this->failForExceptions($callback);
26+
}
27+
28+
$this->callback = $callback;
29+
}
30+
31+
/**
32+
* Indicate that the job should fail if it encounters the given exceptions.
33+
*
34+
* @param array<array-key, class-string<\Throwable>> $exceptions
35+
* @return \Closure(\Throwable, mixed): bool
36+
*/
37+
protected function failForExceptions(array $exceptions)
38+
{
39+
return static function (Throwable $throwable) use ($exceptions) {
40+
foreach ($exceptions as $exception) {
41+
if ($throwable instanceof $exception) {
42+
return true;
43+
}
44+
}
45+
46+
return false;
47+
};
48+
}
49+
50+
/**
51+
* Mark the job as failed if an exception is thrown that passes a truth-test callback.
52+
*
53+
* @param mixed $job
54+
* @param callable $next
55+
* @return mixed
56+
*
57+
* @throws Throwable
58+
*/
59+
public function handle($job, callable $next)
60+
{
61+
try {
62+
return $next($job);
63+
} catch (Throwable $e) {
64+
if (call_user_func($this->callback, $e, $job) === true) {
65+
$job->fail($e);
66+
}
67+
68+
throw $e;
69+
}
70+
}
71+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Queue;
4+
5+
use Illuminate\Bus\Dispatcher;
6+
use Illuminate\Bus\Queueable;
7+
use Illuminate\Contracts\Queue\ShouldQueue;
8+
use Illuminate\Foundation\Bus\Dispatchable;
9+
use Illuminate\Queue\CallQueuedHandler;
10+
use Illuminate\Queue\InteractsWithQueue;
11+
use Illuminate\Queue\Jobs\FakeJob;
12+
use Illuminate\Queue\Middleware\FailOnException;
13+
use InvalidArgumentException;
14+
use LogicException;
15+
use Orchestra\Testbench\TestCase;
16+
use PHPUnit\Framework\Attributes\DataProvider;
17+
use PHPUnit\Framework\Attributes\TestWith;
18+
use Throwable;
19+
20+
class FailOnExceptionMiddlewareTest extends TestCase
21+
{
22+
protected function setUp(): void
23+
{
24+
parent::setUp();
25+
FailOnExceptionMiddlewareTestJob::$_middleware = [];
26+
}
27+
28+
/**
29+
* @return array<string, array{class-string<\Throwable>, FailOnException, bool}>
30+
*/
31+
public static function testMiddlewareDataProvider(): array
32+
{
33+
return [
34+
'exception is in list' => [
35+
InvalidArgumentException::class,
36+
new FailOnException([InvalidArgumentException::class]),
37+
true,
38+
],
39+
'exception is not in list' => [
40+
LogicException::class,
41+
new FailOnException([InvalidArgumentException::class]),
42+
false,
43+
],
44+
];
45+
}
46+
47+
#[DataProvider('testMiddlewareDataProvider')]
48+
public function test_middleware(
49+
string $thrown,
50+
FailOnException $middleware,
51+
bool $expectedToFail
52+
): void {
53+
FailOnExceptionMiddlewareTestJob::$_middleware = [$middleware];
54+
$job = new FailOnExceptionMiddlewareTestJob($thrown);
55+
$instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app);
56+
57+
$fakeJob = new FakeJob();
58+
$job->setJob($fakeJob);
59+
60+
try {
61+
$instance->call($fakeJob, [
62+
'command' => serialize($job),
63+
]);
64+
$this->fail('Did not throw exception');
65+
} catch (Throwable $e) {
66+
$this->assertInstanceOf($thrown, $e);
67+
}
68+
69+
$expectedToFail ? $job->assertFailed() : $job->assertNotFailed();
70+
}
71+
72+
#[TestWith(['abc', true])]
73+
#[TestWith(['tots', false])]
74+
public function test_can_test_against_job_properties($value, bool $expectedToFail): void
75+
{
76+
FailOnExceptionMiddlewareTestJob::$_middleware = [
77+
new FailOnException(fn ($thrown, $job) => $job->value === 'abc'),
78+
];
79+
80+
$job = new FailOnExceptionMiddlewareTestJob(InvalidArgumentException::class, $value);
81+
$instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app);
82+
83+
$fakeJob = new FakeJob();
84+
$job->setJob($fakeJob);
85+
86+
try {
87+
$instance->call($fakeJob, [
88+
'command' => serialize($job),
89+
]);
90+
$this->fail('Did not throw exception');
91+
} catch (Throwable) {
92+
//
93+
}
94+
95+
$expectedToFail ? $job->assertFailed() : $job->assertNotFailed();
96+
}
97+
}
98+
99+
class FailOnExceptionMiddlewareTestJob implements ShouldQueue
100+
{
101+
use InteractsWithQueue;
102+
use Queueable;
103+
use Dispatchable;
104+
105+
public static array $_middleware = [];
106+
107+
public int $tries = 2;
108+
109+
public function __construct(private $throws, public $value = null)
110+
{
111+
}
112+
113+
public function handle()
114+
{
115+
throw new $this->throws;
116+
}
117+
118+
public function middleware(): array
119+
{
120+
return self::$_middleware;
121+
}
122+
}

0 commit comments

Comments
 (0)