Add MotiveUpdateEvent and integrate dispatch logic in ReplaceMotiveCommandHandler

- Introduced `MotiveUpdateEvent` to encapsulate motive updates for tickets.
- Modified `ReplaceMotiveCommandHandler` to dispatch the new event when motives are updated.
- Updated tests to validate `MotiveUpdateEvent` dispatching.
This commit is contained in:
2025-10-15 16:41:44 +02:00
parent 9a7af3b0c9
commit 40aa7b2f35
3 changed files with 94 additions and 7 deletions

View File

@@ -15,8 +15,11 @@ use Chill\TicketBundle\Action\Ticket\ChangeEmergencyStateCommand;
use Chill\TicketBundle\Action\Ticket\ReplaceMotiveCommand;
use Chill\TicketBundle\Entity\MotiveHistory;
use Chill\TicketBundle\Entity\Ticket;
use Chill\TicketBundle\Event\MotiveUpdateEvent;
use Chill\TicketBundle\Event\TicketUpdateEvent;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class ReplaceMotiveCommandHandler
{
@@ -24,6 +27,7 @@ class ReplaceMotiveCommandHandler
private readonly ClockInterface $clock,
private readonly EntityManagerInterface $entityManager,
private readonly ChangeEmergencyStateCommandHandler $changeEmergencyStateCommandHandler,
private readonly EventDispatcherInterface $eventDispatcher,
) {}
public function handle(Ticket $ticket, ReplaceMotiveCommand $command): void
@@ -32,6 +36,8 @@ class ReplaceMotiveCommandHandler
throw new \InvalidArgumentException('The new motive cannot be null');
}
$event = new MotiveUpdateEvent($ticket);
// will add if there are no existing motive
$readyToAdd = 0 === count($ticket->getMotiveHistories());
@@ -45,6 +51,8 @@ class ReplaceMotiveCommandHandler
continue;
}
// collect previous active motives before closing
$event->previousMotive = $history->getMotive();
$history->setEndDate($this->clock->now());
$readyToAdd = true;
}
@@ -52,6 +60,7 @@ class ReplaceMotiveCommandHandler
if ($readyToAdd) {
$history = new MotiveHistory($command->motive, $ticket, $this->clock->now());
$this->entityManager->persist($history);
$event->newMotive = $command->motive;
// Check if the motive has makeTicketEmergency set and update the ticket's emergency status if needed
if ($command->motive->isMakeTicketEmergency()) {
@@ -59,5 +68,9 @@ class ReplaceMotiveCommandHandler
($this->changeEmergencyStateCommandHandler)($ticket, $changeEmergencyCommand);
}
}
if ($event->hasChanges()) {
$this->eventDispatcher->dispatch($event, TicketUpdateEvent::class);
}
}
}

View File

@@ -0,0 +1,31 @@
<?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\Motive;
use Chill\TicketBundle\Entity\Ticket;
final class MotiveUpdateEvent extends TicketUpdateEvent
{
public function __construct(
Ticket $ticket,
public ?Motive $previousMotive = null,
public ?Motive $newMotive = null,
) {
parent::__construct(TicketUpdateKindEnum::UPDATE_MOTIVE, $ticket);
}
public function hasChanges(): bool
{
return null !== $this->newMotive || null !== $this->previousMotive;
}
}

View File

