diff --git a/src/BladeRenderer.php b/src/BladeRenderer.php new file mode 100644 index 00000000..fe527094 --- /dev/null +++ b/src/BladeRenderer.php @@ -0,0 +1,142 @@ +getTemporaryCachePath(); + + File::ensureDirectoryExists($temporaryCachePath); + + $restoreFactory = $this->freezeObjectProperties($this->factory, [ + 'renderCount' => 0, + 'renderedOnce' => [], + 'sections' => [], + 'sectionStack' => [], + 'pushes' => [], + 'prepends' => [], + 'pushStack' => [], + 'componentStack' => [], + 'componentData' => [], + 'currentComponentData' => [], + 'slots' => [], + 'slotStack' => [], + 'fragments' => [], + 'fragmentStack' => [], + 'loopsStack' => [], + 'translationReplacements' => [], + ]); + + $restoreCompiler = $this->freezeObjectProperties($this->blade, [ + 'cachePath' => $temporaryCachePath, + 'rawBlocks' => [], + 'footer' => [], + 'prepareStringsForCompilationUsing' => [ + function ($input) { + if (Unblaze::hasUnblaze($input)) { + $input = Unblaze::processUnblazeDirectives($input); + }; + + $input = $this->manager->compileForFolding($input, $this->blade->getPath()); + + return $input; + }, + ], + 'path' => null, + 'forElseCounter' => 0, + 'firstCaseInSwitch' => true, + 'lastSection' => null, + 'lastFragment' => null, + ]); + + $restoreRuntime = $this->freezeObjectProperties($this->runtime, [ + 'compiled' => [], + 'paths' => [], + 'compiledPath' => $temporaryCachePath, + 'dataStack' => [], + 'slotsStack' => [], + ]); + + try { + $this->manager->startFolding(); + + $result = $this->blade->render($template, deleteCachedView: true); + } finally { + $restoreCompiler(); + $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 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 6d8e8b42..643d884c 100644 --- a/src/BladeService.php +++ b/src/BladeService.php @@ -2,193 +2,28 @@ 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\BladeCompiler; use Illuminate\View\Compilers\ComponentTagCompiler; +use Livewire\Blaze\Compiler\DirectiveCompiler; use Livewire\Blaze\Support\LaravelRegex; -use Livewire\Blaze\Support\Utils; use ReflectionClass; class BladeService { - /** - * Render a Blade template string in an isolated context. - */ - 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. - */ - public static function getTemporaryCachePath(): string - { - return config('view.compiled').'/blaze'; - } - - /** - * Render a Blade template string in isolation by freezing and restoring compiler state. - */ - public static function isolatedRender(string $template): string - { - $compiler = app('blade.compiler'); - - $temporaryCachePath = static::getTemporaryCachePath(); - - File::ensureDirectoryExists($temporaryCachePath); - - $factory = app('view'); - - [$factory, $restoreFactory] = static::freezeObjectProperties($factory, [ - 'renderCount' => 0, - 'renderedOnce' => [], - 'sections' => [], - 'sectionStack' => [], - 'pushes' => [], - 'prepends' => [], - 'pushStack' => [], - 'componentStack' => [], - 'componentData' => [], - 'currentComponentData' => [], - 'slots' => [], - 'slotStack' => [], - 'fragments' => [], - 'fragmentStack' => [], - 'loopsStack' => [], - 'translationReplacements' => [], - ]); - - [$compiler, $restore] = static::freezeObjectProperties($compiler, [ - 'cachePath' => $temporaryCachePath, - 'rawBlocks' => [], - 'footer' => [], - 'prepareStringsForCompilationUsing' => [ - function ($input) use ($compiler) { - if (Unblaze::hasUnblaze($input)) { - $input = Unblaze::processUnblazeDirectives($input); - }; - - $input = Blaze::compileForFolding($input, $compiler->getPath()); - - return $input; - }, - ], - 'path' => null, - 'forElseCounter' => 0, - 'firstCaseInSwitch' => true, - 'lastSection' => null, - 'lastFragment' => null, - ]); - - [$runtime, $restoreRuntime] = static::freezeObjectProperties(app('blaze.runtime'), [ - 'compiled' => [], - 'paths' => [], - 'compiledPath' => $temporaryCachePath, - 'dataStack' => [], - 'slotsStack' => [], - ]); - - try { - Blaze::startFolding(); + protected ComponentTagCompiler $tagCompiler; - $result = $compiler->render($template, deleteCachedView: true); - } finally { - $restore(); - $restoreFactory(); - $restoreRuntime(); - - Blaze::stopFolding(); - } - - $result = Unblaze::replaceUnblazePrecompiledDirectives($result); - - return $result; - } - - /** - * Delete the temporary cache directory created during isolated rendering. - */ - public static function deleteTemporaryCacheDirectory(): void - { - File::deleteDirectory(static::getTemporaryCachePath()); + public function __construct( + public BladeCompiler $compiler, + ) { + $this->tagCompiler = new ComponentTagCompiler(blade: $compiler); } /** * 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'); } @@ -196,24 +31,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->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); + $this->compiler->prepareStringsForCompilationUsing(function ($input) use ($callback) { + return $callback($input, $this->compiler->getPath()); }); }); } @@ -221,12 +43,12 @@ 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 { $output = $input; - $output = static::storeVerbatimBlocks($output); - $output = static::storePhpBlocks($output); + $output = $this->storeVerbatimBlocks($output); + $output = $this->storePhpBlocks($output); return $output; } @@ -234,69 +56,63 @@ 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 { - return static::storeRawBlock(LaravelRegex::VERBATIM_BLOCK, $input); + return $this->storeRawBlock(LaravelRegex::VERBATIM_BLOCK, $input); } /** * Store only @verbatim blocks as raw block placeholders. */ - public static function storePhpBlocks(string $input): string + public function storePhpBlocks(string $input): string { - return static::storeRawBlock(LaravelRegex::PHP_BLOCK, $input); + return $this->storeRawBlock(LaravelRegex::PHP_BLOCK, $input); } /** * Store a raw block placeholder via the Blade compiler. */ - protected static function storeRawBlock(string $pattern, string $content): string + protected function storeRawBlock(string $pattern, string $content): string { - $compiler = app('blade.compiler'); - $reflection = new \ReflectionClass($compiler); + $reflection = new \ReflectionClass($this->compiler); + $method = $reflection->getMethod('storeRawBlock'); - return preg_replace_callback($pattern, function ($matches) use ($compiler, $reflection) { - return $reflection->getMethod('storeRawBlock')->invoke($compiler, $matches[0]); + return preg_replace_callback($pattern, function ($matches) use ($method) { + return $method->invoke($this->compiler, $matches[0]); }, $content); } /** * 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'); - - $reflection = new \ReflectionClass($compiler); + $reflection = new \ReflectionClass($this->compiler); $method = $reflection->getMethod('restoreRawContent'); - return $method->invoke($compiler, $input); + return $method->invoke($this->compiler, $input); } /** * 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'); - - $reflection = new \ReflectionClass($compiler); + $reflection = new \ReflectionClass($this->compiler); $method = $reflection->getMethod('restorePhpBlocks'); - return $method->invoke($compiler, $input); + return $method->invoke($this->compiler, $input); } /** * 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'); - - $reflection = new \ReflectionClass($compiler); + $reflection = new \ReflectionClass($this->compiler); $compileComments = $reflection->getMethod('compileComments'); - return $compileComments->invoke($compiler, $input); + return $compileComments->invoke($this->compiler, $input); } /** @@ -309,10 +125,8 @@ 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')); - // Laravel expects a space at the start of the attribute string... $attributeString = Str::start($attributeString, ' '); @@ -325,46 +139,42 @@ public static function preprocessAttributeString(string $attributeString): strin $str = $this->parseBindAttributes($str); return $str; - })->call($compiler, $attributeString); + })->call($this->tagCompiler, $attributeString); } - public static function compileUseStatements(string $input): string + public function compileUseStatements(string $input): string { - return static::compileDirective($input, 'use', function ($expression) { - $compiler = app('blade.compiler'); - - $reflection = new \ReflectionClass($compiler); + return DirectiveCompiler::make()->directive('use', function ($expression) { + $reflection = new \ReflectionClass($this->compiler); $method = $reflection->getMethod('compileUse'); - return $method->invoke($compiler, $expression); - }); + return $method->invoke($this->compiler, $expression); + })->compile($input); } /** * 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')); - - $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)."'", "''.", ".''"); } /** * 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 $this->tagCompiler->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) { $view = $params[0]; @@ -373,7 +183,7 @@ public static function viewCacheInvalidationHook(callable $callback): void return; } - $invalidate = fn () => app('blade.compiler')->compile($view->getPath()); + $invalidate = fn () => $this->compiler->compile($view->getPath()); $callback($view, $invalidate); }); @@ -382,14 +192,13 @@ 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'); $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); @@ -468,35 +277,4 @@ 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) - { - $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 6ba16fea..7ea5afdf 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,30 @@ 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 BladeRenderer $renderer; + 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, + protected BladeService $blade, ) { + $this->renderer = new BladeRenderer($bladeCompiler, app('view'), $this->runtime, $this); + $this->parser = new Parser(new Tokenizer, $this->blade); + $this->walker = new Walker; + $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; }); @@ -54,8 +69,8 @@ public function compile(string $template, ?string $path = null): string $source = $template; $clean = $template; - $clean = BladeService::preStoreUncompiledBlocks($clean); - $clean = BladeService::compileComments($clean); + $clean = $this->blade->preStoreUncompiledBlocks($clean); + $clean = $this->blade->compileComments($clean); $dataStack = []; @@ -108,10 +123,10 @@ public function compile(string $template, ?string $path = null): string $output = $this->instrumenter->profileView($output, $path, $source); } - $output = BladeService::restoreRawBlocks($output); + $output = $this->blade->restoreRawBlocks($output); try { - BladeService::deleteTemporaryCacheDirectory(); + $this->renderer->deleteTemporaryCacheDirectory(); } catch (\Throwable $e) { // } @@ -124,8 +139,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->blade->preStoreUncompiledBlocks($template); + $template = $this->blade->compileComments($template); $ast = $this->walker->walk( nodes: $this->parser->parse($template), @@ -166,8 +181,8 @@ public function compileForDebug(string $template, ?string $path = null): string $source = $template; $clean = $template; - $clean = BladeService::preStoreUncompiledBlocks($clean); - $clean = BladeService::compileComments($clean); + $clean = $this->blade->preStoreUncompiledBlocks($clean); + $clean = $this->blade->compileComments($clean); $ast = $this->walker->walk( nodes: $this->parser->parse($clean), @@ -187,7 +202,7 @@ public function compileForDebug(string $template, ?string $path = null): string $output = $this->instrumenter->profileView($output, $path, $source); } - $output = BladeService::restoreRawBlocks($output); + $output = $this->blade->restoreRawBlocks($output); return $output; } @@ -200,8 +215,8 @@ public function compileForFolding(string $template, ?string $path = null): strin { $source = $template; - $template = BladeService::preStoreUncompiledBlocks($template); - $template = BladeService::compileComments($template); + $template = $this->blade->preStoreUncompiledBlocks($template); + $template = $this->blade->compileComments($template); $ast = $this->walker->walk( nodes: $this->parser->parse($template), @@ -213,7 +228,7 @@ public function compileForFolding(string $template, ?string $path = null): strin $output = $this->render($ast); - $output = BladeService::restoreRawBlocks($output); + $output = $this->blade->restoreRawBlocks($output); if (! $path) { return $output; @@ -404,7 +419,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($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 1c020f90..f4901ed9 100644 --- a/src/BlazeServiceProvider.php +++ b/src/BlazeServiceProvider.php @@ -14,6 +14,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); @@ -114,21 +115,24 @@ protected function registerBladeMacros(): void */ protected function interceptBladeCompilation(): void { - BladeService::earliestPreCompilationHook(function ($input, $path) { - if (BladeService::containsLaravelExceptionView($input)) { + $blade = $this->app->make(BladeService::class); + $blaze = $this->app->make(BlazeManager::class); + + $blade->earliestPreCompilationHook(function ($input, $path) use ($blade, $blaze) { + if ($blade->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); }); }); } diff --git a/src/Compiler/Compiler.php b/src/Compiler/Compiler.php index fc12f09b..cecaff10 100644 --- a/src/Compiler/Compiler.php +++ b/src/Compiler/Compiler.php @@ -3,7 +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; @@ -17,15 +18,16 @@ */ class Compiler { - protected Config $config; - protected ComponentTagCompiler $blade; + protected ComponentTagCompiler $tagCompiler; protected SlotCompiler $slotCompiler; - public function __construct(Config $config) - { - $this->config = $config; - $this->slotCompiler = new SlotCompiler(fn (string $str) => $this->getAttributesAndBoundKeysArrayStrings($str, true)[0]); - $this->blade = new ComponentTagCompiler([], [], app('blade.compiler')); + public function __construct( + protected Config $config, + protected BladeService $blade, + protected BlazeManager $manager, + ) { + $this->slotCompiler = new SlotCompiler($manager, fn (string $str) => $this->getAttributesAndBoundKeysArrayStrings($str, true)[0]); + $this->tagCompiler = new ComponentTagCompiler([], [], $blade->compiler); } /** @@ -41,7 +43,7 @@ public function compile(Node $node): Node return new TextNode($this->compileDelegateComponentTag($node)); } - $source = new ComponentSource($node->name); + $source = new ComponentSource($this->blade->componentNameToPath($node->name)); if (! $source->exists()) { return $node; @@ -94,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); @@ -123,14 +125,13 @@ 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"; $slotsVariableName = '$slots' . hash('xxh128', $componentName); - $functionName = '(\'' . (Blaze::isFolding() ? '__' : '_') . '\' . $__resolved)'; + $functionName = '(\'' . ($this->manager->isFolding() ? '__' : '_') . '\' . $__resolved)'; $output .= '<' . '?php $__blaze->pushData($attributes->all()); ?>'; @@ -183,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/Compiler/DirectiveCompiler.php b/src/Compiler/DirectiveCompiler.php new file mode 100644 index 00000000..52b2adc0 --- /dev/null +++ b/src/Compiler/DirectiveCompiler.php @@ -0,0 +1,99 @@ + */ + 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(new Filesystem, sys_get_temp_dir()) extends 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], '@')) { + 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]))) { + return $match[0]; + } else { + return $match[0]; + } + + return isset($match[3]) ? $match[0] : $match[0].$match[2]; + } + }; + } +} diff --git a/src/Compiler/Profiler.php b/src/Compiler/Profiler.php index 8c9bb8aa..d5811edb 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; @@ -21,6 +22,7 @@ class Profiler { public function __construct( protected Config $config, + protected BladeService $blade, ) { } @@ -29,7 +31,7 @@ public function __construct( */ public function profile(Node $node, string $componentName, ?string $strategy = null): Node { - $source = new ComponentSource($componentName); + $source = new ComponentSource($this->blade->componentNameToPath($componentName)); if ($strategy === null) { $isBlade = $node instanceof ComponentNode; @@ -156,7 +158,7 @@ protected function isComponentTemplatePath(string $path): bool $dirs = [resource_path('views/components')]; - foreach (app('blade.compiler')->getAnonymousComponentPaths() as $registration) { + foreach ($this->blade->compiler->getAnonymousComponentPaths() as $registration) { $dirs[] = $registration['path']; } 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 cba067d4..a1f78ffe 100644 --- a/src/Compiler/Wrapper.php +++ b/src/Compiler/Wrapper.php @@ -3,8 +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; /** @@ -12,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 $blade, + protected BlazeManager $manager, + ) { + $this->propsCompiler = new PropsCompiler; + $this->awareCompiler = new AwareCompiler; + $this->useExtractor = new UseExtractor; + } /** * Compile a component template into a function definition. @@ -28,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->blade->compileUseStatements($compiled); + $compiled = $this->blade->restoreRawBlocks($compiled); + $compiled = $this->blade->storeVerbatimBlocks($compiled); $imports = ''; @@ -42,7 +50,7 @@ public function wrap(string $compiled, string $path, ?string $source = null): st $imports .= $statement . "\n"; }); - $compiled = BladeService::preStoreUncompiledBlocks($compiled); + $compiled = $this->blade->preStoreUncompiledBlocks($compiled); $output = ''; @@ -63,15 +71,18 @@ 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 = BladeService::restoreRawBlocks($compiled); + $compiled = DirectiveCompiler::make() + ->directive('props', $this->propsCompiler->compile(...)) + ->directive('aware', $this->awareCompiler->compile(...)) + ->compile($compiled); + + $compiled = $this->blade->restoreRawBlocks($compiled); $output .= $compiled; $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"; @@ -120,7 +131,7 @@ protected function globalVariables(string $source, string $compiled): string */ protected function hasEchoHandlers(): bool { - $compiler = app('blade.compiler'); + $compiler = $this->blade->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 cdbad289..d25eff36 100644 --- a/src/Folder/Foldable.php +++ b/src/Folder/Foldable.php @@ -3,13 +3,13 @@ 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; 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. @@ -28,6 +28,8 @@ class Foldable public function __construct( protected ComponentNode $node, protected ComponentSource $source, + protected BladeRenderer $renderer, + protected BladeService $blade, ) { } @@ -49,7 +51,7 @@ public function fold(): string $this->setupSlots(); $this->mergeAwareProps(); - $this->html = BladeService::render($this->renderable->render()); + $this->html = $this->renderer->render($this->renderable->render()); $this->processUncompiledAttributes(); $this->restorePlaceholders(); @@ -104,6 +106,7 @@ protected function setupSlots(): void children: [new TextNode($placeholder)], prefix: $child->prefix, closeHasName: $child->closeHasName, + attributes: $child->attributes, ); } else { $looseContent[] = $child; @@ -224,7 +227,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 : $this->blade->compileAttributeEchos($attribute->value); $content = str_replace("'" . $placeholder . "'", $value, $content); } @@ -269,7 +272,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).' => '.$this->blade->compileAttributeEchos($attribute->value); } } @@ -280,4 +283,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 f90f3f1d..bc7ccc62 100644 --- a/src/Folder/Folder.php +++ b/src/Folder/Folder.php @@ -10,8 +10,10 @@ 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; -use Livewire\Blaze\Blaze; use Livewire\Blaze\Config; /** @@ -20,7 +22,10 @@ class Folder { public function __construct( - protected ?Config $config = null, + protected Config $config, + protected BladeService $blade, + protected BladeRenderer $renderer, + protected BlazeManager $manager, ) { } @@ -35,7 +40,7 @@ public function fold(Node $node): Node $component = $node; - $source = new ComponentSource($component->name); + $source = new ComponentSource($this->blade->componentNameToPath($component->name)); if (! $source->exists()) { return $component; @@ -52,19 +57,19 @@ public function fold(Node $node): Node $this->checkProblematicPatterns($source); try { - $foldable = new Foldable($node, $source); + $foldable = new Foldable($node, $source, $this->renderer, $this->blade); $html = $foldable->fold(); Event::dispatch(new ComponentFolded( - name: $source->name, + name: $component->name, path: $source->path, filemtime: filemtime($source->path), )); 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 5e498ae7..12765557 100644 --- a/src/Memoizer/Memoizer.php +++ b/src/Memoizer/Memoizer.php @@ -2,6 +2,8 @@ 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; @@ -17,6 +19,8 @@ class Memoizer public function __construct( protected Config $config, protected Compiler $compiler, + protected BladeService $blade, + protected BlazeManager $manager, ) { } @@ -38,11 +42,20 @@ 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}' => ".$this->blade->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)) : ?>'; @@ -68,7 +81,7 @@ protected function isMemoizable(Node $node): bool return false; } - $source = new ComponentSource($node->name); + $source = new ComponentSource($this->blade->componentNameToPath($node->name)); if (! $source->exists()) { return false; diff --git a/src/Parser/Nodes/ComponentNode.php b/src/Parser/Nodes/ComponentNode.php index cfd6ef74..32519810 100644 --- a/src/Parser/Nodes/ComponentNode.php +++ b/src/Parser/Nodes/ComponentNode.php @@ -2,10 +2,6 @@ namespace Livewire\Blaze\Parser\Nodes; -use Livewire\Blaze\Support\AttributeParser; -use Livewire\Blaze\Support\Utils; -use Livewire\Blaze\Parser\Attribute; - /** * Represents an or tag in the AST. */ @@ -24,9 +20,6 @@ public function __construct( /** @var Attribute[] */ public array $attributes = [], ) { - if (empty($this->attributes) && ! empty($this->attributeString)) { - $this->attributes = Utils::parseAttributeStringToArray($this->attributeString); - } } /** @@ -88,18 +81,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/Parser/Nodes/SlotNode.php b/src/Parser/Nodes/SlotNode.php index 6b44f91f..6268585b 100644 --- a/src/Parser/Nodes/SlotNode.php +++ b/src/Parser/Nodes/SlotNode.php @@ -2,7 +2,6 @@ namespace Livewire\Blaze\Parser\Nodes; -use Livewire\Blaze\Support\Utils; use Livewire\Blaze\Parser\Attribute; /** @@ -10,9 +9,6 @@ */ 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 = Utils::parseAttributeStringToArray($this->attributeString); } /** {@inheritdoc} */ diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php index 86622921..840c4be9 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. @@ -20,6 +22,7 @@ class Parser { public function __construct( protected Tokenizer $tokenizer, + protected BladeService $blade, ) { } @@ -52,12 +55,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::parse( + $this->blade->preprocessAttributeString($attributeString) + ), ); $stack->pushContainer($node); @@ -68,12 +76,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::parse( + $this->blade->preprocessAttributeString($attributeString) + ), ); $stack->addToRoot($node); @@ -92,13 +105,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::parse( + $this->blade->preprocessAttributeString($attributeString) + ), ); $stack->pushContainer($node); @@ -127,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 b9dc73fe..d0982b25 100644 --- a/src/Runtime/BlazeRuntime.php +++ b/src/Runtime/BlazeRuntime.php @@ -5,22 +5,18 @@ use Illuminate\Contracts\View\Factory; 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. */ class BlazeRuntime { - public readonly Factory $env; - public readonly Application $app; - public readonly Debugger $debugger; - public readonly Compiler $compiler; - // 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; @@ -32,12 +28,13 @@ class BlazeRuntime protected array $dataStack = []; protected array $slotsStack = []; - public function __construct() - { - $this->env = app('view'); - $this->app = app(); - $this->debugger = app('blaze.debugger'); - $this->compiler = app('blade.compiler'); + public function __construct( + public readonly Factory $env, + public readonly Application $app, + public readonly Debugger $debugger, + public readonly BladeCompiler $compiler, + protected BladeService $blade, + ) { } /** @@ -70,7 +67,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->blade->componentNameToPath($component); } if (! $this->isBlazeComponent($path)) { diff --git a/src/Support/AttributeParser.php b/src/Support/AttributeParser.php index 546d044d..f99ffc3c 100644 --- a/src/Support/AttributeParser.php +++ b/src/Support/AttributeParser.php @@ -2,34 +2,28 @@ namespace Livewire\Blaze\Support; -use Livewire\Blaze\BladeService; +use Illuminate\Support\Str; 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 function parseAttributeStringToArray(string $attributesString): array + public static function parse(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 = ''; @@ -80,45 +74,14 @@ public function parseAttributeStringToArray(string $attributesString): array } /** - * Convert parsed attributes into a PHP array string for runtime evaluation. - * - * @param array $attributes + * Strip any quotes from the given string. + * + * @see Illuminate\View\Compilers\ComponentTagCompiler::stripQuotes() */ - public function parseAttributesArrayToRuntimeArrayString(array $attributes): string + protected static function stripQuotes(string $value) { - $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)."' => ".Utils::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).']'; + return Str::startsWith($value, ['"', '\'']) + ? substr($value, 1, -1) + : $value; } } diff --git a/src/Support/ComponentSource.php b/src/Support/ComponentSource.php index 742e25e8..39ab70f8 100644 --- a/src/Support/ComponentSource.php +++ b/src/Support/ComponentSource.php @@ -7,15 +7,13 @@ */ 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 = Utils::componentNameToPath($name); + $this->path = $path; $this->content = $this->exists() ? file_get_contents($this->path) : ''; $this->directives = new Directives($this->content); } 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 20f43a5c..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\-:.@%]+) @@ -51,6 +53,13 @@ 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. * diff --git a/src/Support/Utils.php b/src/Support/Utils.php index 4faa386b..db7c20f3 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. */ @@ -35,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. */ diff --git a/src/Unblaze.php b/src/Unblaze.php index 0eb38172..73a44203 100644 --- a/src/Unblaze.php +++ b/src/Unblaze.php @@ -2,7 +2,7 @@ namespace Livewire\Blaze; -use Illuminate\Support\Arr; +use Livewire\Blaze\Compiler\DirectiveCompiler; use Illuminate\Support\Str; /** @@ -35,23 +35,20 @@ public static function hasUnblaze(string $template): bool */ public static function processUnblazeDirectives(string $template) { - $compiler = BladeService::getHackedBladeCompiler(); - $expressionsByToken = []; - $compiler->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]; diff --git a/tests/BladeCompilerTest.php b/tests/BladeCompilerTest.php new file mode 100644 index 00000000..a98e47b9 --- /dev/null +++ b/tests/BladeCompilerTest.php @@ -0,0 +1,81 @@ +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/Compiler/DirectiveCompilerTest.php b/tests/Compiler/DirectiveCompilerTest.php new file mode 100644 index 00000000..df265ab8 --- /dev/null +++ b/tests/Compiler/DirectiveCompilerTest.php @@ -0,0 +1,34 @@ +directive('custom', fn ($expression) => "") + ->directive('endcustom', fn () => "") + ->compile($input); + + expect($result)->toBe('@if($condition) @endif'); +}); + +test('ignores escaped directives', function () { + $input = '@@custom($value)'; + + $result = DirectiveCompiler::make() + ->directive('custom', fn () => '') + ->compile($input); + + expect($result)->toBe($input); +}); + +test('ignores php blocks', function () { + $input = ''; + + $result = DirectiveCompiler::make() + ->directive('custom', fn () => '') + ->compile($input); + + expect($result)->toBe($input); +}); \ No newline at end of file diff --git a/tests/Compiler/WrapperTest.php b/tests/Compiler/WrapperTest.php index 5c2c5db9..4ad22991 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([ @@ -114,4 +114,4 @@ $input = '@verbatim /* uncompiled */ @endverbatim'; 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 5db749fe..1ab8e1fc 100644 --- a/tests/Folder/FoldableTest.php +++ b/tests/Folder/FoldableTest.php @@ -1,5 +1,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/input.blade.php')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( '' @@ -33,7 +35,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')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace(<<<'HTML'
@@ -51,7 +53,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')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( '' @@ -62,7 +64,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')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( '' @@ -73,7 +75,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')), app(BladeRenderer::class), app(BladeService::class)); $node->setParentsAttributes([ 'type' => new Attribute( @@ -104,7 +106,7 @@ ), ]); - $foldable = new Foldable($node, new ComponentSource($node->name)); + $foldable = new Foldable($node, new ComponentSource(fixture_path('components/foldable/input-aware.blade.php')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( '' @@ -115,7 +117,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')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('', join('', [ @@ -132,7 +134,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')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace(join('', [ 'pushData([\'name\' => \'John\']); $__env->pushConsumableComponentData([\'name\' => \'John\']); ?>', @@ -147,7 +149,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')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace(join('', [ 'pushData([\'name\' => $name]); $__env->pushConsumableComponentData([\'name\' => $name]); ?>', @@ -162,11 +164,11 @@ $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')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace(join('', [ 'pushData([\'name\' => \'Mr. \'.e($name)]); $__env->pushConsumableComponentData([\'name\' => \'Mr. \'.e($name)]); ?>', '
', 'popData(); $__env->popConsumableComponentData(); ?>', ])); -}); \ No newline at end of file +}); diff --git a/tests/Folder/UnblazeTest.php b/tests/Folder/UnblazeTest.php index 7830dae5..07d54133 100644 --- a/tests/Folder/UnblazeTest.php +++ b/tests/Folder/UnblazeTest.php @@ -1,5 +1,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/input-unblaze.blade.php')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('', join('', [ @@ -24,7 +26,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')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('
', join('', [ @@ -40,7 +42,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')), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('', join('', [ @@ -50,4 +52,4 @@ '' ])) ); -}); \ No newline at end of file +}); diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 6f493115..e85c18b9 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -1,8 +1,10 @@ Artisan::call('view:clear')); @@ -57,4 +59,4 @@ public function get($path, array $data = []): string { }); expect(view('antlers-view')->render())->toBe('NO_BLAZE'); -}); \ No newline at end of file +}); diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index 6ab808eb..39921e62 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::parse('class="my-4"'), ), ]); }); @@ -33,6 +35,7 @@ children: [ new TextNode('Footer'), ], + attributes: AttributeParser::parse('class="p-2"'), ) ] ), @@ -54,6 +57,7 @@ children: [ new TextNode('Footer'), ], + attributes: AttributeParser::parse('class="p-2"'), ) ] ), @@ -76,6 +80,7 @@ children: [ new TextNode('Footer'), ], + attributes: AttributeParser::parse('class="p-2"'), ) ] ), @@ -96,6 +101,7 @@ children: [ new TextNode('Body'), ], + attributes: AttributeParser::parse('class="p-2"'), ) ] ), diff --git a/tests/Support/AttributeParserTest.php b/tests/Support/AttributeParserTest.php index bbf3c18a..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 = (new AttributeParser)->parseAttributeStringToArray(':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 = (new 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 = (new 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 = (new 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 = (new 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 = (new 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 = (new 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 = (new 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'); 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