*/ final readonly class TaskCalendarProcessor implements ProcessorInterface { /** * @param ProcessorInterface $persistProcessor * @param ProcessorInterface $removeProcessor */ public function __construct( #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] private ProcessorInterface $persistProcessor, #[Autowire(service: 'api_platform.doctrine.orm.state.remove_processor')] private ProcessorInterface $removeProcessor, private CalDavService $calDavService, private EntityManagerInterface $entityManager, private RecurrenceHandler $recurrenceHandler, ) {} public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed { if ($operation instanceof Delete) { $eventUid = $data->getCalendarEventUid(); $todoUid = $data->getCalendarTodoUid(); $result = $this->removeProcessor->process($data, $operation, $uriVariables, $context); if ($eventUid) { $this->calDavService->deleteEvent($eventUid); } if ($todoUid) { $this->calDavService->deleteTodo($todoUid); } return $result; } // Detect isFinal transition using Doctrine UnitOfWork. // $data already has the NEW values (API Platform deserialized the PATCH). // UnitOfWork originalEntityData stores the DB snapshot with entity references for relations. $uow = $this->entityManager->getUnitOfWork(); $originalData = $uow->getOriginalEntityData($data); $wasAlreadyFinal = false; if (isset($originalData['status']) && $originalData['status'] instanceof TaskStatus) { $wasAlreadyFinal = $originalData['status']->getIsFinal(); } $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context); // Sync to Zimbra after DB flush $this->calDavService->syncTask($data); $this->entityManager->flush(); // Check for recurrence auto-creation (only on STATUS CHANGE to isFinal) $this->recurrenceHandler->handleIfNeeded($data, $wasAlreadyFinal); return $result; } }