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

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