From e59c5c510a8c2838403efe1a6cf189420bad73c2 Mon Sep 17 00:00:00 2001 From: matthieu Date: Thu, 25 Jun 2026 21:14:44 +0200 Subject: [PATCH] fix(mcp) : list-projects crash sur client orphelin (FK danglante) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/Shared/Infrastructure/Mcp/Serializer.php | 36 +++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/Shared/Infrastructure/Mcp/Serializer.php b/src/Shared/Infrastructure/Mcp/Serializer.php index 21e3fbc..6f2dd65 100644 --- a/src/Shared/Infrastructure/Mcp/Serializer.php +++ b/src/Shared/Infrastructure/Mcp/Serializer.php @@ -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; + } + } }