114 lines
4.0 KiB
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,
|
|
);
|
|
}
|
|
}
|