diff --git a/src/Parse/ParseContext.php b/src/Parse/ParseContext.php index db6b19f..6a4d47e 100644 --- a/src/Parse/ParseContext.php +++ b/src/Parse/ParseContext.php @@ -28,21 +28,18 @@ class ParseContext protected bool $partial = false; - protected PartialsCache $partialsCache; - - protected OutputsBag $outputs; - protected Lexer $lexer; protected Parser $parser; public function __construct( + public readonly bool $allowDynamicPartials = false, public readonly TagRegistry $tagRegistry = new TagRegistry(), public readonly LiquidFileSystem $fileSystem = new BlankFileSystem(), + protected PartialsCache $partialsCache = new PartialsCache(), + protected OutputsBag $outputs = new OutputsBag(), ) { $this->lineNumber = 1; - $this->outputs = new OutputsBag(); - $this->partialsCache = new PartialsCache(); $this->lexer = new Lexer($this); $this->parser = new Parser($this); } @@ -72,16 +69,17 @@ public function loadPartial(string $templateName): Template } $partialParseContext = new ParseContext( + allowDynamicPartials: $this->allowDynamicPartials, tagRegistry: $this->tagRegistry, fileSystem: $this->fileSystem, + partialsCache: $this->partialsCache, + outputs: $this->outputs, ); $partialParseContext->partial = true; $partialParseContext->depth = $this->depth; - $partialParseContext->outputs = $this->outputs; - $partialParseContext->partialsCache = $this->partialsCache; try { - $source = $this->fileSystem->readTemplateFile($templateName); + $source = $partialParseContext->fileSystem->readTemplateFile($templateName); $template = Template::parse($partialParseContext, $source, $templateName); diff --git a/src/Render/ContextSharedState.php b/src/Render/ContextSharedState.php index 4346e26..68574ae 100644 --- a/src/Render/ContextSharedState.php +++ b/src/Render/ContextSharedState.php @@ -3,16 +3,11 @@ namespace Keepsuit\Liquid\Render; use Keepsuit\Liquid\Support\OutputsBag; -use Keepsuit\Liquid\Template; +use Keepsuit\Liquid\Support\PartialsCache; use WeakMap; class ContextSharedState { - /** - * @var array - */ - public array $partialsCache = []; - /** * @var WeakMap */ @@ -27,6 +22,7 @@ public function __construct( public array $errors = [], /** @var array */ public array $disabledTags = [], + public PartialsCache $partialsCache = new PartialsCache(), public OutputsBag $outputs = new OutputsBag(), ) { $this->computedObjectsCache = new WeakMap(); diff --git a/src/Render/RenderContext.php b/src/Render/RenderContext.php index 8c1507d..49b89a9 100644 --- a/src/Render/RenderContext.php +++ b/src/Render/RenderContext.php @@ -25,6 +25,8 @@ use Keepsuit\Liquid\Support\FilterRegistry; use Keepsuit\Liquid\Support\MissingValue; use Keepsuit\Liquid\Support\OutputsBag; +use Keepsuit\Liquid\Support\PartialsCache; +use Keepsuit\Liquid\Support\TagRegistry; use Keepsuit\Liquid\Template; use RuntimeException; use Throwable; @@ -79,10 +81,12 @@ public function __construct( array $registers = [], public readonly bool $rethrowExceptions = false, public readonly bool $strictVariables = false, + public readonly bool $allowDynamicPartials = false, bool $profile = false, public readonly FilterRegistry $filterRegistry = new FilterRegistry(), public readonly ResourceLimits $resourceLimits = new ResourceLimits(), public readonly LiquidFileSystem $fileSystem = new BlankFileSystem(), + public readonly TagRegistry $tagRegistry = new TagRegistry(), ) { $this->scopes = [[]]; @@ -320,28 +324,40 @@ public function getProfiler(): ?Profiler public function loadPartial(string $templateName): Template { - if (! Arr::has($this->sharedState->partialsCache, $templateName)) { - throw new StandardException(sprintf("The partial '%s' has not be loaded during parsing", $templateName)); + if ($template = $this->sharedState->partialsCache->get($templateName)) { + return $template; } - return $this->sharedState->partialsCache[$templateName]; + if (! $this->allowDynamicPartials) { + throw new StandardException('Dynamic templates are not allowed'); + } + + $parseContext = new ParseContext( + allowDynamicPartials: $this->allowDynamicPartials, + tagRegistry: $this->tagRegistry, + fileSystem: $this->fileSystem, + partialsCache: $this->sharedState->partialsCache, + outputs: $this->sharedState->outputs, + ); + + return $parseContext->loadPartial($templateName); } - public function setPartialsCache(array $partialsCache): RenderContext + public function setPartialsCache(PartialsCache $partialsCache): RenderContext { $this->sharedState->partialsCache = $partialsCache; return $this; } - public function mergePartialsCache(array $partialsCache): RenderContext + public function mergePartialsCache(PartialsCache $partialsCache): RenderContext { - $this->sharedState->partialsCache = array_merge($this->sharedState->partialsCache, $partialsCache); + $this->sharedState->partialsCache->merge($partialsCache); return $this; } - public function mergeOutputs(array $outputs): RenderContext + public function mergeOutputs(OutputsBag $outputs): RenderContext { $this->sharedState->outputs->merge($outputs); diff --git a/src/Support/OutputsBag.php b/src/Support/OutputsBag.php index 6e0bc75..fa10d15 100644 --- a/src/Support/OutputsBag.php +++ b/src/Support/OutputsBag.php @@ -43,9 +43,9 @@ public function all(): array return $this->bags; } - public function merge(array $outputs): void + public function merge(OutputsBag $outputs): void { - foreach ($outputs as $key => $value) { + foreach ($outputs->all() as $key => $value) { $this->bags[$key] = $value; } } diff --git a/src/Tags/RenderTag.php b/src/Tags/RenderTag.php index f23d75d..f1f8089 100644 --- a/src/Tags/RenderTag.php +++ b/src/Tags/RenderTag.php @@ -21,7 +21,7 @@ */ class RenderTag extends Tag implements CanBeStreamed, HasParseTreeVisitorChildren { - protected string $templateNameExpression; + protected string|VariableLookup $templateNameExpression; protected mixed $variableNameExpression; @@ -48,6 +48,9 @@ public function parse(TagParseContext $context): static $templateNameExpression = $context->params->expression(); $this->templateNameExpression = match (true) { is_string($templateNameExpression) => $templateNameExpression, + $templateNameExpression instanceof VariableLookup => $context->getParseContext()->allowDynamicPartials + ? $templateNameExpression + : throw new SyntaxException('Dynamic partials are not allowed'), default => throw new SyntaxException('Template name must be a string'), }; @@ -80,7 +83,9 @@ public function parse(TagParseContext $context): static $context->params->assertEnd(); - $context->getParseContext()->loadPartial($this->templateNameExpression); + if (is_string($this->templateNameExpression)) { + $context->getParseContext()->loadPartial($this->templateNameExpression); + } }); return $this; @@ -99,9 +104,18 @@ public function render(RenderContext $context): string public function stream(RenderContext $context): \Generator { - $partial = $context->loadPartial($this->templateNameExpression); + $templateName = match (true) { + is_string($this->templateNameExpression) => $this->templateNameExpression, + $this->templateNameExpression instanceof VariableLookup => $this->templateNameExpression->evaluate($context), + }; + + if (! is_string($templateName)) { + throw new SyntaxException('Template name must be a string'); + } + + $partial = $context->loadPartial($templateName); - $contextVariableName = $this->aliasName ?? Arr::last(explode('/', $this->templateNameExpression)); + $contextVariableName = $this->aliasName ?? Arr::last(explode('/', $templateName)); assert(is_string($contextVariableName)); $variable = $this->variableNameExpression ? $context->evaluate($this->variableNameExpression) : null; @@ -110,7 +124,7 @@ public function stream(RenderContext $context): \Generator $variable = $variable instanceof Traversable ? iterator_to_array($variable) : $variable; assert(is_array($variable)); - $forLoop = new ForLoopDrop($this->templateNameExpression, count($variable)); + $forLoop = new ForLoopDrop($templateName, count($variable)); foreach ($variable as $value) { $partialContext = $this->buildPartialContext($partial, $context, [ diff --git a/src/Template.php b/src/Template.php index 8b18ee3..536ef99 100644 --- a/src/Template.php +++ b/src/Template.php @@ -36,8 +36,8 @@ public static function parse(ParseContext $parseContext, string $source, ?string ); if (! $parseContext->isPartial()) { - $template->state->partialsCache = $parseContext->getPartialsCache()->all(); - $template->state->outputs = $parseContext->getOutputs()->all(); + $template->state->partialsCache = $parseContext->getPartialsCache(); + $template->state->outputs = $parseContext->getOutputs(); } return $template; @@ -70,7 +70,7 @@ public function render(RenderContext $context): string throw $e; } finally { $this->state->errors = $context->getErrors(); - $this->state->outputs = $context->getOutputs()->all(); + $this->state->outputs = $context->getOutputs(); } } @@ -91,7 +91,7 @@ public function stream(RenderContext $context): \Generator throw $e; } finally { $this->state->errors = $context->getErrors(); - $this->state->outputs = $context->getOutputs()->all(); + $this->state->outputs = $context->getOutputs(); } } diff --git a/src/TemplateFactory.php b/src/TemplateFactory.php index 6b47883..3a5766c 100644 --- a/src/TemplateFactory.php +++ b/src/TemplateFactory.php @@ -28,6 +28,8 @@ final class TemplateFactory protected bool $strictVariables = false; + protected bool $allowDynamicPartials = false; + public function __construct() { $this->tagRegistry = $this->buildTagRegistry(); @@ -111,6 +113,18 @@ public function getStrictVariables(): bool return $this->strictVariables; } + public function setAllowDynamicPartials(bool $allowDynamicPartials = true): TemplateFactory + { + $this->allowDynamicPartials = $allowDynamicPartials; + + return $this; + } + + public function getAllowDynamicPartials(): bool + { + return $this->allowDynamicPartials; + } + /** * Enable/disabled rethrowExceptions and strictVariables. */ @@ -125,6 +139,7 @@ public function setDebugMode(bool $debugMode = true): TemplateFactory public function newParseContext(): ParseContext { return new ParseContext( + allowDynamicPartials: $this->allowDynamicPartials, tagRegistry: $this->tagRegistry, fileSystem: $this->fileSystem, ); @@ -158,10 +173,12 @@ public function newRenderContext( registers: $registers, rethrowExceptions: $this->rethrowExceptions, strictVariables: $this->strictVariables, + allowDynamicPartials: $this->allowDynamicPartials, profile: $this->profile, filterRegistry: $this->filterRegistry, resourceLimits: $this->resourceLimits, fileSystem: $this->fileSystem, + tagRegistry: $this->tagRegistry, ); } diff --git a/src/TemplateSharedState.php b/src/TemplateSharedState.php index 3f2311e..f2b557c 100644 --- a/src/TemplateSharedState.php +++ b/src/TemplateSharedState.php @@ -2,20 +2,18 @@ namespace Keepsuit\Liquid; +use Keepsuit\Liquid\Support\OutputsBag; +use Keepsuit\Liquid\Support\PartialsCache; + class TemplateSharedState { - /** - * @var array<\Throwable> - */ - public array $errors = []; - - /** - * @var array - */ - public array $partialsCache = []; - - /** - * @var array - */ - public array $outputs = []; + public function __construct( + /** + * @var array<\Throwable> + */ + public array $errors = [], + public PartialsCache $partialsCache = new PartialsCache(), + public OutputsBag $outputs = new OutputsBag(), + ) { + } } diff --git a/tests/Integration/Tags/RenderTagTest.php b/tests/Integration/Tags/RenderTagTest.php index d84ef0d..f95879d 100644 --- a/tests/Integration/Tags/RenderTagTest.php +++ b/tests/Integration/Tags/RenderTagTest.php @@ -75,6 +75,11 @@ ->toThrow(SyntaxException::class); }); +test('dynamically template name', function () { + expect(renderTemplate("{% assign name = 'snippet' %}{% render name %}", partials: ['snippet' => 'echo'], allowDynamicTemplates: true)) + ->toBe('echo'); +}); + test('render tag caches second read of some partial', function () { $factory = TemplateFactory::new() ->setFilesystem($fileSystem = new StubFileSystem(['snippet' => 'echo'])); diff --git a/tests/Pest.php b/tests/Pest.php index 265e0c1..c32b95f 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -50,12 +50,14 @@ function renderTemplate( array $partials = [], bool $renderErrors = false, bool $strictVariables = false, + bool $allowDynamicTemplates = false, TemplateFactory $factory = new TemplateFactory() ): string { $factory = $factory ->setFilesystem(new StubFileSystem(partials: $partials)) ->setRethrowExceptions(! $renderErrors) - ->setStrictVariables($strictVariables); + ->setStrictVariables($strictVariables) + ->setAllowDynamicPartials($allowDynamicTemplates); $template = $factory->parseString($template);