Dompdf). * * Le provider retourne directement une {@see Response} : API Platform court-circuite * alors la serialisation Hydra (le SerializeListener/RespondListener detectent une * Response et la renvoient telle quelle). `Content-Type: application/pdf`, * disposition `inline` (le front ouvre l'apercu — RG-5.08). * * Securite & visibilite — miroir de {@see WeighingTicketProvider::provideItem()} : * - permission `logistique.weighing_tickets.view` portee par l'operation (403) ; * - 404 si ticket introuvable, soft-delete (non expose au M5 — § 2.13), ou hors * perimetre du site courant (anti-enumeration, § 2.3 / RG-5.09). * * @implements ProviderInterface */ final class WeighingTicketPrintProvider implements ProviderInterface { public function __construct( #[Autowire(service: 'App\Module\Logistique\Infrastructure\Doctrine\DoctrineWeighingTicketRepository')] private readonly WeighingTicketRepositoryInterface $repository, private readonly WeighingTicketPdfRenderer $renderer, private readonly CurrentSiteProviderInterface $currentSiteProvider, private readonly Security $security, ) {} public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response { $ticket = $this->findVisibleTicket($uriVariables['id'] ?? null); if (null === $ticket) { throw new NotFoundHttpException('Ticket de pesée introuvable.'); } $pdf = $this->renderer->render($ticket); $response = new Response($pdf); $response->headers->set('Content-Type', 'application/pdf'); $response->headers->set( 'Content-Disposition', sprintf('inline; filename="bon-pesee-%s.pdf"', $ticket->getNumber() ?? (string) $ticket->getId()), ); return $response; } /** * Charge le ticket visible par l'utilisateur courant, ou null (-> 404) : * introuvable, soft-delete, ou hors perimetre du site courant. Logique * identique a WeighingTicketProvider::provideItem() (cloisonnement § 2.3). */ private function findVisibleTicket(mixed $id): ?WeighingTicket { if (!is_int($id) && !(is_string($id) && ctype_digit($id))) { return null; } $ticket = $this->repository->findById((int) $id); if (null === $ticket || null !== $ticket->getDeletedAt()) { return null; } $scopeSite = $this->currentScopeSite(); if (null !== $scopeSite && $ticket->getSite()?->getId() !== $scopeSite->getId()) { return null; } return $ticket; } /** * Site servant a cloisonner, ou null si aucun cloisonnement ne s'applique * (user `sites.bypass_scope`, ou pas de site courant). Miroir de * WeighingTicketProvider::currentScopeSite(). */ private function currentScopeSite(): ?Site { if ($this->security->isGranted('sites.bypass_scope')) { return null; } return $this->currentSiteProvider->get(); } }