Files
Lesstime/src/State/SwitchProjectWorkflowProcessor.php

114 lines
4.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\ApiResource\SwitchWorkflowOutput;
use App\Entity\Project;
use App\Entity\TaskStatus;
use App\Entity\Workflow;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
/**
* Wraps the switch-workflow operation for a project.
* Input: Project (URI variable) + body { workflowId, mapping: { sourceStatusId: targetStatusId|null } }.
*
* @implements ProcessorInterface<Project, SwitchWorkflowOutput>
*/
final readonly class SwitchProjectWorkflowProcessor implements ProcessorInterface
{
public function __construct(
private EntityManagerInterface $entityManager,
) {}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): SwitchWorkflowOutput
{
/** @var Project $project */
$project = $data;
$request = $context['request'] ?? null;
$body = $request ? json_decode($request->getContent(), true) : [];
$workflowId = $body['workflowId'] ?? null;
$mapping = $body['mapping'] ?? [];
if (!is_int($workflowId) || !is_array($mapping)) {
throw new HttpException(422, 'Body must contain workflowId (int) and mapping (object).');
}
$targetWorkflow = $this->entityManager->find(Workflow::class, $workflowId);
if (!$targetWorkflow instanceof Workflow) {
throw new NotFoundHttpException('Target workflow not found.');
}
// 1) Lister les statuts source effectivement référencés par les tâches du projet
$rows = $this->entityManager->getConnection()->fetchAllAssociative(
'SELECT DISTINCT status_id FROM task WHERE project_id = :pid AND status_id IS NOT NULL',
['pid' => $project->getId()],
);
$referencedSourceIds = array_map(static fn ($r) => (int) $r['status_id'], $rows);
// 2) Vérifier que chaque source a un mapping
$missing = [];
foreach ($referencedSourceIds as $srcId) {
if (!array_key_exists((string) $srcId, $mapping)) {
$missing[] = $srcId;
}
}
if ([] !== $missing) {
throw new HttpException(422, 'Missing mapping for source status IDs: '.implode(', ', $missing));
}
// 3) Valider que chaque target appartient au workflow cible (ou est null)
foreach ($mapping as $srcId => $targetId) {
if (null === $targetId) {
continue;
}
$target = $this->entityManager->find(TaskStatus::class, $targetId);
if (!$target instanceof TaskStatus
|| $target->getWorkflow()?->getId() !== $targetWorkflow->getId()) {
throw new HttpException(422, sprintf(
'Target status %s does not belong to workflow %d.',
var_export($targetId, true),
$targetWorkflow->getId(),
));
}
}
// 4) Transaction unique
$conn = $this->entityManager->getConnection();
$conn->beginTransaction();
try {
$migrated = 0;
foreach ($mapping as $srcId => $targetId) {
$affected = $conn->executeStatement(
'UPDATE task SET status_id = :tid WHERE project_id = :pid AND status_id = :sid',
['tid' => $targetId, 'pid' => $project->getId(), 'sid' => (int) $srcId],
);
$migrated += $affected;
}
$project->setWorkflow($targetWorkflow);
$this->entityManager->flush();
$conn->commit();
} catch (Throwable $e) {
$conn->rollBack();
throw $e;
}
return new SwitchWorkflowOutput(
projectId: $project->getId(),
workflowId: $targetWorkflow->getId(),
migratedTaskCount: $migrated,
);
}
}