src/Controller/Frontend/RankingController.php line 53

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Frontend;
  3. use App\Controller\Frontend\Ranking\RankingPdfData;
  4. use App\Controller\Shared\Club\ResolveClubLogo;
  5. use App\Controller\Shared\Ranking\CalculateRankingCached;
  6. use App\Entity\Backend\Club;
  7. use App\Entity\Backend\Competicion;
  8. use App\Entity\Backend\Jugador;
  9. use App\Entity\Gestion\BonusRanking;
  10. use App\Entity\Gestion\ClasificadoRanking;
  11. use App\Entity\Gestion\PenalidadRanking;
  12. use App\Entity\Gestion\Ranking;
  13. use App\Util\PrintPdf;
  14. use Doctrine\Persistence\ManagerRegistry;
  15. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  16. use Symfony\Component\HttpFoundation\RedirectResponse;
  17. use Symfony\Component\HttpFoundation\Request;
  18. use Symfony\Component\HttpFoundation\Response;
  19. use Symfony\Component\HttpFoundation\ResponseHeaderBag;
  20. use Symfony\Component\Routing\Annotation\Route;
  21. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  22. class RankingController extends AbstractController {
  23.     // Gotenberg 7 expects Chromium page dimensions and margins in inches.
  24.     private const RANKING_PDF_PAGE_OPTIONS = [
  25.         'paperWidth' => '8.2677165354',
  26.         'paperHeight' => '11.6929133858',
  27.         'landscape' => 'true',
  28.         'marginTop' => '0.3937007874',
  29.         'marginBottom' => '0.3937007874',
  30.         'marginLeft' => '0.3937007874',
  31.         'marginRight' => '0.3937007874',
  32.         // The template declares A4 landscape too. Let Chromium honour it so the
  33.         // PDF stays horizontal even if a proxy changes its default paper size.
  34.         'preferCssPageSize' => 'true',
  35.     ];
  36.     private const PDF_ASSET_TIMEOUT_SECONDS 2.0;
  37.     private $em;
  38.     public function __construct(ManagerRegistry $doctrine){
  39.         $this->em $doctrine->getManager();
  40.     }
  41.     /**
  42.      * F-122 Visualización del ranking
  43.      *
  44.      * @Route("/ranking/{id}", name="ranking", options={"expose"=true})
  45.      */
  46.     public function ranking($idCalculateRankingCached $calculateRankingCached): RedirectResponse|Response
  47.     {
  48.         $id intval($id);
  49.         $ranking $this->em->getRepository(Ranking::class)->find($id);
  50.         if (!$ranking) {
  51.             $this->addFlash(
  52.                     'error'"El ranking seleccionado no existe");
  53.             return $this->redirect($this->generateUrl('portada'));
  54.         }
  55.         $params $calculateRankingCached->__invoke($idfalse$ranking);
  56.         return $this->render("frontend/Ranking/ranking.html.twig"$params);
  57.     }
  58.     /**
  59.      * @Route("/ranking/{id}/pdf/{filename}", name="ranking_pdf", requirements={"id": "\d+", "filename": ".+\.pdf"}, defaults={"filename"=null})
  60.      */
  61.     public function rankingPdf(
  62.         $id,
  63.         ?string $filename,
  64.         CalculateRankingCached $calculateRankingCached,
  65.         RankingPdfData $rankingPdfData,
  66.         ResolveClubLogo $resolveClubLogo,
  67.         PrintPdf $printPdf,
  68.     ): RedirectResponse|Response {
  69.         $id intval($id);
  70.         $ranking $this->em->getRepository(Ranking::class)->find($id);
  71.         if (!$ranking) {
  72.             $this->addFlash('error'"El ranking seleccionado no existe");
  73.             return $this->redirect($this->generateUrl('portada'));
  74.         }
  75.         if (!$ranking->getActivo() && !$this->isGranted('ROLE_ADMIN') && !$this->isGranted('ROLE_FEDE') && !$this->isGranted('ROLE_CLUB')) {
  76.             throw $this->createAccessDeniedException();
  77.         }
  78.         $pdfFilename $this->pdfFilename($ranking);
  79.         if ($filename !== $pdfFilename) {
  80.             return $this->redirectToRoute('ranking_pdf', [
  81.                 'id' => $ranking->getId(),
  82.                 'filename' => $pdfFilename,
  83.             ]);
  84.         }
  85.         $html $this->renderRankingPdfHtml($id$ranking$calculateRankingCached$rankingPdfData$resolveClubLogo);
  86.         $pdf $this->generateRankingPdfWithGotenberg($html$printPdf);
  87.         $response = new Response($pdf);
  88.         $response->headers->set('Content-Type''application/pdf');
  89.         $response->headers->set(
  90.             'Content-Disposition',
  91.             $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_INLINE$pdfFilename)
  92.         );
  93.         $response->headers->set('X-Robots-Tag''noindex');
  94.         return $response;
  95.     }
  96.     private function generateRankingPdfWithGotenberg(string $htmlPrintPdf $printPdf): string
  97.     {
  98.         $temporaryBaseFile tempnam(sys_get_temp_dir(), 'ranking_pdf_');
  99.         if ($temporaryBaseFile === false) {
  100.             throw new \RuntimeException('No se pudo crear el archivo temporal del ranking');
  101.         }
  102.         $htmlFile $temporaryBaseFile '.html';
  103.         $pdfFile $temporaryBaseFile '.pdf';
  104.         try {
  105.             if (file_exists($temporaryBaseFile)) {
  106.                 unlink($temporaryBaseFile);
  107.             }
  108.             if (file_put_contents($htmlFile$html) === false) {
  109.                 throw new \RuntimeException('No se pudo escribir el HTML temporal del ranking');
  110.             }
  111.             $printPdf->sendHtmlToGeneratePdf($temporaryBaseFileself::RANKING_PDF_PAGE_OPTIONS);
  112.             $pdf file_get_contents($pdfFile);
  113.             if ($pdf === false) {
  114.                 throw new \RuntimeException('No se pudo leer el PDF generado del ranking');
  115.             }
  116.             if (!str_starts_with($pdf'%PDF-')) {
  117.                 throw new \RuntimeException('Gotenberg no devolvió un PDF válido para el ranking');
  118.             }
  119.             return $pdf;
  120.         } finally {
  121.             if (file_exists($temporaryBaseFile)) {
  122.                 unlink($temporaryBaseFile);
  123.             }
  124.             if (file_exists($htmlFile)) {
  125.                 unlink($htmlFile);
  126.             }
  127.             if (file_exists($pdfFile)) {
  128.                 unlink($pdfFile);
  129.             }
  130.         }
  131.     }
  132.     private function renderRankingPdfHtml(
  133.         int $id,
  134.         Ranking $ranking,
  135.         CalculateRankingCached $calculateRankingCached,
  136.         RankingPdfData $rankingPdfData,
  137.         ResolveClubLogo $resolveClubLogo,
  138.     ): string {
  139.         $params $calculateRankingCached->__invoke($idfalse$ranking);
  140.         $ownerCode $rankingPdfData->ownerCode($ranking);
  141.         $pdfData $rankingPdfData->__invoke($params);
  142.         return $this->renderView('frontend/Ranking/ranking-pdf.html.twig'array_merge($params, [
  143.             'rankingPdf' => $pdfData,
  144.             'rankingPdfColors' => $rankingPdfData->colors($ownerCode),
  145.             'rankingPdfLogoUrl' => $this->pdfLogoDataUri($resolveClubLogo((string)$ownerCode)),
  146.             'rankingPdfQrUrl' => $this->generateUrl('ranking', ['id' => $ranking->getId()], UrlGeneratorInterface::ABSOLUTE_URL),
  147.             'rankingPdfNextcaddyLogoUrl' => $this->pdfPublicAssetDataUri('svg/logos/nxt/nextcaddy-line-logo.svg'),
  148.         ]));
  149.     }
  150.     private function pdfLogoDataUri(?string $logoUrl): ?string
  151.     {
  152.         if (null === $logoUrl) {
  153.             return null;
  154.         }
  155.         $logoUrl $this->preferPngLogoUrl($logoUrl);
  156.         $contents = @file_get_contents($logoUrl);
  157.         if ($contents === false || $contents === '') {
  158.             return $logoUrl;
  159.         }
  160.         $extension strtolower((string)pathinfo(parse_url($logoUrlPHP_URL_PATH) ?: ''PATHINFO_EXTENSION));
  161.         $mimeType = match ($extension) {
  162.             'png' => 'image/png',
  163.             'jpg''jpeg' => 'image/jpeg',
  164.             'webp' => 'image/webp',
  165.             'svg' => 'image/svg+xml',
  166.             default => 'image/png',
  167.         };
  168.         return 'data:' $mimeType ';base64,' base64_encode($contents);
  169.     }
  170.     private function preferPngLogoUrl(string $logoUrl): string
  171.     {
  172.         $pngLogoUrl str_replace(['/SVG/''.svg'], ['/PNG/''.png'], $logoUrl);
  173.         return $pngLogoUrl !== $logoUrl && $this->remoteFileExists($pngLogoUrl) ? $pngLogoUrl $logoUrl;
  174.     }
  175.     private function remoteFileExists(string $url): bool
  176.     {
  177.         $headers = @get_headers($url);
  178.         $statusLine $headers[0] ?? '';
  179.         return $statusLine !== '' && (
  180.                 str_contains($statusLine'200')
  181.                 || str_contains($statusLine'301')
  182.                 || str_contains($statusLine'302')
  183.             );
  184.     }
  185.     private function remoteAssetDataUri(string $url): ?string
  186.     {
  187.         $contents = @file_get_contents($urlfalse$this->remoteAssetStreamContext());
  188.         if ($contents === false || $contents === '') {
  189.             return null;
  190.         }
  191.         $extension strtolower((string)pathinfo(parse_url($urlPHP_URL_PATH) ?: ''PATHINFO_EXTENSION));
  192.         $mimeType = match ($extension) {
  193.             'png' => 'image/png',
  194.             'jpg''jpeg' => 'image/jpeg',
  195.             'webp' => 'image/webp',
  196.             'svg' => 'image/svg+xml',
  197.             default => 'image/png',
  198.         };
  199.         return 'data:' $mimeType ';base64,' base64_encode($contents);
  200.     }
  201.     private function pdfLogoUrlCandidates(string $logoUrl): array
  202.     {
  203.         $pngLogoUrl str_replace(['/SVG/''.svg'], ['/PNG/''.png'], $logoUrl);
  204.         if ($pngLogoUrl === $logoUrl) {
  205.             return [$logoUrl];
  206.         }
  207.         return [$pngLogoUrl$logoUrl];
  208.     }
  209.     private function remoteAssetStreamContext()
  210.     {
  211.         return stream_context_create([
  212.             'http' => [
  213.                 'timeout' => self::PDF_ASSET_TIMEOUT_SECONDS,
  214.             ],
  215.         ]);
  216.     }
  217.     private function pdfPublicAssetDataUri(string $relativePath): string
  218.     {
  219.         $path rtrim((string)$this->getParameter('kernel.project_dir'), '/') . '/public/' ltrim($relativePath'/');
  220.         $contents = @file_get_contents($path);
  221.         if ($contents === false || $contents === '') {
  222.             return '/' ltrim($relativePath'/');
  223.         }
  224.         $extension strtolower((string)pathinfo($pathPATHINFO_EXTENSION));
  225.         $mimeType = match ($extension) {
  226.             'png' => 'image/png',
  227.             'jpg''jpeg' => 'image/jpeg',
  228.             'webp' => 'image/webp',
  229.             'svg' => 'image/svg+xml',
  230.             default => 'application/octet-stream',
  231.         };
  232.         return 'data:' $mimeType ';base64,' base64_encode($contents);
  233.     }
  234.     private function pdfFilename(Ranking $ranking): string
  235.     {
  236.         $filename iconv('UTF-8''ASCII//TRANSLIT', (string)$ranking->getNombre());
  237.         $filename preg_replace('/[^A-Za-z0-9._-]+/''-'$filename ?: '');
  238.         $filename trim((string)$filename'-');
  239.         if ($filename === '') {
  240.             $filename 'ranking-' $ranking->getId();
  241.         }
  242.         return $filename '.pdf';
  243.     }
  244.     /**
  245.      * F-123 Visualización de los rankings de un club
  246.      *
  247.      * @Route("/rankings-club/{id}", name="rankings_club")
  248.      */
  249.     public function rankingsClub($id): RedirectResponse|Response
  250.     {
  251.         $club $this->em->getRepository(Club::class)->find($id);
  252.         if (!$club) {
  253.             $this->addFlash('error'"No encontramos el Club que nos ha solicitado");
  254.             return $this->redirect($this->generateUrl('provincias'));
  255.         }
  256.         $isFederation $club->isFederationClub();
  257.         $clientId $isFederation $club->getCliente()->getId() : null;
  258.         $rankings $this->em->getRepository(Ranking::class)->obtainRankingsByClubOrClient($club->getId(), $clientId$isFederationfalse);
  259.         return $this->render("frontend/Ranking/rankingsClub.html.twig", [
  260.             "rankings" => $rankings,
  261.             "club" => $club
  262.         ]);
  263.     }
  264. }