Skip to content
Merged
55 changes: 46 additions & 9 deletions src/BladeRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
namespace Livewire\Blaze;

use Illuminate\Contracts\View\Factory;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\File;
use Illuminate\View\Compilers\BladeCompiler;
use Illuminate\View\Component;
use Illuminate\View\ComponentSlot;
use Livewire\Blaze\Parser\Attribute;
use Livewire\Blaze\Parser\Nodes\ComponentNode;
use Livewire\Blaze\Parser\Nodes\SlotNode;
use Livewire\Blaze\Runtime\BlazeRuntime;
use Livewire\Blaze\Support\ComponentSource;
use Livewire\Blaze\Support\Utils;
use ReflectionClass;

/**
Expand All @@ -32,7 +39,7 @@ public function getTemporaryCachePath(): string
/**
* Render a Blade template string in isolation by freezing and restoring compiler state.
*/
public function render(string $template): string
public function render(ComponentNode $component, ComponentSource $source): string
{
$temporaryCachePath = $this->getTemporaryCachePath();

Expand Down Expand Up @@ -85,21 +92,51 @@ function ($input) {
'slotsStack' => [],
]);

$restoreComponent = $this->freezeObjectProperties(Component::class, [
'bladeViewCache' => [],
]);
$obLevel = ob_get_level();
$hash = Utils::hash($source->path);
$path = $temporaryCachePath . '/' . $hash . '.php';
$fn = '__' . $hash;

$this->manager->startFolding();

try {
$this->manager->startFolding();
if (! file_exists($path)) {
$this->blade->compile($source->path);
}

$attributes = Arr::mapWithKeys($component->attributes, function (Attribute $attribute) {
return [$attribute->name => $attribute->getStaticValue()];
});

$slots = Arr::mapWithKeys($component->children, function (SlotNode $slot) {
return [$slot->name => new ComponentSlot($slot->content())];
});

$this->runtime->pushData($attributes);
$this->runtime->pushSlots($slots);

ob_start();

require_once $path;

$result = $this->blade->render($template, deleteCachedView: true);
$fn(
__blaze: $this->runtime,
__data: $attributes,
__slots: $slots,
);

$result = ltrim(ob_get_clean());
} finally {
while (ob_get_level() > $obLevel) {
ob_end_clean();
}

$this->runtime->popData();
$this->manager->stopFolding();

$restoreCompiler();
$restoreFactory();
$restoreRuntime();
$restoreComponent();

$this->manager->stopFolding();
}

$result = Unblaze::replaceUnblazePrecompiledDirectives($result);
Expand Down
8 changes: 1 addition & 7 deletions src/BlazeManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,6 @@ public function compile(string $template, ?string $path = null): string

$output = $this->blade->restoreRawBlocks($output);

try {
$this->renderer->deleteTemporaryCacheDirectory();
} catch (\Throwable $e) {
//
}

return $output;
}

