feat(mail) : MailCreateTaskController - POST /api/mail/messages/{id}/create-task

- cree une Task avec titre = subject du mail (max 255 chars)
- utilise findMaxNumberByProjectForUpdate pour numero (advisory lock PG)
- transaction wrapInTransaction pour eviter race conditions
- taskGroupId et priorityId optionnels via body JSON
- cree automatiquement le TaskMailLink (mail <-> tache)
- retourne 201 + taskId/taskNumber/taskTitle/messageId

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 00:10:53 +02:00
parent f584ed96fa
commit c7d12f6acd

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\Controller\Mail;
use App\Entity\Project;
use App\Entity\Task;
use App\Entity\TaskGroup;
use App\Entity\TaskMailLink;
use App\Entity\TaskPriority;
use App\Repository\MailMessageRepository;
use App\Repository\TaskRepository;
use App\Security\MailAccessChecker;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route('/api/mail/messages/{id}/create-task', name: 'mail_create_task', methods: ['POST'], priority: 1, requirements: ['id' => '\d+'])]
#[IsGranted('IS_AUTHENTICATED_FULLY')]
class MailCreateTaskController extends AbstractController
{
public function __construct(
private readonly MailMessageRepository $messageRepository,
private readonly EntityManagerInterface $em,
private readonly MailAccessChecker $accessChecker,
private readonly TaskRepository $taskRepository,
) {}
public function __invoke(Request $request, int $id): JsonResponse
{
$this->accessChecker->ensureCanAccessMail($this->getUser());
$message = $this->messageRepository->find($id);
if (null === $message) {
throw new NotFoundHttpException('Message not found');
}
$body = json_decode($request->getContent(), true);
$projectId = $body['projectId'] ?? null;
if (null === $projectId) {
throw new UnprocessableEntityHttpException('projectId is required');
}
$project = $this->em->getRepository(Project::class)->find($projectId);
if (null === $project) {
throw new NotFoundHttpException('Project not found');
}
$title = $message->getSubject() ?? 'Mail sans sujet';
if (mb_strlen($title) > 255) {
$title = mb_substr($title, 0, 252).'...';
}
$result = $this->em->wrapInTransaction(function () use ($project, $title, $body, $message) {
$maxNumber = $this->taskRepository->findMaxNumberByProjectForUpdate($project);
$task = new Task();
$task->setProject($project);
$task->setTitle($title);
$task->setNumber($maxNumber + 1);
if (isset($body['taskGroupId']) && null !== $body['taskGroupId']) {
$taskGroup = $this->em->getRepository(TaskGroup::class)->find($body['taskGroupId']);
if (null !== $taskGroup) {
$task->setGroup($taskGroup);
}
}
if (isset($body['priorityId']) && null !== $body['priorityId']) {
$priority = $this->em->getRepository(TaskPriority::class)->find($body['priorityId']);
if (null !== $priority) {
$task->setPriority($priority);
}
}
$this->em->persist($task);
$link = new TaskMailLink();
$link->setTask($task);
$link->setMailMessage($message);
$link->setLinkedAt(new DateTimeImmutable());
$link->setLinkedBy($this->getUser());
$this->em->persist($link);
$this->em->flush();
return $task;
});
return $this->json([
'taskId' => $result->getId(),
'taskNumber' => $result->getNumber(),
'taskTitle' => $result->getTitle(),
'messageId' => $message->getId(),
], 201);
}
}