From 5a47adace5039de5a05510b83a717a4a83a23bf1 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 19 Mar 2026 10:19:18 +0100 Subject: [PATCH] feat : add TaskCalendarProcessor for CalDAV sync after DB operations Handles Patch (persist + sync + recurrence check) and Delete (remove + cleanup Zimbra events). Updates TaskNumberProcessor to sync newly created tasks to calendar. Wires TaskCalendarProcessor as processor for Patch/Delete on Task entity. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Entity/Task.php | 5 ++- src/State/TaskCalendarProcessor.php | 67 +++++++++++++++++++++++++++++ src/State/TaskNumberProcessor.php | 9 +++- 3 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 src/State/TaskCalendarProcessor.php diff --git a/src/Entity/Task.php b/src/Entity/Task.php index a15c794..320468f 100644 --- a/src/Entity/Task.php +++ b/src/Entity/Task.php @@ -16,6 +16,7 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use App\Repository\TaskRepository; +use App\State\TaskCalendarProcessor; use App\State\TaskNumberProcessor; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; @@ -30,8 +31,8 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"), new Get(security: "is_granted('ROLE_USER')"), new Post(security: "is_granted('ROLE_ADMIN')", processor: TaskNumberProcessor::class), - new Patch(security: "is_granted('ROLE_ADMIN')"), - new Delete(security: "is_granted('ROLE_ADMIN')"), + new Patch(security: "is_granted('ROLE_ADMIN')", processor: TaskCalendarProcessor::class), + new Delete(security: "is_granted('ROLE_ADMIN')", processor: TaskCalendarProcessor::class), ], normalizationContext: ['groups' => ['task:read']], denormalizationContext: ['groups' => ['task:write']], diff --git a/src/State/TaskCalendarProcessor.php b/src/State/TaskCalendarProcessor.php new file mode 100644 index 0000000..8d97217 --- /dev/null +++ b/src/State/TaskCalendarProcessor.php @@ -0,0 +1,67 @@ + + */ +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; + } + + // CRITICAL: Store original status BEFORE persist to detect isFinal transition + $originalStatus = $data->getStatus(); + $wasAlreadyFinal = $originalStatus?->isFinal() ?? false; + + $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; + } +} diff --git a/src/State/TaskNumberProcessor.php b/src/State/TaskNumberProcessor.php index f8e5b23..038a865 100644 --- a/src/State/TaskNumberProcessor.php +++ b/src/State/TaskNumberProcessor.php @@ -9,6 +9,7 @@ use ApiPlatform\Metadata\Post; use ApiPlatform\State\ProcessorInterface; use App\Entity\Task; use App\Repository\TaskRepository; +use App\Service\CalDavService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; @@ -25,6 +26,7 @@ final readonly class TaskNumberProcessor implements ProcessorInterface private ProcessorInterface $persistProcessor, private TaskRepository $taskRepository, private EntityManagerInterface $entityManager, + private CalDavService $calDavService, ) {} /** @@ -33,12 +35,17 @@ final readonly class TaskNumberProcessor implements ProcessorInterface public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed { if ($operation instanceof Post && null !== $data->getProject()) { - return $this->entityManager->wrapInTransaction(function () use ($data, $operation, $uriVariables, $context) { + $result = $this->entityManager->wrapInTransaction(function () use ($data, $operation, $uriVariables, $context) { $maxNumber = $this->taskRepository->findMaxNumberByProjectForUpdate($data->getProject()); $data->setNumber($maxNumber + 1); return $this->persistProcessor->process($data, $operation, $uriVariables, $context); }); + + $this->calDavService->syncTask($data); + $this->entityManager->flush(); + + return $result; } return $this->persistProcessor->process($data, $operation, $uriVariables, $context);