Expand Down Expand Up @@ -420,7 +414,7 @@ protected function hasAwareDescendant(ComponentNode|SlotNode $node): bool
{
foreach ($node->children as $child) {
if ($child instanceof ComponentNode) {
$source = new ComponentSource($this->blade->componentNameToPath($child->name));
$source = ComponentSource::for($this->blade->componentNameToPath($child->name));

if (str_ends_with($child->name, 'delegate-component')) {
return true;
Expand Down
4 changes: 4 additions & 0 deletions src/BlazeServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public function register(): void
$this->app->singleton(Profiler::class);
$this->app->singleton(BlazeManager::class);

$this->app->singleton(\PhpParser\Parser::class, function () {
return (new \PhpParser\ParserFactory)->createForNewestSupportedVersion();
});

$this->app->alias(BlazeManager::class, Blaze::class);

$this->app->alias(BlazeManager::class, 'blaze');
Expand Down
5 changes: 2 additions & 3 deletions src/Compiler/ArrayParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PhpParser\Node\Scalar\Int_;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Parser;
use PhpParser\ParserFactory;

/**
Expand Down Expand Up @@ -64,10 +65,8 @@ public static function parse(string $expression): array
*/
protected static function parseToArrayNode(string $expression): Array_
{
$parser = (new ParserFactory)->createForNewestSupportedVersion();

try {
$ast = $parser->parse('<?php ' . $expression . ';');
$ast = app(Parser::class)->parse('<?php ' . $expression . ';');
} catch (\Throwable $e) {
throw new ArrayParserException($expression, $e->getMessage());
}
Expand Down
2 changes: 1 addition & 1 deletion src/Compiler/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function compile(Node $node): Node
return new TextNode($this->compileDelegateComponentTag($node));
}

$source = new ComponentSource($this->blade->componentNameToPath($node->name));
$source = ComponentSource::for($this->blade->componentNameToPath($node->name));

if (! $source->exists()) {
return $node;
Expand Down
2 changes: 1 addition & 1 deletion src/Compiler/Profiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function __construct(
*/
public function profile(Node $node, string $componentName, ?string $strategy = null): Node
{
$source = new ComponentSource($this->blade->componentNameToPath($componentName));
$source = ComponentSource::for($this->blade->componentNameToPath($componentName));

if ($strategy === null) {
$isBlade = $node instanceof ComponentNode;
Expand Down
6 changes: 2 additions & 4 deletions src/Compiler/UseExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use PhpParser\Node\Stmt\GroupUse;
use PhpParser\Node\Stmt\Use_;
use PhpParser\ParserFactory;
use PhpParser\Parser;

/**
* Extracts use statements from raw PHP blocks in compiled templates.
Expand All @@ -24,10 +24,8 @@ public function extract(string $compiled, callable $callback): string
$inner = $isDirective ? $match[2] : $match[1];
$block = '<?php' . $inner;

$parser = (new ParserFactory)->createForNewestSupportedVersion();

try {
$ast = $parser->parse($block);
$ast = app(Parser::class)->parse($block);
} catch (\Throwable) {
return $match[0];
}
Expand Down
13 changes: 7 additions & 6 deletions src/Folder/Foldable.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Foldable
{
protected array $attributeByPlaceholder = [];
protected array $slotByPlaceholder = [];
protected int $placeholderIndex = 0;

protected ComponentNode $renderable;
protected string $html;
Expand Down Expand Up @@ -51,7 +52,7 @@ public function fold(): string
$this->setupSlots();
$this->mergeAwareProps();

$this->html = $this->renderer->render($this->renderable->render());
$this->html = $this->renderer->render($this->renderable, $this->source);

$this->processUncompiledAttributes();
$this->restorePlaceholders();
Expand All @@ -67,7 +68,7 @@ protected function setupAttributes(): void
{
foreach ($this->node->attributes as $key => $attribute) {
if (! $attribute->isStaticValue()) {
$placeholder = 'BLAZE_PLACEHOLDER_' . strtoupper(str()->random());
$placeholder = 'BLAZE_PLACEHOLDER_' . $this->placeholderIndex++ . '_';

$this->attributeByPlaceholder[$placeholder] = $attribute;

Expand Down Expand Up @@ -95,7 +96,7 @@ protected function setupSlots(): void

foreach ($this->node->children as $child) {
if ($child instanceof SlotNode) {
$placeholder = 'BLAZE_PLACEHOLDER_' . strtoupper(str()->random());
$placeholder = 'BLAZE_PLACEHOLDER_' . $this->placeholderIndex++ . '_';

$this->slotByPlaceholder[$placeholder] = $child;

Expand All @@ -115,7 +116,7 @@ protected function setupSlots(): void

// Synthesize a default slot from loose content when there's not an explicit one
if ($looseContent && ! isset($slots['slot'])) {
$placeholder = 'BLAZE_PLACEHOLDER_' . strtoupper(str()->random());
$placeholder = 'BLAZE_PLACEHOLDER_' . $this->placeholderIndex++ . '_';

$defaultSlot = new SlotNode(
name: 'slot',
Expand Down Expand Up @@ -160,7 +161,7 @@ protected function mergeAwareProps(): void
$attribute = $this->node->parentsAttributes[$prop];

if (! $attribute->isStaticValue()) {
$placeholder = 'BLAZE_PLACEHOLDER_' . strtoupper(str()->random());
$placeholder = 'BLAZE_PLACEHOLDER_' . $this->placeholderIndex++ . '_';

$this->attributeByPlaceholder[$placeholder] = $attribute;

Expand Down Expand Up @@ -201,7 +202,7 @@ protected function mergeAwareProps(): void
*/
protected function processUncompiledAttributes(): void
{
$this->html = preg_replace_callback('/\[BLAZE_ATTR:(BLAZE_PLACEHOLDER_[A-Z0-9]+)\](\r?\n)?/', function ($matches) {
$this->html = preg_replace_callback('/\[BLAZE_ATTR:(BLAZE_PLACEHOLDER_[0-9]+_)\](\r?\n)?/', function ($matches) {
$attribute = $this->attributeByPlaceholder[$matches[1]];

if ($attribute->bound()) {
Expand Down
9 changes: 5 additions & 4 deletions src/Folder/Folder.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Livewire\Blaze\BlazeManager;
use Illuminate\Support\Arr;
use Livewire\Blaze\Config;
use Throwable;

/**
* Determines whether a component should be folded and orchestrates the folding process.
Expand All @@ -40,7 +41,7 @@ public function fold(Node $node): Node

$component = $node;

$source = new ComponentSource($this->blade->componentNameToPath($component->name));
$source = ComponentSource::for($this->blade->componentNameToPath($component->name));

if (! $source->exists()) {
return $component;
Expand Down Expand Up @@ -68,9 +69,9 @@ public function fold(Node $node): Node
));

return new TextNode('<?php ob_start(); ?>' . $html . '<?php echo ltrim(ob_get_clean()); ?>');
} catch (\Exception $e) {
} catch (Throwable $th) {
if ($this->manager->shouldThrow()) {
throw $e;
throw $th;
}

return $node;
Expand Down Expand Up @@ -179,7 +180,7 @@ protected function slotHasDynamicAttributes(SlotNode $slot): bool
protected function checkProblematicPatterns(ComponentSource $source): void
{
// @unblaze blocks can contain dynamic content and are excluded from validation
$sourceWithoutUnblaze = preg_replace('/@unblaze.*?@endunblaze/s', '', $source->content);
$sourceWithoutUnblaze = preg_replace('/@unblaze.*?@endunblaze/s', '', $source->content());

$problematicPatterns = [
'@once' => 'forOnce',
Expand Down
2 changes: 1 addition & 1 deletion src/Memoizer/Memoizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ protected function isMemoizable(Node $node): bool
return false;
}

$source = new ComponentSource($this->blade->componentNameToPath($node->name));
$source = ComponentSource::for($this->blade->componentNameToPath($node->name));

if (! $source->exists()) {
return false;
Expand Down
14 changes: 14 additions & 0 deletions src/Parser/Attribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,18 @@ public function isStaticValue(): bool
{
return $this->dynamic === false || in_array($this->value, ['true', 'false', 'null'], true);
}

public function getStaticValue(): mixed
{
if (! $this->isStaticValue()) {
throw new \LogicException("Cannot get static value of dynamic attribute '{$this->name}'.");
}

return match ($this->value) {
'true' => true,
'false' => false,
'null' => null,
default => $this->value,
};
}
}
2 changes: 1 addition & 1 deletion src/Runtime/BlazeAttributeBag.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ public function __toString()

$attr = $key.'="'.str_replace('"', '\\"', trim($value)).'"';

if (Str::match('/^BLAZE_PLACEHOLDER_[A-Z0-9]+$/', $value)) {
if (Str::match('/^BLAZE_PLACEHOLDER_[0-9]+_$/', $value)) {
$string .= ' [BLAZE_ATTR:'.$value.']';
} else {
$string .= ' '.$attr;
Expand Down
25 changes: 21 additions & 4 deletions src/Support/ComponentSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,43 @@
namespace Livewire\Blaze\Support;

/**
* Resolves and caches a component's file path, content, and directive metadata.
* Resolves and caches a component's file path and directive metadata.
*/
class ComponentSource
{
/** @var array<string, static> */
protected static array $cache = [];

public readonly string $path;
public readonly string $content;
public readonly Directives $directives;

public function __construct(string $path)
{
$this->path = $path;
$this->content = $this->exists() ? file_get_contents($this->path) : '';
$this->directives = new Directives($this->content);
$this->directives = new Directives($this->exists() ? $this->content() : '');
}

/**
* Get a cached instance for a given path, or create one.
*/
public static function for(string $path): static
{
return static::$cache[$path] ??= new static($path);
}

/**
* Check if the component file exists on disk.
*/
public function exists(): bool
{
return file_exists($this->path);
}

/**
* Get the raw source content of the component file.
*/
public function content(): string
{
return file_get_contents($this->path);
}
}
Loading