Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Streaming #11

Merged
merged 4 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions performance/CompiledThemeTestTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ public function render(array $assigns = []): void
}
}

public function stream(array $assigns = []): void
{
$content = $this->template->stream($this->buildContext($assigns));

if ($this->layout) {
$content = $this->layout->stream($this->buildContext([
...$assigns,
'content_for_layout' => $content,
]));
}

while ($content->valid()) {
$content->next();
}
}

protected function buildContext(array $assigns = []): RenderContext
{
return $this->factory->newRenderContext(
Expand Down
23 changes: 22 additions & 1 deletion performance/ThemeRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,32 @@ public function render(): void
}
}

public function stream(): void
{
$database = [...Database::tables()];

foreach ($this->compiledTemplates as $compiled) {
$compiled->stream($database);
}
}

public function run(): void
{
$database = [...Database::tables()];

foreach ($this->tests as $test) {
$compiled = $test->compile();
$compiled->render($database);
}
}

public function runStreaming(): void
{
$database = [...Database::tables()];

foreach ($this->tests as $test) {
$compiled = $test->compile();
$compiled->render();
$compiled->stream($database);
}
}

Expand Down
10 changes: 10 additions & 0 deletions performance/benchmarks/LiquidBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,21 @@ public function benchRender(): void
$this->themeRunner->render();
}

public function benchStream(): void
{
$this->themeRunner->stream();
}

public function benchParsingAndRendering(): void
{
$this->themeRunner->run();
}

public function benchParsingAndStreaming(): void
{
$this->themeRunner->runStreaming();
}

