src/Cms/ContentBundle/Service/FrontendRenderer.php line 448

Open in your IDE?
  1. <?php
  2. namespace Cms\ContentBundle\Service;
  3. use Cms\AssetsBundle\Model\StructureBag\ScriptStructureBag;
  4. use Cms\AssetsBundle\Service\AssetCatalog;
  5. use Cms\ContainerBundle\Entity\Container;
  6. use Cms\ContentBundle\Model\Inliner;
  7. use Cms\ContentBundle\Model\Inliners\ContentInliner;
  8. use Cms\ContentBundle\Model\Inliners\RegionInliner;
  9. use Cms\CoreBundle\Model\HtmlOverridableInterface;
  10. use Cms\CoreBundle\Service\Transcoding\Transcoder;
  11. use Cms\FrontendBundle\Model\FrontendGlobals;
  12. use Cms\ModuleBundle\Entity\ModuleEntity;
  13. use Cms\ThemeBundle\Entity\InnerLayout;
  14. use Cms\ThemeBundle\Entity\OuterLayout;
  15. use Cms\ThemeBundle\Entity\Template;
  16. use Cms\ThemeBundle\Model\Layout;
  17. use Cms\ThemeBundle\Service\PackageManager;
  18. use Cms\ThemeBundle\Service\ThemeManager;
  19. use Cms\ThemeBundle\Service\Twig\Loader\DatabaseLoader;
  20. use Cms\WidgetBundle\Model\AbstractWidgetProcessor;
  21. use Cms\WidgetBundle\Model\WidgetObject;
  22. use Cms\WidgetBundle\Service\WidgetDependencyInjector;
  23. use Cms\WidgetBundle\Service\WidgetManager;
  24. use Doctrine\Common\Util\ClassUtils;
  25. use Twig\Environment;
  26. /**
  27.  * Class FrontendRenderer
  28.  * @package Cms\ContentBundle\Service
  29.  */
  30. final class FrontendRenderer
  31. {
  32.     const MODES = array(
  33.         self::MODES__LIVE,
  34.         self::MODES__EDITOR,
  35.     );
  36.     const MODES__LIVE 'live';
  37.     const MODES__EDITOR 'editor';
  38.     /**
  39.      * @var AssetCatalog
  40.      */
  41.     private AssetCatalog $assetCatalog;
  42.     /**
  43.      * @var Environment
  44.      */
  45.     private Environment $twig;
  46.     /**
  47.      * @var DatabaseLoader
  48.      */
  49.     private DatabaseLoader $databaseLoader;
  50.     /**
  51.      * @var PackageManager
  52.      */
  53.     private PackageManager $packageManager;
  54.     /**
  55.      * @var ThemeManager
  56.      */
  57.     private ThemeManager $themeManager;
  58.     /**
  59.      * @var WidgetManager
  60.      */
  61.     private WidgetManager $widgetManager;
  62.     /**
  63.      * @var WidgetDependencyInjector
  64.      */
  65.     private WidgetDependencyInjector $widgetDependencyInjector;
  66.     /**
  67.      * @var array
  68.      */
  69.     private $activeContent;
  70.     /**
  71.      * @var FrontendGlobals
  72.      */
  73.     private $globals;
  74.     /**
  75.      * @var string
  76.      */
  77.     private $mode;
  78.     /**
  79.      * @var string
  80.      */
  81.     private $realMode;
  82.     /**
  83.      * @param AssetCatalog $assetCatalog
  84.      * @param Environment $twig
  85.      * @param DatabaseLoader $databaseLoader
  86.      * @param PackageManager $packageManager
  87.      * @param ThemeManager $themeManager
  88.      * @param WidgetManager $widgetManager
  89.      * @param WidgetDependencyInjector $widgetDependencyInjector
  90.      */
  91.     public function __construct(
  92.         AssetCatalog $assetCatalog,
  93.         Environment $twig,
  94.         DatabaseLoader $databaseLoader,
  95.         PackageManager $packageManager,
  96.         ThemeManager $themeManager,
  97.         WidgetManager $widgetManager,
  98.         WidgetDependencyInjector $widgetDependencyInjector
  99.     )
  100.     {
  101.         $this->assetCatalog $assetCatalog;
  102.         $this->twig $twig;
  103.         $this->databaseLoader $databaseLoader;
  104.         $this->packageManager $packageManager;
  105.         $this->themeManager $themeManager;
  106.         $this->widgetManager $widgetManager;
  107.         $this->widgetDependencyInjector $widgetDependencyInjector;
  108.     }
  109.     /**
  110.      * @return FrontendGlobals
  111.      */
  112.     public function peekGlobals()
  113.     {
  114.         return $this->globals;
  115.     }
  116.     /**
  117.      * @return Inliner
  118.      */
  119.     public function peekActiveContent()
  120.     {
  121.         return $this->activeContent;
  122.     }
  123.     /**
  124.      * @return string
  125.      */
  126.     public function peekMode()
  127.     {
  128.         return $this->mode;
  129.     }
  130.     /**
  131.      * @return string
  132.      */
  133.     public function peekRealMode()
  134.     {
  135.         return $this->realMode;
  136.     }
  137.     /**
  138.      * @param FrontendGlobals $globals
  139.      * @param string $mode
  140.      * @return $this
  141.      */
  142.     public function prepare(FrontendGlobals $globals$mode null)
  143.     {
  144.         $this->globals $globals;
  145.         if ( ! array_search($modeself::MODES)) {
  146.             $mode null;
  147.         }
  148.         if ($mode === null) {
  149.             $mode self::MODES__LIVE;
  150.         }
  151.         $this->mode $mode;
  152.         $this->realMode $mode;
  153.         return $this;
  154.     }
  155.     /**
  156.      * @param mixed $content
  157.      * @param mixed $default
  158.      */
  159.     private function generateActiveContent($content$default null)
  160.     {
  161.         // set the active content
  162.         $this->activeContent $this->flattenContent(
  163.             $this->normalizeContent($content$default)
  164.         );
  165.         // if we are editing, we now want to treat as live; only topmost content is editable
  166.         if ($this->mode === self::MODES__EDITOR) {
  167.             $this->mode self::MODES__LIVE;
  168.         }
  169.     }
  170.     /**
  171.      * @param string $template
  172.      * @param array $parameters
  173.      * @return string
  174.      * @throws \Exception
  175.      */
  176.     public function renderAjax(string $template, array $parameters = []): string
  177.     {
  178.         return $this->renderTheme(array(
  179.             'content' => $this->renderRaw(
  180.                 $template,
  181.                 $parameters
  182.             ),
  183.         ));
  184.     }
  185.     /**
  186.      * @param mixed $content
  187.      * @return string
  188.      * @throws \Exception
  189.      */
  190.     public function renderPage($content)
  191.     {
  192.         // set editing class
  193.         if ($this->mode === self::MODES__EDITOR) {
  194.             $this->peekGlobals()->getAssetsOrganizer()->getHtmlTag()->append(
  195.                 'class',
  196.                 'edit--content'
  197.             );
  198.         }
  199.         // set the active content
  200.         $this->generateActiveContent($content);
  201.         // with this done, we then need to render out our inner layout, using its own content
  202.         return $this->renderInnerLayout(true);
  203.     }
  204.     /**
  205.      * @param WidgetObject $widget
  206.      * @return string
  207.      * @throws \Exception
  208.      */
  209.     public function renderWidget(WidgetObject $widget)
  210.     {
  211.         static $dbgStack = [];
  212.         array_push($dbgStacksprintf(
  213.             '%s::%s',
  214.             ClassUtils::getClass($widget),
  215.             $widget->__uid__
  216.         ));
  217.         $dbgevt trim(sprintf(
  218.             'WIDGET PROCESSING %s',
  219.             implode(' '$dbgStack)
  220.         ));
  221.         $this->globals->debugStart($dbgevt);
  222.         // obtain the widget handler
  223.         $handler $this->widgetManager->loadFor($widget);
  224.         // get the processor if it has one
  225.         $processor null;
  226.         if ($handler->hasProcessor()) {
  227.             $processor $handler->getProcessor(
  228.                 $this->widgetDependencyInjector->getContainer($this),
  229.             );
  230.         }
  231.         // run the processor to generate additional parameters for the widget
  232.         $parameters = [];
  233.         $plugin = [];
  234.         $styles = [];
  235.         $scripts = [];
  236.         $error null;
  237.         if ($processor !== null) {
  238.             try {
  239.                 $parameters $processor->process($widget$this);
  240.                 $plugin $processor->plugin($widget$this);
  241.                 $styles $processor->styles($widget$this);
  242.                 $scripts $processor->scripts($widget$this);
  243.             } catch (\Exception $e) {
  244.                 $error $e;
  245.             }
  246.         }
  247.         // generate the final parameters array
  248.         $parameters array_merge(
  249.             $parameters,
  250.             array(
  251.                 '_plugin' => $plugin,
  252.                 '_styles' => $styles,
  253.                 '_scripts' => $scripts,
  254.                 '_htmlId' => ($widget->getContainer() !== null && ! empty($widget->getContainer()->getHtmlId()))
  255.                     ? $widget->getContainer()->getHtmlId()
  256.                     : sprintf(
  257.                         'cms-widget-%s',
  258.                         $widget->__uid__
  259.                     ),
  260.                 '_widget' => $widget,
  261.                 '_handler' => $handler,
  262.                 '_error' => $error,
  263.                 '_twig' => $this->databaseLoader->determine(
  264.                     $this->globals->getTheme(),
  265.                     sprintf(
  266.                         '/widgets/%s/build/%s.html.twig',
  267.                         $handler->getKey(),
  268.                         (isset($parameters[AbstractWidgetProcessor::SCHOOLNOW_OVERRIDE]) && $parameters[AbstractWidgetProcessor::SCHOOLNOW_OVERRIDE] === true) ? 'sn' 'tpl'
  269.                     )
  270.                 ),
  271.                 '_loader' => $this->databaseLoader,
  272.             )
  273.         );
  274.         $this->globals->debugStop($dbgevt);
  275.         array_pop($dbgStack);
  276.         // render based on the template
  277.         $rendered $this->render('Widget'$parameters);
  278.         // done
  279.         return $rendered;
  280.     }
  281.     /**
  282.      * @param string $template
  283.      * @param array $parameters
  284.      * @return string
  285.      */
  286.     public function renderRaw($template, array $parameters = [])
  287.     {
  288.         // generate the final parameters array
  289.         $parameters array_merge(
  290.             $parameters,
  291.             array(
  292.                 '_twig' => $this->databaseLoader->determine(
  293.                     $this->globals->getTheme(),
  294.                     $template
  295.                 ),
  296.                 '_loader' => $this->databaseLoader,
  297.             )
  298.         );
  299.         // render based on the template
  300.         $rendered $this->render($template$parameters);
  301.         // done
  302.         return $rendered;
  303.     }
  304.     /**
  305.      * Takes a set of widgets and generates on string of rendered content from them.
  306.      *
  307.      * @param array $widgets
  308.      * @return string
  309.      */
  310.     public function renderWidgets(array &$widgets)
  311.     {
  312.         // holder
  313.         $rendered = [];
  314.         // loop over all widgets and append their result
  315.         foreach ($widgets as $widget) {
  316.             $rendered[] = $this->renderWidget($widget);
  317.         }
  318.         // done
  319.         return implode("\n\n"$rendered);
  320.     }
  321.     /**
  322.      * Takes a content array, and flattens the widgets contained within it.
  323.      *
  324.      * @param Inliner $content
  325.      * @return Inliner
  326.      */
  327.     private function flattenContent(Inliner $content)
  328.     {
  329.         // get keys
  330.         $keys $content->keys();
  331.         // loop over all the keys
  332.         foreach ($keys as $key) {
  333.             // get the content item
  334.             $stuff $content->get($key);
  335.             // if we have a string, assume we are already rendered
  336.             if ( ! is_string($stuff)) {
  337.                 // render this one, expect an array of widget objects
  338.                 $content->set($key$this->renderWidgets($stuff));
  339.             }
  340.         }
  341.         // done
  342.         return $content;
  343.     }
  344.     /**
  345.      * @param mixed $content
  346.      * @return string
  347.      */
  348.     public function renderInnerLayout($content null)
  349.     {
  350.         // set editing class
  351.         $taken $this->peekGlobals()->getAssetsOrganizer()->getHtmlTag()->contains(
  352.             'class',
  353.             array(
  354.                 'edit--content',
  355.             )
  356.         );
  357.         if ($this->mode === self::MODES__EDITOR && ! $taken) {
  358.             $this->peekGlobals()->getAssetsOrganizer()->getHtmlTag()->append(
  359.                 'class',
  360.                 'edit--inner-tpl'
  361.             );
  362.         }
  363.         // set the active
  364.         $this->generateActiveContent($content$this->globals->getInnerLayout());
  365.         // render the outer layout using its content
  366.         return $this->renderOuterLayout(true);
  367.     }
  368.     /**
  369.      * @param mixed $content
  370.      * @return string
  371.      */
  372.     public function renderOuterLayout($content null)
  373.     {
  374.         // normalize the content, default to our inner layout content
  375.         $content $this->normalizeContent(
  376.             $content,
  377.             $this->globals->getOuterLayout()
  378.         );
  379.         // set editing class
  380.         $taken $this->peekGlobals()->getAssetsOrganizer()->getHtmlTag()->contains(
  381.             'class',
  382.             array(
  383.                 'edit--inner-tpl',
  384.                 'edit--content',
  385.             )
  386.         );
  387.         if ($this->mode === self::MODES__EDITOR && ! $taken) {
  388.             $this->peekGlobals()->getAssetsOrganizer()->getHtmlTag()->append(
  389.                 'class',
  390.                 'edit--outer-tpl'
  391.             );
  392.         }
  393.         // pass our content to the base
  394.         return $this->renderOuterLayoutBase(
  395.             ($content instanceof ContentInliner)
  396.             ? $content->content()
  397.             : false
  398.         );
  399.     }
  400.     /**
  401.      * @param mixed $content
  402.      * @return string
  403.      */
  404.     public function renderOuterLayoutBase($content null)
  405.     {
  406.         // set the active
  407.         $this->generateActiveContent($content$this->globals->getPackage()->getLayout(
  408.             $this->globals->getOuterLayout()->getBase()
  409.         ));
  410.         // this needs to actually render out a file as we need to merge inline content regions
  411.         $rendered $this->render(
  412.             'OuterLayoutBase',
  413.             array(
  414.                 '_twig' => $this->databaseLoader->determine(
  415.                     $this->globals->getTheme(),
  416.                     sprintf(
  417.                         '/layouts/%s/build/tpl.html.twig',
  418.                         $this->globals->getOuterLayout()->getBase()
  419.                     )
  420.                 )
  421.             )
  422.         );
  423.         // now, we render the theme
  424.         return $this->renderTheme(array(
  425.             'content' => $rendered,
  426.         ));
  427.     }
  428.     /**
  429.      *
  430.      */
  431.     private function mergeHtmlOverrides()
  432.     {
  433.         // starting overrides, the inner and outer layouts
  434.         $overrides = array(
  435.             $this->globals->getOuterLayout(),
  436.             $this->globals->getInnerLayout(),
  437.         );
  438.         // add in the containers
  439.         $overrides array_merge($overridesarray_reverse(
  440.             $this->globals->getContainers()
  441.         ));
  442.         // if there is a thing, check and see if we need to add something
  443.         $thing $this->globals->getThing();
  444.         if ($thing instanceof ModuleEntity) {
  445.             $data $thing->getData();
  446.             if ($data instanceof HtmlOverridableInterface) {
  447.                 $overrides[] = $data;
  448.             }
  449.         }
  450.         // TODO: why was this change made with the favorite container fix?
  451.         $overrides array_values(array_filter($overrides, function ($override) {
  452.             return ( ! empty($override));
  453.         }));
  454.         // set overrides from items
  455.         /** @var HtmlOverridableInterface $override */
  456.         foreach ($overrides as $override) {
  457.             $this->mergeHtmlOverride($override);
  458.         }
  459.     }
  460.     /**
  461.      * @param HtmlOverridableInterface $override
  462.      */
  463.     private function mergeHtmlOverride(HtmlOverridableInterface $override null)
  464.     {
  465.         if (empty($override)) {
  466.             return;
  467.         }
  468.         if ( ! empty($override->getBodyClass())) {
  469.             $this->globals->getAssetsOrganizer()->getBodyTag()
  470.                 ->prepend('class'$override->getBodyClass());
  471.         }
  472.         if ( ! empty($override->getHeadScripts())) {
  473.             $this->globals->getAssetsOrganizer()->getHtml()
  474.                 ->addInline($override->getHeadScripts(), null'head');
  475.         }
  476.         if ( ! empty($override->getTopScripts())) {
  477.             $this->globals->getAssetsOrganizer()->getHtml()
  478.                 ->addInline($override->getTopScripts(), null'top');
  479.         }
  480.         if ( ! empty($override->getBottomScripts())) {
  481.             $this->globals->getAssetsOrganizer()->getHtml()
  482.                 ->addInline($override->getBottomScripts(), null'bottom');
  483.         }
  484.     }
  485.     /**
  486.      * @param mixed $content
  487.      * @return string
  488.      */
  489.     public function renderTheme($content null)
  490.     {
  491.         // set the active
  492.         $this->generateActiveContent($content);
  493.         // TODO: if editing, ensure a base uri
  494.         if ($this->peekRealMode() === self::MODES__EDITOR) {
  495.             $this->peekGlobals()->getAssetsOrganizer()->getBaseTag()->set(
  496.                 'href',
  497.                 sprintf(
  498.                     '%s://%s/%s',
  499.                     $this->peekGlobals()->getDomain()->getCertificate() ? 'https' 'http',
  500.                     $this->peekGlobals()->getDomain()->getHost(),
  501.                     implode('/'array_values(array_merge(
  502.                         array_filter(
  503.                             array_map(
  504.                                 function (Container $container) {
  505.                                     if (empty($container->getParent())) {
  506.                                         return null;
  507.                                     }
  508.                                     return $container->getSlug();
  509.                                 },
  510.                                 array_reverse($this->peekGlobals()->getContainers())
  511.                             )
  512.                         ),
  513.                         array('index')
  514.                     )))
  515.                 )
  516.             );
  517.         }
  518.         // add scripts and body tag class overrides set in admin panel
  519.         $this->mergeHtmlOverrides();
  520.         // preparations
  521.         $this->packageManager->mergeAssets(
  522.             $this->globals->getPackage(),
  523.             $this->globals->getAssetsOrganizer()
  524.         );
  525.         $this->themeManager->mergeAssets(
  526.             $this->globals->getTheme(),
  527.             $this->globals->getAssetsOrganizer()
  528.         );
  529.         $this->mergeAssets();
  530.         // just render the file
  531.         $rendered $this->render(
  532.             'Theme',
  533.             []
  534.         );
  535.         // need to clear out the active content, as this method will end the rendering chain
  536.         $this->activeContent null;
  537.         // done
  538.         return $rendered;
  539.     }
  540.     /**
  541.      *
  542.      */
  543.     private function mergeAssets()
  544.     {
  545.         // in all cases, we need to supply our global campussuite extension
  546.         $this->globals->getAssetsOrganizer()->getScripts()->addLinked(
  547.             sprintf(
  548.                 'https://%s/bundles/cmscore/campussuite.js',
  549.                 $this->globals->getDashboard()
  550.             ),
  551.             null,
  552.             ScriptStructureBag::GROUPS__HEAD
  553.         );
  554.         // check for whether the mimic is in effect
  555.         // if so we need to add a class to the html that can be hooked
  556.         // into to fix theme topbar stuff with static headers in themes
  557.         if ($this->globals->getMimic() !== null) {
  558.             $this->globals->getAssetsOrganizer()->getHtmlTag()->append('class''admin-public');
  559.         }
  560.         // check for editor
  561.         if ($this->realMode === self::MODES__EDITOR) {
  562.             // add in the page editor css and js
  563.             $this->globals->getAssetsOrganizer()->getStyles()->addLinked(sprintf(
  564.                 'https://%s/ui/css/page-builder.css',
  565.                 $this->globals->getDashboard()
  566.             ));
  567.             $this->globals->getAssetsOrganizer()->getScripts()->addInline(sprintf(
  568.                 'window.head.load(\'%s\');',
  569.                 $this->assetCatalog->locate('bootstrap::/dist/js/bootstrap.min.js')
  570.             ));
  571.         }
  572.     }
  573.     /**
  574.      * @param string $twig
  575.      * @param array $parameters
  576.      * @return string
  577.      */
  578.     private function render($twig, array $parameters)
  579.     {
  580.         static $dbgStack = [];
  581.         // fix the twig file
  582.         if (preg_match('/^[a-zA-Z0-9]+$/'$twig) === 1) {
  583.             $twig sprintf(
  584.                 '@CmsContent/Render/%s.html.twig',
  585.                 $twig
  586.             );
  587.         }
  588.         // default some of the parameters
  589.         $parameters array_merge($parameters, array(
  590.             '_renderer' => $this,
  591.             '_globals' => $this->globals,
  592.             '_doc' => $this->globals->getAssetsOrganizer(),
  593.             '_mode' => $this->mode,
  594.             '_realMode' => $this->realMode,
  595.             '_editing' => ($this->mode === self::MODES__EDITOR),
  596.             '_realEditing' => ($this->realMode === self::MODES__EDITOR),
  597.             '_editor' => ($this->mode === self::MODES__EDITOR || $this->realMode === self::MODES__EDITOR),
  598.         ));
  599.         // render the template
  600.         array_push($dbgStacksprintf(
  601.             '%s::%s',
  602.             (isset($parameters['_twig'])) ? $parameters['_twig'] : '???',
  603.             (isset($parameters['_htmlId'])) ? str_replace('cms-widget-'''$parameters['_htmlId']) : '???'
  604.         ));
  605.         $dbgevt trim(sprintf(
  606.             '%s %s',
  607.             $twig,
  608.             implode(' '$dbgStack)
  609.         ));
  610.         $this->globals->debugStart($dbgevt);
  611.         $template $this->twig->loadTemplate($twig);
  612.         $rendered $template->render($parameters);
  613.         /*
  614.         $rendered = $this->container->get('twig')->render(
  615.             $twig,
  616.             $parameters
  617.         );
  618.         */
  619.         $this->globals->debugStop($dbgevt);
  620.         array_pop($dbgStack);
  621.         // send back the result
  622.         return $rendered;
  623.     }
  624.     /**
  625.      * @param mixed $content
  626.      * @param mixed $default
  627.      * @return Inliner
  628.      * @throws \Exception
  629.      */
  630.     private function normalizeContent($content$default = [])
  631.     {
  632.         // make sure we are initialized
  633.         if ($this->mode === null) {
  634.             throw new \Exception();
  635.         }
  636.         // we must have something
  637.         if ($content === null) {
  638.             throw new \Exception();
  639.         }
  640.         // if false, then we don't want to use any content
  641.         if ($content === false) {
  642.             return new RegionInliner();
  643.         }
  644.         // if true, then we are defaulting the content
  645.         if ($content === true) {
  646.             switch (true) {
  647.                 case $default instanceof OuterLayout:
  648.                     $default Transcoder::fromStorage($default->getContents());
  649.                     break;
  650.                 case $default instanceof InnerLayout:
  651.                     $default Transcoder::fromStorage($default->getContents());
  652.                     break;
  653.                 case $default instanceof Layout:
  654.                     $default $default->getEditorRegions();
  655.                     break;
  656.                 case is_array($default):
  657.                     break;
  658.                 default:
  659.                     $default = [];
  660.             }
  661.             $content $default;
  662.         }
  663.         // make a content inline
  664.         return new ContentInliner($content);
  665.     }
  666. }