fix(mcp) : list-projects crash sur client orphelin (FK danglante)

Serializer::project() forçait l'hydratation d'un proxy Doctrine Client via
getId()/getName() même quand la FK pointait vers un Client supprimé, ce qui
levait EntityNotFoundException et faisait planter tout l'outil (-32603).
Extraction d'un helper clientRef() qui catch EntityNotFoundException et
renvoie null (sémantique ON DELETE SET NULL). Robustifie aussi get-project,
create-project, update-project qui réutilisent ce serializer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 21:14:44 +02:00
parent 6d95f9e782
commit e59c5c510a
+31 -5
View File
@@ -23,11 +23,13 @@ use App\Module\ProjectManagement\Domain\Entity\TaskPriority;
use App\Module\ProjectManagement\Domain\Entity\TaskStatus;
use App\Module\ProjectManagement\Domain\Entity\TaskTag;
use App\Module\TimeTracking\Domain\Entity\TimeEntry;
use App\Shared\Domain\Contract\ClientInterface;
use App\Shared\Domain\Contract\ProjectInterface;
use App\Shared\Domain\Contract\TaskInterface;
use App\Shared\Domain\Contract\TaskTagInterface;
use App\Shared\Domain\Contract\UserInterface;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\EntityNotFoundException;
/**
* Shared serialization helpers for MCP tools.
@@ -59,11 +61,8 @@ final class Serializer
'name' => $project->getName(),
'description' => $project->getDescription(),
'color' => $project->getColor(),
'client' => $project->getClient() ? [
'id' => $project->getClient()->getId(),
'name' => $project->getClient()->getName(),
] : null,
'archived' => $project->isArchived(),
'client' => self::clientRef($project->getClient()),
'archived' => $project->isArchived(),
];
}
@@ -516,4 +515,31 @@ final class Serializer
'initialLeaveBalance' => $u->getInitialLeaveBalance(),
];
}
/**
* Safely serialize a project's client reference.
*
* The client association uses ON DELETE SET NULL, but a stale row may leave
* a dangling foreign key (e.g. data imported with the constraint disabled).
* In that case Doctrine returns an uninitialized proxy whose hydration
* throws EntityNotFoundException; we treat such a reference as absent rather
* than letting it crash the whole tool.
*
* @return null|array{id: ?int, name: ?string}
*/
private static function clientRef(?ClientInterface $client): ?array
{
if (null === $client) {
return null;
}
try {
return [
'id' => $client->getId(),
'name' => $client->getName(),
];
} catch (EntityNotFoundException) {
return null;
}
}
}