Add trait for Queue event handling
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Philipp Lang 2023-08-17 12:46:48 +02:00
parent 687ec80069
commit c260fcb4e4
6 changed files with 112 additions and 38 deletions

View File

@ -7,6 +7,8 @@ use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Ramsey\Uuid\Lazy\LazyUuidFromString;
use Ramsey\Uuid\UuidInterface;
class JobEvent implements ShouldBroadcastNow class JobEvent implements ShouldBroadcastNow
{ {
@ -15,13 +17,13 @@ class JobEvent implements ShouldBroadcastNow
public bool $reload = false; public bool $reload = false;
public string $message = ''; public string $message = '';
final private function __construct(public string $channel) final private function __construct(public string $channel, public UuidInterface $jobId)
{ {
} }
public static function on(string $channel): static public static function on(string $channel, UuidInterface $jobId): static
{ {
return new static($channel); return new static($channel, $jobId);
} }
public function withMessage(string $message): static public function withMessage(string $message): static
@ -31,14 +33,22 @@ class JobEvent implements ShouldBroadcastNow
return $this; return $this;
} }
public function dispatch(): void
{
event($this);
}
/** /**
* Get the channels the event should broadcast on. * Get the channels the event should broadcast on.
* *
* @return \Illuminate\Broadcasting\Channel * @return array<int, Channel>
*/ */
public function broadcastOn() public function broadcastOn()
{ {
return new Channel($this->channel); return [
new Channel($this->channel),
new Channel('jobs'),
];
} }
public function shouldReload(): static public function shouldReload(): static

View File

@ -7,64 +7,72 @@ use App\Lib\Events\JobFinished;
use App\Lib\Events\JobStarted; use App\Lib\Events\JobStarted;
use Closure; use Closure;
use Lorisleiva\Actions\Decorators\JobDecorator; use Lorisleiva\Actions\Decorators\JobDecorator;
use Ramsey\Uuid\Lazy\LazyUuidFromString;
use Ramsey\Uuid\UuidInterface;
use Throwable; use Throwable;
class WithJobState class WithJobState
{ {
public JobStarted $beforeMessage; public ?JobStarted $beforeMessage = null;
public JobFinished $afterMessage; public ?JobFinished $afterMessage = null;
public JobFailed $failedMessage; public ?JobFailed $failedMessage = null;
private function __construct(public string $channel) private function __construct(public string $channel, public UuidInterface $jobId)
{ {
} }
public static function make(string $channel): self public static function make(string $channel, UuidInterface $jobId): self
{ {
return new self($channel); return new self($channel, $jobId);
} }
public function before(string $message): self public function before(string $message): self
{ {
$this->beforeMessage = JobStarted::on($this->channel)->withMessage($message); $this->beforeMessage = JobStarted::on($this->channel, $this->jobId)->withMessage($message);
return $this; return $this;
} }
public function after(string $message): self public function after(string $message): self
{ {
$this->afterMessage = JobFinished::on($this->channel)->withMessage($message); $this->afterMessage = JobFinished::on($this->channel, $this->jobId)->withMessage($message);
return $this; return $this;
} }
public function failed(string $message): self public function failed(string $message): self
{ {
$this->failedMessage = JobFailed::on($this->channel)->withMessage($message); $this->failedMessage = JobFailed::on($this->channel, $this->jobId)->withMessage($message);
return $this; return $this;
} }
public function shouldReload(): self public function shouldReload(): self
{ {
$this->afterMessage->shouldReload(); $this->afterMessage?->shouldReload();
$this->failedMessage->shouldReload(); $this->failedMessage?->shouldReload();
return $this; return $this;
} }
public function handle(JobDecorator $job, Closure $next): void public function handle(JobDecorator $job, Closure $next): void
{ {
event($this->beforeMessage); if ($this->beforeMessage) {
event($this->beforeMessage);
}
try { try {
$next($job); $next($job);
} catch (Throwable $e) { } catch (Throwable $e) {
event($this->failedMessage); if ($this->failedMessage) {
event($this->failedMessage);
}
throw $e; throw $e;
} }
event($this->afterMessage); if ($this->afterMessage) {
event($this->afterMessage);
}
} }
} }