@@ -19,16 +19,19 @@ use Chill\TicketBundle\Entity\EmergencyStatusEnum;
use Chill\TicketBundle\Entity\Motive;
use Chill\TicketBundle\Entity\MotiveHistory;
use Chill\TicketBundle\Entity\Ticket;
use Chill\TicketBundle\Event\MotiveUpdateEvent;
use Chill\TicketBundle\Event\TicketUpdateEvent;
use Doctrine\ORM\EntityManagerInterface;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Clock\MockClock;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* @internal
*
* @coversNothing
* @covers \Chill\TicketBundle\Action\Ticket\Handler\ReplaceMotiveCommandHandler
*/
final class ReplaceMotiveCommandHandlerTest extends KernelTestCase
{
@@ -37,14 +40,18 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase
private function buildHandler(
EntityManagerInterface $entityManager,
?ChangeEmergencyStateCommandHandler $changeEmergencyStateCommandHandler = null,
?EventDispatcherInterface $eventDispatcher = null,
): ReplaceMotiveCommandHandler {
$clock = new MockClock();
if (null === $changeEmergencyStateCommandHandler) {
$changeEmergencyStateCommandHandler = $this->prophesize(ChangeEmergencyStateCommandHandler::class)->reveal();
}
if (null === $eventDispatcher) {
$eventDispatcher = $this->prophesize(EventDispatcherInterface::class)->reveal();
}
return new ReplaceMotiveCommandHandler($clock, $entityManager, $changeEmergencyStateCommandHandler);
return new ReplaceMotiveCommandHandler($clock, $entityManager, $changeEmergencyStateCommandHandler, $eventDispatcher);
}
public function testHandleOnTicketWithoutMotive(): void
@@ -61,7 +68,18 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase
return $arg->getMotive() === $motive;
}))->shouldBeCalled();
$handler = $this->buildHandler($entityManager->reveal());
$eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
$eventDispatcher->dispatch(
Argument::that(function ($event) use ($motive) {
return $event instanceof MotiveUpdateEvent
&& $event->newMotive === $motive
&& null === $event->previousMotive
&& $event->hasChanges();
}),
TicketUpdateEvent::class
)->shouldBeCalled();
$handler = $this->buildHandler($entityManager->reveal(), null, $eventDispatcher->reveal());
$handler->handle($ticket, new ReplaceMotiveCommand($motive));
@@ -83,7 +101,19 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase
return $arg->getMotive() === $motive;
}))->shouldBeCalled();
$handler = $this->buildHandler($entityManager->reveal());
$previous = $history->getMotive();
$eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
$eventDispatcher->dispatch(
Argument::that(function ($event) use ($motive, $previous) {
return $event instanceof MotiveUpdateEvent
&& $event->newMotive === $motive
&& $previous === $event->previousMotive
&& $event->hasChanges();
}),
TicketUpdateEvent::class
)->shouldBeCalled();
$handler = $this->buildHandler($entityManager->reveal(), null, $eventDispatcher->reveal());
$handler->handle($ticket, new ReplaceMotiveCommand($motive));
@@ -106,7 +136,10 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase
return $arg->getMotive() === $motive;
}))->shouldNotBeCalled();
$handler = $this->buildHandler($entityManager->reveal());
$eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
$eventDispatcher->dispatch(Argument::any(), TicketUpdateEvent::class)->shouldNotBeCalled();
$handler = $this->buildHandler($entityManager->reveal(), null, $eventDispatcher->reveal());
$handler->handle($ticket, new ReplaceMotiveCommand($motive));
@@ -134,10 +167,15 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase
Argument::that(fn (ChangeEmergencyStateCommand $command) => EmergencyStatusEnum::YES === $command->newEmergencyStatus)
)->shouldBeCalled();
// Expect event dispatch for motive update
$eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
$eventDispatcher->dispatch(Argument::type(MotiveUpdateEvent::class), TicketUpdateEvent::class)->shouldBeCalled();
// Create the handler with our mocks
$handler = $this->buildHandler(
$entityManager->reveal(),
$changeEmergencyStateCommandHandler->reveal()
$changeEmergencyStateCommandHandler->reveal(),
$eventDispatcher->reveal()
);
// Handle the command
@@ -166,10 +204,15 @@ final class ReplaceMotiveCommandHandlerTest extends KernelTestCase
Argument::cetera()
)->shouldNotBeCalled();
// Expect event dispatch for motive update
$eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
$eventDispatcher->dispatch(Argument::type(MotiveUpdateEvent::class), TicketUpdateEvent::class)->shouldBeCalled();
// Create the handler with our mocks
$handler = $this->buildHandler(
$entityManager->reveal(),
$changeEmergencyStateCommandHandler->reveal()
$changeEmergencyStateCommandHandler->reveal(),
$eventDispatcher->reveal()
);
// Handle the command