diff --git a/app/Lib/Events/JobEvent.php b/app/Lib/Events/JobEvent.php index 0e4c7782..ed318d0d 100644 --- a/app/Lib/Events/JobEvent.php +++ b/app/Lib/Events/JobEvent.php @@ -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 */ public function broadcastOn() { - return new Channel($this->channel); + return [ + new Channel($this->channel), + new Channel('jobs'), + ]; } public function shouldReload(): static diff --git a/app/Lib/JobMiddleware/WithJobState.php b/app/Lib/JobMiddleware/WithJobState.php index b346cdc0..ce6068a4 100644 --- a/app/Lib/JobMiddleware/WithJobState.php +++ b/app/Lib/JobMiddleware/WithJobState.php @@ -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 { - event($this->beforeMessage); + if ($this->beforeMessage) { + event($this->beforeMessage); + } try { $next($job); } catch (Throwable $e) { - event($this->failedMessage); + if ($this->failedMessage) { + event($this->failedMessage); + } throw $e; } - event($this->afterMessage); + if ($this->afterMessage) { + event($this->afterMessage); + } } } diff --git a/app/Lib/Queue/TracksJob.php b/app/Lib/Queue/TracksJob.php new file mode 100644 index 00000000..3ebe83da --- /dev/null +++ b/app/Lib/Queue/TracksJob.php @@ -0,0 +1,41 @@ +jobChannel(), $jobId); + $this->jobState(...[$jobState, ...$parameters])->beforeMessage->dispatch(); + $parameters[] = $jobId; + static::dispatch(...$parameters); + } + + /** + * @param mixed $parameters + * + * @return array + */ + 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 + ]; + } +} diff --git a/app/Member/Actions/MemberDeleteAction.php b/app/Member/Actions/MemberDeleteAction.php index 35fbeda3..40ff09ab 100644 --- a/app/Member/Actions/MemberDeleteAction.php +++ b/app/Member/Actions/MemberDeleteAction.php @@ -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 + * @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) - ->after('Mitglied ' . $member->fullname . ' gelöscht') - ->failed('Löschen von ' . $member->fullname . ' fehlgeschlagen.') - ->shouldReload(), - ]; + return $jobState + ->before('Mitglied ' . $member->fullname . ' wird gelöscht') + ->after('Mitglied ' . $member->fullname . ' gelöscht') + ->failed('Löschen von ' . $member->fullname . ' fehlgeschlagen.') + ->shouldReload(); + } + + public function jobChannel(): string + { + return 'member'; } } diff --git a/config/horizon.php b/config/horizon.php index f8bc28f6..a0385b08 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -55,7 +55,7 @@ return [ 'prefix' => env( 'HORIZON_PREFIX', - Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:' + Str::slug(env('APP_NAME', 'laravel'), '_') . '_horizon:' ), /* diff --git a/tests/Feature/Member/DeleteTest.php b/tests/Feature/Member/DeleteTest.php index 8e37ff68..5a78cfd3 100644 --- a/tests/Feature/Member/DeleteTest.php +++ b/tests/Feature/Member/DeleteTest.php @@ -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); } }