View File

@ -0,0 +1,41 @@
<?php
namespace App\Lib\Queue;
use Illuminate\Support\Str;
use App\Lib\JobMiddleware\WithJobState;
trait TracksJob
{
abstract public function jobState(WithJobState $jobState, ...$parameters): WithJobState;
abstract public function jobChannel(): string;
/**
* @param mixed $parameters
*/
public function startJob(...$parameters): void
{
$jobId = Str::uuid();
$jobState = WithJobState::make($this->jobChannel(), $jobId);
$this->jobState(...[$jobState, ...$parameters])->beforeMessage->dispatch();
$parameters[] = $jobId;
static::dispatch(...$parameters);
}
/**
* @param mixed $parameters
*
* @return array<int, object>
*/
public function getJobMiddleware(...$parameters): array
{
$jobId = array_pop($parameters);
$jobState = WithJobState::make($this->jobChannel(), $jobId);
$jobState = $this->jobState(...[$jobState, ...$parameters]);
$jobState->beforeMessage = null;
return [
$jobState
];
}
}

View File

@ -3,6 +3,7 @@
namespace App\Member\Actions; namespace App\Member\Actions;
use App\Lib\JobMiddleware\WithJobState; use App\Lib\JobMiddleware\WithJobState;
use App\Lib\Queue\TracksJob;
use App\Maildispatcher\Actions\ResyncAction; use App\Maildispatcher\Actions\ResyncAction;
use App\Member\Member; use App\Member\Member;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
@ -12,6 +13,7 @@ class MemberDeleteAction
{ {
use AsAction; use AsAction;
use TracksJob;
public function handle(int $memberId): void public function handle(int $memberId): void
{ {
@ -27,24 +29,27 @@ class MemberDeleteAction
public function asController(Member $member): RedirectResponse public function asController(Member $member): RedirectResponse
{ {
static::dispatch($member->id); $this->startJob($member->id);
return redirect()->back(); return redirect()->back();
} }
/** /**
* @return array<int, object> * @param mixed $parameters
*/ */
public function getJobMiddleware(int $memberId): array public function jobState(WithJobState $jobState, ...$parameters): WithJobState
{ {
$member = Member::findOrFail($memberId); $member = Member::findOrFail($parameters[0]);
return [ return $jobState
WithJobState::make('member') ->before('Mitglied ' . $member->fullname . ' wird gelöscht')
->before('Lösche Mitglied ' . $member->fullname) ->after('Mitglied ' . $member->fullname . ' gelöscht')
->after('Mitglied ' . $member->fullname . ' gelöscht') ->failed('Löschen von ' . $member->fullname . ' fehlgeschlagen.')
->failed('Löschen von ' . $member->fullname . ' fehlgeschlagen.') ->shouldReload();
->shouldReload(), }
];
public function jobChannel(): string
{
return 'member';
} }
} }

View File

@ -55,7 +55,7 @@ return [
'prefix' => env( 'prefix' => env(
'HORIZON_PREFIX', 'HORIZON_PREFIX',
Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:' Str::slug(env('APP_NAME', 'laravel'), '_') . '_horizon:'
), ),
/* /*

View File

@ -2,6 +2,7 @@
namespace Tests\Feature\Member; namespace Tests\Feature\Member;
use Illuminate\Support\Str;
use App\Course\Models\Course; use App\Course\Models\Course;
use App\Course\Models\CourseMember; use App\Course\Models\CourseMember;
use App\Lib\Events\JobFailed; use App\Lib\Events\JobFailed;
@ -21,6 +22,12 @@ class DeleteTest extends TestCase
{ {
use DatabaseTransactions; use DatabaseTransactions;
public function setUp(): void
{
parent::setUp();
Event::fake([JobStarted::class, JobFinished::class, JobFailed::class]);
}
public function testItFiresJob(): void public function testItFiresJob(): void
{ {
Queue::fake(); Queue::fake();
@ -31,6 +38,7 @@ class DeleteTest extends TestCase
$response->assertRedirect('/member'); $response->assertRedirect('/member');
Event::assertDispatched(JobStarted::class);
MemberDeleteAction::assertPushed(fn ($action, $parameters) => $parameters[0] === $member->id); MemberDeleteAction::assertPushed(fn ($action, $parameters) => $parameters[0] === $member->id);
} }
@ -64,7 +72,7 @@ class DeleteTest extends TestCase
MemberDeleteAction::run($member->id); MemberDeleteAction::run($member->id);
$this->assertDatabaseCount('members', 0); $this->assertDatabaseMissing('members', ['id' => $member->id]);
} }
public function testItFiresEventWhenFinished(): void public function testItFiresEventWhenFinished(): void
@ -72,11 +80,12 @@ class DeleteTest extends TestCase
Event::fake([JobStarted::class, JobFinished::class]); Event::fake([JobStarted::class, JobFinished::class]);
$this->withoutExceptionHandling()->login()->loginNami(); $this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()->defaults()->create(['firstname' => 'Max', 'lastname' => 'Muster']); $member = Member::factory()->defaults()->create(['firstname' => 'Max', 'lastname' => 'Muster']);
$uuid = Str::uuid();
MemberDeleteAction::dispatch($member->id); MemberDeleteAction::dispatch($member->id, $uuid);
Event::assertDispatched(JobStarted::class, fn ($event) => $event->broadcastOn()->name === 'member' && $event->message === 'Lösche Mitglied Max Muster' && $event->reload === false); Event::assertNotDispatched(JobStarted::class);
Event::assertDispatched(JobFinished::class, fn ($event) => $event->message === 'Mitglied Max Muster gelöscht' && $event->reload === true); Event::assertDispatched(JobFinished::class, fn ($event) => $event->message === 'Mitglied Max Muster gelöscht' && $event->reload === true && $event->jobId->serialize() === $uuid->serialize());
} }
public function testItFiresEventWhenDeletingFailed(): void public function testItFiresEventWhenDeletingFailed(): void
@ -85,14 +94,15 @@ class DeleteTest extends TestCase
$this->login()->loginNami(); $this->login()->loginNami();
$member = Member::factory()->defaults()->create(['firstname' => 'Max', 'lastname' => 'Muster']); $member = Member::factory()->defaults()->create(['firstname' => 'Max', 'lastname' => 'Muster']);
MemberDeleteAction::partialMock()->shouldReceive('handle')->andThrow(new Exception('sorry')); MemberDeleteAction::partialMock()->shouldReceive('handle')->andThrow(new Exception('sorry'));
$uuid = Str::uuid();
try { try {
MemberDeleteAction::dispatch($member->id); MemberDeleteAction::dispatch($member->id, $uuid);
} catch (Throwable) { } catch (Throwable) {
} }
Event::assertDispatched(JobStarted::class, fn ($event) => $event->broadcastOn()->name === 'member' && $event->message === 'Lösche Mitglied Max Muster' && $event->reload === false); Event::assertNotDispatched(JobStarted::class);
Event::assertDispatched(JobFailed::class, fn ($event) => $event->message === 'Löschen von Max Muster fehlgeschlagen.' && $event->reload === true); Event::assertDispatched(JobFailed::class, fn ($event) => $event->message === 'Löschen von Max Muster fehlgeschlagen.' && $event->reload === true && $event->jobId->serialize() === $uuid->serialize());
Event::assertNotDispatched(JobFinished::class); Event::assertNotDispatched(JobFinished::class);
} }
} }