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

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\Contracts\EventDispatcher\EventDispatcherInterface;
  25. class BlockHelper
  26. {
  27.     /**
  28.      * @var BlockServiceManagerInterface
  29.      */
  30.     private $blockServiceManager;
  31.     /**
  32.      * @var CacheManagerInterface|null
  33.      */
  34.     private $cacheManager;
  35.     /**
  36.      * @var array
  37.      */
  38.     private $cacheBlocks;
  39.     /**
  40.      * @var BlockRendererInterface
  41.      */
  42.     private $blockRenderer;
  43.     /**
  44.      * @var BlockContextManagerInterface
  45.      */
  46.     private $blockContextManager;
  47.     /**
  48.      * @var HttpCacheHandlerInterface|null
  49.      */
  50.     private $cacheHandler;
  51.     /**
  52.      * @var EventDispatcherInterface
  53.      */
  54.     private $eventDispatcher;
  55.     /**
  56.      * This property is a state variable holdings all assets used by the block for the current PHP request
  57.      * It is used to correctly render the javascripts and stylesheets tags on the main layout.
  58.      *
  59.      * @var array
  60.      */
  61.     private $assets;
  62.     /**
  63.      * @var array
  64.      */
  65.     private $traces;
  66.     /**
  67.      * @var Stopwatch|null
  68.      */
  69.     private $stopwatch;
  70.     public function __construct(
  71.         BlockServiceManagerInterface $blockServiceManager,
  72.         array $cacheBlocks,
  73.         BlockRendererInterface $blockRenderer,
  74.         BlockContextManagerInterface $blockContextManager,
  75.         EventDispatcherInterface $eventDispatcher,
  76.         ?CacheManagerInterface $cacheManager null,
  77.         ?HttpCacheHandlerInterface $cacheHandler null,
  78.         ?Stopwatch $stopwatch null
  79.     ) {
  80.         $this->blockServiceManager $blockServiceManager;
  81.         $this->cacheBlocks $cacheBlocks;
  82.         $this->blockRenderer $blockRenderer;
  83.         $this->eventDispatcher $eventDispatcher;
  84.         $this->cacheManager $cacheManager;
  85.         $this->blockContextManager $blockContextManager;
  86.         $this->cacheHandler $cacheHandler;
  87.         $this->stopwatch $stopwatch;
  88.         $this->assets = [
  89.             'js' => [],
  90.             'css' => [],
  91.         ];
  92.         $this->traces = [
  93.             '_events' => [],
  94.         ];
  95.     }
  96.     /**
  97.      * @param string $media    Unused, only kept to not break existing code
  98.      * @param string $basePath Base path to prepend to the stylesheet urls
  99.      *
  100.      * @return array|string
  101.      */
  102.     public function includeJavascripts($media$basePath '')
  103.     {
  104.         $html '';
  105.         foreach ($this->assets['js'] as $javascript) {
  106.             $html .= "\n".sprintf('<script src="%s%s" type="text/javascript"></script>'$basePath$javascript);
  107.         }
  108.         return $html;
  109.     }
  110.     /**
  111.      * @param string $media    The css media type to use: all|screen|...
  112.      * @param string $basePath Base path to prepend to the stylesheet urls
  113.      *
  114.      * @return array|string
  115.      */
  116.     public function includeStylesheets($media$basePath '')
  117.     {
  118.         if (=== \count($this->assets['css'])) {
  119.             return '';
  120.         }
  121.         $html sprintf("<style type='text/css' media='%s'>"$media);
  122.         foreach ($this->assets['css'] as $stylesheet) {
  123.             $html .= "\n".sprintf('@import url(%s%s);'$basePath$stylesheet);
  124.         }
  125.         $html .= "\n</style>";
  126.         return $html;
  127.     }
  128.     public function renderEvent(string $name, array $options = []): string
  129.     {
  130.         $eventName sprintf('sonata.block.event.%s'$name);
  131.         /**
  132.          * @psalm-suppress TooManyArguments
  133.          *
  134.          * @todo remove annotation when Symfony 4.4.x support is dropped
  135.          */
  136.         $event $this->eventDispatcher->dispatch(new BlockEvent($options), $eventName);
  137.         \assert($event instanceof BlockEvent);
  138.         $content '';
  139.         foreach ($event->getBlocks() as $block) {
  140.             $content .= $this->render($block);
  141.         }
  142.         if (null !== $this->stopwatch) {
  143.             $this->traces['_events'][uniqid(''true)] = [
  144.                 'template_code' => $name,
  145.                 'event_name' => $eventName,
  146.                 'blocks' => $this->getEventBlocks($event),
  147.                 'listeners' => $this->getEventListeners($eventName),
  148.             ];
  149.         }
  150.         return $content;
  151.     }
  152.     /**
  153.      * Check if a given block type exists.
  154.      *
  155.      * @param string $type Block type to check for
  156.      */
  157.     public function exists(string $type): bool
  158.     {
  159.         return $this->blockContextManager->exists($type);
  160.     }
  161.     /**
  162.      * @param mixed $block
  163.      */
  164.     public function render($block, array $options = []): string
  165.     {
  166.         $blockContext $this->blockContextManager->get($block$options);
  167.         $stats = [];
  168.         if ($this->stopwatch) {
  169.             $stats $this->startTracing($blockContext->getBlock());
  170.         }
  171.         $service $this->blockServiceManager->get($blockContext->getBlock());
  172.         $useCache $blockContext->getSetting('use_cache');
  173.         $cacheKeys $response false;
  174.         $cacheService $useCache $this->getCacheService($blockContext->getBlock(), $stats) : false;
  175.         if ($cacheService) {
  176.             $cacheKeys array_merge(
  177.                 $service->getCacheKeys($blockContext->getBlock()),
  178.                 $blockContext->getSetting('extra_cache_keys')
  179.             );
  180.             if ($this->stopwatch) {
  181.                 $stats['cache']['keys'] = $cacheKeys;
  182.             }
  183.             // Please note, some cache handler will always return true (js for instance)
  184.             // This will allows to have a non cacheable block, but the global page can still be cached by
  185.             // a reverse proxy, as the generated page will never get the generated Response from the block.
  186.             if ($cacheService->has($cacheKeys)) {
  187.                 $cacheElement $cacheService->get($cacheKeys);
  188.                 if ($this->stopwatch) {
  189.                     $stats['cache']['from_cache'] = false;
  190.                 }
  191.                 if (!$cacheElement->isExpired() && $cacheElement->getData() instanceof Response) {
  192.                     /* @var Response $response */
  193.                     if ($this->stopwatch) {
  194.                         $stats['cache']['from_cache'] = true;
  195.                     }
  196.                     $response $cacheElement->getData();
  197.                 }
  198.             }
  199.         }
  200.         if (!$response) {
  201.             $recorder null;
  202.             if ($this->cacheManager) {
  203.                 $recorder $this->cacheManager->getRecorder();
  204.                 $recorder->add($blockContext->getBlock());
  205.                 $recorder->push();
  206.             }
  207.             $response $this->blockRenderer->render($blockContext);
  208.             $contextualKeys $recorder $recorder->pop() : [];
  209.             if ($this->stopwatch) {
  210.                 $stats['cache']['contextual_keys'] = $contextualKeys;
  211.             }
  212.             if ($response->isCacheable() && $cacheKeys && $cacheService) {
  213.                 $cacheService->set($cacheKeys$response, (int) $response->getTtl(), $contextualKeys);
  214.             }
  215.         }
  216.         if ($this->stopwatch) {
  217.             // avoid \DateTime because of serialize/unserialize issue in PHP7.3 (https://bugs.php.net/bug.php?id=77302)
  218.             $stats['cache']['created_at'] = null === $response->getDate() ? null $response->getDate()->getTimestamp();
  219.             $stats['cache']['ttl'] = $response->getTtl() ?: 0;
  220.             $stats['cache']['age'] = $response->getAge();
  221.         }
  222.         // update final ttl for the whole Response
  223.         if ($this->cacheHandler) {
  224.             $this->cacheHandler->updateMetadata($response$blockContext);
  225.         }
  226.         if ($this->stopwatch) {
  227.             $this->stopTracing($blockContext->getBlock(), $stats);
  228.         }
  229.         return (string) $response->getContent();
  230.     }
  231.     /**
  232.      * Returns the rendering traces.
  233.      */
  234.     public function getTraces(): array
  235.     {
  236.         return $this->traces;
  237.     }
  238.     private function stopTracing(BlockInterface $block, array $stats): void
  239.     {
  240.         $e $this->traces[$block->getId()]->stop();
  241.         $this->traces[$block->getId()] = array_merge($stats, [
  242.             'duration' => $e->getDuration(),
  243.             'memory_end' => memory_get_usage(true),
  244.             'memory_peak' => memory_get_peak_usage(true),
  245.         ]);
  246.         $this->traces[$block->getId()]['cache']['lifetime'] = $this->traces[$block->getId()]['cache']['age'] + $this->traces[$block->getId()]['cache']['ttl'];
  247.     }
  248.     private function getEventBlocks(BlockEvent $event): array
  249.     {
  250.         $results = [];
  251.         foreach ($event->getBlocks() as $block) {
  252.             $results[] = [$block->getId(), $block->getType()];
  253.         }
  254.         return $results;
  255.     }
  256.     private function getEventListeners(string $eventName): array
  257.     {
  258.         $results = [];
  259.         if (!$this->eventDispatcher instanceof EventDispatcherComponentInterface) {
  260.             return $results;
  261.         }
  262.         foreach ($this->eventDispatcher->getListeners($eventName) as $listener) {
  263.             if ($listener instanceof \Closure) {
  264.                 $results[] = '{closure}()';
  265.             } elseif (\is_object($listener[0])) {
  266.                 $results[] = \get_class($listener[0]);
  267.             } elseif (\is_string($listener[0])) {
  268.                 $results[] = $listener[0];
  269.             } else {
  270.                 $results[] = 'Unknown type!';
  271.             }
  272.         }
  273.         return $results;
  274.     }
  275.     private function getCacheService(BlockInterface $block, ?array &$stats null): ?CacheAdapterInterface
  276.     {
  277.         if (!$this->cacheManager) {
  278.             return null;
  279.         }
  280.         // type by block class
  281.         $class ClassUtils::getClass($block);
  282.         $cacheServiceId $this->cacheBlocks['by_class'][$class] ?? null;
  283.         // type by block service
  284.         if (null === $cacheServiceId) {
  285.             $cacheServiceId $this->cacheBlocks['by_type'][$block->getType()] ?? null;
  286.         }
  287.         if (null === $cacheServiceId) {
  288.             return null;
  289.         }
  290.         if ($this->stopwatch) {
  291.             $stats['cache']['handler'] = $cacheServiceId;
  292.         }
  293.         return $this->cacheManager->getCacheService((string) $cacheServiceId);
  294.     }
  295.     private function startTracing(BlockInterface $block): array
  296.     {
  297.         if (null !== $this->stopwatch) {
  298.             $this->traces[$block->getId()] = $this->stopwatch->start(
  299.                 sprintf('%s (id: %s, type: %s)'$block->getName(), $block->getId(), $block->getType())
  300.             );
  301.         }
  302.         return [
  303.             'name' => $block->getName(),
  304.             'type' => $block->getType(),
  305.             'duration' => false,
  306.             'memory_start' => memory_get_usage(true),
  307.             'memory_end' => false,
  308.             'memory_peak' => false,
  309.             'cache' => [
  310.                 'keys' => [],
  311.                 'contextual_keys' => [],
  312.                 'handler' => false,
  313.                 'from_cache' => false,
  314.                 'ttl' => 0,
  315.                 'created_at' => false,
  316.                 'lifetime' => 0,
  317.                 'age' => 0,
  318.             ],
  319.             'assets' => [
  320.                 'js' => [],
  321.                 'css' => [],
  322.             ],
  323.         ];
  324.     }
  325. }