*/ final readonly class ClientTicketStatusProcessor implements ProcessorInterface { private const FORBIDDEN_TRANSITIONS = [ 'done' => ['new'], 'rejected' => ['new'], ]; public function __construct( private EntityManagerInterface $entityManager, private NotificationService $notificationService, private Security $security, ) {} public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): ClientTicket { assert($data instanceof ClientTicket); $originalData = $context['previous_data'] ?? null; $statusChanged = false; if ($originalData instanceof ClientTicket) { // ROLE_CLIENT: can only edit content fields, not status if (!$this->security->isGranted('ROLE_ADMIN')) { $data->setStatus($originalData->getStatus()); $data->setStatusComment($originalData->getStatusComment()); } $oldStatus = $originalData->getStatus(); $newStatus = $data->getStatus(); if ($oldStatus !== $newStatus) { $statusChanged = true; $forbidden = self::FORBIDDEN_TRANSITIONS[$oldStatus] ?? []; if (in_array($newStatus, $forbidden, true)) { throw new BadRequestHttpException(sprintf('Transition from "%s" to "%s" is not allowed.', $oldStatus, $newStatus)); } if ('rejected' === $newStatus && (null === $data->getStatusComment() || '' === trim($data->getStatusComment()))) { throw new BadRequestHttpException('A comment is required when rejecting a ticket.'); } } } $data->setUpdatedAt(new DateTimeImmutable()); $this->entityManager->persist($data); $this->entityManager->flush(); if ($statusChanged) { $this->notificationService->createForStatusChange($data); } return $data; } }