protected function getThemeRunner(): ThemeRunner
{
$templateFactory = TemplateFactory::new()
Expand Down
13 changes: 13 additions & 0 deletions src/Contracts/CanBeStreamed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Keepsuit\Liquid\Contracts;

use Keepsuit\Liquid\Render\RenderContext;

interface CanBeStreamed extends CanBeRendered
{
/**
* @return \Generator<string>
*/
public function stream(RenderContext $context): \Generator;
}
51 changes: 50 additions & 1 deletion src/Nodes/BodyNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

namespace Keepsuit\Liquid\Nodes;

use Keepsuit\Liquid\Contracts\CanBeStreamed;
use Keepsuit\Liquid\Exceptions\LiquidException;
use Keepsuit\Liquid\Exceptions\UndefinedDropMethodException;
use Keepsuit\Liquid\Exceptions\UndefinedFilterException;
use Keepsuit\Liquid\Exceptions\UndefinedVariableException;
use Keepsuit\Liquid\Render\RenderContext;
use Keepsuit\Liquid\Tag;

class BodyNode extends Node
class BodyNode extends Node implements CanBeStreamed
{
public function __construct(
/** @var array<Node> */
Expand Down Expand Up @@ -74,6 +75,34 @@ public function render(RenderContext $context): string
return $output;
}

public function stream(RenderContext $context): \Generator
{
$context->resourceLimits->incrementRenderScore(count($this->children));

foreach ($this->children as $node) {
try {
if ($node instanceof Tag) {
$node->ensureTagIsEnabled($context);
}

foreach ($this->streamChild($context, $node) as $output) {
$context->resourceLimits->incrementWriteScore($output);
yield $output;
}
} catch (UndefinedVariableException|UndefinedDropMethodException|UndefinedFilterException $exception) {
$context->handleError($exception, $node->lineNumber);
} catch (\Throwable $exception) {
$output = $context->handleError($exception, $node->lineNumber);
$context->resourceLimits->incrementWriteScore($output);
yield $output;
}

if ($context->hasInterrupt()) {
break;
}
}
}

protected function renderChild(RenderContext $context, Node $node): string
{
if ($context->getProfiler() !== null) {
Expand All @@ -87,6 +116,26 @@ protected function renderChild(RenderContext $context, Node $node): string
return $node->render($context);
}

/**
* @return \Generator<string>
*/
public function streamChild(RenderContext $context, Node $node): \Generator
{
if ($context->getProfiler() !== null) {
yield $this->renderChild($context, $node);

return;
}

if ($node instanceof CanBeStreamed) {
yield from $node->stream($context);

return;
}

yield $node->render($context);
}

public function blank(): bool
{
foreach ($this->children as $node) {
Expand Down
17 changes: 16 additions & 1 deletion src/Nodes/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
namespace Keepsuit\Liquid\Nodes;

use Keepsuit\Liquid\Contracts\CanBeRendered;
use Keepsuit\Liquid\Contracts\CanBeStreamed;
use Keepsuit\Liquid\Exceptions\LiquidException;
use Keepsuit\Liquid\Render\RenderContext;

class Document implements CanBeRendered
class Document implements CanBeRendered, CanBeStreamed
{
public function __construct(
protected BodyNode $body,
Expand All @@ -29,6 +30,20 @@ public function render(RenderContext $context): string
return $this->body->render($context);
}

/**
* @throws LiquidException
*/
public function stream(RenderContext $context): \Generator
{
if ($context->getProfiler() !== null) {
yield $this->render($context);

return;
}

yield from $this->body->stream($context);
}

/**
* @return array<Node>
*/
Expand Down
77 changes: 64 additions & 13 deletions src/Nodes/Variable.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Keepsuit\Liquid\Contracts\CanBeEvaluated;
use Keepsuit\Liquid\Contracts\CanBeRendered;
use Keepsuit\Liquid\Contracts\CanBeStreamed;
use Keepsuit\Liquid\Contracts\HasParseTreeVisitorChildren;
use Keepsuit\Liquid\Parse\ExpressionParser;
use Keepsuit\Liquid\Render\RenderContext;
Expand All @@ -12,7 +13,7 @@
/**
* @phpstan-import-type Expression from ExpressionParser
*/
class Variable extends Node implements CanBeEvaluated, HasParseTreeVisitorChildren
class Variable extends Node implements CanBeEvaluated, CanBeStreamed, HasParseTreeVisitorChildren
{
public function __construct(
/** @var Expression $name */
Expand All @@ -30,27 +31,40 @@ public function render(RenderContext $context): string
return $output->render($context);
}

if (is_array($output)) {
return implode('', $output);
}
return $this->renderOutput($output);
}

if ($output === null) {
return '';
public function stream(RenderContext $context): \Generator
{
if ($this->filters !== []) {
yield $this->render($context);

return;
}

if (is_bool($output)) {
return $output ? 'true' : 'false';
$output = $this->evaluate($context);

if ($output instanceof CanBeStreamed) {
yield from $output->stream($context);

return;
}

if (is_string($output) || is_numeric($output)) {
return (string) $output;
if ($output instanceof CanBeRendered) {
yield $output->render($context);

return;
}

if (is_object($output) && method_exists($output, '__toString')) {
return (string) $output;
if ($output instanceof \Generator) {
foreach ($output as $chunk) {
yield $this->renderOutput($chunk);
}

return;
}

return '';
yield $this->renderOutput($output);
}

public function parseTreeVisitorChildren(): array
Expand All @@ -62,6 +76,10 @@ public function evaluate(RenderContext $context): mixed
{
$output = $context->evaluate($this->name);

if ($this->filters === []) {
return $output;
}

if ($output instanceof \Generator) {
$output = iterator_to_array($output);
}
Expand All @@ -75,6 +93,39 @@ public function evaluate(RenderContext $context): mixed
return $output;
}

protected function renderOutput(mixed $output): string
{
if (is_string($output)) {
return $output;
}

if ($output === null) {
return '';
}

if (is_bool($output)) {
return $output ? 'true' : 'false';
}

if (is_numeric($output)) {
return (string) $output;
}

if ($output instanceof \Generator) {
$output = iterator_to_array($output);
}

if (is_array($output)) {
return implode('', $output);
}

if (is_object($output) && method_exists($output, '__toString')) {
return (string) $output;
}

return '';
}

protected static function evaluateFilterExpressions(RenderContext $context, array $filterArgs): array
{
return array_map(
Expand Down
21 changes: 16 additions & 5 deletions src/Tags/RenderTag.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Keepsuit\Liquid\Tags;

use Keepsuit\Liquid\Contracts\CanBeStreamed;
use Keepsuit\Liquid\Contracts\HasParseTreeVisitorChildren;
use Keepsuit\Liquid\Drops\ForLoopDrop;
use Keepsuit\Liquid\Exceptions\SyntaxException;
Expand All @@ -18,7 +19,7 @@
/**
* @phpstan-import-type Expression from ExpressionParser
*/
class RenderTag extends Tag implements HasParseTreeVisitorChildren
class RenderTag extends Tag implements CanBeStreamed, HasParseTreeVisitorChildren
{
protected string $templateNameExpression;

Expand Down Expand Up @@ -86,6 +87,17 @@ public function parse(TagParseContext $context): static
}

public function render(RenderContext $context): string
{
$output = '';

foreach ($this->stream($context) as $chunk) {
$output .= $chunk;
}

return $output;
}

public function stream(RenderContext $context): \Generator
{
$partial = $context->loadPartial($this->templateNameExpression);

Expand All @@ -100,26 +112,25 @@ public function render(RenderContext $context): string

$forLoop = new ForLoopDrop($this->templateNameExpression, count($variable));

$output = '';
foreach ($variable as $value) {
$partialContext = $this->buildPartialContext($partial, $context, [
'forloop' => $forLoop,
$contextVariableName => $value,
]);

$output .= $partial->render($partialContext);
yield from $partial->stream($partialContext);

$forLoop->increment();
}

return $output;
return;
}

$partialContext = $this->buildPartialContext($partial, $context, [
$contextVariableName => $variable,
]);

return $partial->render($partialContext);
yield from $partial->stream($partialContext);
}

public function parseTreeVisitorChildren(): array
Expand Down
Loading