vendor/sonata-project/block-bundle/src/Templating/Helper/BlockHelper.php line 451

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4.  * This file is part of the Sonata Project package.
  5.  *
  6.  * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
  7.  *
  8.  * For the full copyright and license information, please view the LICENSE
  9.  * file that was distributed with this source code.
  10.  */
  11. namespace Sonata\BlockBundle\Templating\Helper;
  12. use Doctrine\Common\Util\ClassUtils;
  13. use Sonata\BlockBundle\Block\BlockContextManagerInterface;
  14. use Sonata\BlockBundle\Block\BlockRendererInterface;
  15. use Sonata\BlockBundle\Block\BlockServiceManagerInterface;
  16. use Sonata\BlockBundle\Cache\HttpCacheHandlerInterface;
  17. use Sonata\BlockBundle\Event\BlockEvent;
  18. use Sonata\BlockBundle\Model\BlockInterface;
  19. use Sonata\Cache\CacheAdapterInterface;
  20. use Sonata\Cache\CacheManagerInterface;
  21. use Symfony\Component\EventDispatcher\EventDispatcherInterface as EventDispatcherComponentInterface;
  22. use Symfony\Component\HttpFoundation\Response;
  23. use Symfony\Component\Stopwatch\Stopwatch;
  24. use Symfony\Component\Stopwatch\StopwatchEvent;
  25. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  26. /**
  27.  * @phpstan-type Trace = array{
  28.  *     name: string,
  29.  *     type: string,
  30.  *     duration: int|float|false,
  31.  *     memory_start: int|false,
  32.  *     memory_end: int|false,
  33.  *     memory_peak: int|false,
  34.  *     cache: array{
  35.  *         keys: mixed[],
  36.  *         contextual_keys: mixed[],
  37.  *         handler: false,
  38.  *         from_cache: false,
  39.  *         ttl: int,
  40.  *         created_at: false,
  41.  *         lifetime: int,
  42.  *         age: int,
  43.  *     },
  44.  *     assets: array{
  45.  *         js: string[],
  46.  *         css: string[],
  47.  *     }
  48.  * }
  49.  */
  50. class BlockHelper
  51. {
  52.     /**
  53.      * NEXT_MAJOR: remove.
  54.      */
  55.     private ?BlockServiceManagerInterface $blockServiceManager null;
  56.     /**
  57.      * NEXT_MAJOR: remove this member and all related code to usages within this class.
  58.      */
  59.     private ?CacheManagerInterface $cacheManager null;
  60.     /**
  61.      * NEXT_MAJOR: remove.
  62.      *
  63.      * @var array{by_class: array<class-string, string>, by_type: array<string, string>}
  64.      */
  65.     private array $cacheBlocks = ['by_class' => [], 'by_type' => []];
  66.     private BlockRendererInterface $blockRenderer;
  67.     private BlockContextManagerInterface $blockContextManager;
  68.     private ?HttpCacheHandlerInterface $cacheHandler null;
  69.     private EventDispatcherInterface $eventDispatcher;
  70.     /**
  71.      * This property is a state variable holdings all assets used by the block for the current PHP request
  72.      * It is used to correctly render the javascripts and stylesheets tags on the main layout.
  73.      *
  74.      * @var array{css: array<string>, js: array<string>}
  75.      */
  76.     private array $assets = ['css' => [], 'js' => []];
  77.     /**
  78.      * @var array<StopwatchEvent|array<string, mixed>>
  79.      *
  80.      * @phpstan-var array<StopwatchEvent|Trace>
  81.      */
  82.     private array $traces = [];
  83.     /**
  84.      * @var array<string, mixed>
  85.      */
  86.     private array $eventTraces = [];
  87.     private ?Stopwatch $stopwatch null;
  88.     /**
  89.      * NEXT_MAJOR: remove the deprecated signature and cleanup the constructor.
  90.      *
  91.      * @param array{by_class: array<class-string, string>, by_type: array<string, string>}|BlockContextManagerInterface|BlockRendererInterface $blockContextManagerOrBlockRendererOrCacheBlocks
  92.      *
  93.      * @internal
  94.      */
  95.     public function __construct(
  96.         object $blockServiceManagerOrBlockRenderer,
  97.         $blockContextManagerOrBlockRendererOrCacheBlocks,
  98.         object $eventDispatcherOrBlockContextManagerOrBlockRenderer,
  99.         ?object $stopWatchOrEventDispatcherOrBlockContextManager null,
  100.         ?object $stopwatchOrEventDispatcher null,
  101.         ?CacheManagerInterface $cacheManager null,
  102.         ?HttpCacheHandlerInterface $cacheHandler null,
  103.         ?Stopwatch $stopwatch null
  104.     ) {
  105.         if ($blockServiceManagerOrBlockRenderer instanceof BlockServiceManagerInterface) {
  106.             $this->blockServiceManager $blockServiceManagerOrBlockRenderer;
  107.             @trigger_error(
  108.                 sprintf(
  109.                     'Passing an instance of "%s" as argument 1 for method "%s" is deprecated since sonata-project/block-bundle 4.12. The argument will change to "%s" in 5.0.',
  110.                     BlockServiceManagerInterface::class,
  111.                     __METHOD__,
  112.                     BlockRendererInterface::class
  113.                 ),
  114.                 \E_USER_DEPRECATED
  115.             );
  116.         } elseif ($blockServiceManagerOrBlockRenderer instanceof BlockRendererInterface) {
  117.             $this->blockRenderer $blockServiceManagerOrBlockRenderer;
  118.         } else {
  119.             throw new \TypeError(
  120.                 sprintf(
  121.                     'Argument 1 of method "%s" must be an instance of "%s" or "%s"',
  122.                     __METHOD__,
  123.                     BlockRendererInterface::class,
  124.                     BlockServiceManagerInterface::class
  125.                 )
  126.             );
  127.         }
  128.         if ($blockContextManagerOrBlockRendererOrCacheBlocks instanceof BlockContextManagerInterface) {
  129.             $this->blockContextManager $blockContextManagerOrBlockRendererOrCacheBlocks;
  130.         } elseif ($blockContextManagerOrBlockRendererOrCacheBlocks instanceof BlockRendererInterface) {
  131.             $this->blockRenderer $blockContextManagerOrBlockRendererOrCacheBlocks;
  132.             @trigger_error(
  133.                 sprintf(
  134.                     'Passing an instance of "%s" as argument 2 for method "%s" is deprecated since sonata-project/block-bundle 4.12. The argument will change to "%s" in 5.0.',
  135.                     BlockRendererInterface::class,
  136.                     __METHOD__,
  137.                     BlockContextManagerInterface::class
  138.                 ),
  139.                 \E_USER_DEPRECATED
  140.             );
  141.         } elseif (\is_array($blockContextManagerOrBlockRendererOrCacheBlocks)) {
  142.             $this->cacheBlocks $blockContextManagerOrBlockRendererOrCacheBlocks;
  143.             @trigger_error(
  144.                 sprintf(
  145.                     'Passing an array as argument 2 for method "%s" is deprecated since sonata-project/block-bundle 4.11. The argument will change to "%s" in 5.0.',
  146.                     __METHOD__,
  147.                     BlockContextManagerInterface::class
  148.                 ),
  149.                 \E_USER_DEPRECATED
  150.             );
  151.         } else {
  152.             throw new \TypeError(
  153.                 sprintf(
  154.                     'Argument 2 of method "%s" must be an array or an instance of "%s" or "%s"',
  155.                     __METHOD__,
  156.                     BlockRendererInterface::class,
  157.                     BlockContextManagerInterface::class
  158.                 )
  159.             );
  160.         }
  161.         if ($eventDispatcherOrBlockContextManagerOrBlockRenderer instanceof EventDispatcherInterface) {
  162.             $this->eventDispatcher $eventDispatcherOrBlockContextManagerOrBlockRenderer;
  163.         } elseif ($eventDispatcherOrBlockContextManagerOrBlockRenderer instanceof BlockContextManagerInterface) {
  164.             $this->blockContextManager $eventDispatcherOrBlockContextManagerOrBlockRenderer;
  165.             @trigger_error(
  166.                 sprintf(
  167.                     'Passing an instance of "%s" as argument 3 for method "%s" is deprecated since sonata-project/block-bundle 4.12. The argument will change to "%s" in 5.0.',
  168.                     BlockContextManagerInterface::class,
  169.                     __METHOD__,
  170.                     EventDispatcherInterface::class
  171.                 ),
  172.                 \E_USER_DEPRECATED
  173.             );
  174.         } elseif ($eventDispatcherOrBlockContextManagerOrBlockRenderer instanceof BlockRendererInterface) {
  175.             $this->blockRenderer $eventDispatcherOrBlockContextManagerOrBlockRenderer;
  176.             @trigger_error(
  177.                 sprintf(
  178.                     'Passing an instance of "%s" as argument 3 for method "%s" is deprecated since sonata-project/block-bundle 4.11. The argument will change to "%s" in 5.0.',
  179.                     BlockRendererInterface::class,
  180.                     __METHOD__,
  181.                     EventDispatcherInterface::class
  182.                 ),
  183.                 \E_USER_DEPRECATED
  184.             );
  185.         } else {
  186.             throw new \TypeError(
  187.                 sprintf(
  188.                     'Argument 3 of method "%s" must be an instance of "%s" or "%s" or "%s"',
  189.                     __METHOD__,
  190.                     EventDispatcherInterface::class,
  191.                     BlockContextManagerInterface::class,
  192.                     BlockRendererInterface::class
  193.                 )
  194.             );
  195.         }
  196.         if ($stopWatchOrEventDispatcherOrBlockContextManager instanceof Stopwatch || null === $stopWatchOrEventDispatcherOrBlockContextManager) {
  197.             $this->stopwatch $stopWatchOrEventDispatcherOrBlockContextManager;
  198.         } elseif ($stopWatchOrEventDispatcherOrBlockContextManager instanceof EventDispatcherInterface) {
  199.             $this->eventDispatcher $stopWatchOrEventDispatcherOrBlockContextManager;
  200.             @trigger_error(
  201.                 sprintf(
  202.                     'Passing an instance of "%s" as argument 4 for method "%s" is deprecated since sonata-project/block-bundle 4.12. The argument will change to "?%s" in 5.0.',
  203.                     EventDispatcherInterface::class,
  204.                     __METHOD__,
  205.                     Stopwatch::class
  206.                 ),
  207.                 \E_USER_DEPRECATED
  208.             );
  209.         } elseif ($stopWatchOrEventDispatcherOrBlockContextManager instanceof BlockContextManagerInterface) {
  210.             $this->blockContextManager $stopWatchOrEventDispatcherOrBlockContextManager;
  211.             @trigger_error(
  212.                 sprintf(
  213.                     'Passing an instance of "%s" as argument 4 for method "%s" is deprecated since sonata-project/block-bundle 4.11. The argument will change to "?%s" in 5.0.',
  214.                     BlockContextManagerInterface::class,
  215.                     __METHOD__,
  216.                     Stopwatch::class
  217.                 ),
  218.                 \E_USER_DEPRECATED
  219.             );
  220.         } else {
  221.             throw new \TypeError(
  222.                 sprintf(
  223.                     'Argument 4 of method "%s" must be an instance of "%s" or "%s" or "%s" or null',
  224.                     __METHOD__,
  225.                     Stopwatch::class,
  226.                     EventDispatcherInterface::class,
  227.                     BlockContextManagerInterface::class
  228.                 )
  229.             );
  230.         }
  231.         if ($stopwatchOrEventDispatcher instanceof Stopwatch) {
  232.             $this->stopwatch $stopwatchOrEventDispatcher;
  233.             @trigger_error(
  234.                 sprintf(
  235.                     'Passing an instance of "%s" as argument 5 for method "%s" is deprecated since sonata-project/block-bundle 4.12. The argument will be removed in 5.0.',
  236.                     Stopwatch::class,
  237.                     __METHOD__,
  238.                 ),
  239.                 \E_USER_DEPRECATED
  240.             );
  241.         } elseif ($stopwatchOrEventDispatcher instanceof EventDispatcherInterface) {
  242.             $this->eventDispatcher $stopwatchOrEventDispatcher;
  243.             $this->stopwatch $stopwatch;
  244.             @trigger_error(
  245.                 sprintf(
  246.                     'Passing an instance of "%s" as argument 5 for method "%s" is deprecated since sonata-project/block-bundle 4.11. The argument will be removed in 5.0.',
  247.                     EventDispatcherInterface::class,
  248.                     __METHOD__,
  249.                 ),
  250.                 \E_USER_DEPRECATED
  251.             );
  252.         } elseif (null !== $stopwatchOrEventDispatcher) {
  253.             throw new \TypeError(
  254.                 sprintf(
  255.                     'Argument 5 of method "%s" must be "null" or an instance of "%s" or "%s"',
  256.                     __METHOD__,
  257.                     Stopwatch::class,
  258.                     EventDispatcherInterface::class
  259.                 )
  260.             );
  261.         }
  262.         if (null !== $cacheManager) {
  263.             $this->cacheManager $cacheManager;
  264.             @trigger_error(
  265.                 sprintf(
  266.                     'Passing an instance of "%s" as argument 6 for method "%s" is deprecated since sonata-project/block-bundle 4.11. The argument will be removed in 5.0.',
  267.                     CacheAdapterInterface::class,
  268.                     __METHOD__
  269.                 ),
  270.                 \E_USER_DEPRECATED
  271.             );
  272.         }
  273.         if (null !== $cacheHandler) {
  274.             $this->cacheHandler $cacheHandler;
  275.             @trigger_error(
  276.                 sprintf(
  277.                     'Passing an instance of "%s" as argument 7 for method "%s" is deprecated since sonata-project/block-bundle 4.11. The argument will be removed in 5.0.',
  278.                     HttpCacheHandlerInterface::class,
  279.                     __METHOD__
  280.                 ),
  281.                 \E_USER_DEPRECATED
  282.             );
  283.         }
  284.     }
  285.     /**
  286.      * @param string $media    Unused, only kept to not break existing code
  287.      * @param string $basePath Base path to prepend to the stylesheet urls
  288.      *
  289.      * @return string
  290.      */
  291.     public function includeJavascripts($media$basePath '')
  292.     {
  293.         $html '';
  294.         foreach ($this->assets['js'] as $javascript) {
  295.             $html .= "\n".sprintf('<script src="%s%s" type="text/javascript"></script>'$basePath$javascript);
  296.         }
  297.         return $html;
  298.     }
  299.     /**
  300.      * @param string $media    The css media type to use: all|screen|...
  301.      * @param string $basePath Base path to prepend to the stylesheet urls
  302.      *
  303.      * @return string
  304.      */
  305.     public function includeStylesheets($media$basePath '')
  306.     {
  307.         if (=== \count($this->assets['css'])) {
  308.             return '';
  309.         }
  310.         $html sprintf("<style type='text/css' media='%s'>"$media);
  311.         foreach ($this->assets['css'] as $stylesheet) {
  312.             $html .= "\n".sprintf('@import url(%s%s);'$basePath$stylesheet);
  313.         }
  314.         $html .= "\n</style>";
  315.         return $html;
  316.     }
  317.     /**
  318.      * @param array<string, mixed> $options
  319.      */
  320.     public function renderEvent(string $name, array $options = []): string
  321.     {
  322.         $eventName sprintf('sonata.block.event.%s'$name);
  323.         $event $this->eventDispatcher->dispatch(new BlockEvent($options), $eventName);
  324.         $content '';
  325.         foreach ($event->getBlocks() as $block) {
  326.             $content .= $this->render($block);
  327.         }
  328.         if (null !== $this->stopwatch) {
  329.             $this->eventTraces[uniqid(''true)] = [
  330.                 'template_code' => $name,
  331.                 'event_name' => $eventName,
  332.                 'blocks' => $this->getEventBlocks($event),
  333.                 'listeners' => $this->getEventListeners($eventName),
  334.             ];
  335.         }
  336.         return $content;
  337.     }
  338.     /**
  339.      * Check if a given block type exists.
  340.      *
  341.      * @param string $type Block type to check for
  342.      */
  343.     public function exists(string $type): bool
  344.     {
  345.         return $this->blockContextManager->exists($type);
  346.     }
  347.     /**
  348.      * @param string|array<string, mixed>|BlockInterface $block
  349.      * @param array<string, mixed>                       $options
  350.      */
  351.     public function render($block, array $options = []): string
  352.     {
  353.         $blockContext $this->blockContextManager->get($block$options);
  354.         $stats = [];
  355.         if (null !== $this->stopwatch) {
  356.             $stats $this->startTracing($blockContext->getBlock());
  357.         }
  358.         // NEXT_MAJOR: simplify code and remove all cache-related usages
  359.         $useCache true === $blockContext->getSetting('use_cache');
  360.         $cacheService $useCache $this->getCacheService($blockContext->getBlock(), $stats) : null;
  361.         if (null !== $cacheService) {
  362.             if (null === $this->blockServiceManager) {
  363.                 throw new \LogicException(
  364.                     sprintf(
  365.                         'For caching functionality an instance of "%s" needs to be passed as first argument for "%s::__construct"',
  366.                         BlockContextManagerInterface::class,
  367.                         self::class
  368.                     )
  369.                 );
  370.             }
  371.             $service $this->blockServiceManager->get($blockContext->getBlock());
  372.             $cacheKeys array_merge(
  373.                 $service->getCacheKeys($blockContext->getBlock()),
  374.                 $blockContext->getSetting('extra_cache_keys')
  375.             );
  376.             if (null !== $this->stopwatch) {
  377.                 $stats['cache']['keys'] = $cacheKeys;
  378.             }
  379.             // Please note, some cache handler will always return true (js for instance)
  380.             // This will allow to have a non cacheable block, but the global page can still be cached by
  381.             // a reverse proxy, as the generated page will never get the generated Response from the block.
  382.             if ($cacheService->has($cacheKeys)) {
  383.                 $cacheElement $cacheService->get($cacheKeys);
  384.                 if (null !== $this->stopwatch) {
  385.                     $stats['cache']['from_cache'] = false;
  386.                 }
  387.                 if (!$cacheElement->isExpired() && $cacheElement->getData() instanceof Response) {
  388.                     if (null !== $this->stopwatch) {
  389.                         $stats['cache']['from_cache'] = true;
  390.                     }
  391.                     $response $cacheElement->getData();
  392.                 }
  393.             }
  394.         }
  395.         if (!isset($response)) {
  396.             $recorder null;
  397.             if (null !== $this->cacheManager) {
  398.                 $recorder $this->cacheManager->getRecorder();
  399.                 $recorder->add($blockContext->getBlock());
  400.                 $recorder->push();
  401.             }
  402.             $response $this->blockRenderer->render($blockContext);
  403.             $contextualKeys null !== $recorder $recorder->pop() : [];
  404.             if (null !== $this->stopwatch) {
  405.                 $stats['cache']['contextual_keys'] = $contextualKeys;
  406.             }
  407.             if ($response->isCacheable() && isset($cacheKeys) && null !== $cacheService) {
  408.                 $cacheService->set($cacheKeys$response, (int) $response->getTtl(), $contextualKeys);
  409.             }
  410.         }
  411.         if (null !== $this->stopwatch) {
  412.             // avoid \DateTime because of serialize/unserialize issue in PHP7.3 (https://bugs.php.net/bug.php?id=77302)
  413.             $responseDate $response->getDate();
  414.             $stats['cache']['created_at'] = null === $responseDate null $responseDate->getTimestamp();
  415.             $stats['cache']['ttl'] = $response->getTtl() ?? 0;
  416.             $stats['cache']['age'] = $response->getAge();
  417.             $stats['cache']['lifetime'] = $stats['cache']['age'] + $stats['cache']['ttl'];
  418.         }
  419.         // update final ttl for the whole Response
  420.         if (null !== $this->cacheHandler) {
  421.             $this->cacheHandler->updateMetadata($response$blockContext);
  422.         }
  423.         if (null !== $this->stopwatch) {
  424.             $this->stopTracing($blockContext->getBlock(), $stats);
  425.         }
  426.         return (string) $response->getContent();
  427.     }
  428.     /**
  429.      * Returns the rendering traces.
  430.      *
  431.      * @return array<string, mixed>
  432.      */
  433.     public function getTraces(): array
  434.     {
  435.         return ['_events' => $this->eventTraces] + $this->traces;
  436.     }
  437.     /**
  438.      * @param array<string, mixed> $stats
  439.      *
  440.      * @phpstan-param Trace $stats
  441.      */
  442.     private function stopTracing(BlockInterface $block, array $stats): void
  443.     {
  444.         $event $this->traces[$block->getId() ?? ''];
  445.         if (!$event instanceof StopwatchEvent) {
  446.             throw new \InvalidArgumentException(
  447.                 sprintf('The block %s has no stopwatch event to stop.'$block->getId() ?? '')
  448.             );
  449.         }
  450.         $event->stop();
  451.         $this->traces[$block->getId() ?? ''] = [
  452.             'duration' => $event->getDuration(),
  453.             'memory_end' => memory_get_usage(true),
  454.             'memory_peak' => memory_get_peak_usage(true),
  455.         ] + $stats;
  456.     }
  457.     /**
  458.      * @return array<array{string|int, string}>
  459.      */
  460.     private function getEventBlocks(BlockEvent $event): array
  461.     {
  462.         $results = [];
  463.         foreach ($event->getBlocks() as $block) {
  464.             $results[] = [$block->getId() ?? ''$block->getType() ?? ''];
  465.         }
  466.         return $results;
  467.     }
  468.     /**
  469.      * @return string[]
  470.      */
  471.     private function getEventListeners(string $eventName): array
  472.     {
  473.         $results = [];
  474.         if (!$this->eventDispatcher instanceof EventDispatcherComponentInterface) {
  475.             return $results;
  476.         }
  477.         foreach ($this->eventDispatcher->getListeners($eventName) as $listener) {
  478.             if ($listener instanceof \Closure) {
  479.                 $results[] = '{closure}()';
  480.             } elseif (\is_array($listener) && \is_object($listener[0])) {
  481.                 $results[] = \get_class($listener[0]);
  482.             } elseif (\is_array($listener) && \is_string($listener[0])) {
  483.                 $results[] = $listener[0];
  484.             } else {
  485.                 $results[] = 'Unknown type!';
  486.             }
  487.         }
  488.         return $results;
  489.     }
  490.     /**
  491.      * @param array<string, mixed>|null $stats
  492.      *
  493.      * @phpstan-param Trace|null $stats
  494.      */
  495.     private function getCacheService(BlockInterface $block, ?array &$stats null): ?CacheAdapterInterface
  496.     {
  497.         if (null === $this->cacheManager) {
  498.             return null;
  499.         }
  500.         // type by block class
  501.         $class ClassUtils::getClass($block);
  502.         $cacheServiceId $this->cacheBlocks['by_class'][$class] ?? null;
  503.         // type by block service
  504.         if (null === $cacheServiceId) {
  505.             $cacheServiceId $this->cacheBlocks['by_type'][$block->getType() ?? ''] ?? null;
  506.         }
  507.         if (null === $cacheServiceId) {
  508.             return null;
  509.         }
  510.         if (null !== $this->stopwatch) {
  511.             $stats['cache']['handler'] = $cacheServiceId;
  512.         }
  513.         return $this->cacheManager->getCacheService($cacheServiceId);
  514.     }
  515.     /**
  516.      * @return array<string, mixed>
  517.      *
  518.      * @phpstan-return Trace
  519.      */
  520.     private function startTracing(BlockInterface $block): array
  521.     {
  522.         if (null !== $this->stopwatch) {
  523.             $this->traces[$block->getId() ?? ''] = $this->stopwatch->start(
  524.                 sprintf(
  525.                     '%s (id: %s, type: %s)',
  526.                     $block->getName() ?? '',
  527.                     $block->getId() ?? '',
  528.                     $block->getType() ?? ''
  529.                 )
  530.             );
  531.         }
  532.         return [
  533.             'name' => $block->getName() ?? '',
  534.             'type' => $block->getType() ?? '',
  535.             'duration' => false,
  536.             'memory_start' => memory_get_usage(true),
  537.             'memory_end' => false,
  538.             'memory_peak' => false,
  539.             'cache' => [
  540.                 'keys' => [],
  541.                 'contextual_keys' => [],
  542.                 'handler' => false,
  543.                 'from_cache' => false,
  544.                 'ttl' => 0,
  545.                 'created_at' => false,
  546.                 'lifetime' => 0,
  547.                 'age' => 0,
  548.             ],
  549.             'assets' => [
  550.                 'js' => [],
  551.                 'css' => [],
  552.             ],
  553.         ];
  554.     }
  555. }