Trigger a PostTicketUpdateEvent asynchronously

This commit is contained in:
2025-10-16 10:04:36 +02:00
parent 40aa7b2f35
commit a889940165
8 changed files with 304 additions and 0 deletions

View File

@@ -66,6 +66,7 @@ framework:
'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority
'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async
'Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage': async
'Chill\TicketBundle\Messenger\PostTicketUpdateMessage': async
# end of routes added by chill-bundles recipes
# Route your messages to the transports
# 'App\Message\YourMessage': async

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Event\EventSubscriber;
use Chill\TicketBundle\Event\TicketUpdateEvent;
use Chill\TicketBundle\Messenger\PostTicketUpdateMessage;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Messenger\MessageBusInterface;
/**
* Subscribe to TicketUpdateEvents and dispatch a message for each one when the kernel terminates.
*/
final class GeneratePostUpdateTicketEventSubscriber implements EventSubscriberInterface
{
/**
* @var list<PostTicketUpdateMessage>
*/
private array $toDispatch = [];
public function __construct(private readonly MessageBusInterface $messageBus) {}
public static function getSubscribedEvents(): array
{
return [
TicketUpdateEvent::class => ['onTicketUpdate', 0],
KernelEvents::TERMINATE => ['onKernelTerminate', 8096],
];
}
public function onTicketUpdate(TicketUpdateEvent $event): void
{
$this->toDispatch[] = new PostTicketUpdateMessage($event->ticket, $event->updateKind);
}
public function onKernelTerminate(TerminateEvent $event): void
{
foreach ($this->toDispatch as $message) {
$this->messageBus->dispatch($message);
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Event;
use Chill\TicketBundle\Entity\Ticket;
/**
* Event triggered asynchronously after a ticket has been updated.
*
* This event is trigged by PostTicketUpdateMessageHandler, using Messenger component.
*
* To use a synchronous event, see @see{TicketUpdateEvent}
*/
class PostTicketUpdateEvent
{
public function __construct(
public readonly TicketUpdateKindEnum $updateKind,
public readonly Ticket $ticket,
) {}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Messenger\Handler;
use Chill\TicketBundle\Event\PostTicketUpdateEvent;
use Chill\TicketBundle\Messenger\PostTicketUpdateMessage;
use Chill\TicketBundle\Repository\TicketRepositoryInterface;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
final readonly class PostTicketUpdateMessageHandler
{
public function __construct(
private EventDispatcherInterface $eventDispatcher,
private TicketRepositoryInterface $ticketRepository,
) {}
public function __invoke(PostTicketUpdateMessage $event): void
{
$ticket = $this->ticketRepository->find($event->ticketId);
if (null === $ticket) {
throw new UnrecoverableMessageHandlingException('Ticket not found');
}
$this->eventDispatcher->dispatch(new PostTicketUpdateEvent($event->updateKind, $ticket));
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Messenger;
use Chill\TicketBundle\Entity\Ticket;
use Chill\TicketBundle\Event\TicketUpdateKindEnum;
final readonly class PostTicketUpdateMessage
{
public readonly int $ticketId;
public function __construct(
Ticket $ticket,
public TicketUpdateKindEnum $updateKind,
) {
$this->ticketId = $ticket->getId();
}
}

View File

@@ -17,6 +17,9 @@ services:
tags:
- controller.service_arguments
Chill\TicketBundle\Event\EventSubscriber\:
resource: '../Event/EventSubscriber/'
Chill\TicketBundle\Repository\:
resource: '../Repository/'
@@ -35,6 +38,9 @@ services:
Chill\TicketBundle\Menu\:
resource: '../Menu/'
Chill\TicketBundle\Messenger\Handler\:
resource: '../Messenger/Handler'
Chill\TicketBundle\Validation\:
resource: '../Validation/'

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\tests\Event\EventSubscriber;
use Chill\TicketBundle\Entity\Ticket;
use Chill\TicketBundle\Event\EventSubscriber\GeneratePostUpdateTicketEventSubscriber;
use Chill\TicketBundle\Event\TicketUpdateEvent;
use Chill\TicketBundle\Event\TicketUpdateKindEnum;
use Chill\TicketBundle\Messenger\PostTicketUpdateMessage;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
/**
* @covers \Chill\TicketBundle\Event\EventSubscriber\GeneratePostUpdateTicketEventSubscriber
*
* @internal
*/
class GeneratePostUpdateTicketEventSubscriberTest extends TestCase
{
use ProphecyTrait;
public function testOnTicketUpdate(): void
{
$ticket = new Ticket();
$reflection = new \ReflectionClass(Ticket::class);
$idProperty = $reflection->getProperty('id');
$idProperty->setValue($ticket, 1);
$event = new class (TicketUpdateKindEnum::UPDATE_MOTIVE, $ticket) extends TicketUpdateEvent {};
$messageBus = $this->prophesize(MessageBusInterface::class);
$messageBus->dispatch(Argument::that(fn ($arg) => $arg instanceof PostTicketUpdateMessage && TicketUpdateKindEnum::UPDATE_MOTIVE === $arg->updateKind && 1 === $arg->ticketId))
->will(fn ($args) => new Envelope($args[0]))
->shouldBeCalled();
$eventSubscriber = new GeneratePostUpdateTicketEventSubscriber($messageBus->reveal());
$eventSubscriber->onTicketUpdate($event);
$kernel = $this->prophesize(KernelInterface::class);
$terminate = new TerminateEvent($kernel->reveal(), new Request(), new Response());
$eventSubscriber->onKernelTerminate($terminate);
}
}

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\tests\Messenger\Handler;
use Chill\TicketBundle\Entity\Ticket;
use Chill\TicketBundle\Event\PostTicketUpdateEvent;
use Chill\TicketBundle\Event\TicketUpdateKindEnum;
use Chill\TicketBundle\Messenger\PostTicketUpdateMessage;
use Chill\TicketBundle\Messenger\Handler\PostTicketUpdateMessageHandler;
use Chill\TicketBundle\Repository\TicketRepositoryInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* @covers \Chill\TicketBundle\Messenger\Handler\PostTicketUpdateMessageHandler
*
* @internal
*/
class PostTicketUpdateMessageHandlerTest extends TestCase
{
use ProphecyTrait;
public function testDispatchesEventWhenTicketExists(): void
{
// Arrange: a Ticket with an ID
$ticket = new Ticket();
$reflection = new \ReflectionClass(Ticket::class);
$idProperty = $reflection->getProperty('id');
$idProperty->setValue($ticket, 123);
$message = new PostTicketUpdateMessage($ticket, TicketUpdateKindEnum::UPDATE_MOTIVE);
// Mock repository to return the Ticket when searching by id
$ticketRepository = $this->prophesize(TicketRepositoryInterface::class);
$ticketRepository->find(123)->willReturn($ticket)->shouldBeCalledOnce();
// Expect the dispatcher to dispatch a PostTicketUpdateEvent with correct data
$eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
$eventDispatcher
->dispatch(Argument::that(function ($event) use ($ticket) {
return $event instanceof PostTicketUpdateEvent
&& TicketUpdateKindEnum::UPDATE_MOTIVE === $event->updateKind
&& $event->ticket === $ticket;
}))
->will(function ($args) { return $args[0]; })
->shouldBeCalledOnce();
$handler = new PostTicketUpdateMessageHandler($eventDispatcher->reveal(), $ticketRepository->reveal());
// Act
$handler($message);
// Assert: expectations asserted by Prophecy
self::assertTrue(true);
}
public function testThrowsWhenTicketNotFound(): void
{
// Arrange: a Ticket with an ID for the message, but repository will return null
$ticket = new Ticket();
$reflection = new \ReflectionClass(Ticket::class);
$idProperty = $reflection->getProperty('id');
$idProperty->setValue($ticket, 999);
$message = new PostTicketUpdateMessage($ticket, TicketUpdateKindEnum::UPDATE_MOTIVE);
$ticketRepository = $this->prophesize(TicketRepositoryInterface::class);
$ticketRepository->find(999)->willReturn(null)->shouldBeCalledOnce();
$eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
$eventDispatcher->dispatch(Argument::any())->shouldNotBeCalled();
$handler = new PostTicketUpdateMessageHandler($eventDispatcher->reveal(), $ticketRepository->reveal());
// Assert: exception is thrown
$this->expectException(UnrecoverableMessageHandlingException::class);
$this->expectExceptionMessage('Ticket not found');
// Act
$handler($message);
}
}