From 10f534dd13bd9ba562ee6413057eeaf7ef7f862d Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sat, 28 Feb 2026 11:25:47 +0100 Subject: [PATCH 01/28] Refactor Utils calls --- src/Folder/Foldable.php | 5 ++--- src/Support/AttributeParser.php | 2 +- src/Support/ComponentSource.php | 4 +++- src/Support/Utils.php | 17 ----------------- 4 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/Folder/Foldable.php b/src/Folder/Foldable.php index 8ee5c379..93b686ea 100644 --- a/src/Folder/Foldable.php +++ b/src/Folder/Foldable.php @@ -9,7 +9,6 @@ use Livewire\Blaze\Parser\Nodes\SlotNode; use Livewire\Blaze\Parser\Nodes\TextNode; use Livewire\Blaze\Support\ComponentSource; -use Livewire\Blaze\Support\Utils; /** * Performs compile-time folding of a component by rendering it with placeholder substitution. @@ -224,7 +223,7 @@ protected function restorePlaceholders(): void $content = $match[0]; foreach ($this->attributeByPlaceholder as $placeholder => $attribute) { - $value = $attribute->bound() ? $attribute->value : Utils::compileAttributeEchos($attribute->value); + $value = $attribute->bound() ? $attribute->value : BladeService::compileAttributeEchos($attribute->value); $content = str_replace("'" . $placeholder . "'", $value, $content); } @@ -263,7 +262,7 @@ protected function wrapWithAwareMacros(): void if ($attribute->bound()) { $data[] = var_export($attribute->propName, true).' => '.$attribute->value; } else { - $data[] = var_export($attribute->propName, true).' => '.Utils::compileAttributeEchos($attribute->value); + $data[] = var_export($attribute->propName, true).' => '.BladeService::compileAttributeEchos($attribute->value); } } diff --git a/src/Support/AttributeParser.php b/src/Support/AttributeParser.php index 546d044d..1183b23b 100644 --- a/src/Support/AttributeParser.php +++ b/src/Support/AttributeParser.php @@ -93,7 +93,7 @@ public function parseAttributesArrayToRuntimeArrayString(array $attributes): str // Blade echo syntax (e.g. {{ $order->avatar }} or {!! $rawHtml !!}) must be compiled // to a PHP expression so the runtime value is used (not the literal template string). // This is critical for memoization keys to be unique per evaluated value. - $arrayParts[] = "'".addslashes($attributeName)."' => ".Utils::compileAttributeEchos($attr->value); + $arrayParts[] = "'".addslashes($attributeName)."' => ".BladeService::compileAttributeEchos($attr->value); continue; } diff --git a/src/Support/ComponentSource.php b/src/Support/ComponentSource.php index 742e25e8..e3bad0da 100644 --- a/src/Support/ComponentSource.php +++ b/src/Support/ComponentSource.php @@ -2,6 +2,8 @@ namespace Livewire\Blaze\Support; +use Livewire\Blaze\BladeService; + /** * Resolves and caches a component's file path, content, and directive metadata. */ @@ -15,7 +17,7 @@ class ComponentSource public function __construct($name) { $this->name = $name; - $this->path = Utils::componentNameToPath($name); + $this->path = BladeService::componentNameToPath($name); $this->content = $this->exists() ? file_get_contents($this->path) : ''; $this->directives = new Directives($this->content); } diff --git a/src/Support/Utils.php b/src/Support/Utils.php index 4faa386b..5ae73d3e 100644 --- a/src/Support/Utils.php +++ b/src/Support/Utils.php @@ -2,7 +2,6 @@ namespace Livewire\Blaze\Support; -use Livewire\Blaze\BladeService; use Livewire\Blaze\Directive\BlazeDirective; use Livewire\Blaze\Parser\Attribute; @@ -11,22 +10,6 @@ */ class Utils { - /** - * Resolve a component name to its file path. - */ - public static function componentNameToPath(string $name): string - { - return BladeService::componentNameToPath($name); - } - - /** - * Compile Blade echo syntax within an attribute value. - */ - public static function compileAttributeEchos(string $value): string - { - return BladeService::compileAttributeEchos($value); - } - /** * Parse a @blaze directive expression into its parameters. */ From c5b2a5bc2027a78e5a38f570fb6299f77277fa1b Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sat, 28 Feb 2026 12:14:24 +0100 Subject: [PATCH 02/28] Remove parseAttributesArrayToRuntimeArrayString --- src/Memoizer/Memoizer.php | 12 ++++++++- src/Parser/Nodes/ComponentNode.php | 13 ---------- src/Support/AttributeParser.php | 41 ------------------------------ 3 files changed, 11 insertions(+), 55 deletions(-) diff --git a/src/Memoizer/Memoizer.php b/src/Memoizer/Memoizer.php index 5e498ae7..9dce8b75 100644 --- a/src/Memoizer/Memoizer.php +++ b/src/Memoizer/Memoizer.php @@ -2,6 +2,7 @@ namespace Livewire\Blaze\Memoizer; +use Livewire\Blaze\BladeService; use Livewire\Blaze\Parser\Nodes\ComponentNode; use Livewire\Blaze\Parser\Nodes\TextNode; use Livewire\Blaze\Parser\Nodes\Node; @@ -38,7 +39,16 @@ public function memoize(Node $node): Node } $name = $node->name; - $attributes = $node->getAttributesAsRuntimeArrayString(); + + $parts = []; + foreach ($node->attributes as $attr) { + if ($attr->bound()) { + $parts[] = "'{$attr->propName}' => {$attr->value}"; + } else { + $parts[] = "'{$attr->propName}' => ".BladeService::compileAttributeEchos($attr->value); + } + } + $attributes = '['.implode(', ', $parts).']'; $compiled = $this->compiler->compile($node)->render(); diff --git a/src/Parser/Nodes/ComponentNode.php b/src/Parser/Nodes/ComponentNode.php index cfd6ef74..1a65fbc8 100644 --- a/src/Parser/Nodes/ComponentNode.php +++ b/src/Parser/Nodes/ComponentNode.php @@ -2,7 +2,6 @@ namespace Livewire\Blaze\Parser\Nodes; -use Livewire\Blaze\Support\AttributeParser; use Livewire\Blaze\Support\Utils; use Livewire\Blaze\Parser\Attribute; @@ -88,18 +87,6 @@ public function render(): string return $output; } - /** - * Convert attributes to a PHP array string for runtime evaluation. - */ - public function getAttributesAsRuntimeArrayString(): string - { - $attributeParser = new AttributeParser; - - $attributesArray = $attributeParser->parseAttributeStringToArray($this->attributeString); - - return $attributeParser->parseAttributesArrayToRuntimeArrayString($attributesArray); - } - /** * Strip the namespace prefix from a component name for tag rendering. */ diff --git a/src/Support/AttributeParser.php b/src/Support/AttributeParser.php index 1183b23b..cccaab44 100644 --- a/src/Support/AttributeParser.php +++ b/src/Support/AttributeParser.php @@ -79,46 +79,5 @@ public function parseAttributeStringToArray(string $attributesString): array return $attributes; } - /** - * Convert parsed attributes into a PHP array string for runtime evaluation. - * - * @param array $attributes - */ - public function parseAttributesArrayToRuntimeArrayString(array $attributes): string - { - $arrayParts = []; - - foreach ($attributes as $attributeName => $attr) { - if ($attr->dynamic && is_string($attr->value) && (str_contains($attr->value, '{{') || str_contains($attr->value, '{!!'))) { - // Blade echo syntax (e.g. {{ $order->avatar }} or {!! $rawHtml !!}) must be compiled - // to a PHP expression so the runtime value is used (not the literal template string). - // This is critical for memoization keys to be unique per evaluated value. - $arrayParts[] = "'".addslashes($attributeName)."' => ".BladeService::compileAttributeEchos($attr->value); - - continue; - } - - if ($attr->dynamic) { - $arrayParts[] = "'".addslashes($attributeName)."' => ".$attr->value; - continue; - } - - $value = $attr->value; - - if (is_bool($value)) { - $valueString = $value ? 'true' : 'false'; - } elseif (is_string($value)) { - $valueString = "'".addslashes($value)."'"; - } elseif (is_null($value)) { - $valueString = 'null'; - } else { - $valueString = (string) $value; - } - - $arrayParts[] = "'".addslashes($attributeName)."' => ".$valueString; - } - - return '['.implode(', ', $arrayParts).']'; - } } From 6c77b28ab08a497da62dfe0f9dc1417bcd72ecf7 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sat, 28 Feb 2026 13:57:13 +0100 Subject: [PATCH 03/28] Add DirectiveCompiler --- src/BladeService.php | 75 +---------------------- src/Compiler/DirectiveCompiler.php | 97 ++++++++++++++++++++++++++++++ src/Compiler/Wrapper.php | 5 +- src/Support/Directives.php | 16 ++--- src/Support/LaravelRegex.php | 31 ++++++++-- src/Unblaze.php | 24 ++++---- 6 files changed, 149 insertions(+), 99 deletions(-) create mode 100644 src/Compiler/DirectiveCompiler.php diff --git a/src/BladeService.php b/src/BladeService.php index 746f8060..40d9c7e3 100644 --- a/src/BladeService.php +++ b/src/BladeService.php @@ -2,12 +2,12 @@ namespace Livewire\Blaze; -use Illuminate\Support\Arr; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\File; use Illuminate\Support\Str; use Illuminate\View\Compilers\ComponentTagCompiler; use ReflectionClass; +use Livewire\Blaze\Compiler\DirectiveCompiler; use Livewire\Blaze\Support\Utils; class BladeService @@ -20,75 +20,6 @@ public static function render(string $template): string return static::isolatedRender($template); } - /** - * Compile a single directive within a template using a sandboxed Blade compiler. - */ - public static function compileDirective(string $template, string $directive, callable $callback) - { - // Protect raw block placeholders so restoreRawContent doesn't resolve them - $template = preg_replace('/@__raw_block_(\d+)__@/', '__BLAZE_RAW_BLOCK_$1__', $template); - - $compiler = static::getHackedBladeCompiler(); - - $compiler->directive($directive, $callback); - - $result = $compiler->compileStatementsMadePublic($template); - - return preg_replace('/__BLAZE_RAW_BLOCK_(\d+)__/', '@__raw_block_$1__@', $result); - } - - /** - * Create a BladeCompiler that only processes custom directives, ignoring built-in ones. - */ - public static function getHackedBladeCompiler() - { - $instance = new class(app('files'), config('view.compiled')) extends \Illuminate\View\Compilers\BladeCompiler - { - public function compileStatementsMadePublic($template) - { - $result = ''; - - foreach (token_get_all($template) as $token) { - if (! is_array($token)) { - $result .= $token; - - continue; - } - - [$id, $content] = $token; - - if ($id == T_INLINE_HTML) { - $result .= $this->compileStatements($content); - } else { - $result .= $content; - } - } - - return $result; - } - - /** - * Only process custom directives, skip built-in ones. - */ - protected function compileStatement($match) - { - if (str_contains($match[1], '@')) { - $match[0] = isset($match[3]) ? $match[1].$match[3] : $match[1]; - } elseif (isset($this->customDirectives[$match[1]])) { - $match[0] = $this->callCustomDirective($match[1], Arr::get($match, 3)); - } elseif (method_exists($this, $method = 'compile'.ucfirst($match[1]))) { - return $match[0]; - } else { - return $match[0]; - } - - return isset($match[3]) ? $match[0] : $match[0].$match[2]; - } - }; - - return $instance; - } - /** * Get the temporary cache directory path used during isolated rendering. */ @@ -323,14 +254,14 @@ public static function preprocessAttributeString(string $attributeString): strin public static function compileUseStatements(string $input): string { - return static::compileDirective($input, 'use', function ($expression) { + return DirectiveCompiler::make()->directive('use', function ($expression) { $compiler = app('blade.compiler'); $reflection = new \ReflectionClass($compiler); $method = $reflection->getMethod('compileUse'); return $method->invoke($compiler, $expression); - }); + })->compile($input); } /** diff --git a/src/Compiler/DirectiveCompiler.php b/src/Compiler/DirectiveCompiler.php new file mode 100644 index 00000000..4992017b --- /dev/null +++ b/src/Compiler/DirectiveCompiler.php @@ -0,0 +1,97 @@ + */ + protected array $directives = []; + + /** + * Create a new DirectiveCompiler instance. + */ + public static function make(): static + { + return new static(); + } + + /** + * Register a directive on this compiler instance. + */ + public function directive(string $name, callable $handler): static + { + $this->directives[$name] = $handler; + + return $this; + } + + /** + * Compile all registered directives within a template using a sandboxed Blade compiler. + */ + public function compile(string $template): string + { + $compiler = $this->createSandboxedCompiler(); + + foreach ($this->directives as $name => $handler) { + $compiler->directive($name, $handler); + } + + return $compiler->compileStatementsMadePublic($template); + } + + /** + * Create a BladeCompiler that only processes custom directives, ignoring built-in ones. + */ + private function createSandboxedCompiler() + { + return new class(app('files'), config('view.compiled')) extends \Illuminate\View\Compilers\BladeCompiler + { + public function compileStatementsMadePublic($template) + { + $result = ''; + + foreach (token_get_all($template) as $token) { + if (! is_array($token)) { + $result .= $token; + + continue; + } + + [$id, $content] = $token; + + if ($id == T_INLINE_HTML) { + $result .= $this->compileStatements($content); + } else { + $result .= $content; + } + } + + return $result; + } + + /** + * Only process custom directives, skip built-in ones. + */ + protected function compileStatement($match) + { + if (str_contains($match[1], '@')) { + $match[0] = isset($match[3]) ? $match[1].$match[3] : $match[1]; + } elseif (isset($this->customDirectives[$match[1]])) { + $match[0] = $this->callCustomDirective($match[1], Arr::get($match, 3)); + } elseif (method_exists($this, $method = 'compile'.ucfirst($match[1]))) { + return $match[0]; + } else { + return $match[0]; + } + + return isset($match[3]) ? $match[0] : $match[0].$match[2]; + } + }; + } +} diff --git a/src/Compiler/Wrapper.php b/src/Compiler/Wrapper.php index 8254c287..27a8ee96 100644 --- a/src/Compiler/Wrapper.php +++ b/src/Compiler/Wrapper.php @@ -3,6 +3,7 @@ namespace Livewire\Blaze\Compiler; use Livewire\Blaze\BladeService; +use Livewire\Blaze\Compiler\DirectiveCompiler; use Livewire\Blaze\Support\Utils; use Livewire\Blaze\Blaze; use Illuminate\Support\Arr; @@ -63,8 +64,8 @@ public function wrap(string $compiled, string $path, ?string $source = null): st $output .= 'ob_start();' . "\n"; $output .= '?>' . "\n"; - $compiled = BladeService::compileDirective($compiled, 'props', $this->propsCompiler->compile(...)); - $compiled = BladeService::compileDirective($compiled, 'aware', $this->awareCompiler->compile(...)); + $compiled = DirectiveCompiler::make()->directive('props', $this->propsCompiler->compile(...))->compile($compiled); + $compiled = DirectiveCompiler::make()->directive('aware', $this->awareCompiler->compile(...))->compile($compiled); $compiled = BladeService::restoreRawBlocks($compiled); $output .= $compiled; diff --git a/src/Support/Directives.php b/src/Support/Directives.php index e48cf3b2..820cf73b 100644 --- a/src/Support/Directives.php +++ b/src/Support/Directives.php @@ -2,8 +2,8 @@ namespace Livewire\Blaze\Support; -use Livewire\Blaze\BladeService; use Livewire\Blaze\Compiler\ArrayParser; +use Livewire\Blaze\Compiler\DirectiveCompiler; /** * Extracts and queries Blade directives from component source content. @@ -13,9 +13,9 @@ class Directives public function __construct( protected string $content, ) { - $this->content = BladeService::compileComments($this->content); - $this->content = preg_replace('/(?content); - $this->content = preg_replace('/(?content); + $this->content = preg_replace(LaravelRegex::BLADE_COMMENT, '', $this->content); + $this->content = preg_replace(LaravelRegex::VERBATIM_BLOCK, '', $this->content); + $this->content = preg_replace(LaravelRegex::PHP_BLOCK, '', $this->content); } /** @@ -25,11 +25,11 @@ public function has(string $name): bool { $result = false; - BladeService::compileDirective($this->content, $name, function () use (&$result) { + DirectiveCompiler::make()->directive($name, function () use (&$result) { $result = true; return ''; - }); + })->compile($this->content); return $result; } @@ -41,11 +41,11 @@ public function get(string $name): ?string { $result = null; - BladeService::compileDirective($this->content, $name, function ($expression) use (&$result) { + DirectiveCompiler::make()->directive($name, function ($expression) use (&$result) { $result = $expression; return ''; - }); + })->compile($this->content); return $result; } diff --git a/src/Support/LaravelRegex.php b/src/Support/LaravelRegex.php index 47329d52..e035ee63 100644 --- a/src/Support/LaravelRegex.php +++ b/src/Support/LaravelRegex.php @@ -3,7 +3,7 @@ namespace Livewire\Blaze\Support; /** - * Regex patterns sourced from Laravel's ComponentTagCompiler. + * Regex patterns sourced from Laravel's view compiler (ComponentTagCompiler, BladeCompiler). * * Every constant in this class MUST match the corresponding regex * in Laravel's source exactly. Do not modify these without first @@ -11,6 +11,8 @@ * constant's docblock. * * @see vendor/laravel/framework/src/Illuminate/View/Compilers/ComponentTagCompiler.php + * @see vendor/laravel/framework/src/Illuminate/View/Compilers/BladeCompiler.php + * @see vendor/laravel/framework/src/Illuminate/View/Compilers/Concerns/CompilesComments.php */ class LaravelRegex { @@ -26,14 +28,14 @@ class LaravelRegex /** * Pattern for matching a slot inline name (e.g., ). * - * @see ComponentTagCompiler::compileSlots() — (\w+(?:-\w+)*) + * @see ComponentTagCompiler::compileSlots() — line 522, (?:\:(?\w+(?:-\w+)*))? */ const SLOT_INLINE_NAME = '/^\w+(?:-\w+)*/'; /** - * Full pattern for matching individual attributes after preprocessing. + * Pattern for matching individual attributes after preprocessing. * - * @see ComponentTagCompiler::getAttributesFromAttributeString() — line 605 + * @see ComponentTagCompiler::getAttributesFromAttributeString() — lines 605-619 */ const ATTRIBUTE_PATTERN = '/ (?[\w\-:.@%]+) @@ -50,4 +52,25 @@ class LaravelRegex ) )? /x'; + + /** + * Pattern for matching Blade comments ({{-- ... --}}). + * + * @see CompilesComments::compileComments() — sprintf('/%s--(.*?)--%s/s', contentTags) + */ + const BLADE_COMMENT = '/\{\{--(.*?)--\}\}/s'; + + /** + * Pattern for matching @verbatim...@endverbatim blocks. + * + * @see BladeCompiler::storeVerbatimBlocks() — /(?directive('unblaze', function ($expression) use (&$expressionsByToken) { - $token = str()->random(10); - - $expressionsByToken[$token] = $expression; - - return '[STARTUNBLAZE:'.$token.']'; - }); + $result = DirectiveCompiler::make() + ->directive('unblaze', function ($expression) use (&$expressionsByToken) { + $token = str()->random(10); - $compiler->directive('endunblaze', function () { - return '[ENDUNBLAZE]'; - }); + $expressionsByToken[$token] = $expression; - $result = $compiler->compileStatementsMadePublic($template); + return '[STARTUNBLAZE:'.$token.']'; + }) + ->directive('endunblaze', function () { + return '[ENDUNBLAZE]'; + }) + ->compile($template); $result = preg_replace_callback('/(\[STARTUNBLAZE:([0-9a-zA-Z]+)\])(.*?)(\[ENDUNBLAZE\])/s', function ($matches) use (&$expressionsByToken) { $token = $matches[2]; From dc66f9869459bb43fe180241e031071cf4882a87 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sat, 28 Feb 2026 15:00:32 +0100 Subject: [PATCH 04/28] Delete parseAttributeStringToArray usage --- src/Compiler/Compiler.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Compiler/Compiler.php b/src/Compiler/Compiler.php index 7dc87763..ec7ad7d2 100644 --- a/src/Compiler/Compiler.php +++ b/src/Compiler/Compiler.php @@ -123,8 +123,7 @@ protected function compileComponentTag(ComponentNode $node, ComponentSource $sou */ protected function compileDelegateComponentTag(ComponentNode $node): string { - $attributesArray = Utils::parseAttributeStringToArray($node->attributeString); - $componentName = "'flux::' . " . $attributesArray['component']->value; + $componentName = "'flux::' . " . $node->attributes['component']->value; $output = '<' . '?php $__resolved = $__blaze->resolve(' . $componentName . '); ?>' . "\n"; From d18223f70b4c7470a616256b7d2d339c3ac9774b Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sat, 28 Feb 2026 15:01:49 +0100 Subject: [PATCH 05/28] Remove Utils:: parseAttributeStringToArray --- src/Parser/Nodes/ComponentNode.php | 2 +- src/Parser/Nodes/SlotNode.php | 2 +- src/Support/AttributeParser.php | 2 +- src/Support/Utils.php | 10 ---------- 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/Parser/Nodes/ComponentNode.php b/src/Parser/Nodes/ComponentNode.php index 1a65fbc8..fbc96335 100644 --- a/src/Parser/Nodes/ComponentNode.php +++ b/src/Parser/Nodes/ComponentNode.php @@ -24,7 +24,7 @@ public function __construct( public array $attributes = [], ) { if (empty($this->attributes) && ! empty($this->attributeString)) { - $this->attributes = Utils::parseAttributeStringToArray($this->attributeString); + $this->attributes = AttributeParser::parseAttributeStringToArray($this->attributeString); } } diff --git a/src/Parser/Nodes/SlotNode.php b/src/Parser/Nodes/SlotNode.php index 6b44f91f..07c82cb9 100644 --- a/src/Parser/Nodes/SlotNode.php +++ b/src/Parser/Nodes/SlotNode.php @@ -21,7 +21,7 @@ public function __construct( public string $prefix = 'x-slot', public bool $closeHasName = false, ) { - $this->attributes = Utils::parseAttributeStringToArray($this->attributeString); + $this->attributes = AttributeParser::parseAttributeStringToArray($this->attributeString); } /** {@inheritdoc} */ diff --git a/src/Support/AttributeParser.php b/src/Support/AttributeParser.php index cccaab44..a4068830 100644 --- a/src/Support/AttributeParser.php +++ b/src/Support/AttributeParser.php @@ -19,7 +19,7 @@ class AttributeParser * * @return array */ - public function parseAttributeStringToArray(string $attributesString): array + public static function parseAttributeStringToArray(string $attributesString): array { $attributesString = BladeService::preprocessAttributeString($attributesString); diff --git a/src/Support/Utils.php b/src/Support/Utils.php index 5ae73d3e..db7c20f3 100644 --- a/src/Support/Utils.php +++ b/src/Support/Utils.php @@ -18,16 +18,6 @@ public static function parseBlazeDirective(string $expression): array return BlazeDirective::parseParameters($expression); } - /** - * Parse an attribute string into a keyed array of Attribute objects. - * - * @return array - */ - public static function parseAttributeStringToArray(string $attributeString): array - { - return (new AttributeParser)->parseAttributeStringToArray($attributeString); - } - /** * Generate a unique hash for a component path. */ From 7f4f03c65d750ba2477ad61c1d1e949a7bd9c658 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sat, 28 Feb 2026 15:05:28 +0100 Subject: [PATCH 06/28] Fix imports --- src/Parser/Nodes/ComponentNode.php | 3 +-- src/Parser/Nodes/SlotNode.php | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Parser/Nodes/ComponentNode.php b/src/Parser/Nodes/ComponentNode.php index fbc96335..b653fc1d 100644 --- a/src/Parser/Nodes/ComponentNode.php +++ b/src/Parser/Nodes/ComponentNode.php @@ -2,8 +2,7 @@ namespace Livewire\Blaze\Parser\Nodes; -use Livewire\Blaze\Support\Utils; -use Livewire\Blaze\Parser\Attribute; +use Livewire\Blaze\Support\AttributeParser; /** * Represents an or tag in the AST. diff --git a/src/Parser/Nodes/SlotNode.php b/src/Parser/Nodes/SlotNode.php index 07c82cb9..8319eb72 100644 --- a/src/Parser/Nodes/SlotNode.php +++ b/src/Parser/Nodes/SlotNode.php @@ -2,8 +2,8 @@ namespace Livewire\Blaze\Parser\Nodes; -use Livewire\Blaze\Support\Utils; use Livewire\Blaze\Parser\Attribute; +use Livewire\Blaze\Support\AttributeParser; /** * Represents an tag in the AST. From ce9dcdba06d4bc3f658ab7d32d3d72e018c0ccc4 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sat, 28 Feb 2026 15:21:23 +0100 Subject: [PATCH 07/28] Move attribute preprocessing to Parser --- src/Folder/Foldable.php | 1 + src/Parser/Nodes/ComponentNode.php | 5 ---- src/Parser/Nodes/SlotNode.php | 7 ++--- src/Parser/Parser.php | 39 +++++++++++++++++++-------- src/Support/AttributeParser.php | 18 +++++++++---- tests/Parser/ParserTest.php | 6 +++++ tests/Support/AttributeParserTest.php | 16 +++++------ 7 files changed, 58 insertions(+), 34 deletions(-) diff --git a/src/Folder/Foldable.php b/src/Folder/Foldable.php index 93b686ea..1e79c555 100644 --- a/src/Folder/Foldable.php +++ b/src/Folder/Foldable.php @@ -103,6 +103,7 @@ protected function setupSlots(): void children: [new TextNode($placeholder)], prefix: $child->prefix, closeHasName: $child->closeHasName, + attributes: $child->attributes, ); } else { $looseContent[] = $child; diff --git a/src/Parser/Nodes/ComponentNode.php b/src/Parser/Nodes/ComponentNode.php index b653fc1d..32519810 100644 --- a/src/Parser/Nodes/ComponentNode.php +++ b/src/Parser/Nodes/ComponentNode.php @@ -2,8 +2,6 @@ namespace Livewire\Blaze\Parser\Nodes; -use Livewire\Blaze\Support\AttributeParser; - /** * Represents an or tag in the AST. */ @@ -22,9 +20,6 @@ public function __construct( /** @var Attribute[] */ public array $attributes = [], ) { - if (empty($this->attributes) && ! empty($this->attributeString)) { - $this->attributes = AttributeParser::parseAttributeStringToArray($this->attributeString); - } } /** diff --git a/src/Parser/Nodes/SlotNode.php b/src/Parser/Nodes/SlotNode.php index 8319eb72..6268585b 100644 --- a/src/Parser/Nodes/SlotNode.php +++ b/src/Parser/Nodes/SlotNode.php @@ -3,16 +3,12 @@ namespace Livewire\Blaze\Parser\Nodes; use Livewire\Blaze\Parser\Attribute; -use Livewire\Blaze\Support\AttributeParser; /** * Represents an tag in the AST. */ class SlotNode extends Node { - /** @var Attribute[] */ - public array $attributes = []; - public function __construct( public string $name, public string $attributeString = '', @@ -20,8 +16,9 @@ public function __construct( public array $children = [], public string $prefix = 'x-slot', public bool $closeHasName = false, + /** @var Attribute[] */ + public array $attributes = [], ) { - $this->attributes = AttributeParser::parseAttributeStringToArray($this->attributeString); } /** {@inheritdoc} */ diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php index 86622921..6be76246 100644 --- a/src/Parser/Parser.php +++ b/src/Parser/Parser.php @@ -2,16 +2,18 @@ namespace Livewire\Blaze\Parser; -use Livewire\Blaze\Parser\Tokens\TagSelfCloseToken; +use Livewire\Blaze\BladeService; +use Livewire\Blaze\Parser\Nodes\ComponentNode; +use Livewire\Blaze\Parser\Nodes\SlotNode; +use Livewire\Blaze\Parser\Nodes\TextNode; +use Livewire\Blaze\Parser\Tokenizer; use Livewire\Blaze\Parser\Tokens\SlotCloseToken; -use Livewire\Blaze\Parser\Tokens\TagCloseToken; use Livewire\Blaze\Parser\Tokens\SlotOpenToken; +use Livewire\Blaze\Parser\Tokens\TagCloseToken; use Livewire\Blaze\Parser\Tokens\TagOpenToken; +use Livewire\Blaze\Parser\Tokens\TagSelfCloseToken; use Livewire\Blaze\Parser\Tokens\TextToken; -use Livewire\Blaze\Parser\Nodes\ComponentNode; -use Livewire\Blaze\Parser\Nodes\TextNode; -use Livewire\Blaze\Parser\Nodes\SlotNode; -use Livewire\Blaze\Parser\Tokenizer; +use Livewire\Blaze\Support\AttributeParser; /** * Converts a flat token stream into a nested AST of component, slot, and text nodes. @@ -52,12 +54,17 @@ public function parse(string $content): array */ protected function handleTagOpen(TagOpenToken $token, ParseStack $stack): void { + $attributeString = implode(' ', $token->attributes); + $node = new ComponentNode( name: $token->namespace . $token->name, prefix: $token->prefix, - attributeString: implode(' ', $token->attributes), + attributeString: $attributeString, children: [], - selfClosing: false + selfClosing: false, + attributes: AttributeParser::parseAttributeStringToArray( + BladeService::preprocessAttributeString($attributeString) + ), ); $stack->pushContainer($node); @@ -68,12 +75,17 @@ protected function handleTagOpen(TagOpenToken $token, ParseStack $stack): void */ protected function handleTagSelfClose(TagSelfCloseToken $token, ParseStack $stack): void { + $attributeString = implode(' ', $token->attributes); + $node = new ComponentNode( name: $token->namespace . $token->name, prefix: $token->prefix, - attributeString: implode(' ', $token->attributes), + attributeString: $attributeString, children: [], - selfClosing: true + selfClosing: true, + attributes: AttributeParser::parseAttributeStringToArray( + BladeService::preprocessAttributeString($attributeString) + ), ); $stack->addToRoot($node); @@ -92,13 +104,18 @@ protected function handleTagClose(TagCloseToken $token, ParseStack $stack): void */ protected function handleSlotOpen(SlotOpenToken $token, ParseStack $stack): void { + $attributeString = implode(' ', $token->attributes); + $node = new SlotNode( name: $token->name ?? 'slot', - attributeString: implode(' ', $token->attributes), + attributeString: $attributeString, slotStyle: $token->slotStyle, children: [], prefix: $token->prefix, closeHasName: false, + attributes: AttributeParser::parseAttributeStringToArray( + BladeService::preprocessAttributeString($attributeString) + ), ); $stack->pushContainer($node); diff --git a/src/Support/AttributeParser.php b/src/Support/AttributeParser.php index a4068830..c21d7a5d 100644 --- a/src/Support/AttributeParser.php +++ b/src/Support/AttributeParser.php @@ -2,7 +2,7 @@ namespace Livewire\Blaze\Support; -use Livewire\Blaze\BladeService; +use Illuminate\Support\Str; use Livewire\Blaze\Parser\Attribute; /** @@ -21,15 +21,13 @@ class AttributeParser */ public static function parseAttributeStringToArray(string $attributesString): array { - $attributesString = BladeService::preprocessAttributeString($attributesString); - preg_match_all(LaravelRegex::ATTRIBUTE_PATTERN, $attributesString, $matches, PREG_SET_ORDER); $attributes = []; foreach ($matches as $match) { $name = $match['attribute']; - $value = isset($match['value']) ? BladeService::stripQuotes($match['value']) : null; + $value = isset($match['value']) ? static::stripQuotes($match['value']) : null; $isDynamic = false; $prefix = ''; @@ -79,5 +77,15 @@ public static function parseAttributeStringToArray(string $attributesString): ar return $attributes; } - + /** + * Strip any quotes from the given string. + * + * @see Illuminate\View\Compilers\ComponentTagCompiler::stripQuotes() + */ + protected static function stripQuotes(string $value) + { + return Str::startsWith($value, ['"', '\'']) + ? substr($value, 1, -1) + : $value; + } } diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index 6ab808eb..6a35602d 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -4,6 +4,7 @@ use Livewire\Blaze\Parser\Nodes\ComponentNode; use Livewire\Blaze\Parser\Nodes\SlotNode; use Livewire\Blaze\Parser\Nodes\TextNode; +use Livewire\Blaze\Support\AttributeParser; test('parses self-closing components', function () { @@ -15,6 +16,7 @@ prefix: 'x-', selfClosing: true, attributeString: 'class="my-4"', + attributes: AttributeParser::parseAttributeStringToArray('class="my-4"'), ), ]); }); @@ -33,6 +35,7 @@ children: [ new TextNode('Footer'), ], + attributes: AttributeParser::parseAttributeStringToArray('class="p-2"'), ) ] ), @@ -54,6 +57,7 @@ children: [ new TextNode('Footer'), ], + attributes: AttributeParser::parseAttributeStringToArray('class="p-2"'), ) ] ), @@ -76,6 +80,7 @@ children: [ new TextNode('Footer'), ], + attributes: AttributeParser::parseAttributeStringToArray('class="p-2"'), ) ] ), @@ -96,6 +101,7 @@ children: [ new TextNode('Body'), ], + attributes: AttributeParser::parseAttributeStringToArray('class="p-2"'), ) ] ), diff --git a/tests/Support/AttributeParserTest.php b/tests/Support/AttributeParserTest.php index bbf3c18a..aa13a221 100644 --- a/tests/Support/AttributeParserTest.php +++ b/tests/Support/AttributeParserTest.php @@ -3,7 +3,7 @@ use Livewire\Blaze\Support\AttributeParser; test('parses bound attributes', function () { - $attrs = (new AttributeParser)->parseAttributeStringToArray(':foo="bar"'); + $attrs = AttributeParser::parseAttributeStringToArray('bind:foo="bar"'); expect($attrs)->toHaveKey('foo'); expect($attrs['foo']) @@ -14,7 +14,7 @@ }); test('parses escaped bound attributes', function () { - $attrs = (new AttributeParser)->parseAttributeStringToArray('::key="value"'); + $attrs = AttributeParser::parseAttributeStringToArray('::key="value"'); expect($attrs)->toHaveKey(':key'); expect($attrs[':key']) @@ -25,7 +25,7 @@ }); test('parses attributes without value', function () { - $attrs = (new AttributeParser)->parseAttributeStringToArray('disabled'); + $attrs = AttributeParser::parseAttributeStringToArray('disabled'); expect($attrs)->toHaveKey('disabled'); expect($attrs['disabled']) @@ -36,7 +36,7 @@ }); test('parses attributes with blade echo', function () { - $attrs = (new AttributeParser)->parseAttributeStringToArray('title="{{ $x }}"'); + $attrs = AttributeParser::parseAttributeStringToArray('title="{{ $x }}"'); expect($attrs)->toHaveKey('title'); expect($attrs['title']) @@ -46,7 +46,7 @@ }); test('parses attributes with raw blade echo', function () { - $attrs = (new AttributeParser)->parseAttributeStringToArray('title="{!! $x !!}"'); + $attrs = AttributeParser::parseAttributeStringToArray('title="{!! $x !!}"'); expect($attrs)->toHaveKey('title'); expect($attrs['title']) @@ -56,14 +56,14 @@ }); test('parses quotes', function () { - $attrs = (new AttributeParser)->parseAttributeStringToArray('double="hello" single=\'hello\''); + $attrs = AttributeParser::parseAttributeStringToArray('double="hello" single=\'hello\''); expect($attrs['double']->quotes)->toBe('"'); expect($attrs['single']->quotes)->toBe("'"); }); test('parses kebab case attributes', function () { - $attrs = (new AttributeParser)->parseAttributeStringToArray('foo-bar="first"'); + $attrs = AttributeParser::parseAttributeStringToArray('foo-bar="first"'); expect($attrs)->toHaveKey('fooBar'); expect($attrs['fooBar']) @@ -72,7 +72,7 @@ }); test('keeps first attribute when multiple camelize to same key', function () { - $attrs = (new AttributeParser)->parseAttributeStringToArray('foo-bar="first" foo_bar="second"'); + $attrs = AttributeParser::parseAttributeStringToArray('foo-bar="first" foo_bar="second"'); expect($attrs)->toHaveCount(1)->toHaveKey('fooBar'); expect($attrs['fooBar']->value)->toBe('first'); From afe89e9a77b267294c4212f894499e76015a2beb Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sat, 28 Feb 2026 15:30:14 +0100 Subject: [PATCH 08/28] Update ComponentSource to accept path --- src/BlazeManager.php | 2 +- src/Compiler/Compiler.php | 3 ++- src/Compiler/Profiler.php | 3 ++- src/Folder/Folder.php | 5 +++-- src/Memoizer/Memoizer.php | 2 +- src/Support/ComponentSource.php | 8 ++------ tests/Folder/FoldableTest.php | 23 ++++++++++++----------- tests/Folder/UnblazeTest.php | 7 ++++--- 8 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/BlazeManager.php b/src/BlazeManager.php index 6ba16fea..6c847cfd 100644 --- a/src/BlazeManager.php +++ b/src/BlazeManager.php @@ -404,7 +404,7 @@ protected function hasAwareDescendant(ComponentNode|SlotNode $node): bool { foreach ($node->children as $child) { if ($child instanceof ComponentNode) { - $source = new ComponentSource($child->name); + $source = new ComponentSource(BladeService::componentNameToPath($child->name)); if (str_ends_with($child->name, 'delegate-component')) { return true; diff --git a/src/Compiler/Compiler.php b/src/Compiler/Compiler.php index ec7ad7d2..7d159f36 100644 --- a/src/Compiler/Compiler.php +++ b/src/Compiler/Compiler.php @@ -4,6 +4,7 @@ use Illuminate\View\Compilers\ComponentTagCompiler; use Livewire\Blaze\Blaze; +use Livewire\Blaze\BladeService; use Livewire\Blaze\Parser\Nodes\ComponentNode; use Livewire\Blaze\Parser\Nodes\SlotNode; use Livewire\Blaze\Parser\Nodes\TextNode; @@ -41,7 +42,7 @@ public function compile(Node $node): Node return new TextNode($this->compileDelegateComponentTag($node)); } - $source = new ComponentSource($node->name); + $source = new ComponentSource(BladeService::componentNameToPath($node->name)); if (! $source->exists()) { return $node; diff --git a/src/Compiler/Profiler.php b/src/Compiler/Profiler.php index 8c9bb8aa..b55f3eca 100644 --- a/src/Compiler/Profiler.php +++ b/src/Compiler/Profiler.php @@ -2,6 +2,7 @@ namespace Livewire\Blaze\Compiler; +use Livewire\Blaze\BladeService; use Livewire\Blaze\Config; use Livewire\Blaze\Parser\Nodes\ComponentNode; use Livewire\Blaze\Parser\Nodes\Node; @@ -29,7 +30,7 @@ public function __construct( */ public function profile(Node $node, string $componentName, ?string $strategy = null): Node { - $source = new ComponentSource($componentName); + $source = new ComponentSource(BladeService::componentNameToPath($componentName)); if ($strategy === null) { $isBlade = $node instanceof ComponentNode; diff --git a/src/Folder/Folder.php b/src/Folder/Folder.php index 0753e2e4..8e5c5639 100644 --- a/src/Folder/Folder.php +++ b/src/Folder/Folder.php @@ -10,6 +10,7 @@ use Livewire\Blaze\Parser\Nodes\SlotNode; use Livewire\Blaze\Parser\Nodes\TextNode; use Livewire\Blaze\Support\ComponentSource; +use Livewire\Blaze\BladeService; use Illuminate\Support\Arr; use Livewire\Blaze\Blaze; use Livewire\Blaze\Config; @@ -35,7 +36,7 @@ public function fold(Node $node): Node $component = $node; - $source = new ComponentSource($component->name); + $source = new ComponentSource(BladeService::componentNameToPath($component->name)); if (! $source->exists()) { return $component; @@ -57,7 +58,7 @@ public function fold(Node $node): Node $html = $foldable->fold(); Event::dispatch(new ComponentFolded( - name: $source->name, + name: $component->name, path: $source->path, filemtime: filemtime($source->path), )); diff --git a/src/Memoizer/Memoizer.php b/src/Memoizer/Memoizer.php index 9dce8b75..463b5306 100644 --- a/src/Memoizer/Memoizer.php +++ b/src/Memoizer/Memoizer.php @@ -78,7 +78,7 @@ protected function isMemoizable(Node $node): bool return false; } - $source = new ComponentSource($node->name); + $source = new ComponentSource(BladeService::componentNameToPath($node->name)); if (! $source->exists()) { return false; diff --git a/src/Support/ComponentSource.php b/src/Support/ComponentSource.php index e3bad0da..39ab70f8 100644 --- a/src/Support/ComponentSource.php +++ b/src/Support/ComponentSource.php @@ -2,22 +2,18 @@ namespace Livewire\Blaze\Support; -use Livewire\Blaze\BladeService; - /** * Resolves and caches a component's file path, content, and directive metadata. */ class ComponentSource { - public readonly string $name; public readonly string $path; public readonly string $content; public readonly Directives $directives; - public function __construct($name) + public function __construct(string $path) { - $this->name = $name; - $this->path = BladeService::componentNameToPath($name); + $this->path = $path; $this->content = $this->exists() ? file_get_contents($this->path) : ''; $this->directives = new Directives($this->content); } diff --git a/tests/Folder/FoldableTest.php b/tests/Folder/FoldableTest.php index a826c7bc..c6e027e0 100644 --- a/tests/Folder/FoldableTest.php +++ b/tests/Folder/FoldableTest.php @@ -1,5 +1,6 @@ '; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource($node->name)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php'))); expect($foldable->fold())->toEqualCollapsingWhitespace( '' @@ -31,7 +32,7 @@ ; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource($node->name)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php'))); expect($foldable->fold())->toEqualCollapsingWhitespace( '
{{ $title }} | {{ $content }} | {{ $author }}
' @@ -55,7 +56,7 @@ ; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource($node->name)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php'))); expect($foldable->fold())->toEqualCollapsingWhitespace( '
{{ $title }} | Before {{ $content }} After | {{ $author }}
' @@ -66,7 +67,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource($node->name)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php'))); expect($foldable->fold())->toEqualCollapsingWhitespace( '' @@ -77,7 +78,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource($node->name)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php'))); expect($foldable->fold())->toEqualCollapsingWhitespace( '' @@ -88,7 +89,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource($node->name)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-aware.blade.php'))); $node->setParentsAttributes([ 'type' => new Attribute( @@ -119,7 +120,7 @@ ), ]); - $foldable = new Foldable($node, new ComponentSource($node->name)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-aware.blade.php'))); expect($foldable->fold())->toEqualCollapsingWhitespace( '' @@ -130,7 +131,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource($node->name)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php'))); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('', join('', [ @@ -147,7 +148,7 @@ $node = app(Parser::class)->parse($input)[0]; $node->hasAwareDescendants = true; - $foldable = new Foldable($node, new ComponentSource($node->name)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php'))); expect($foldable->fold())->toEqualCollapsingWhitespace(join('', [ 'pushData([\'name\' => \'John\']); $__env->pushConsumableComponentData([\'name\' => \'John\']); ?>', @@ -162,7 +163,7 @@ $node = app(Parser::class)->parse($input)[0]; $node->hasAwareDescendants = true; - $foldable = new Foldable($node, new ComponentSource($node->name)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php'))); expect($foldable->fold())->toEqualCollapsingWhitespace(join('', [ 'pushData([\'name\' => $name]); $__env->pushConsumableComponentData([\'name\' => $name]); ?>', @@ -177,7 +178,7 @@ $node = app(Parser::class)->parse($input)[0]; $node->hasAwareDescendants = true; - $foldable = new Foldable($node, new ComponentSource($node->name)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php'))); expect($foldable->fold())->toEqualCollapsingWhitespace(join('', [ 'pushData([\'name\' => \'Mr. \'.e($name)]); $__env->pushConsumableComponentData([\'name\' => \'Mr. \'.e($name)]); ?>', diff --git a/tests/Folder/UnblazeTest.php b/tests/Folder/UnblazeTest.php index 7830dae5..e3164288 100644 --- a/tests/Folder/UnblazeTest.php +++ b/tests/Folder/UnblazeTest.php @@ -1,5 +1,6 @@ '; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource($node->name)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-unblaze.blade.php'))); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('', join('', [ @@ -24,7 +25,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource($node->name)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/nested-input-unblaze.blade.php'))); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('
', join('', [ @@ -40,7 +41,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource($node->name)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-unblaze.blade.php'))); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('', join('', [ From 602e5a1ca063e152495e94c0009bc0ca1606c074 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sat, 28 Feb 2026 18:23:53 +0100 Subject: [PATCH 09/28] Add failing test --- tests/IntegrationTest.php | 22 ++++++++++++++++++++++ tests/fixtures/views/blaze.blade.php | 3 +++ 2 files changed, 25 insertions(+) create mode 100644 tests/fixtures/views/blaze.blade.php diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index a9ae7aa9..ec78969b 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -1,8 +1,12 @@ render(); +})->throwsNoExceptions(); + +test('doesnt resolve blade compiler', function () { + Artisan::call('view:clear'); + + $compiler = app('blade.compiler'); + + Blaze::clearResolvedInstance(); + + app()->forgetInstance(BlazeManager::class); + app()->forgetInstance(BladeService::class); + app()->forgetInstance('blade.compiler'); + + app()->resolving('blade.compiler', function () { + test()->fail('Blade compiler was resolved from container'); + }); + + $compiler->compile(fixture_path('views/blaze.blade.php')); })->throwsNoExceptions(); \ No newline at end of file diff --git a/tests/fixtures/views/blaze.blade.php b/tests/fixtures/views/blaze.blade.php new file mode 100644 index 00000000..e1c3b946 --- /dev/null +++ b/tests/fixtures/views/blaze.blade.php @@ -0,0 +1,3 @@ + + + \ No newline at end of file From bc37a900373a0b9c2f113ff528c2ddc9eeb54181 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sat, 28 Feb 2026 19:49:01 +0100 Subject: [PATCH 10/28] Refactor BladeService to an instance --- src/BladeService.php | 97 +++++++++++++++++++--------------- src/BlazeManager.php | 56 ++++++++++++-------- src/BlazeServiceProvider.php | 36 ++++++++----- src/Compiler/Compiler.php | 19 +++---- src/Compiler/Profiler.php | 8 ++- src/Compiler/Wrapper.php | 31 ++++++----- src/DebuggerMiddleware.php | 3 +- src/Folder/Foldable.php | 9 ++-- src/Folder/Folder.php | 12 +++-- src/Memoizer/Memoizer.php | 9 ++-- src/Parser/Parser.php | 9 ++-- src/Runtime/BlazeRuntime.php | 10 ++-- tests/Compiler/WrapperTest.php | 4 +- tests/Folder/FoldableTest.php | 24 ++++----- tests/Folder/UnblazeTest.php | 8 +-- 15 files changed, 195 insertions(+), 140 deletions(-) diff --git a/src/BladeService.php b/src/BladeService.php index 40d9c7e3..52408398 100644 --- a/src/BladeService.php +++ b/src/BladeService.php @@ -5,25 +5,32 @@ use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\File; use Illuminate\Support\Str; +use Illuminate\View\Compilers\BladeCompiler; use Illuminate\View\Compilers\ComponentTagCompiler; -use ReflectionClass; use Livewire\Blaze\Compiler\DirectiveCompiler; -use Livewire\Blaze\Support\Utils; +use Livewire\Blaze\Runtime\BlazeRuntime; +use ReflectionClass; class BladeService { + public function __construct( + public BladeCompiler $compiler, + public BlazeRuntime $runtime, + public BlazeManager $manager, + ) {} + /** * Render a Blade template string in an isolated context. */ - public static function render(string $template): string + public function render(string $template): string { - return static::isolatedRender($template); + return $this->isolatedRender($template); } /** * Get the temporary cache directory path used during isolated rendering. */ - public static function getTemporaryCachePath(): string + public function getTemporaryCachePath(): string { return config('view.compiled').'/blaze'; } @@ -31,17 +38,17 @@ public static function getTemporaryCachePath(): string /** * Render a Blade template string in isolation by freezing and restoring compiler state. */ - public static function isolatedRender(string $template): string + public function isolatedRender(string $template): string { - $compiler = app('blade.compiler'); + $compiler = $this->compiler; - $temporaryCachePath = static::getTemporaryCachePath(); + $temporaryCachePath = $this->getTemporaryCachePath(); File::ensureDirectoryExists($temporaryCachePath); $factory = app('view'); - [$factory, $restoreFactory] = static::freezeObjectProperties($factory, [ + [$factory, $restoreFactory] = $this->freezeObjectProperties($factory, [ 'renderCount' => 0, 'renderedOnce' => [], 'sections' => [], @@ -60,7 +67,7 @@ public static function isolatedRender(string $template): string 'translationReplacements' => [], ]); - [$compiler, $restore] = static::freezeObjectProperties($compiler, [ + [$compiler, $restore] = $this->freezeObjectProperties($compiler, [ 'cachePath' => $temporaryCachePath, 'rawBlocks' => [], 'footer' => [], @@ -70,7 +77,7 @@ function ($input) use ($compiler) { $input = Unblaze::processUnblazeDirectives($input); }; - $input = Blaze::compileForFolding($input, $compiler->getPath()); + $input = $this->manager->compileForFolding($input, $compiler->getPath()); return $input; }, @@ -82,7 +89,7 @@ function ($input) use ($compiler) { 'lastFragment' => null, ]); - [$runtime, $restoreRuntime] = static::freezeObjectProperties(app('blaze.runtime'), [ + [$runtime, $restoreRuntime] = $this->freezeObjectProperties($this->runtime, [ 'compiled' => [], 'paths' => [], 'compiledPath' => $temporaryCachePath, @@ -91,7 +98,7 @@ function ($input) use ($compiler) { ]); try { - Blaze::startFolding(); + $this->manager->startFolding(); $result = $compiler->render($template, deleteCachedView: true); } finally { @@ -99,7 +106,7 @@ function ($input) use ($compiler) { $restoreFactory(); $restoreRuntime(); - Blaze::stopFolding(); + $this->manager->stopFolding(); } $result = Unblaze::replaceUnblazePrecompiledDirectives($result); @@ -110,15 +117,15 @@ function ($input) use ($compiler) { /** * Delete the temporary cache directory created during isolated rendering. */ - public static function deleteTemporaryCacheDirectory(): void + public function deleteTemporaryCacheDirectory(): void { - File::deleteDirectory(static::getTemporaryCachePath()); + File::deleteDirectory($this->getTemporaryCachePath()); } /** * Check if template content is a Laravel exception view. */ - public static function containsLaravelExceptionView(string $input): bool + public function containsLaravelExceptionView(string $input): bool { return str_contains($input, 'laravel-exceptions'); } @@ -126,11 +133,11 @@ public static function containsLaravelExceptionView(string $input): bool /** * Register a callback to run at the earliest Blade pre-compilation phase. */ - public static function earliestPreCompilationHook(callable $callback): void + public function earliestPreCompilationHook(callable $callback): void { - app()->booted(function () use ($callback) { - $compiler = app('blade.compiler'); + $compiler = $this->compiler; + app()->booted(function () use ($callback, $compiler) { $compiler->prepareStringsForCompilationUsing(function ($input) use ($callback, $compiler) { // We call getPath() on the captured $compiler instance rather than resolving it // via app('blade.compiler')->getPath() inside BlazeManager, this fixes #43. @@ -151,9 +158,9 @@ public static function earliestPreCompilationHook(callable $callback): void /** * Invoke the Blade compiler's storeUncompiledBlocks via reflection. */ - public static function preStoreUncompiledBlocks(string $input): string + public function preStoreUncompiledBlocks(string $input): string { - $compiler = app('blade.compiler'); + $compiler = $this->compiler; $reflection = new \ReflectionClass($compiler); $storeRawBlock = $reflection->getMethod('storeRawBlock'); @@ -174,9 +181,9 @@ public static function preStoreUncompiledBlocks(string $input): string /** * Store only @verbatim blocks as raw block placeholders. */ - public static function storeVerbatimBlocks(string $input): string + public function storeVerbatimBlocks(string $input): string { - $compiler = app('blade.compiler'); + $compiler = $this->compiler; $reflection = new \ReflectionClass($compiler); $method = $reflection->getMethod('storeVerbatimBlocks'); @@ -187,9 +194,9 @@ public static function storeVerbatimBlocks(string $input): string /** * Restore raw block placeholders to their original content. */ - public static function restoreRawBlocks(string $input): string + public function restoreRawBlocks(string $input): string { - $compiler = app('blade.compiler'); + $compiler = $this->compiler; $reflection = new \ReflectionClass($compiler); $method = $reflection->getMethod('restoreRawContent'); @@ -200,9 +207,9 @@ public static function restoreRawBlocks(string $input): string /** * Restore raw block placeholders to their original content. */ - public static function restorePhpBlocks(string $input): string + public function restorePhpBlocks(string $input): string { - $compiler = app('blade.compiler'); + $compiler = $this->compiler; $reflection = new \ReflectionClass($compiler); $method = $reflection->getMethod('restorePhpBlocks'); @@ -213,9 +220,9 @@ public static function restorePhpBlocks(string $input): string /** * Invoke the Blade compiler's compileComments via reflection. */ - public static function compileComments(string $input): string + public function compileComments(string $input): string { - $compiler = app('blade.compiler'); + $compiler = $this->compiler; $reflection = new \ReflectionClass($compiler); $compileComments = $reflection->getMethod('compileComments'); @@ -233,9 +240,9 @@ public static function compileComments(string $input): string * @style(...) → :style="..." (parseComponentTagStyleStatements) * :attr= → bind:attr= (parseBindAttributes) */ - public static function preprocessAttributeString(string $attributeString): string + public function preprocessAttributeString(string $attributeString): string { - $compiler = new ComponentTagCompiler(blade: app('blade.compiler')); + $compiler = new ComponentTagCompiler(blade: $this->compiler); // Laravel expects a space at the start of the attribute string... $attributeString = Str::start($attributeString, ' '); @@ -252,10 +259,10 @@ public static function preprocessAttributeString(string $attributeString): strin })->call($compiler, $attributeString); } - public static function compileUseStatements(string $input): string + public function compileUseStatements(string $input): string { return DirectiveCompiler::make()->directive('use', function ($expression) { - $compiler = app('blade.compiler'); + $compiler = $this->compiler; $reflection = new \ReflectionClass($compiler); $method = $reflection->getMethod('compileUse'); @@ -267,9 +274,9 @@ public static function compileUseStatements(string $input): string /** * Compile Blade echo syntax within attribute values using ComponentTagCompiler. */ - public static function compileAttributeEchos(string $input): string + public function compileAttributeEchos(string $input): string { - $compiler = new ComponentTagCompiler(blade: app('blade.compiler')); + $compiler = new ComponentTagCompiler(blade: $this->compiler); $reflection = new \ReflectionClass($compiler); $method = $reflection->getMethod('compileAttributeEchos'); @@ -280,24 +287,26 @@ public static function compileAttributeEchos(string $input): string /** * Strip surrounding quotes from a string using ComponentTagCompiler. */ - public static function stripQuotes(string $input): string + public function stripQuotes(string $input): string { - return (new ComponentTagCompiler(blade: app('blade.compiler')))->stripQuotes($input); + return (new ComponentTagCompiler(blade: $this->compiler))->stripQuotes($input); } /** * Register a callback to intercept view cache invalidation events. */ - public static function viewCacheInvalidationHook(callable $callback): void + public function viewCacheInvalidationHook(callable $callback): void { - Event::listen('composing:*', function ($event, $params) use ($callback) { + $compiler = $this->compiler; + + Event::listen('composing:*', function ($event, $params) use ($callback, $compiler) { $view = $params[0]; if (! $view instanceof \Illuminate\View\View) { return; } - $invalidate = fn () => app('blade.compiler')->compile($view->getPath()); + $invalidate = fn () => $compiler->compile($view->getPath()); $callback($view, $invalidate); }); @@ -306,9 +315,9 @@ public static function viewCacheInvalidationHook(callable $callback): void /** * Resolve a component name to its file path using registered anonymous component paths. */ - public static function componentNameToPath($name): string + public function componentNameToPath($name): string { - $compiler = app('blade.compiler'); + $compiler = $this->compiler; $viewFinder = app('view')->getFinder(); $reflection = new \ReflectionClass($compiler); @@ -395,7 +404,7 @@ public static function componentNameToPath($name): string /** * Snapshot object properties and return a restore closure to revert them. */ - protected static function freezeObjectProperties(object $object, array $properties) + protected function freezeObjectProperties(object $object, array $properties) { $reflection = new ReflectionClass($object); diff --git a/src/BlazeManager.php b/src/BlazeManager.php index 6c847cfd..3b4f1fa5 100644 --- a/src/BlazeManager.php +++ b/src/BlazeManager.php @@ -3,6 +3,7 @@ namespace Livewire\Blaze; use Illuminate\Support\Facades\Event; +use Illuminate\View\Compilers\BladeCompiler; use Illuminate\View\Engines\CompilerEngine; use Livewire\Blaze\Compiler\Wrapper; use Livewire\Blaze\Compiler\Compiler; @@ -18,6 +19,7 @@ use Livewire\Blaze\Support\Directives; use Livewire\Blaze\Support\ComponentSource; use Livewire\Blaze\Parser\Nodes\SlotNode; +use Livewire\Blaze\Runtime\BlazeRuntime; class BlazeManager { @@ -30,17 +32,29 @@ class BlazeManager protected $foldedEvents = []; protected $expiredMemo = []; + protected Parser $parser; + protected Walker $walker; + protected Compiler $compiler; + protected Folder $folder; + protected Memoizer $memoizer; + protected Wrapper $wrapper; + protected Profiler $instrumenter; + protected BladeService $bladeService; + public function __construct( - protected Tokenizer $tokenizer, - protected Parser $parser, - protected Walker $walker, - protected Compiler $compiler, - protected Folder $folder, - protected Memoizer $memoizer, - protected Wrapper $wrapper, - protected Profiler $instrumenter, protected Config $config, + protected BladeCompiler $bladeCompiler, + protected BlazeRuntime $runtime, ) { + $this->bladeService = new BladeService($bladeCompiler, $this->runtime, $this); + $this->parser = new Parser(new Tokenizer, $this->bladeService); + $this->walker = new Walker; + $this->compiler = new Compiler($config, $this->bladeService, $this); + $this->folder = new Folder($config, $this->bladeService, $this); + $this->memoizer = new Memoizer($config, $this->compiler, $this->bladeService, $this); + $this->wrapper = new Wrapper($this->bladeService, $this); + $this->instrumenter = new Profiler($config, $this->bladeService); + Event::listen(ComponentFolded::class, function (ComponentFolded $event) { $this->foldedEvents[] = $event; }); @@ -54,8 +68,8 @@ public function compile(string $template, ?string $path = null): string $source = $template; $clean = $template; - $clean = BladeService::preStoreUncompiledBlocks($clean); - $clean = BladeService::compileComments($clean); + $clean = $this->bladeService->preStoreUncompiledBlocks($clean); + $clean = $this->bladeService->compileComments($clean); $dataStack = []; @@ -108,10 +122,10 @@ public function compile(string $template, ?string $path = null): string $output = $this->instrumenter->profileView($output, $path, $source); } - $output = BladeService::restoreRawBlocks($output); + $output = $this->bladeService->restoreRawBlocks($output); try { - BladeService::deleteTemporaryCacheDirectory(); + $this->bladeService->deleteTemporaryCacheDirectory(); } catch (\Throwable $e) { // } @@ -124,8 +138,8 @@ public function compile(string $template, ?string $path = null): string */ public function compileForUnblaze(string $template): string { - $template = BladeService::preStoreUncompiledBlocks($template); - $template = BladeService::compileComments($template); + $template = $this->bladeService->preStoreUncompiledBlocks($template); + $template = $this->bladeService->compileComments($template); $ast = $this->walker->walk( nodes: $this->parser->parse($template), @@ -166,8 +180,8 @@ public function compileForDebug(string $template, ?string $path = null): string $source = $template; $clean = $template; - $clean = BladeService::preStoreUncompiledBlocks($clean); - $clean = BladeService::compileComments($clean); + $clean = $this->bladeService->preStoreUncompiledBlocks($clean); + $clean = $this->bladeService->compileComments($clean); $ast = $this->walker->walk( nodes: $this->parser->parse($clean), @@ -187,7 +201,7 @@ public function compileForDebug(string $template, ?string $path = null): string $output = $this->instrumenter->profileView($output, $path, $source); } - $output = BladeService::restoreRawBlocks($output); + $output = $this->bladeService->restoreRawBlocks($output); return $output; } @@ -200,8 +214,8 @@ public function compileForFolding(string $template, ?string $path = null): strin { $source = $template; - $template = BladeService::preStoreUncompiledBlocks($template); - $template = BladeService::compileComments($template); + $template = $this->bladeService->preStoreUncompiledBlocks($template); + $template = $this->bladeService->compileComments($template); $ast = $this->walker->walk( nodes: $this->parser->parse($template), @@ -213,7 +227,7 @@ public function compileForFolding(string $template, ?string $path = null): strin $output = $this->render($ast); - $output = BladeService::restoreRawBlocks($output); + $output = $this->bladeService->restoreRawBlocks($output); if (! $path) { return $output; @@ -404,7 +418,7 @@ protected function hasAwareDescendant(ComponentNode|SlotNode $node): bool { foreach ($node->children as $child) { if ($child instanceof ComponentNode) { - $source = new ComponentSource(BladeService::componentNameToPath($child->name)); + $source = new ComponentSource($this->bladeService->componentNameToPath($child->name)); if (str_ends_with($child->name, 'delegate-component')) { return true; diff --git a/src/BlazeServiceProvider.php b/src/BlazeServiceProvider.php index 3af4afe5..7ec44cae 100644 --- a/src/BlazeServiceProvider.php +++ b/src/BlazeServiceProvider.php @@ -15,6 +15,7 @@ public function register(): void { $this->registerConfig(); + $this->app->singleton(BladeService::class); $this->app->singleton(BlazeRuntime::class); $this->app->singleton(Config::class); $this->app->singleton(Debugger::class); @@ -53,14 +54,17 @@ public function boot(): void */ protected function registerBlazeRuntime(): void { - View::composer('*', function (\Illuminate\View\View $view) { - if (Blaze::isDisabled() && ! Blaze::isDebugging()) { + $blaze = $this->app->make(BlazeManager::class); + $runtime = $this->app->make(BlazeRuntime::class); + + View::composer('*', function (\Illuminate\View\View $view) use ($blaze, $runtime) { + if ($blaze->isDisabled() && ! $blaze->isDebugging()) { return; } // Avoid injecting the BlazeRuntime into non-Blade views (like Statamic's Antlers) if ($view->getEngine() instanceof CompilerEngine) { - $view->with('__blaze', $this->app->make(BlazeRuntime::class)); + $view->with('__blaze', $runtime); } }); } @@ -108,21 +112,24 @@ protected function registerBladeMacros(): void */ protected function interceptBladeCompilation(): void { - BladeService::earliestPreCompilationHook(function ($input, $path) { - if (BladeService::containsLaravelExceptionView($input)) { + $bladeService = $this->app->make(BladeService::class); + $blaze = $this->app->make(BlazeManager::class); + + $bladeService->earliestPreCompilationHook(function ($input, $path) use ($bladeService, $blaze) { + if ($bladeService->containsLaravelExceptionView($input)) { return $input; } - if (Blaze::isDisabled()) { - if (Blaze::isDebugging()) { - return Blaze::compileForDebug($input, $path); + if ($blaze->isDisabled()) { + if ($blaze->isDebugging()) { + return $blaze->compileForDebug($input, $path); } return $input; } - return Blaze::collectAndAppendFrontMatter($input, function ($input) use ($path) { - return Blaze::compile($input, $path); + return $blaze->collectAndAppendFrontMatter($input, function ($input) use ($path, $blaze) { + return $blaze->compile($input, $path); }); }); } @@ -132,12 +139,15 @@ protected function interceptBladeCompilation(): void */ protected function interceptViewCacheInvalidation(): void { - BladeService::viewCacheInvalidationHook(function ($view, $invalidate) { - if (Blaze::isDisabled()) { + $bladeService = $this->app->make(BladeService::class); + $blaze = $this->app->make(BlazeManager::class); + + $bladeService->viewCacheInvalidationHook(function ($view, $invalidate) use ($blaze) { + if ($blaze->isDisabled()) { return; } - if (Blaze::viewContainsExpiredFrontMatter($view)) { + if ($blaze->viewContainsExpiredFrontMatter($view)) { $invalidate(); } }); diff --git a/src/Compiler/Compiler.php b/src/Compiler/Compiler.php index 7d159f36..80377912 100644 --- a/src/Compiler/Compiler.php +++ b/src/Compiler/Compiler.php @@ -3,8 +3,8 @@ namespace Livewire\Blaze\Compiler; use Illuminate\View\Compilers\ComponentTagCompiler; -use Livewire\Blaze\Blaze; use Livewire\Blaze\BladeService; +use Livewire\Blaze\BlazeManager; use Livewire\Blaze\Parser\Nodes\ComponentNode; use Livewire\Blaze\Parser\Nodes\SlotNode; use Livewire\Blaze\Parser\Nodes\TextNode; @@ -18,15 +18,16 @@ */ class Compiler { - protected Config $config; protected ComponentTagCompiler $blade; protected SlotCompiler $slotCompiler; - public function __construct(Config $config) - { - $this->config = $config; + public function __construct( + protected Config $config, + protected BladeService $bladeService, + protected BlazeManager $manager, + ) { $this->slotCompiler = new SlotCompiler(fn (string $str) => $this->getAttributesAndBoundKeysArrayStrings($str, true)[0]); - $this->blade = new ComponentTagCompiler([], [], app('blade.compiler')); + $this->blade = new ComponentTagCompiler([], [], $bladeService->compiler); } /** @@ -42,7 +43,7 @@ public function compile(Node $node): Node return new TextNode($this->compileDelegateComponentTag($node)); } - $source = new ComponentSource(BladeService::componentNameToPath($node->name)); + $source = new ComponentSource($this->bladeService->componentNameToPath($node->name)); if (! $source->exists()) { return $node; @@ -95,7 +96,7 @@ protected function hasDynamicSlotNames(ComponentNode $node): bool protected function compileComponentTag(ComponentNode $node, ComponentSource $source): string { $hash = Utils::hash($source->path); - $functionName = (Blaze::isFolding() ? '__' : '_') . $hash; + $functionName = ($this->manager->isFolding() ? '__' : '_') . $hash; $slotsVariableName = '$slots' . $hash; [$attributesArrayString, $boundKeysArrayString] = $this->getAttributesAndBoundKeysArrayStrings($node->attributeString); @@ -130,7 +131,7 @@ protected function compileDelegateComponentTag(ComponentNode $node): string $slotsVariableName = '$slots' . hash('xxh128', $componentName); - $functionName = '(\'' . (Blaze::isFolding() ? '__' : '_') . '\' . $__resolved)'; + $functionName = '(\'' . ($this->manager->isFolding() ? '__' : '_') . '\' . $__resolved)'; $output .= '<' . '?php $__blaze->pushData($attributes->all()); ?>'; diff --git a/src/Compiler/Profiler.php b/src/Compiler/Profiler.php index b55f3eca..c0c3b058 100644 --- a/src/Compiler/Profiler.php +++ b/src/Compiler/Profiler.php @@ -20,9 +20,13 @@ */ class Profiler { + protected BladeService $bladeService; + public function __construct( protected Config $config, + BladeService $bladeService, ) { + $this->bladeService = $bladeService; } /** @@ -30,7 +34,7 @@ public function __construct( */ public function profile(Node $node, string $componentName, ?string $strategy = null): Node { - $source = new ComponentSource(BladeService::componentNameToPath($componentName)); + $source = new ComponentSource($this->bladeService->componentNameToPath($componentName)); if ($strategy === null) { $isBlade = $node instanceof ComponentNode; @@ -157,7 +161,7 @@ protected function isComponentTemplatePath(string $path): bool $dirs = [resource_path('views/components')]; - foreach (app('blade.compiler')->getAnonymousComponentPaths() as $registration) { + foreach ($this->bladeService->compiler->getAnonymousComponentPaths() as $registration) { $dirs[] = $registration['path']; } diff --git a/src/Compiler/Wrapper.php b/src/Compiler/Wrapper.php index 27a8ee96..be3af1e7 100644 --- a/src/Compiler/Wrapper.php +++ b/src/Compiler/Wrapper.php @@ -3,9 +3,9 @@ namespace Livewire\Blaze\Compiler; use Livewire\Blaze\BladeService; +use Livewire\Blaze\BlazeManager; use Livewire\Blaze\Compiler\DirectiveCompiler; use Livewire\Blaze\Support\Utils; -use Livewire\Blaze\Blaze; use Illuminate\Support\Arr; /** @@ -13,11 +13,18 @@ */ class Wrapper { + protected PropsCompiler $propsCompiler; + protected AwareCompiler $awareCompiler; + protected UseExtractor $useExtractor; + public function __construct( - protected PropsCompiler $propsCompiler = new PropsCompiler, - protected AwareCompiler $awareCompiler = new AwareCompiler, - protected UseExtractor $useExtractor = new UseExtractor, - ) {} + protected BladeService $bladeService, + protected BlazeManager $manager, + ) { + $this->propsCompiler = new PropsCompiler; + $this->awareCompiler = new AwareCompiler; + $this->useExtractor = new UseExtractor; + } /** * Compile a component template into a function definition. @@ -29,13 +36,13 @@ public function __construct( public function wrap(string $compiled, string $path, ?string $source = null): string { $source ??= $compiled; - $name = (Blaze::isFolding() ? '__' : '_') . Utils::hash($path); + $name = ($this->manager->isFolding() ? '__' : '_') . Utils::hash($path); $sourceUsesThis = str_contains($source, '$this') || str_contains($compiled, '@entangle') || str_contains($compiled, '@script') || str_contains($compiled, '@assets'); - $compiled = BladeService::compileUseStatements($compiled); - $compiled = BladeService::restoreRawBlocks($compiled); - $compiled = BladeService::storeVerbatimBlocks($compiled); + $compiled = $this->bladeService->compileUseStatements($compiled); + $compiled = $this->bladeService->restoreRawBlocks($compiled); + $compiled = $this->bladeService->storeVerbatimBlocks($compiled); $imports = ''; @@ -43,7 +50,7 @@ public function wrap(string $compiled, string $path, ?string $source = null): st $imports .= $statement . "\n"; }); - $compiled = BladeService::preStoreUncompiledBlocks($compiled); + $compiled = $this->bladeService->preStoreUncompiledBlocks($compiled); $output = ''; @@ -66,7 +73,7 @@ public function wrap(string $compiled, string $path, ?string $source = null): st $compiled = DirectiveCompiler::make()->directive('props', $this->propsCompiler->compile(...))->compile($compiled); $compiled = DirectiveCompiler::make()->directive('aware', $this->awareCompiler->compile(...))->compile($compiled); - $compiled = BladeService::restoreRawBlocks($compiled); + $compiled = $this->bladeService->restoreRawBlocks($compiled); $output .= $compiled; @@ -119,7 +126,7 @@ protected function globalVariables(string $source, string $compiled): string */ protected function hasEchoHandlers(): bool { - $compiler = app('blade.compiler'); + $compiler = $this->bladeService->compiler; $reflection = new \ReflectionProperty($compiler, 'echoHandlers'); return ! empty($reflection->getValue($compiler)); diff --git a/src/DebuggerMiddleware.php b/src/DebuggerMiddleware.php index 2430cd73..141c5504 100644 --- a/src/DebuggerMiddleware.php +++ b/src/DebuggerMiddleware.php @@ -159,7 +159,8 @@ protected function storeProfilerTrace(string $url, Debugger $debugger, bool $isB */ protected function injectRenderTimer(\Illuminate\View\View $view): bool { - $compiler = app('blade.compiler'); + $bladeService = app(BladeService::class); + $compiler = $bladeService->compiler; $path = $view->getPath(); // Some views (e.g. Livewire virtual views) may not have a real path. diff --git a/src/Folder/Foldable.php b/src/Folder/Foldable.php index 1e79c555..7214fdce 100644 --- a/src/Folder/Foldable.php +++ b/src/Folder/Foldable.php @@ -27,6 +27,7 @@ class Foldable public function __construct( protected ComponentNode $node, protected ComponentSource $source, + protected BladeService $bladeService, ) { } @@ -48,7 +49,7 @@ public function fold(): string $this->setupSlots(); $this->mergeAwareProps(); - $this->html = BladeService::render($this->renderable->render()); + $this->html = $this->bladeService->render($this->renderable->render()); $this->processUncompiledAttributes(); $this->restorePlaceholders(); @@ -224,7 +225,7 @@ protected function restorePlaceholders(): void $content = $match[0]; foreach ($this->attributeByPlaceholder as $placeholder => $attribute) { - $value = $attribute->bound() ? $attribute->value : BladeService::compileAttributeEchos($attribute->value); + $value = $attribute->bound() ? $attribute->value : $this->bladeService->compileAttributeEchos($attribute->value); $content = str_replace("'" . $placeholder . "'", $value, $content); } @@ -263,7 +264,7 @@ protected function wrapWithAwareMacros(): void if ($attribute->bound()) { $data[] = var_export($attribute->propName, true).' => '.$attribute->value; } else { - $data[] = var_export($attribute->propName, true).' => '.BladeService::compileAttributeEchos($attribute->value); + $data[] = var_export($attribute->propName, true).' => '.$this->bladeService->compileAttributeEchos($attribute->value); } } @@ -274,4 +275,4 @@ protected function wrapWithAwareMacros(): void 'popData(); $__env->popConsumableComponentData(); ?>', ); } -} \ No newline at end of file +} diff --git a/src/Folder/Folder.php b/src/Folder/Folder.php index 8e5c5639..d207ef49 100644 --- a/src/Folder/Folder.php +++ b/src/Folder/Folder.php @@ -11,8 +11,8 @@ use Livewire\Blaze\Parser\Nodes\TextNode; use Livewire\Blaze\Support\ComponentSource; use Livewire\Blaze\BladeService; +use Livewire\Blaze\BlazeManager; use Illuminate\Support\Arr; -use Livewire\Blaze\Blaze; use Livewire\Blaze\Config; /** @@ -21,7 +21,9 @@ class Folder { public function __construct( - protected ?Config $config = null, + protected Config $config, + protected BladeService $bladeService, + protected BlazeManager $manager, ) { } @@ -36,7 +38,7 @@ public function fold(Node $node): Node $component = $node; - $source = new ComponentSource(BladeService::componentNameToPath($component->name)); + $source = new ComponentSource($this->bladeService->componentNameToPath($component->name)); if (! $source->exists()) { return $component; @@ -53,7 +55,7 @@ public function fold(Node $node): Node $this->checkProblematicPatterns($source); try { - $foldable = new Foldable($node, $source); + $foldable = new Foldable($node, $source, $this->bladeService); $html = $foldable->fold(); @@ -65,7 +67,7 @@ public function fold(Node $node): Node return new TextNode($html); } catch (\Exception $e) { - if (Blaze::shouldThrow()) { + if ($this->manager->shouldThrow()) { throw $e; } diff --git a/src/Memoizer/Memoizer.php b/src/Memoizer/Memoizer.php index 463b5306..0ad767f9 100644 --- a/src/Memoizer/Memoizer.php +++ b/src/Memoizer/Memoizer.php @@ -3,6 +3,7 @@ namespace Livewire\Blaze\Memoizer; use Livewire\Blaze\BladeService; +use Livewire\Blaze\BlazeManager; use Livewire\Blaze\Parser\Nodes\ComponentNode; use Livewire\Blaze\Parser\Nodes\TextNode; use Livewire\Blaze\Parser\Nodes\Node; @@ -18,6 +19,8 @@ class Memoizer public function __construct( protected Config $config, protected Compiler $compiler, + protected BladeService $bladeService, + protected BlazeManager $manager, ) { } @@ -45,14 +48,14 @@ public function memoize(Node $node): Node if ($attr->bound()) { $parts[] = "'{$attr->propName}' => {$attr->value}"; } else { - $parts[] = "'{$attr->propName}' => ".BladeService::compileAttributeEchos($attr->value); + $parts[] = "'{$attr->propName}' => ".$this->bladeService->compileAttributeEchos($attr->value); } } $attributes = '['.implode(', ', $parts).']'; $compiled = $this->compiler->compile($node)->render(); - $isDebugging = app('blaze')->isDebugging() && ! app('blaze')->isFolding(); + $isDebugging = $this->manager->isDebugging() && ! $this->manager->isFolding(); $output = '<' . '?php $blaze_memoized_key = \Livewire\Blaze\Memoizer\Memo::key("' . $name . '", ' . $attributes . '); ?>'; $output .= '<' . '?php if ($blaze_memoized_key !== null && \Livewire\Blaze\Memoizer\Memo::has($blaze_memoized_key)) : ?>'; @@ -78,7 +81,7 @@ protected function isMemoizable(Node $node): bool return false; } - $source = new ComponentSource(BladeService::componentNameToPath($node->name)); + $source = new ComponentSource($this->bladeService->componentNameToPath($node->name)); if (! $source->exists()) { return false; diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php index 6be76246..53ad33ce 100644 --- a/src/Parser/Parser.php +++ b/src/Parser/Parser.php @@ -22,6 +22,7 @@ class Parser { public function __construct( protected Tokenizer $tokenizer, + protected BladeService $bladeService, ) { } @@ -63,7 +64,7 @@ protected function handleTagOpen(TagOpenToken $token, ParseStack $stack): void children: [], selfClosing: false, attributes: AttributeParser::parseAttributeStringToArray( - BladeService::preprocessAttributeString($attributeString) + $this->bladeService->preprocessAttributeString($attributeString) ), ); @@ -84,7 +85,7 @@ protected function handleTagSelfClose(TagSelfCloseToken $token, ParseStack $stac children: [], selfClosing: true, attributes: AttributeParser::parseAttributeStringToArray( - BladeService::preprocessAttributeString($attributeString) + $this->bladeService->preprocessAttributeString($attributeString) ), ); @@ -114,7 +115,7 @@ protected function handleSlotOpen(SlotOpenToken $token, ParseStack $stack): void prefix: $token->prefix, closeHasName: false, attributes: AttributeParser::parseAttributeStringToArray( - BladeService::preprocessAttributeString($attributeString) + $this->bladeService->preprocessAttributeString($attributeString) ), ); @@ -144,4 +145,4 @@ protected function handleText(TextToken $token, ParseStack $stack): void $stack->addToRoot($node); } -} \ No newline at end of file +} diff --git a/src/Runtime/BlazeRuntime.php b/src/Runtime/BlazeRuntime.php index cc82112a..400472bb 100644 --- a/src/Runtime/BlazeRuntime.php +++ b/src/Runtime/BlazeRuntime.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\View\Factory; use Illuminate\Foundation\Application; use Illuminate\Support\ViewErrorBag; +use Illuminate\View\Compilers\BladeCompiler; use Livewire\Blaze\BladeService; use Livewire\Blaze\Support\Utils; use Livewire\Blaze\Debugger; @@ -32,12 +33,13 @@ class BlazeRuntime protected array $dataStack = []; protected array $slotsStack = []; - public function __construct() - { + public function __construct( + BladeCompiler $compiler, + ) { + $this->compiler = $compiler; $this->env = app('view'); $this->app = app(); $this->debugger = app('blaze.debugger'); - $this->compiler = app('blade.compiler'); } /** @@ -70,7 +72,7 @@ public function resolve(string $component): string|false if (isset($this->paths[$component])) { $path = $this->paths[$component]; } else { - $path = $this->paths[$component] = BladeService::componentNameToPath($component); + $path = $this->paths[$component] = $this->bladeService->componentNameToPath($component); } if (! $this->isBlazeComponent($path)) { diff --git a/tests/Compiler/WrapperTest.php b/tests/Compiler/WrapperTest.php index f24b8145..a765bc22 100644 --- a/tests/Compiler/WrapperTest.php +++ b/tests/Compiler/WrapperTest.php @@ -95,7 +95,7 @@ test('hoists use statements to top of output', function ($statement) { // Replace raw @php blocks for placeholders. This normally happens in BlazeManager before the template gets to the Wrapper - $source = BladeService::preStoreUncompiledBlocks($statement); + $source = app(BladeService::class)->preStoreUncompiledBlocks($statement); expect(app(Wrapper::class)->wrap($source, '', $source))->toStartWith("with([ @@ -108,4 +108,4 @@ $input = '@php something @endphp'; expect(app(Wrapper::class)->wrap($input, ''))->toContain($input); -}); \ No newline at end of file +}); diff --git a/tests/Folder/FoldableTest.php b/tests/Folder/FoldableTest.php index c6e027e0..94dd24e6 100644 --- a/tests/Folder/FoldableTest.php +++ b/tests/Folder/FoldableTest.php @@ -10,7 +10,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php'))); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php')), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( '' @@ -32,7 +32,7 @@ ; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php'))); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php')), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( '
{{ $title }} | {{ $content }} | {{ $author }}
' @@ -56,7 +56,7 @@ ; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php'))); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php')), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( '
{{ $title }} | Before {{ $content }} After | {{ $author }}
' @@ -67,7 +67,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php'))); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php')), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( '' @@ -78,7 +78,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php'))); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php')), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( '' @@ -89,7 +89,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-aware.blade.php'))); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-aware.blade.php')), app(BladeService::class)); $node->setParentsAttributes([ 'type' => new Attribute( @@ -120,7 +120,7 @@ ), ]); - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-aware.blade.php'))); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-aware.blade.php')), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( '' @@ -131,7 +131,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php'))); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php')), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('', join('', [ @@ -148,7 +148,7 @@ $node = app(Parser::class)->parse($input)[0]; $node->hasAwareDescendants = true; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php'))); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php')), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace(join('', [ 'pushData([\'name\' => \'John\']); $__env->pushConsumableComponentData([\'name\' => \'John\']); ?>', @@ -163,7 +163,7 @@ $node = app(Parser::class)->parse($input)[0]; $node->hasAwareDescendants = true; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php'))); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php')), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace(join('', [ 'pushData([\'name\' => $name]); $__env->pushConsumableComponentData([\'name\' => $name]); ?>', @@ -178,11 +178,11 @@ $node = app(Parser::class)->parse($input)[0]; $node->hasAwareDescendants = true; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php'))); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php')), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace(join('', [ 'pushData([\'name\' => \'Mr. \'.e($name)]); $__env->pushConsumableComponentData([\'name\' => \'Mr. \'.e($name)]); ?>', '
Default | | Default
', 'popData(); $__env->popConsumableComponentData(); ?>', ])); -}); \ No newline at end of file +}); diff --git a/tests/Folder/UnblazeTest.php b/tests/Folder/UnblazeTest.php index e3164288..e1c0803c 100644 --- a/tests/Folder/UnblazeTest.php +++ b/tests/Folder/UnblazeTest.php @@ -9,7 +9,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-unblaze.blade.php'))); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-unblaze.blade.php')), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('', join('', [ @@ -25,7 +25,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/nested-input-unblaze.blade.php'))); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/nested-input-unblaze.blade.php')), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('
', join('', [ @@ -41,7 +41,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-unblaze.blade.php'))); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-unblaze.blade.php')), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('', join('', [ @@ -51,4 +51,4 @@ '' ])) ); -}); \ No newline at end of file +}); From 43bb269dc060acbbd6a16182733901efc0d15acd Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sat, 28 Feb 2026 20:14:25 +0100 Subject: [PATCH 11/28] Add tests --- tests/IntegrationTest.php | 45 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index ec78969b..7e0edf2c 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -55,7 +55,7 @@ view('php-view')->render(); })->throwsNoExceptions(); -test('doesnt resolve blade compiler', function () { +test('doesnt resolve blade compiler from the container', function () { Artisan::call('view:clear'); $compiler = app('blade.compiler'); @@ -71,4 +71,45 @@ }); $compiler->compile(fixture_path('views/blaze.blade.php')); -})->throwsNoExceptions(); \ No newline at end of file +})->throwsNoExceptions(); + +test('doesnt resolve blade compiler from the container when using debug mode', function () { + Artisan::call('view:clear'); + + Blaze::debug(); + + $compiler = app('blade.compiler'); + + Blaze::clearResolvedInstance(); + + app()->forgetInstance(BlazeManager::class); + app()->forgetInstance(BladeService::class); + app()->forgetInstance('blade.compiler'); + + app()->resolving('blade.compiler', function () { + test()->fail('Blade compiler was resolved from container'); + }); + + $compiler->compile(fixture_path('views/blaze.blade.php')); +})->throwsNoExceptions(); + +test('doesnt resolve blade compiler from the container when using debug mode with blaze off', function () { + Artisan::call('view:clear'); + + Blaze::debug(); + Blaze::disable(); + + $compiler = app('blade.compiler'); + + Blaze::clearResolvedInstance(); + + app()->forgetInstance(BlazeManager::class); + app()->forgetInstance(BladeService::class); + app()->forgetInstance('blade.compiler'); + + app()->resolving('blade.compiler', function () { + test()->fail('Blade compiler was resolved from container'); + }); + + $compiler->compile(fixture_path('views/blaze.blade.php')); +})->throwsNoExceptions(); From e86846714f4f13d1d30dec1f54fdba776d20c3ab Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 1 Mar 2026 13:05:10 +0100 Subject: [PATCH 12/28] Extract isolated rendering from BladeService --- src/BladeRenderer.php | 157 ++++++++++++++++++++++++++++++++++ src/BladeService.php | 138 ------------------------------ src/BlazeManager.php | 9 +- src/Folder/Foldable.php | 4 +- src/Folder/Folder.php | 4 +- src/Runtime/BlazeRuntime.php | 10 ++- tests/Folder/FoldableTest.php | 23 ++--- tests/Folder/UnblazeTest.php | 7 +- 8 files changed, 191 insertions(+), 161 deletions(-) create mode 100644 src/BladeRenderer.php diff --git a/src/BladeRenderer.php b/src/BladeRenderer.php new file mode 100644 index 00000000..d5ef7c5a --- /dev/null +++ b/src/BladeRenderer.php @@ -0,0 +1,157 @@ +isolatedRender($template); + } + + /** + * Get the temporary cache directory path used during isolated rendering. + */ + public function getTemporaryCachePath(): string + { + return config('view.compiled').'/blaze'; + } + + /** + * Render a Blade template string in isolation by freezing and restoring compiler state. + */ + public function isolatedRender(string $template): string + { + $compiler = $this->compiler; + + $temporaryCachePath = $this->getTemporaryCachePath(); + + File::ensureDirectoryExists($temporaryCachePath); + + $factory = $this->factory; + + [$factory, $restoreFactory] = $this->freezeObjectProperties($factory, [ + 'renderCount' => 0, + 'renderedOnce' => [], + 'sections' => [], + 'sectionStack' => [], + 'pushes' => [], + 'prepends' => [], + 'pushStack' => [], + 'componentStack' => [], + 'componentData' => [], + 'currentComponentData' => [], + 'slots' => [], + 'slotStack' => [], + 'fragments' => [], + 'fragmentStack' => [], + 'loopsStack' => [], + 'translationReplacements' => [], + ]); + + [$compiler, $restore] = $this->freezeObjectProperties($compiler, [ + 'cachePath' => $temporaryCachePath, + 'rawBlocks' => [], + 'footer' => [], + 'prepareStringsForCompilationUsing' => [ + function ($input) use ($compiler) { + if (Unblaze::hasUnblaze($input)) { + $input = Unblaze::processUnblazeDirectives($input); + }; + + $input = $this->manager->compileForFolding($input, $compiler->getPath()); + + return $input; + }, + ], + 'path' => null, + 'forElseCounter' => 0, + 'firstCaseInSwitch' => true, + 'lastSection' => null, + 'lastFragment' => null, + ]); + + [$runtime, $restoreRuntime] = $this->freezeObjectProperties($this->runtime, [ + 'compiled' => [], + 'paths' => [], + 'compiledPath' => $temporaryCachePath, + 'dataStack' => [], + 'slotsStack' => [], + ]); + + try { + $this->manager->startFolding(); + + $result = $compiler->render($template, deleteCachedView: true); + } finally { + $restore(); + $restoreFactory(); + $restoreRuntime(); + + $this->manager->stopFolding(); + } + + $result = Unblaze::replaceUnblazePrecompiledDirectives($result); + + return $result; + } + + /** + * Delete the temporary cache directory created during isolated rendering. + */ + public function deleteTemporaryCacheDirectory(): void + { + File::deleteDirectory($this->getTemporaryCachePath()); + } + + /** + * Snapshot object properties and return a restore closure to revert them. + */ + protected function freezeObjectProperties(object $object, array $properties) + { + $reflection = new ReflectionClass($object); + + $frozen = []; + + foreach ($properties as $key => $value) { + $name = is_numeric($key) ? $value : $key; + + $property = $reflection->getProperty($name); + + $frozen[$name] = $property->getValue($object); + + if (! is_numeric($key)) { + $property->setValue($object, $value); + } + } + + return [ + $object, + function () use ($reflection, $object, $frozen) { + foreach ($frozen as $name => $value) { + $property = $reflection->getProperty($name); + $property->setValue($object, $value); + } + }, + ]; + } +} diff --git a/src/BladeService.php b/src/BladeService.php index 52408398..cd31578d 100644 --- a/src/BladeService.php +++ b/src/BladeService.php @@ -3,125 +3,18 @@ namespace Livewire\Blaze; use Illuminate\Support\Facades\Event; -use Illuminate\Support\Facades\File; use Illuminate\Support\Str; use Illuminate\View\Compilers\BladeCompiler; use Illuminate\View\Compilers\ComponentTagCompiler; use Livewire\Blaze\Compiler\DirectiveCompiler; -use Livewire\Blaze\Runtime\BlazeRuntime; use ReflectionClass; class BladeService { public function __construct( public BladeCompiler $compiler, - public BlazeRuntime $runtime, - public BlazeManager $manager, ) {} - /** - * Render a Blade template string in an isolated context. - */ - public function render(string $template): string - { - return $this->isolatedRender($template); - } - - /** - * Get the temporary cache directory path used during isolated rendering. - */ - public function getTemporaryCachePath(): string - { - return config('view.compiled').'/blaze'; - } - - /** - * Render a Blade template string in isolation by freezing and restoring compiler state. - */ - public function isolatedRender(string $template): string - { - $compiler = $this->compiler; - - $temporaryCachePath = $this->getTemporaryCachePath(); - - File::ensureDirectoryExists($temporaryCachePath); - - $factory = app('view'); - - [$factory, $restoreFactory] = $this->freezeObjectProperties($factory, [ - 'renderCount' => 0, - 'renderedOnce' => [], - 'sections' => [], - 'sectionStack' => [], - 'pushes' => [], - 'prepends' => [], - 'pushStack' => [], - 'componentStack' => [], - 'componentData' => [], - 'currentComponentData' => [], - 'slots' => [], - 'slotStack' => [], - 'fragments' => [], - 'fragmentStack' => [], - 'loopsStack' => [], - 'translationReplacements' => [], - ]); - - [$compiler, $restore] = $this->freezeObjectProperties($compiler, [ - 'cachePath' => $temporaryCachePath, - 'rawBlocks' => [], - 'footer' => [], - 'prepareStringsForCompilationUsing' => [ - function ($input) use ($compiler) { - if (Unblaze::hasUnblaze($input)) { - $input = Unblaze::processUnblazeDirectives($input); - }; - - $input = $this->manager->compileForFolding($input, $compiler->getPath()); - - return $input; - }, - ], - 'path' => null, - 'forElseCounter' => 0, - 'firstCaseInSwitch' => true, - 'lastSection' => null, - 'lastFragment' => null, - ]); - - [$runtime, $restoreRuntime] = $this->freezeObjectProperties($this->runtime, [ - 'compiled' => [], - 'paths' => [], - 'compiledPath' => $temporaryCachePath, - 'dataStack' => [], - 'slotsStack' => [], - ]); - - try { - $this->manager->startFolding(); - - $result = $compiler->render($template, deleteCachedView: true); - } finally { - $restore(); - $restoreFactory(); - $restoreRuntime(); - - $this->manager->stopFolding(); - } - - $result = Unblaze::replaceUnblazePrecompiledDirectives($result); - - return $result; - } - - /** - * Delete the temporary cache directory created during isolated rendering. - */ - public function deleteTemporaryCacheDirectory(): void - { - File::deleteDirectory($this->getTemporaryCachePath()); - } - /** * Check if template content is a Laravel exception view. */ @@ -401,35 +294,4 @@ public function componentNameToPath($name): string } } - /** - * Snapshot object properties and return a restore closure to revert them. - */ - protected function freezeObjectProperties(object $object, array $properties) - { - $reflection = new ReflectionClass($object); - - $frozen = []; - - foreach ($properties as $key => $value) { - $name = is_numeric($key) ? $value : $key; - - $property = $reflection->getProperty($name); - - $frozen[$name] = $property->getValue($object); - - if (! is_numeric($key)) { - $property->setValue($object, $value); - } - } - - return [ - $object, - function () use ($reflection, $object, $frozen) { - foreach ($frozen as $name => $value) { - $property = $reflection->getProperty($name); - $property->setValue($object, $value); - } - }, - ]; - } } diff --git a/src/BlazeManager.php b/src/BlazeManager.php index 3b4f1fa5..85d7511d 100644 --- a/src/BlazeManager.php +++ b/src/BlazeManager.php @@ -39,18 +39,19 @@ class BlazeManager protected Memoizer $memoizer; protected Wrapper $wrapper; protected Profiler $instrumenter; - protected BladeService $bladeService; + protected BladeRenderer $renderer; public function __construct( protected Config $config, protected BladeCompiler $bladeCompiler, protected BlazeRuntime $runtime, + protected BladeService $bladeService, ) { - $this->bladeService = new BladeService($bladeCompiler, $this->runtime, $this); + $this->renderer = new BladeRenderer($bladeCompiler, app('view'), $this->runtime, $this); $this->parser = new Parser(new Tokenizer, $this->bladeService); $this->walker = new Walker; $this->compiler = new Compiler($config, $this->bladeService, $this); - $this->folder = new Folder($config, $this->bladeService, $this); + $this->folder = new Folder($config, $this->bladeService, $this->renderer, $this); $this->memoizer = new Memoizer($config, $this->compiler, $this->bladeService, $this); $this->wrapper = new Wrapper($this->bladeService, $this); $this->instrumenter = new Profiler($config, $this->bladeService); @@ -125,7 +126,7 @@ public function compile(string $template, ?string $path = null): string $output = $this->bladeService->restoreRawBlocks($output); try { - $this->bladeService->deleteTemporaryCacheDirectory(); + $this->renderer->deleteTemporaryCacheDirectory(); } catch (\Throwable $e) { // } diff --git a/src/Folder/Foldable.php b/src/Folder/Foldable.php index 7214fdce..c2192c95 100644 --- a/src/Folder/Foldable.php +++ b/src/Folder/Foldable.php @@ -3,6 +3,7 @@ namespace Livewire\Blaze\Folder; use Illuminate\Support\Str; +use Livewire\Blaze\BladeRenderer; use Livewire\Blaze\BladeService; use Livewire\Blaze\Parser\Attribute; use Livewire\Blaze\Parser\Nodes\ComponentNode; @@ -27,6 +28,7 @@ class Foldable public function __construct( protected ComponentNode $node, protected ComponentSource $source, + protected BladeRenderer $renderer, protected BladeService $bladeService, ) { } @@ -49,7 +51,7 @@ public function fold(): string $this->setupSlots(); $this->mergeAwareProps(); - $this->html = $this->bladeService->render($this->renderable->render()); + $this->html = $this->renderer->render($this->renderable->render()); $this->processUncompiledAttributes(); $this->restorePlaceholders(); diff --git a/src/Folder/Folder.php b/src/Folder/Folder.php index d207ef49..aeec34a8 100644 --- a/src/Folder/Folder.php +++ b/src/Folder/Folder.php @@ -10,6 +10,7 @@ use Livewire\Blaze\Parser\Nodes\SlotNode; use Livewire\Blaze\Parser\Nodes\TextNode; use Livewire\Blaze\Support\ComponentSource; +use Livewire\Blaze\BladeRenderer; use Livewire\Blaze\BladeService; use Livewire\Blaze\BlazeManager; use Illuminate\Support\Arr; @@ -23,6 +24,7 @@ class Folder public function __construct( protected Config $config, protected BladeService $bladeService, + protected BladeRenderer $renderer, protected BlazeManager $manager, ) { } @@ -55,7 +57,7 @@ public function fold(Node $node): Node $this->checkProblematicPatterns($source); try { - $foldable = new Foldable($node, $source, $this->bladeService); + $foldable = new Foldable($node, $source, $this->renderer, $this->bladeService); $html = $foldable->fold(); diff --git a/src/Runtime/BlazeRuntime.php b/src/Runtime/BlazeRuntime.php index 400472bb..3d49b857 100644 --- a/src/Runtime/BlazeRuntime.php +++ b/src/Runtime/BlazeRuntime.php @@ -6,11 +6,11 @@ use Illuminate\Foundation\Application; use Illuminate\Support\ViewErrorBag; use Illuminate\View\Compilers\BladeCompiler; +use Illuminate\View\Compilers\Compiler; use Livewire\Blaze\BladeService; +use Livewire\Blaze\Support\Directives; use Livewire\Blaze\Support\Utils; use Livewire\Blaze\Debugger; -use Illuminate\View\Compilers\Compiler; -use Livewire\Blaze\Support\Directives; /** * Runtime context shared with all Blaze-compiled components via $__blaze. @@ -20,7 +20,9 @@ class BlazeRuntime public readonly Factory $env; public readonly Application $app; public readonly Debugger $debugger; - public readonly Compiler $compiler; + public readonly BladeCompiler $compiler; + + protected BladeService $bladeService; // Lazily cached from config('view.compiled') on first access via __get. // This ensures parallel-testing per-worker path overrides are respected. @@ -35,11 +37,13 @@ class BlazeRuntime public function __construct( BladeCompiler $compiler, + BladeService $bladeService, ) { $this->compiler = $compiler; $this->env = app('view'); $this->app = app(); $this->debugger = app('blaze.debugger'); + $this->bladeService = $bladeService; } /** diff --git a/tests/Folder/FoldableTest.php b/tests/Folder/FoldableTest.php index 94dd24e6..55c8c77d 100644 --- a/tests/Folder/FoldableTest.php +++ b/tests/Folder/FoldableTest.php @@ -1,5 +1,6 @@ '; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php')), app(BladeService::class)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( '' @@ -32,7 +33,7 @@ ; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php')), app(BladeService::class)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( '
{{ $title }} | {{ $content }} | {{ $author }}
' @@ -56,7 +57,7 @@ ; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php')), app(BladeService::class)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( '
{{ $title }} | Before {{ $content }} After | {{ $author }}
' @@ -67,7 +68,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php')), app(BladeService::class)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( '' @@ -78,7 +79,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php')), app(BladeService::class)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( '' @@ -89,7 +90,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-aware.blade.php')), app(BladeService::class)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-aware.blade.php')), app(BladeRenderer::class), app(BladeService::class)); $node->setParentsAttributes([ 'type' => new Attribute( @@ -120,7 +121,7 @@ ), ]); - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-aware.blade.php')), app(BladeService::class)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-aware.blade.php')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( '' @@ -131,7 +132,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php')), app(BladeService::class)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input.blade.php')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('', join('', [ @@ -148,7 +149,7 @@ $node = app(Parser::class)->parse($input)[0]; $node->hasAwareDescendants = true; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php')), app(BladeService::class)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace(join('', [ 'pushData([\'name\' => \'John\']); $__env->pushConsumableComponentData([\'name\' => \'John\']); ?>', @@ -163,7 +164,7 @@ $node = app(Parser::class)->parse($input)[0]; $node->hasAwareDescendants = true; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php')), app(BladeService::class)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace(join('', [ 'pushData([\'name\' => $name]); $__env->pushConsumableComponentData([\'name\' => $name]); ?>', @@ -178,7 +179,7 @@ $node = app(Parser::class)->parse($input)[0]; $node->hasAwareDescendants = true; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php')), app(BladeService::class)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/card.blade.php')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace(join('', [ 'pushData([\'name\' => \'Mr. \'.e($name)]); $__env->pushConsumableComponentData([\'name\' => \'Mr. \'.e($name)]); ?>', diff --git a/tests/Folder/UnblazeTest.php b/tests/Folder/UnblazeTest.php index e1c0803c..07d54133 100644 --- a/tests/Folder/UnblazeTest.php +++ b/tests/Folder/UnblazeTest.php @@ -1,5 +1,6 @@ '; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-unblaze.blade.php')), app(BladeService::class)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-unblaze.blade.php')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('', join('', [ @@ -25,7 +26,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/nested-input-unblaze.blade.php')), app(BladeService::class)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/nested-input-unblaze.blade.php')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('
', join('', [ @@ -41,7 +42,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-unblaze.blade.php')), app(BladeService::class)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-unblaze.blade.php')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('', join('', [ From 83faf5e73d9667dc93224989214c9e4e31a87d90 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 1 Mar 2026 14:05:25 +0100 Subject: [PATCH 13/28] Refactor --- src/BladeService.php | 90 +++++++++++++----------------------- src/BlazeManager.php | 38 +++++++-------- src/BlazeServiceProvider.php | 10 ++-- src/Compiler/Compiler.php | 6 +-- src/Runtime/BlazeRuntime.php | 21 +++------ tests/BladeCompilerTest.php | 76 ++++++++++++++++++++++++++++++ tests/IntegrationTest.php | 59 ----------------------- 7 files changed, 141 insertions(+), 159 deletions(-) create mode 100644 tests/BladeCompilerTest.php diff --git a/src/BladeService.php b/src/BladeService.php index cd31578d..2e66fe47 100644 --- a/src/BladeService.php +++ b/src/BladeService.php @@ -7,13 +7,18 @@ use Illuminate\View\Compilers\BladeCompiler; use Illuminate\View\Compilers\ComponentTagCompiler; use Livewire\Blaze\Compiler\DirectiveCompiler; +use Livewire\Blaze\Support\LaravelRegex; use ReflectionClass; class BladeService { + protected ComponentTagCompiler $tagCompiler; + public function __construct( public BladeCompiler $compiler, - ) {} + ) { + $this->tagCompiler = new ComponentTagCompiler(blade: $compiler); + } /** * Check if template content is a Laravel exception view. @@ -28,22 +33,9 @@ public function containsLaravelExceptionView(string $input): bool */ public function earliestPreCompilationHook(callable $callback): void { - $compiler = $this->compiler; - - app()->booted(function () use ($callback, $compiler) { - $compiler->prepareStringsForCompilationUsing(function ($input) use ($callback, $compiler) { - // We call getPath() on the captured $compiler instance rather than resolving it - // via app('blade.compiler')->getPath() inside BlazeManager, this fixes #43. - - // Packages like Sentry force blade resolution during boot using app('view')->getEngineResolver()->resolve('blade'). - // When Laravel runs `config:cache` as part of `optimize`, it swaps the application instance in the container, - // but later in `view:cache` it uses the original app instance from $this->laravel to compile the views. - // Because of the early resolution, Laravel doesn't resolve blade compiler again from the new instance - // and runs compile() on the stale one. Calling app('blade.compiler') returns a different instance - // than the one used to compile the view, therefore $path isn't set and getPath() returns null. - $path = $compiler->getPath(); - - return $callback($input, $path); + app()->booted(function () use ($callback) { + $this->compiler->prepareStringsForCompilationUsing(function ($input) use ($callback) { + return $callback($input, $this->compiler->getPath()); }); }); } @@ -53,19 +45,18 @@ public function earliestPreCompilationHook(callable $callback): void */ public function preStoreUncompiledBlocks(string $input): string { - $compiler = $this->compiler; - $reflection = new \ReflectionClass($compiler); + $reflection = new \ReflectionClass($this->compiler); $storeRawBlock = $reflection->getMethod('storeRawBlock'); $output = $input; - $output = preg_replace_callback('/(?invoke($compiler, "@verbatim{$matches[2]}@endverbatim"); + $output = preg_replace_callback(LaravelRegex::VERBATIM_BLOCK, function ($matches) use ($storeRawBlock) { + return $matches[1].$storeRawBlock->invoke($this->compiler, "@verbatim{$matches[2]}@endverbatim"); }, $output); - $output = preg_replace_callback('/(?invoke($compiler, "@php{$matches[1]}@endphp"); + $output = preg_replace_callback(LaravelRegex::PHP_BLOCK, function ($matches) use ($storeRawBlock) { + return $storeRawBlock->invoke($this->compiler, "@php{$matches[1]}@endphp"); }, $output); return $output; @@ -76,12 +67,10 @@ public function preStoreUncompiledBlocks(string $input): string */ public function storeVerbatimBlocks(string $input): string { - $compiler = $this->compiler; - - $reflection = new \ReflectionClass($compiler); + $reflection = new \ReflectionClass($this->compiler); $method = $reflection->getMethod('storeVerbatimBlocks'); - return $method->invoke($compiler, $input); + return $method->invoke($this->compiler, $input); } /** @@ -89,12 +78,10 @@ public function storeVerbatimBlocks(string $input): string */ public function restoreRawBlocks(string $input): string { - $compiler = $this->compiler; - - $reflection = new \ReflectionClass($compiler); + $reflection = new \ReflectionClass($this->compiler); $method = $reflection->getMethod('restoreRawContent'); - return $method->invoke($compiler, $input); + return $method->invoke($this->compiler, $input); } /** @@ -102,12 +89,10 @@ public function restoreRawBlocks(string $input): string */ public function restorePhpBlocks(string $input): string { - $compiler = $this->compiler; - - $reflection = new \ReflectionClass($compiler); + $reflection = new \ReflectionClass($this->compiler); $method = $reflection->getMethod('restorePhpBlocks'); - return $method->invoke($compiler, $input); + return $method->invoke($this->compiler, $input); } /** @@ -115,12 +100,10 @@ public function restorePhpBlocks(string $input): string */ public function compileComments(string $input): string { - $compiler = $this->compiler; - - $reflection = new \ReflectionClass($compiler); + $reflection = new \ReflectionClass($this->compiler); $compileComments = $reflection->getMethod('compileComments'); - return $compileComments->invoke($compiler, $input); + return $compileComments->invoke($this->compiler, $input); } /** @@ -135,8 +118,6 @@ public function compileComments(string $input): string */ public function preprocessAttributeString(string $attributeString): string { - $compiler = new ComponentTagCompiler(blade: $this->compiler); - // Laravel expects a space at the start of the attribute string... $attributeString = Str::start($attributeString, ' '); @@ -149,18 +130,16 @@ public function preprocessAttributeString(string $attributeString): string $str = $this->parseBindAttributes($str); return $str; - })->call($compiler, $attributeString); + })->call($this->tagCompiler, $attributeString); } public function compileUseStatements(string $input): string { return DirectiveCompiler::make()->directive('use', function ($expression) { - $compiler = $this->compiler; - - $reflection = new \ReflectionClass($compiler); + $reflection = new \ReflectionClass($this->compiler); $method = $reflection->getMethod('compileUse'); - return $method->invoke($compiler, $expression); + return $method->invoke($this->compiler, $expression); })->compile($input); } @@ -169,12 +148,10 @@ public function compileUseStatements(string $input): string */ public function compileAttributeEchos(string $input): string { - $compiler = new ComponentTagCompiler(blade: $this->compiler); - - $reflection = new \ReflectionClass($compiler); + $reflection = new \ReflectionClass($this->tagCompiler); $method = $reflection->getMethod('compileAttributeEchos'); - return Str::unwrap("'".$method->invoke($compiler, $input)."'", "''.", ".''"); + return Str::unwrap("'".$method->invoke($this->tagCompiler, $input)."'", "''.", ".''"); } /** @@ -182,7 +159,7 @@ public function compileAttributeEchos(string $input): string */ public function stripQuotes(string $input): string { - return (new ComponentTagCompiler(blade: $this->compiler))->stripQuotes($input); + return $this->tagCompiler->stripQuotes($input); } /** @@ -190,16 +167,14 @@ public function stripQuotes(string $input): string */ public function viewCacheInvalidationHook(callable $callback): void { - $compiler = $this->compiler; - - Event::listen('composing:*', function ($event, $params) use ($callback, $compiler) { + Event::listen('composing:*', function ($event, $params) use ($callback) { $view = $params[0]; if (! $view instanceof \Illuminate\View\View) { return; } - $invalidate = fn () => $compiler->compile($view->getPath()); + $invalidate = fn () => $this->compiler->compile($view->getPath()); $callback($view, $invalidate); }); @@ -210,12 +185,11 @@ public function viewCacheInvalidationHook(callable $callback): void */ public function componentNameToPath($name): string { - $compiler = $this->compiler; $viewFinder = app('view')->getFinder(); - $reflection = new \ReflectionClass($compiler); + $reflection = new \ReflectionClass($this->compiler); $pathsProperty = $reflection->getProperty('anonymousComponentPaths'); - $paths = $pathsProperty->getValue($compiler) ?? []; + $paths = $pathsProperty->getValue($this->compiler) ?? []; if (str_contains($name, '::')) { [$namespace, $componentName] = explode('::', $name, 2); diff --git a/src/BlazeManager.php b/src/BlazeManager.php index 85d7511d..7ea5afdf 100644 --- a/src/BlazeManager.php +++ b/src/BlazeManager.php @@ -45,16 +45,16 @@ public function __construct( protected Config $config, protected BladeCompiler $bladeCompiler, protected BlazeRuntime $runtime, - protected BladeService $bladeService, + protected BladeService $blade, ) { $this->renderer = new BladeRenderer($bladeCompiler, app('view'), $this->runtime, $this); - $this->parser = new Parser(new Tokenizer, $this->bladeService); + $this->parser = new Parser(new Tokenizer, $this->blade); $this->walker = new Walker; - $this->compiler = new Compiler($config, $this->bladeService, $this); - $this->folder = new Folder($config, $this->bladeService, $this->renderer, $this); - $this->memoizer = new Memoizer($config, $this->compiler, $this->bladeService, $this); - $this->wrapper = new Wrapper($this->bladeService, $this); - $this->instrumenter = new Profiler($config, $this->bladeService); + $this->compiler = new Compiler($config, $this->blade, $this); + $this->folder = new Folder($config, $this->blade, $this->renderer, $this); + $this->memoizer = new Memoizer($config, $this->compiler, $this->blade, $this); + $this->wrapper = new Wrapper($this->blade, $this); + $this->instrumenter = new Profiler($config, $this->blade); Event::listen(ComponentFolded::class, function (ComponentFolded $event) { $this->foldedEvents[] = $event; @@ -69,8 +69,8 @@ public function compile(string $template, ?string $path = null): string $source = $template; $clean = $template; - $clean = $this->bladeService->preStoreUncompiledBlocks($clean); - $clean = $this->bladeService->compileComments($clean); + $clean = $this->blade->preStoreUncompiledBlocks($clean); + $clean = $this->blade->compileComments($clean); $dataStack = []; @@ -123,7 +123,7 @@ public function compile(string $template, ?string $path = null): string $output = $this->instrumenter->profileView($output, $path, $source); } - $output = $this->bladeService->restoreRawBlocks($output); + $output = $this->blade->restoreRawBlocks($output); try { $this->renderer->deleteTemporaryCacheDirectory(); @@ -139,8 +139,8 @@ public function compile(string $template, ?string $path = null): string */ public function compileForUnblaze(string $template): string { - $template = $this->bladeService->preStoreUncompiledBlocks($template); - $template = $this->bladeService->compileComments($template); + $template = $this->blade->preStoreUncompiledBlocks($template); + $template = $this->blade->compileComments($template); $ast = $this->walker->walk( nodes: $this->parser->parse($template), @@ -181,8 +181,8 @@ public function compileForDebug(string $template, ?string $path = null): string $source = $template; $clean = $template; - $clean = $this->bladeService->preStoreUncompiledBlocks($clean); - $clean = $this->bladeService->compileComments($clean); + $clean = $this->blade->preStoreUncompiledBlocks($clean); + $clean = $this->blade->compileComments($clean); $ast = $this->walker->walk( nodes: $this->parser->parse($clean), @@ -202,7 +202,7 @@ public function compileForDebug(string $template, ?string $path = null): string $output = $this->instrumenter->profileView($output, $path, $source); } - $output = $this->bladeService->restoreRawBlocks($output); + $output = $this->blade->restoreRawBlocks($output); return $output; } @@ -215,8 +215,8 @@ public function compileForFolding(string $template, ?string $path = null): strin { $source = $template; - $template = $this->bladeService->preStoreUncompiledBlocks($template); - $template = $this->bladeService->compileComments($template); + $template = $this->blade->preStoreUncompiledBlocks($template); + $template = $this->blade->compileComments($template); $ast = $this->walker->walk( nodes: $this->parser->parse($template), @@ -228,7 +228,7 @@ public function compileForFolding(string $template, ?string $path = null): strin $output = $this->render($ast); - $output = $this->bladeService->restoreRawBlocks($output); + $output = $this->blade->restoreRawBlocks($output); if (! $path) { return $output; @@ -419,7 +419,7 @@ protected function hasAwareDescendant(ComponentNode|SlotNode $node): bool { foreach ($node->children as $child) { if ($child instanceof ComponentNode) { - $source = new ComponentSource($this->bladeService->componentNameToPath($child->name)); + $source = new ComponentSource($this->blade->componentNameToPath($child->name)); if (str_ends_with($child->name, 'delegate-component')) { return true; diff --git a/src/BlazeServiceProvider.php b/src/BlazeServiceProvider.php index 7ec44cae..d6caa265 100644 --- a/src/BlazeServiceProvider.php +++ b/src/BlazeServiceProvider.php @@ -112,11 +112,11 @@ protected function registerBladeMacros(): void */ protected function interceptBladeCompilation(): void { - $bladeService = $this->app->make(BladeService::class); + $blade = $this->app->make(BladeService::class); $blaze = $this->app->make(BlazeManager::class); - $bladeService->earliestPreCompilationHook(function ($input, $path) use ($bladeService, $blaze) { - if ($bladeService->containsLaravelExceptionView($input)) { + $blade->earliestPreCompilationHook(function ($input, $path) use ($blade, $blaze) { + if ($blade->containsLaravelExceptionView($input)) { return $input; } @@ -139,10 +139,10 @@ protected function interceptBladeCompilation(): void */ protected function interceptViewCacheInvalidation(): void { - $bladeService = $this->app->make(BladeService::class); + $blade = $this->app->make(BladeService::class); $blaze = $this->app->make(BlazeManager::class); - $bladeService->viewCacheInvalidationHook(function ($view, $invalidate) use ($blaze) { + $blade->viewCacheInvalidationHook(function ($view, $invalidate) use ($blaze) { if ($blaze->isDisabled()) { return; } diff --git a/src/Compiler/Compiler.php b/src/Compiler/Compiler.php index 80377912..d9100f6e 100644 --- a/src/Compiler/Compiler.php +++ b/src/Compiler/Compiler.php @@ -18,7 +18,7 @@ */ class Compiler { - protected ComponentTagCompiler $blade; + protected ComponentTagCompiler $tagCompiler; protected SlotCompiler $slotCompiler; public function __construct( @@ -27,7 +27,7 @@ public function __construct( protected BlazeManager $manager, ) { $this->slotCompiler = new SlotCompiler(fn (string $str) => $this->getAttributesAndBoundKeysArrayStrings($str, true)[0]); - $this->blade = new ComponentTagCompiler([], [], $bladeService->compiler); + $this->tagCompiler = new ComponentTagCompiler([], [], $bladeService->compiler); } /** @@ -184,7 +184,7 @@ protected function getAttributesAndBoundKeysArrayStrings(string $attributeString $boundKeysString = '[' . implode(', ', array_map(fn ($k) => "'{$k}'", $boundKeys)) . ']'; return [$attributesString, $boundKeysString]; - })->call($this->blade, $attributeString, $escapeBound); + })->call($this->tagCompiler, $attributeString, $escapeBound); } } diff --git a/src/Runtime/BlazeRuntime.php b/src/Runtime/BlazeRuntime.php index 3d49b857..c4263953 100644 --- a/src/Runtime/BlazeRuntime.php +++ b/src/Runtime/BlazeRuntime.php @@ -17,13 +17,6 @@ */ class BlazeRuntime { - public readonly Factory $env; - public readonly Application $app; - public readonly Debugger $debugger; - public readonly BladeCompiler $compiler; - - protected BladeService $bladeService; - // Lazily cached from config('view.compiled') on first access via __get. // This ensures parallel-testing per-worker path overrides are respected. protected ?string $compiledPath = null; @@ -36,14 +29,12 @@ class BlazeRuntime protected array $slotsStack = []; public function __construct( - BladeCompiler $compiler, - BladeService $bladeService, + public readonly Factory $env, + public readonly Application $app, + public readonly Debugger $debugger, + public readonly BladeCompiler $compiler, + protected BladeService $blade, ) { - $this->compiler = $compiler; - $this->env = app('view'); - $this->app = app(); - $this->debugger = app('blaze.debugger'); - $this->bladeService = $bladeService; } /** @@ -76,7 +67,7 @@ public function resolve(string $component): string|false if (isset($this->paths[$component])) { $path = $this->paths[$component]; } else { - $path = $this->paths[$component] = $this->bladeService->componentNameToPath($component); + $path = $this->paths[$component] = $this->blade->componentNameToPath($component); } if (! $this->isBlazeComponent($path)) { diff --git a/tests/BladeCompilerTest.php b/tests/BladeCompilerTest.php new file mode 100644 index 00000000..6f5d21f5 --- /dev/null +++ b/tests/BladeCompilerTest.php @@ -0,0 +1,76 @@ +forgetInstance(BlazeManager::class); + app()->forgetInstance(BladeService::class); + app()->forgetInstance('blade.compiler'); + + app()->resolving('blade.compiler', function () { + test()->fail('Blade compiler was resolved from container'); + }); + + $compiler->compile(fixture_path('views/blaze.blade.php')); +})->throwsNoExceptions(); + +test('doesnt resolve blade compiler from the container when using debug mode', function () { + Blaze::debug(); + + $compiler = app('blade.compiler'); + + Blaze::clearResolvedInstance(); + + app()->forgetInstance(BlazeManager::class); + app()->forgetInstance(BladeService::class); + app()->forgetInstance('blade.compiler'); + + app()->resolving('blade.compiler', function () { + test()->fail('Blade compiler was resolved from container'); + }); + + $compiler->compile(fixture_path('views/blaze.blade.php')); +})->throwsNoExceptions(); + +test('doesnt resolve blade compiler from the container when using debug mode with blaze off', function () { + Blaze::debug(); + Blaze::disable(); + + $compiler = app('blade.compiler'); + + Blaze::clearResolvedInstance(); + + app()->forgetInstance(BlazeManager::class); + app()->forgetInstance(BladeService::class); + app()->forgetInstance('blade.compiler'); + + app()->resolving('blade.compiler', function () { + test()->fail('Blade compiler was resolved from container'); + }); + + $compiler->compile(fixture_path('views/blaze.blade.php')); +})->throwsNoExceptions(); diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 7e0edf2c..ed706e9d 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -54,62 +54,3 @@ // rendered using the regular php engine. view('php-view')->render(); })->throwsNoExceptions(); - -test('doesnt resolve blade compiler from the container', function () { - Artisan::call('view:clear'); - - $compiler = app('blade.compiler'); - - Blaze::clearResolvedInstance(); - - app()->forgetInstance(BlazeManager::class); - app()->forgetInstance(BladeService::class); - app()->forgetInstance('blade.compiler'); - - app()->resolving('blade.compiler', function () { - test()->fail('Blade compiler was resolved from container'); - }); - - $compiler->compile(fixture_path('views/blaze.blade.php')); -})->throwsNoExceptions(); - -test('doesnt resolve blade compiler from the container when using debug mode', function () { - Artisan::call('view:clear'); - - Blaze::debug(); - - $compiler = app('blade.compiler'); - - Blaze::clearResolvedInstance(); - - app()->forgetInstance(BlazeManager::class); - app()->forgetInstance(BladeService::class); - app()->forgetInstance('blade.compiler'); - - app()->resolving('blade.compiler', function () { - test()->fail('Blade compiler was resolved from container'); - }); - - $compiler->compile(fixture_path('views/blaze.blade.php')); -})->throwsNoExceptions(); - -test('doesnt resolve blade compiler from the container when using debug mode with blaze off', function () { - Artisan::call('view:clear'); - - Blaze::debug(); - Blaze::disable(); - - $compiler = app('blade.compiler'); - - Blaze::clearResolvedInstance(); - - app()->forgetInstance(BlazeManager::class); - app()->forgetInstance(BladeService::class); - app()->forgetInstance('blade.compiler'); - - app()->resolving('blade.compiler', function () { - test()->fail('Blade compiler was resolved from container'); - }); - - $compiler->compile(fixture_path('views/blaze.blade.php')); -})->throwsNoExceptions(); From 607cc92b2b8cbdb84b170ce43382168ec9dced55 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 1 Mar 2026 14:07:14 +0100 Subject: [PATCH 14/28] Refactor --- src/Compiler/Compiler.php | 6 +++--- src/Compiler/Profiler.php | 9 +++------ src/Compiler/Wrapper.php | 14 +++++++------- src/Folder/Foldable.php | 6 +++--- src/Folder/Folder.php | 6 +++--- src/Memoizer/Memoizer.php | 6 +++--- 6 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/Compiler/Compiler.php b/src/Compiler/Compiler.php index d9100f6e..0c4009c1 100644 --- a/src/Compiler/Compiler.php +++ b/src/Compiler/Compiler.php @@ -23,11 +23,11 @@ class Compiler public function __construct( protected Config $config, - protected BladeService $bladeService, + protected BladeService $blade, protected BlazeManager $manager, ) { $this->slotCompiler = new SlotCompiler(fn (string $str) => $this->getAttributesAndBoundKeysArrayStrings($str, true)[0]); - $this->tagCompiler = new ComponentTagCompiler([], [], $bladeService->compiler); + $this->tagCompiler = new ComponentTagCompiler([], [], $blade->compiler); } /** @@ -43,7 +43,7 @@ public function compile(Node $node): Node return new TextNode($this->compileDelegateComponentTag($node)); } - $source = new ComponentSource($this->bladeService->componentNameToPath($node->name)); + $source = new ComponentSource($this->blade->componentNameToPath($node->name)); if (! $source->exists()) { return $node; diff --git a/src/Compiler/Profiler.php b/src/Compiler/Profiler.php index c0c3b058..d5811edb 100644 --- a/src/Compiler/Profiler.php +++ b/src/Compiler/Profiler.php @@ -20,13 +20,10 @@ */ class Profiler { - protected BladeService $bladeService; - public function __construct( protected Config $config, - BladeService $bladeService, + protected BladeService $blade, ) { - $this->bladeService = $bladeService; } /** @@ -34,7 +31,7 @@ public function __construct( */ public function profile(Node $node, string $componentName, ?string $strategy = null): Node { - $source = new ComponentSource($this->bladeService->componentNameToPath($componentName)); + $source = new ComponentSource($this->blade->componentNameToPath($componentName)); if ($strategy === null) { $isBlade = $node instanceof ComponentNode; @@ -161,7 +158,7 @@ protected function isComponentTemplatePath(string $path): bool $dirs = [resource_path('views/components')]; - foreach ($this->bladeService->compiler->getAnonymousComponentPaths() as $registration) { + foreach ($this->blade->compiler->getAnonymousComponentPaths() as $registration) { $dirs[] = $registration['path']; } diff --git a/src/Compiler/Wrapper.php b/src/Compiler/Wrapper.php index be3af1e7..013e491b 100644 --- a/src/Compiler/Wrapper.php +++ b/src/Compiler/Wrapper.php @@ -18,7 +18,7 @@ class Wrapper protected UseExtractor $useExtractor; public function __construct( - protected BladeService $bladeService, + protected BladeService $blade, protected BlazeManager $manager, ) { $this->propsCompiler = new PropsCompiler; @@ -40,9 +40,9 @@ public function wrap(string $compiled, string $path, ?string $source = null): st $sourceUsesThis = str_contains($source, '$this') || str_contains($compiled, '@entangle') || str_contains($compiled, '@script') || str_contains($compiled, '@assets'); - $compiled = $this->bladeService->compileUseStatements($compiled); - $compiled = $this->bladeService->restoreRawBlocks($compiled); - $compiled = $this->bladeService->storeVerbatimBlocks($compiled); + $compiled = $this->blade->compileUseStatements($compiled); + $compiled = $this->blade->restoreRawBlocks($compiled); + $compiled = $this->blade->storeVerbatimBlocks($compiled); $imports = ''; @@ -50,7 +50,7 @@ public function wrap(string $compiled, string $path, ?string $source = null): st $imports .= $statement . "\n"; }); - $compiled = $this->bladeService->preStoreUncompiledBlocks($compiled); + $compiled = $this->blade->preStoreUncompiledBlocks($compiled); $output = ''; @@ -73,7 +73,7 @@ public function wrap(string $compiled, string $path, ?string $source = null): st $compiled = DirectiveCompiler::make()->directive('props', $this->propsCompiler->compile(...))->compile($compiled); $compiled = DirectiveCompiler::make()->directive('aware', $this->awareCompiler->compile(...))->compile($compiled); - $compiled = $this->bladeService->restoreRawBlocks($compiled); + $compiled = $this->blade->restoreRawBlocks($compiled); $output .= $compiled; @@ -126,7 +126,7 @@ protected function globalVariables(string $source, string $compiled): string */ protected function hasEchoHandlers(): bool { - $compiler = $this->bladeService->compiler; + $compiler = $this->blade->compiler; $reflection = new \ReflectionProperty($compiler, 'echoHandlers'); return ! empty($reflection->getValue($compiler)); diff --git a/src/Folder/Foldable.php b/src/Folder/Foldable.php index c2192c95..db8761ec 100644 --- a/src/Folder/Foldable.php +++ b/src/Folder/Foldable.php @@ -29,7 +29,7 @@ public function __construct( protected ComponentNode $node, protected ComponentSource $source, protected BladeRenderer $renderer, - protected BladeService $bladeService, + protected BladeService $blade, ) { } @@ -227,7 +227,7 @@ protected function restorePlaceholders(): void $content = $match[0]; foreach ($this->attributeByPlaceholder as $placeholder => $attribute) { - $value = $attribute->bound() ? $attribute->value : $this->bladeService->compileAttributeEchos($attribute->value); + $value = $attribute->bound() ? $attribute->value : $this->blade->compileAttributeEchos($attribute->value); $content = str_replace("'" . $placeholder . "'", $value, $content); } @@ -266,7 +266,7 @@ protected function wrapWithAwareMacros(): void if ($attribute->bound()) { $data[] = var_export($attribute->propName, true).' => '.$attribute->value; } else { - $data[] = var_export($attribute->propName, true).' => '.$this->bladeService->compileAttributeEchos($attribute->value); + $data[] = var_export($attribute->propName, true).' => '.$this->blade->compileAttributeEchos($attribute->value); } } diff --git a/src/Folder/Folder.php b/src/Folder/Folder.php index aeec34a8..712c2ddf 100644 --- a/src/Folder/Folder.php +++ b/src/Folder/Folder.php @@ -23,7 +23,7 @@ class Folder { public function __construct( protected Config $config, - protected BladeService $bladeService, + protected BladeService $blade, protected BladeRenderer $renderer, protected BlazeManager $manager, ) { @@ -40,7 +40,7 @@ public function fold(Node $node): Node $component = $node; - $source = new ComponentSource($this->bladeService->componentNameToPath($component->name)); + $source = new ComponentSource($this->blade->componentNameToPath($component->name)); if (! $source->exists()) { return $component; @@ -57,7 +57,7 @@ public function fold(Node $node): Node $this->checkProblematicPatterns($source); try { - $foldable = new Foldable($node, $source, $this->renderer, $this->bladeService); + $foldable = new Foldable($node, $source, $this->renderer, $this->blade); $html = $foldable->fold(); diff --git a/src/Memoizer/Memoizer.php b/src/Memoizer/Memoizer.php index 0ad767f9..12765557 100644 --- a/src/Memoizer/Memoizer.php +++ b/src/Memoizer/Memoizer.php @@ -19,7 +19,7 @@ class Memoizer public function __construct( protected Config $config, protected Compiler $compiler, - protected BladeService $bladeService, + protected BladeService $blade, protected BlazeManager $manager, ) { } @@ -48,7 +48,7 @@ public function memoize(Node $node): Node if ($attr->bound()) { $parts[] = "'{$attr->propName}' => {$attr->value}"; } else { - $parts[] = "'{$attr->propName}' => ".$this->bladeService->compileAttributeEchos($attr->value); + $parts[] = "'{$attr->propName}' => ".$this->blade->compileAttributeEchos($attr->value); } } $attributes = '['.implode(', ', $parts).']'; @@ -81,7 +81,7 @@ protected function isMemoizable(Node $node): bool return false; } - $source = new ComponentSource($this->bladeService->componentNameToPath($node->name)); + $source = new ComponentSource($this->blade->componentNameToPath($node->name)); if (! $source->exists()) { return false; From ef70625b7c41f3356be5e3d2534aadac7d8bab34 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 1 Mar 2026 14:09:42 +0100 Subject: [PATCH 15/28] Refactor --- src/Compiler/Wrapper.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Compiler/Wrapper.php b/src/Compiler/Wrapper.php index 013e491b..5aaf24e6 100644 --- a/src/Compiler/Wrapper.php +++ b/src/Compiler/Wrapper.php @@ -71,8 +71,11 @@ public function wrap(string $compiled, string $path, ?string $source = null): st $output .= 'ob_start();' . "\n"; $output .= '?>' . "\n"; - $compiled = DirectiveCompiler::make()->directive('props', $this->propsCompiler->compile(...))->compile($compiled); - $compiled = DirectiveCompiler::make()->directive('aware', $this->awareCompiler->compile(...))->compile($compiled); + $compiled = DirectiveCompiler::make() + ->directive('props', $this->propsCompiler->compile(...)) + ->directive('aware', $this->awareCompiler->compile(...)) + ->compile($compiled); + $compiled = $this->blade->restoreRawBlocks($compiled); $output .= $compiled; From 3c8f1641ede809f7c71190cd2f54ce732a74cb88 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 1 Mar 2026 14:13:09 +0100 Subject: [PATCH 16/28] Refactor --- src/Parser/Parser.php | 14 +++++++------- src/Support/AttributeParser.php | 10 +++------- tests/Parser/ParserTest.php | 10 +++++----- tests/Support/AttributeParserTest.php | 16 ++++++++-------- 4 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php index 53ad33ce..840c4be9 100644 --- a/src/Parser/Parser.php +++ b/src/Parser/Parser.php @@ -22,7 +22,7 @@ class Parser { public function __construct( protected Tokenizer $tokenizer, - protected BladeService $bladeService, + protected BladeService $blade, ) { } @@ -63,8 +63,8 @@ protected function handleTagOpen(TagOpenToken $token, ParseStack $stack): void attributeString: $attributeString, children: [], selfClosing: false, - attributes: AttributeParser::parseAttributeStringToArray( - $this->bladeService->preprocessAttributeString($attributeString) + attributes: AttributeParser::parse( + $this->blade->preprocessAttributeString($attributeString) ), ); @@ -84,8 +84,8 @@ protected function handleTagSelfClose(TagSelfCloseToken $token, ParseStack $stac attributeString: $attributeString, children: [], selfClosing: true, - attributes: AttributeParser::parseAttributeStringToArray( - $this->bladeService->preprocessAttributeString($attributeString) + attributes: AttributeParser::parse( + $this->blade->preprocessAttributeString($attributeString) ), ); @@ -114,8 +114,8 @@ protected function handleSlotOpen(SlotOpenToken $token, ParseStack $stack): void children: [], prefix: $token->prefix, closeHasName: false, - attributes: AttributeParser::parseAttributeStringToArray( - $this->bladeService->preprocessAttributeString($attributeString) + attributes: AttributeParser::parse( + $this->blade->preprocessAttributeString($attributeString) ), ); diff --git a/src/Support/AttributeParser.php b/src/Support/AttributeParser.php index c21d7a5d..f99ffc3c 100644 --- a/src/Support/AttributeParser.php +++ b/src/Support/AttributeParser.php @@ -6,20 +6,16 @@ use Livewire\Blaze\Parser\Attribute; /** - * Parses component attribute strings into structured arrays, handling all Blade syntaxes. + * Parses component attribute strings into structured arrays. */ class AttributeParser { /** - * Parse an attribute string into a keyed array of Attribute objects. - * - * Uses Laravel's preprocessing pipeline to normalize all attribute syntaxes - * (:$var, :attr, {{ $attributes }}, @class, @style) into a uniform format, - * then matches with Laravel's single attribute regex. + * Parse preprocessed attribute string into a keyed array of Attribute objects. * * @return array */ - public static function parseAttributeStringToArray(string $attributesString): array + public static function parse(string $attributesString): array { preg_match_all(LaravelRegex::ATTRIBUTE_PATTERN, $attributesString, $matches, PREG_SET_ORDER); diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index 6a35602d..39921e62 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -16,7 +16,7 @@ prefix: 'x-', selfClosing: true, attributeString: 'class="my-4"', - attributes: AttributeParser::parseAttributeStringToArray('class="my-4"'), + attributes: AttributeParser::parse('class="my-4"'), ), ]); }); @@ -35,7 +35,7 @@ children: [ new TextNode('Footer'), ], - attributes: AttributeParser::parseAttributeStringToArray('class="p-2"'), + attributes: AttributeParser::parse('class="p-2"'), ) ] ), @@ -57,7 +57,7 @@ children: [ new TextNode('Footer'), ], - attributes: AttributeParser::parseAttributeStringToArray('class="p-2"'), + attributes: AttributeParser::parse('class="p-2"'), ) ] ), @@ -80,7 +80,7 @@ children: [ new TextNode('Footer'), ], - attributes: AttributeParser::parseAttributeStringToArray('class="p-2"'), + attributes: AttributeParser::parse('class="p-2"'), ) ] ), @@ -101,7 +101,7 @@ children: [ new TextNode('Body'), ], - attributes: AttributeParser::parseAttributeStringToArray('class="p-2"'), + attributes: AttributeParser::parse('class="p-2"'), ) ] ), diff --git a/tests/Support/AttributeParserTest.php b/tests/Support/AttributeParserTest.php index aa13a221..1c5ffea1 100644 --- a/tests/Support/AttributeParserTest.php +++ b/tests/Support/AttributeParserTest.php @@ -3,7 +3,7 @@ use Livewire\Blaze\Support\AttributeParser; test('parses bound attributes', function () { - $attrs = AttributeParser::parseAttributeStringToArray('bind:foo="bar"'); + $attrs = AttributeParser::parse('bind:foo="bar"'); expect($attrs)->toHaveKey('foo'); expect($attrs['foo']) @@ -14,7 +14,7 @@ }); test('parses escaped bound attributes', function () { - $attrs = AttributeParser::parseAttributeStringToArray('::key="value"'); + $attrs = AttributeParser::parse('::key="value"'); expect($attrs)->toHaveKey(':key'); expect($attrs[':key']) @@ -25,7 +25,7 @@ }); test('parses attributes without value', function () { - $attrs = AttributeParser::parseAttributeStringToArray('disabled'); + $attrs = AttributeParser::parse('disabled'); expect($attrs)->toHaveKey('disabled'); expect($attrs['disabled']) @@ -36,7 +36,7 @@ }); test('parses attributes with blade echo', function () { - $attrs = AttributeParser::parseAttributeStringToArray('title="{{ $x }}"'); + $attrs = AttributeParser::parse('title="{{ $x }}"'); expect($attrs)->toHaveKey('title'); expect($attrs['title']) @@ -46,7 +46,7 @@ }); test('parses attributes with raw blade echo', function () { - $attrs = AttributeParser::parseAttributeStringToArray('title="{!! $x !!}"'); + $attrs = AttributeParser::parse('title="{!! $x !!}"'); expect($attrs)->toHaveKey('title'); expect($attrs['title']) @@ -56,14 +56,14 @@ }); test('parses quotes', function () { - $attrs = AttributeParser::parseAttributeStringToArray('double="hello" single=\'hello\''); + $attrs = AttributeParser::parse('double="hello" single=\'hello\''); expect($attrs['double']->quotes)->toBe('"'); expect($attrs['single']->quotes)->toBe("'"); }); test('parses kebab case attributes', function () { - $attrs = AttributeParser::parseAttributeStringToArray('foo-bar="first"'); + $attrs = AttributeParser::parse('foo-bar="first"'); expect($attrs)->toHaveKey('fooBar'); expect($attrs['fooBar']) @@ -72,7 +72,7 @@ }); test('keeps first attribute when multiple camelize to same key', function () { - $attrs = AttributeParser::parseAttributeStringToArray('foo-bar="first" foo_bar="second"'); + $attrs = AttributeParser::parse('foo-bar="first" foo_bar="second"'); expect($attrs)->toHaveCount(1)->toHaveKey('fooBar'); expect($attrs['fooBar']->value)->toBe('first'); From 9366f76bf6fde2032a1bd1c5fc79f0bccda57fbc Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 1 Mar 2026 14:16:07 +0100 Subject: [PATCH 17/28] Refactor --- src/Compiler/DirectiveCompiler.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Compiler/DirectiveCompiler.php b/src/Compiler/DirectiveCompiler.php index 4992017b..266657f6 100644 --- a/src/Compiler/DirectiveCompiler.php +++ b/src/Compiler/DirectiveCompiler.php @@ -3,6 +3,7 @@ namespace Livewire\Blaze\Compiler; use Illuminate\Support\Arr; +use Illuminate\View\Compilers\BladeCompiler; /** * Compiles individual Blade directives using a sandboxed compiler @@ -50,7 +51,7 @@ public function compile(string $template): string */ private function createSandboxedCompiler() { - return new class(app('files'), config('view.compiled')) extends \Illuminate\View\Compilers\BladeCompiler + return new class(app('files'), config('view.compiled')) extends BladeCompiler { public function compileStatementsMadePublic($template) { From 6d128658bc40bb6455fe4fc13bed6a930c2158d3 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 1 Mar 2026 14:17:58 +0100 Subject: [PATCH 18/28] Refactor --- src/Compiler/DirectiveCompiler.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Compiler/DirectiveCompiler.php b/src/Compiler/DirectiveCompiler.php index 266657f6..f9f6008d 100644 --- a/src/Compiler/DirectiveCompiler.php +++ b/src/Compiler/DirectiveCompiler.php @@ -2,6 +2,7 @@ namespace Livewire\Blaze\Compiler; +use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Arr; use Illuminate\View\Compilers\BladeCompiler; @@ -51,7 +52,7 @@ public function compile(string $template): string */ private function createSandboxedCompiler() { - return new class(app('files'), config('view.compiled')) extends BladeCompiler + return new class(new Filesystem, sys_get_temp_dir()) extends BladeCompiler { public function compileStatementsMadePublic($template) { From efa5018f612c0b3ed439da8561949c9aa03297f6 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 1 Mar 2026 15:35:16 +0100 Subject: [PATCH 19/28] Refactor --- tests/BladeCompilerTest.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/BladeCompilerTest.php b/tests/BladeCompilerTest.php index 6f5d21f5..a98e47b9 100644 --- a/tests/BladeCompilerTest.php +++ b/tests/BladeCompilerTest.php @@ -10,12 +10,17 @@ * * During `php artisan optimize`, Laravel bootstraps a fresh application instance * in `config:cache`, but later in `view:cache` it uses the original instance - * to compile the views. Now, when we resolve blade compiler anywhere, + * to compile the views. Now, when we resolve the blade compiler anywhere, * it returns a different instance than the one compiling the view. * - * This only happens if blade engine is resolved during boot, - * which packages like Sentry can trigger. Normally Laravel - * would resolve the compiler from the new instance. + * This only happens if blade engine (which uses the compiler as a dependency) + * is resolved during boot by a package. Sentry does this to decorate it. + * Normally the compiler would get resolved from the new app instance. + * + * Blaze heavily depends on the blade compiler and its internal state. + * Since we can't trust the container, these tests guarantee that + * we never use it to resolve the compiler. We must always use + * the instance that registered the precompilation hooks. */ beforeEach(function () { From 1ddfa2c0e8e8babfd44f6b97377407a001c5ea26 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 1 Mar 2026 15:41:00 +0100 Subject: [PATCH 20/28] Refactor --- src/BladeRenderer.php | 45 +++++-------- tests/Compiler/DirectiveCompilerTest.php | 84 ++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 30 deletions(-) create mode 100644 tests/Compiler/DirectiveCompilerTest.php diff --git a/src/BladeRenderer.php b/src/BladeRenderer.php index d5ef7c5a..fe527094 100644 --- a/src/BladeRenderer.php +++ b/src/BladeRenderer.php @@ -14,20 +14,12 @@ class BladeRenderer { public function __construct( - protected BladeCompiler $compiler, + protected BladeCompiler $blade, protected Factory $factory, protected BlazeRuntime $runtime, protected BlazeManager $manager, ) {} - /** - * Render a Blade template string in an isolated context. - */ - public function render(string $template): string - { - return $this->isolatedRender($template); - } - /** * Get the temporary cache directory path used during isolated rendering. */ @@ -39,17 +31,13 @@ public function getTemporaryCachePath(): string /** * Render a Blade template string in isolation by freezing and restoring compiler state. */ - public function isolatedRender(string $template): string + public function render(string $template): string { - $compiler = $this->compiler; - $temporaryCachePath = $this->getTemporaryCachePath(); File::ensureDirectoryExists($temporaryCachePath); - $factory = $this->factory; - - [$factory, $restoreFactory] = $this->freezeObjectProperties($factory, [ + $restoreFactory = $this->freezeObjectProperties($this->factory, [ 'renderCount' => 0, 'renderedOnce' => [], 'sections' => [], @@ -68,17 +56,17 @@ public function isolatedRender(string $template): string 'translationReplacements' => [], ]); - [$compiler, $restore] = $this->freezeObjectProperties($compiler, [ + $restoreCompiler = $this->freezeObjectProperties($this->blade, [ 'cachePath' => $temporaryCachePath, 'rawBlocks' => [], 'footer' => [], 'prepareStringsForCompilationUsing' => [ - function ($input) use ($compiler) { + function ($input) { if (Unblaze::hasUnblaze($input)) { $input = Unblaze::processUnblazeDirectives($input); }; - $input = $this->manager->compileForFolding($input, $compiler->getPath()); + $input = $this->manager->compileForFolding($input, $this->blade->getPath()); return $input; }, @@ -90,7 +78,7 @@ function ($input) use ($compiler) { 'lastFragment' => null, ]); - [$runtime, $restoreRuntime] = $this->freezeObjectProperties($this->runtime, [ + $restoreRuntime = $this->freezeObjectProperties($this->runtime, [ 'compiled' => [], 'paths' => [], 'compiledPath' => $temporaryCachePath, @@ -101,9 +89,9 @@ function ($input) use ($compiler) { try { $this->manager->startFolding(); - $result = $compiler->render($template, deleteCachedView: true); + $result = $this->blade->render($template, deleteCachedView: true); } finally { - $restore(); + $restoreCompiler(); $restoreFactory(); $restoreRuntime(); @@ -144,14 +132,11 @@ protected function freezeObjectProperties(object $object, array $properties) } } - return [ - $object, - function () use ($reflection, $object, $frozen) { - foreach ($frozen as $name => $value) { - $property = $reflection->getProperty($name); - $property->setValue($object, $value); - } - }, - ]; + return function () use ($reflection, $object, $frozen) { + foreach ($frozen as $name => $value) { + $property = $reflection->getProperty($name); + $property->setValue($object, $value); + } + }; } } diff --git a/tests/Compiler/DirectiveCompilerTest.php b/tests/Compiler/DirectiveCompilerTest.php new file mode 100644 index 00000000..6a4a4bea --- /dev/null +++ b/tests/Compiler/DirectiveCompilerTest.php @@ -0,0 +1,84 @@ +directive('greet', fn ($expression) => "") + ->compile('@greet($name)'); + + expect($result)->toBe(""); +}); + +test('ignores built-in blade directives', function ($directive) { + $input = $directive; + + $result = DirectiveCompiler::make()->compile($input); + + expect($result)->toBe($input); +})->with([ + '@if($condition)', + '@foreach($items as $item)', + '@include("partial")', + '@extends("layout")', + '@yield("content")', + '@section("content")', +]); + +test('compiles custom directives while preserving built-in ones', function () { + $result = DirectiveCompiler::make() + ->directive('custom', fn ($expression) => "") + ->compile('@if($condition) @custom($value) @endif'); + + expect($result)->toBe('@if($condition) @endif'); +}); + +test('handles escaped directives', function () { + $result = DirectiveCompiler::make() + ->directive('custom', fn ($expression) => "COMPILED") + ->compile('@@custom($value)'); + + expect($result)->toBe('@custom($value)'); +}); + +test('preserves templates with no directives', function () { + $input = '
Hello World
'; + + $result = DirectiveCompiler::make()->compile($input); + + expect($result)->toBe($input); +}); + +test('compiles multiple custom directives', function () { + $result = DirectiveCompiler::make() + ->directive('foo', fn ($expr) => "[FOO:{$expr}]") + ->directive('bar', fn ($expr) => "[BAR:{$expr}]") + ->compile('@foo($a) @bar($b)'); + + expect($result)->toBe('[FOO:$a] [BAR:$b]'); +}); + +test('compiles directive without arguments', function () { + $result = DirectiveCompiler::make() + ->directive('separator', fn () => '
') + ->compile('@separator'); + + expect($result)->toBe('
'); +}); + +test('preserves php blocks', function () { + $input = ' @custom($val)'; + + $result = DirectiveCompiler::make() + ->directive('custom', fn ($expr) => "[{$expr}]") + ->compile($input); + + expect($result)->toBe(' [$val]'); +}); + +test('make returns a fluent instance', function () { + $compiler = DirectiveCompiler::make(); + + expect($compiler)->toBeInstanceOf(DirectiveCompiler::class); + expect($compiler->directive('test', fn () => ''))->toBe($compiler); +}); From dffb7e853efa4038e8f42bf024fb01d52509b125 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 1 Mar 2026 16:00:56 +0100 Subject: [PATCH 21/28] Refactor tests --- tests/Compiler/DirectiveCompilerTest.php | 78 +++++------------------- 1 file changed, 14 insertions(+), 64 deletions(-) diff --git a/tests/Compiler/DirectiveCompilerTest.php b/tests/Compiler/DirectiveCompilerTest.php index 6a4a4bea..86e76113 100644 --- a/tests/Compiler/DirectiveCompilerTest.php +++ b/tests/Compiler/DirectiveCompilerTest.php @@ -2,83 +2,33 @@ use Livewire\Blaze\Compiler\DirectiveCompiler; -test('compiles a registered custom directive', function () { - $result = DirectiveCompiler::make() - ->directive('greet', fn ($expression) => "") - ->compile('@greet($name)'); - - expect($result)->toBe(""); -}); - -test('ignores built-in blade directives', function ($directive) { - $input = $directive; - - $result = DirectiveCompiler::make()->compile($input); - - expect($result)->toBe($input); -})->with([ - '@if($condition)', - '@foreach($items as $item)', - '@include("partial")', - '@extends("layout")', - '@yield("content")', - '@section("content")', -]); - test('compiles custom directives while preserving built-in ones', function () { + $input = '@if($condition) @custom($value) @endcustom @endif'; + $result = DirectiveCompiler::make() ->directive('custom', fn ($expression) => "") - ->compile('@if($condition) @custom($value) @endif'); + ->directive('endcustom', fn () => "") + ->compile($input); - expect($result)->toBe('@if($condition) @endif'); + expect($result)->toBe('@if($condition) @endif'); }); -test('handles escaped directives', function () { +test('preserves escaped directives', function () { + $input = '@@custom($value)'; + $result = DirectiveCompiler::make() - ->directive('custom', fn ($expression) => "COMPILED") - ->compile('@@custom($value)'); - - expect($result)->toBe('@custom($value)'); -}); - -test('preserves templates with no directives', function () { - $input = '
Hello World
'; - - $result = DirectiveCompiler::make()->compile($input); + ->directive('custom', fn () => '') + ->compile($input); expect($result)->toBe($input); }); -test('compiles multiple custom directives', function () { - $result = DirectiveCompiler::make() - ->directive('foo', fn ($expr) => "[FOO:{$expr}]") - ->directive('bar', fn ($expr) => "[BAR:{$expr}]") - ->compile('@foo($a) @bar($b)'); - - expect($result)->toBe('[FOO:$a] [BAR:$b]'); -}); - -test('compiles directive without arguments', function () { - $result = DirectiveCompiler::make() - ->directive('separator', fn () => '
') - ->compile('@separator'); - - expect($result)->toBe('
'); -}); - test('preserves php blocks', function () { - $input = ' @custom($val)'; + $input = ''; $result = DirectiveCompiler::make() - ->directive('custom', fn ($expr) => "[{$expr}]") + ->directive('custom', fn () => '') ->compile($input); - expect($result)->toBe(' [$val]'); -}); - -test('make returns a fluent instance', function () { - $compiler = DirectiveCompiler::make(); - - expect($compiler)->toBeInstanceOf(DirectiveCompiler::class); - expect($compiler->directive('test', fn () => ''))->toBe($compiler); -}); + expect($result)->toBe($input); +}); \ No newline at end of file From f65b4aac209e7bbae854bd16179ea17af34c1ce7 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Sun, 1 Mar 2026 16:04:07 +0100 Subject: [PATCH 22/28] Fix DirectiveCompiler --- src/Compiler/DirectiveCompiler.php | 2 +- tests/Compiler/DirectiveCompilerTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Compiler/DirectiveCompiler.php b/src/Compiler/DirectiveCompiler.php index f9f6008d..52b2adc0 100644 --- a/src/Compiler/DirectiveCompiler.php +++ b/src/Compiler/DirectiveCompiler.php @@ -83,7 +83,7 @@ public function compileStatementsMadePublic($template) protected function compileStatement($match) { if (str_contains($match[1], '@')) { - $match[0] = isset($match[3]) ? $match[1].$match[3] : $match[1]; + return $match[0]; } elseif (isset($this->customDirectives[$match[1]])) { $match[0] = $this->callCustomDirective($match[1], Arr::get($match, 3)); } elseif (method_exists($this, $method = 'compile'.ucfirst($match[1]))) { diff --git a/tests/Compiler/DirectiveCompilerTest.php b/tests/Compiler/DirectiveCompilerTest.php index 86e76113..df265ab8 100644 --- a/tests/Compiler/DirectiveCompilerTest.php +++ b/tests/Compiler/DirectiveCompilerTest.php @@ -13,7 +13,7 @@ expect($result)->toBe('@if($condition) @endif'); }); -test('preserves escaped directives', function () { +test('ignores escaped directives', function () { $input = '@@custom($value)'; $result = DirectiveCompiler::make() @@ -23,7 +23,7 @@ expect($result)->toBe($input); }); -test('preserves php blocks', function () { +test('ignores php blocks', function () { $input = ''; $result = DirectiveCompiler::make() From be6b5c565a2a7bd7885d34f3bf4374a34526e806 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Mon, 2 Mar 2026 09:29:43 +0100 Subject: [PATCH 23/28] Test CI --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1170f0d7..6bd14f27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,6 +77,9 @@ jobs: - name: Install dependencies run: composer install --prefer-dist --no-progress --no-interaction + - name: Fetch baseline snapshot from main + run: git show origin/${{ github.event.pull_request.base.ref }}:.github/benchmark-snapshot.json > .github/benchmark-snapshot.json 2>/dev/null || true + - name: Run benchmark run: vendor/bin/testbench benchmark --ci > benchmark-comment.md From 7ff4d3b108dd178fb01905c06c1ff9fa78fd8dfc Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Mon, 2 Mar 2026 09:36:43 +0100 Subject: [PATCH 24/28] Test CI --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bd14f27..7610ec93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,9 @@ jobs: run: composer install --prefer-dist --no-progress --no-interaction - name: Fetch baseline snapshot from main - run: git show origin/${{ github.event.pull_request.base.ref }}:.github/benchmark-snapshot.json > .github/benchmark-snapshot.json 2>/dev/null || true + run: | + git fetch origin ${{ github.event.pull_request.base.ref }} --depth=1 + git checkout origin/${{ github.event.pull_request.base.ref }} -- .github/benchmark-snapshot.json - name: Run benchmark run: vendor/bin/testbench benchmark --ci > benchmark-comment.md From badecc517ffed45adaccd900bfcd3b14199750ed Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Mon, 2 Mar 2026 09:47:41 +0100 Subject: [PATCH 25/28] Test CI --- .github/workflows/benchmark-comment.yml | 45 +++++++++++++++++++++++++ .github/workflows/ci.yml | 25 ++++---------- 2 files changed, 52 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/benchmark-comment.yml diff --git a/.github/workflows/benchmark-comment.yml b/.github/workflows/benchmark-comment.yml new file mode 100644 index 00000000..5cfd82a8 --- /dev/null +++ b/.github/workflows/benchmark-comment.yml @@ -0,0 +1,45 @@ +name: Benchmark Comment + +on: + workflow_run: + workflows: [CI] + types: [completed] + +jobs: + comment: + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - name: Download benchmark result + uses: actions/download-artifact@v4 + with: + name: benchmark-result + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract PR number and comment body + id: pr + run: | + echo "number=$(head -1 benchmark-result.md)" >> $GITHUB_OUTPUT + tail -n +2 benchmark-result.md > comment-body.md + + - name: Find existing comment + uses: peter-evans/find-comment@v3 + id: find + with: + issue-number: ${{ steps.pr.outputs.number }} + comment-author: 'github-actions[bot]' + body-includes: '## Benchmark Results' + + - name: Post or update comment + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ steps.pr.outputs.number }} + comment-id: ${{ steps.find.outputs.comment-id }} + edit-mode: replace + body-path: comment-body.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7610ec93..5c77fcb4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,9 +58,6 @@ jobs: needs: pest if: github.event_name == 'pull_request' runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write steps: - name: Checkout @@ -83,23 +80,15 @@ jobs: git checkout origin/${{ github.event.pull_request.base.ref }} -- .github/benchmark-snapshot.json - name: Run benchmark - run: vendor/bin/testbench benchmark --ci > benchmark-comment.md - - - name: Find existing comment - uses: peter-evans/find-comment@v3 - id: find - with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: 'github-actions[bot]' - body-includes: '## Benchmark Results' + run: | + echo "${{ github.event.pull_request.number }}" > benchmark-result.md + vendor/bin/testbench benchmark --ci >> benchmark-result.md - - name: Post or update comment - uses: peter-evans/create-or-update-comment@v4 + - name: Upload benchmark result + uses: actions/upload-artifact@v4 with: - issue-number: ${{ github.event.pull_request.number }} - comment-id: ${{ steps.find.outputs.comment-id }} - edit-mode: replace - body-path: benchmark-comment.md + name: benchmark-result + path: benchmark-result.md snapshot: needs: pest From ed808b27a140ea8dc666df0af70bdd21e3f3d653 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Mon, 2 Mar 2026 09:52:50 +0100 Subject: [PATCH 26/28] Test CI --- .github/workflows/benchmark-comment.yml | 12 +++--------- .github/workflows/ci.yml | 4 +--- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/benchmark-comment.yml b/.github/workflows/benchmark-comment.yml index 5cfd82a8..5337dbff 100644 --- a/.github/workflows/benchmark-comment.yml +++ b/.github/workflows/benchmark-comment.yml @@ -22,24 +22,18 @@ jobs: run-id: ${{ github.event.workflow_run.id }} github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Extract PR number and comment body - id: pr - run: | - echo "number=$(head -1 benchmark-result.md)" >> $GITHUB_OUTPUT - tail -n +2 benchmark-result.md > comment-body.md - - name: Find existing comment uses: peter-evans/find-comment@v3 id: find with: - issue-number: ${{ steps.pr.outputs.number }} + issue-number: ${{ github.event.workflow_run.pull_requests[0].number }} comment-author: 'github-actions[bot]' body-includes: '## Benchmark Results' - name: Post or update comment uses: peter-evans/create-or-update-comment@v4 with: - issue-number: ${{ steps.pr.outputs.number }} + issue-number: ${{ github.event.workflow_run.pull_requests[0].number }} comment-id: ${{ steps.find.outputs.comment-id }} edit-mode: replace - body-path: comment-body.md + body-path: benchmark-result.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c77fcb4..16fbb238 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,9 +80,7 @@ jobs: git checkout origin/${{ github.event.pull_request.base.ref }} -- .github/benchmark-snapshot.json - name: Run benchmark - run: | - echo "${{ github.event.pull_request.number }}" > benchmark-result.md - vendor/bin/testbench benchmark --ci >> benchmark-result.md + run: vendor/bin/testbench benchmark --ci > benchmark-result.md - name: Upload benchmark result uses: actions/upload-artifact@v4 From e669dcbd26330f37569c529752b5e07fe9db9bc2 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Mon, 2 Mar 2026 10:28:11 +0100 Subject: [PATCH 27/28] Test CI --- .github/benchmark-snapshot.json | 51 ---------------- .github/workflows/ci.yml | 59 ++++++------------- .../app/Console/Commands/BenchmarkCommand.php | 6 +- 3 files changed, 19 insertions(+), 97 deletions(-) delete mode 100644 .github/benchmark-snapshot.json diff --git a/.github/benchmark-snapshot.json b/.github/benchmark-snapshot.json deleted file mode 100644 index b60a5e7d..00000000 --- a/.github/benchmark-snapshot.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "iterations": 10000, - "rounds": 5, - "benchmarks": { - "No attributes": { - "blade_ms": 394.94, - "blaze_ms": 16.14, - "improvement": 95.9 - }, - "Attributes only": { - "blade_ms": 464.95, - "blaze_ms": 29.47, - "improvement": 93.7 - }, - "Attributes + merge()": { - "blade_ms": 604.67, - "blaze_ms": 43.53, - "improvement": 92.8 - }, - "Attributes + class()": { - "blade_ms": 608.13, - "blaze_ms": 47, - "improvement": 92.3 - }, - "Props + attributes": { - "blade_ms": 828.81, - "blaze_ms": 39.16, - "improvement": 95.3 - }, - "Default slot": { - "blade_ms": 433.42, - "blaze_ms": 24.62, - "improvement": 94.3 - }, - "Named slots": { - "blade_ms": 607.45, - "blaze_ms": 38.07, - "improvement": 93.7 - }, - "`@aware` (nested)": { - "blade_ms": 1922.24, - "blaze_ms": 93.17, - "improvement": 95.2 - }, - "Attribute forwarding": { - "blade_ms": 1702.71, - "blaze_ms": 57.1, - "improvement": 96.6 - } - } -} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f6d1039..1180fa16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,8 +60,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout + - name: Checkout base branch uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha }} - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -71,13 +73,23 @@ jobs: coverage: none extensions: mbstring, dom, curl, json, libxml, xml, xmlwriter, simplexml, tokenizer - - name: Install dependencies + - name: Install base dependencies + run: composer install --prefer-dist --no-progress --no-interaction + + - name: Generate baseline snapshot + run: vendor/bin/testbench benchmark --snapshot --ci + + - name: Save baseline snapshot + run: cp benchmark-snapshot.json ${{ runner.temp }}/benchmark-snapshot.json + + - name: Checkout PR + uses: actions/checkout@v4 + + - name: Install PR dependencies run: composer install --prefer-dist --no-progress --no-interaction - - name: Fetch baseline snapshot from main - run: | - git fetch origin ${{ github.event.pull_request.base.ref }} --depth=1 - git checkout origin/${{ github.event.pull_request.base.ref }} -- .github/benchmark-snapshot.json + - name: Restore baseline snapshot + run: cp ${{ runner.temp }}/benchmark-snapshot.json benchmark-snapshot.json - name: Save PR number run: echo "${{ github.event.pull_request.number }}" > benchmark-result.md @@ -90,38 +102,3 @@ jobs: with: name: benchmark-result path: benchmark-result.md - - snapshot: - needs: pest - if: github.event_name == 'push' - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.4' - tools: composer:v2 - coverage: none - extensions: mbstring, dom, curl, json, libxml, xml, xmlwriter, simplexml, tokenizer - - - name: Install dependencies - run: composer install --prefer-dist --no-progress --no-interaction - - - name: Generate snapshot - run: vendor/bin/testbench benchmark --snapshot --ci - - - name: Commit snapshot - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add .github/benchmark-snapshot.json - git diff --staged --quiet || git commit -m "Update benchmark snapshot" - git push diff --git a/workbench/app/Console/Commands/BenchmarkCommand.php b/workbench/app/Console/Commands/BenchmarkCommand.php index 2fdaf209..faad1577 100644 --- a/workbench/app/Console/Commands/BenchmarkCommand.php +++ b/workbench/app/Console/Commands/BenchmarkCommand.php @@ -234,11 +234,7 @@ protected function loadSnapshot(): ?array protected function snapshotPath(): string { - $root = dirname(__DIR__, 4); - - return $this->option('ci') - ? $root . '/.github/benchmark-snapshot.json' - : $root . '/benchmark-snapshot.json'; + return dirname(__DIR__, 4) . '/benchmark-snapshot.json'; } protected function improvement(array $result): float From 93349fd39cc17d07df71cc6537d7e5bc239762d2 Mon Sep 17 00:00:00 2001 From: Filip Ganyicz Date: Wed, 4 Mar 2026 22:07:44 +0100 Subject: [PATCH 28/28] Fix merge conflicts --- src/Compiler/Compiler.php | 2 +- src/Compiler/SlotCompiler.php | 8 +++++--- src/Compiler/Wrapper.php | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Compiler/Compiler.php b/src/Compiler/Compiler.php index bc4931d6..cecaff10 100644 --- a/src/Compiler/Compiler.php +++ b/src/Compiler/Compiler.php @@ -26,7 +26,7 @@ public function __construct( protected BladeService $blade, protected BlazeManager $manager, ) { - $this->slotCompiler = new SlotCompiler(fn (string $str) => $this->getAttributesAndBoundKeysArrayStrings($str, true)[0]); + $this->slotCompiler = new SlotCompiler($manager, fn (string $str) => $this->getAttributesAndBoundKeysArrayStrings($str, true)[0]); $this->tagCompiler = new ComponentTagCompiler([], [], $blade->compiler); } diff --git a/src/Compiler/SlotCompiler.php b/src/Compiler/SlotCompiler.php index 1ed2df30..3bf94970 100644 --- a/src/Compiler/SlotCompiler.php +++ b/src/Compiler/SlotCompiler.php @@ -2,10 +2,11 @@ namespace Livewire\Blaze\Compiler; -use Illuminate\Support\Str; -use Livewire\Blaze\Parser\Nodes\SlotNode; use Closure; +use Illuminate\Support\Str; use Livewire\Blaze\Blaze; +use Livewire\Blaze\BlazeManager; +use Livewire\Blaze\Parser\Nodes\SlotNode; /** * Compiles slot nodes into output buffering PHP code. @@ -13,6 +14,7 @@ class SlotCompiler { public function __construct( + protected BlazeManager $manager, protected Closure $getAttributesArrayString ) { } @@ -99,7 +101,7 @@ protected function renderLooseContent(array $children): string */ protected function compileSlot(string $name, string $content, string $attributes, string $slotsVariableName): string { - $contentHandler = Blaze::isFolding() ? '$__blaze->processPassthroughContent(\'trim\', trim(ob_get_clean()))' : 'trim(ob_get_clean())'; + $contentHandler = $this->manager->isFolding() ? '$__blaze->processPassthroughContent(\'trim\', trim(ob_get_clean()))' : 'trim(ob_get_clean())'; return '<' . '?php ob_start(); ?>' . $content diff --git a/src/Compiler/Wrapper.php b/src/Compiler/Wrapper.php index 831aa7c3..a1f78ffe 100644 --- a/src/Compiler/Wrapper.php +++ b/src/Compiler/Wrapper.php @@ -82,7 +82,7 @@ public function wrap(string $compiled, string $path, ?string $source = null): st $output .= 'processPassthroughContent(\'ltrim\', ltrim(ob_get_clean()))' : 'ltrim(ob_get_clean())'; + $contentHandler = $this->manager->isFolding() ? '$__blaze->processPassthroughContent(\'ltrim\', ltrim(ob_get_clean()))' : 'ltrim(ob_get_clean())'; $output .= 'echo ' . $contentHandler . ';' . "\n";