<?php
namespace Cms\CoreBundle\Service;
use ArrayAccess;
use Cms\CoreBundle\Model\Scenes\AbstractScene;
use Cms\WidgetBundle\Service\WidgetDependencyInjector;
use Twig\Environment;
/**
* Class SceneRenderer
* @package Cms\CoreBundle\Service
*/
final class SceneRenderer
{
/**
* @var Environment
*/
private Environment $twig;
/**
* @var WidgetDependencyInjector
*/
private WidgetDependencyInjector $widgetDependencyInjector;
/**
* @var array|AbstractScene[]
*/
private array $stack = [];
/**
* @var mixed
*/
private $extras;
/**
* @param Environment $twig
* @param WidgetDependencyInjector $widgetDependencyInjector
*/
public function __construct(
Environment $twig,
WidgetDependencyInjector $widgetDependencyInjector
)
{
$this->twig = $twig;
$this->widgetDependencyInjector = $widgetDependencyInjector;
}
/**
* @param string $template
* @param array $parameters
* @return string
*/
public function renderRawFile(string $template, array $parameters = []): string
{
return $this->twig->render(
$template,
$parameters,
);
}
/**
* @param string $source
* @param array $parameters
* @return string
*/
public function renderRawString(string $source, array $parameters = []): string
{
return $this->twig
->createTemplate($source)
->render($parameters);
}
/**
* Used to launch a new rendering stack.
* If we are currently rendering something else, an error is thrown.
*
* @param AbstractScene $scene
* @param mixed $extras
* @return string
*/
public function render(AbstractScene $scene, $extras = null): string
{
// this should only be called if we are not already rendering something else
if ($this->isRendering()) {
throw new \LogicException();
}
// set current globals
$this->extras = $extras;
// run render logic
$result = $this->doRender($scene);
// clear globals
$this->extras = null;
// done
return $result;
}
/**
* This should be used when scenes need to spin off "child" scenes to help with rendering.
*
* @param AbstractScene $scene
* @return string
* @throws \Exception
*/
public function subrender(AbstractScene $scene): string
{
// this should only be called if we are already rendering something
if ($this->isRendering() === false) {
throw new \LogicException();
}
// run render code after forking
return $this->doRender(
$this->currentScene()->fork($scene)
);
}
/**
* Adds a new scene to the stack.
* Runs checks to help ensure legal state of rendering.
*
* @param AbstractScene $scene
* @throws \Exception
*/
private function pushScene(AbstractScene $scene): void
{
// prevent us from rendering the same scene
if (in_array($scene, $this->stack, true)) {
throw new \RuntimeException();
}
// push onto front of queue
array_unshift($this->stack, $scene);
}
/**
* Removes the currently processing scene from the stack.
* Runs checks to help ensure legal state of rendering.
*
* @param AbstractScene $scene
* @throws \Exception
*/
private function popScene(AbstractScene $scene): void
{
// if there are no scenes, this was called incorrectly
if (count($this->stack) === 0) {
throw new \RuntimeException();
}
// make sure we are in fact the first thing in the queue
if ($this->stack[0] !== $scene) {
throw new \RuntimeException();
}
// can pop us off the front
array_shift($this->stack);
}
/**
* Ensures that a template is rendered properly for a scene.
*
* @param AbstractScene $scene
* @return string
*/
private function obtainTemplate(AbstractScene $scene): string
{
// try to generate
$template = $scene->generateTemplate(
$this->widgetDependencyInjector->getContainer($this),
$this->extras,
);
// make sure template is valid
if ( ! is_string($template) || $template === '') {
throw new \RuntimeException();
}
return $template;
}
/**
* Ensures that parameters are generated properly for a scene.
*
* @param AbstractScene $scene
* @return iterable
*/
private function obtainParameters(AbstractScene $scene): iterable
{
// try to generate
$parameters = $scene->generateParameters(
$this->widgetDependencyInjector->getContainer($this),
$this->extras,
);
// init params if not given
if ($parameters === null) {
$parameters = [];
}
// make sure params are valid
if ( ! is_array($parameters) && ! $parameters instanceof ArrayAccess) {
throw new \RuntimeException();
}
return $parameters;
}
/**
* Responsible for rendering out a thing based on the current context stack.
*
* @param AbstractScene $scene
* @return string
*/
private function doRender(AbstractScene $scene): string
{
// push us onto the stack
$this->pushScene($scene);
// get things we need
$template = $this->obtainTemplate($scene);
$parameters = $this->obtainParameters($scene);
// actually do the twig rendering
if ($scene->isFileBased()) {
$result = $this->renderRawFile($template, (array) $parameters);
} else {
$result = $this->renderRawString($template, (array) $parameters);
}
// we are done, can pop us off the stack
$this->popScene($scene);
// done, send back content that was rendered
return $result;
}
/**
* Get the current item in the rendering stack.
*
* @return AbstractScene|null
*/
public function currentScene(): ?AbstractScene
{
// if we are not rendering, nothing to report
// pull from the front of the queue
return $this->isRendering() ? $this->stack[0] : null;
}
/**
* Obtain the rendering stack.
*
* @return array|AbstractScene[]
*/
public function currentStack(): array
{
return $this->stack;
}
/**
* Determines the first context that started the stack, if any.
*
* @return AbstractScene|null
*/
public function initialScene(): ?AbstractScene
{
// there is nothing to report if we are not rendering
// need to pull the last off of the queue
return $this->isRendering() ? $this->stack[count($this->stack) - 1] : null;
}
/**
* Determines whether we are currently in a rendering stack.
*
* @return bool
*/
public function isRendering(): bool
{
return (count($this->stack) !== 0);
}
}