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

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