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

View File

@ -7,64 +7,72 @@ use App\Lib\Events\JobFinished;
use App\Lib\Events\JobStarted;
use Closure;
use Lorisleiva\Actions\Decorators\JobDecorator;
use Ramsey\Uuid\Lazy\LazyUuidFromString;
use Ramsey\Uuid\UuidInterface;
use Throwable;
class WithJobState
{
public JobStarted $beforeMessage;
public JobFinished $afterMessage;
public JobFailed $failedMessage;
public ?JobStarted $beforeMessage = null;
public ?JobFinished $afterMessage = null;
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
{
$this->beforeMessage = JobStarted::on($this->channel)->withMessage($message);
$this->beforeMessage = JobStarted::on($this->channel, $this->jobId)->withMessage($message);
return $this;
}
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;
}
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;
}
public function shouldReload(): self
{
$this->afterMessage->shouldReload();
$this->failedMessage->shouldReload();
$this->afterMessage?->shouldReload();
$this->failedMessage?->shouldReload();
return $this;
}
public function handle(JobDecorator $job, Closure $next): void
{
if ($this->beforeMessage) {
event($this->beforeMessage);
}
try {
$next($job);
} catch (Throwable $e) {
if ($this->failedMessage) {
event($this->failedMessage);
}
throw $e;
}
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;
use App\Lib\JobMiddleware\WithJobState;
use App\Lib\Queue\TracksJob;
use App\Maildispatcher\Actions\ResyncAction;
use App\Member\Member;
use Illuminate\Http\RedirectResponse;
@ -12,6 +13,7 @@ class MemberDeleteAction
{
use AsAction;
use TracksJob;
public function handle(int $memberId): void
{
@ -27,24 +29,27 @@ class MemberDeleteAction
public function asController(Member $member): RedirectResponse
{
static::dispatch($member->id);
$this->startJob($member->id);
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 [
WithJobState::make('member')
->before('Lösche Mitglied ' . $member->fullname)
return $jobState
->before('Mitglied ' . $member->fullname . ' wird gelöscht')
->after('Mitglied ' . $member->fullname . ' gelöscht')
->failed('Löschen von ' . $member->fullname . ' fehlgeschlagen.')
->shouldReload(),
];
->shouldReload();
}
public function jobChannel(): string
{
return 'member';
}
}

View File

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