From f3e0b01e346a87823fb98b849b9453963b99c4c3 Mon Sep 17 00:00:00 2001 From: John Koster Date: Mon, 14 Apr 2025 23:40:53 -0500 Subject: [PATCH 01/46] Initial commit --- composer.json | 14 ++ src/Compiler/ComponentCompiler.php | 87 +++++++ src/Compiler/ComponentHashes.php | 18 ++ src/Compiler/FluxComponentDirectives.php | 122 ++++++++++ src/Compiler/TagCompiler.php | 229 ++++++++++++++++++ src/FluxComponentCache.php | 178 ++++++++++++++ src/FluxManager.php | 15 ++ src/FluxServiceProvider.php | 74 +++++- src/FluxTagCompiler.php | 40 ++- tests/Feature/CacheTest.php | 146 +++++++++++ tests/Feature/ComponentDataTest.php | 19 ++ tests/Feature/GeneralComponentsTest.php | 80 ++++++ tests/Feature/InlineNamedSlotTest.php | 20 ++ tests/Feature/NamedSlotTest.php | 157 ++++++++++++ tests/Feature/SlotDataTest.php | 46 ++++ tests/FluxTestCase.php | 34 +++ tests/Pest.php | 3 + tests/TestCounter.php | 13 + tests/components/blade/default_slot.blade.php | 3 + tests/components/blade/named_slots.blade.php | 3 + .../flux/tests/attribute_cache.blade.php | 2 + .../flux/tests/component_a.blade.php | 1 + .../flux/tests/component_b.blade.php | 1 + .../flux/tests/default_slot.blade.php | 3 + .../flux/tests/full_cache.blade.php | 3 + .../flux/tests/multiple_nocache.blade.php | 15 ++ .../flux/tests/named_slots.blade.php | 3 + .../tests/nocache_component_use.blade.php | 7 + .../flux/tests/nocache_directive.blade.php | 10 + .../flux/tests/nocache_named_slot.blade.php | 16 ++ .../flux/tests/nocache_slot.blade.php | 6 + tests/components/flux/tests/simple.blade.php | 1 + 32 files changed, 1367 insertions(+), 2 deletions(-) create mode 100644 src/Compiler/ComponentCompiler.php create mode 100644 src/Compiler/ComponentHashes.php create mode 100644 src/Compiler/FluxComponentDirectives.php create mode 100644 src/Compiler/TagCompiler.php create mode 100644 src/FluxComponentCache.php create mode 100644 tests/Feature/CacheTest.php create mode 100644 tests/Feature/ComponentDataTest.php create mode 100644 tests/Feature/GeneralComponentsTest.php create mode 100644 tests/Feature/InlineNamedSlotTest.php create mode 100644 tests/Feature/NamedSlotTest.php create mode 100644 tests/Feature/SlotDataTest.php create mode 100644 tests/FluxTestCase.php create mode 100644 tests/Pest.php create mode 100644 tests/TestCounter.php create mode 100644 tests/components/blade/default_slot.blade.php create mode 100644 tests/components/blade/named_slots.blade.php create mode 100644 tests/components/flux/tests/attribute_cache.blade.php create mode 100644 tests/components/flux/tests/component_a.blade.php create mode 100644 tests/components/flux/tests/component_b.blade.php create mode 100644 tests/components/flux/tests/default_slot.blade.php create mode 100644 tests/components/flux/tests/full_cache.blade.php create mode 100644 tests/components/flux/tests/multiple_nocache.blade.php create mode 100644 tests/components/flux/tests/named_slots.blade.php create mode 100644 tests/components/flux/tests/nocache_component_use.blade.php create mode 100644 tests/components/flux/tests/nocache_directive.blade.php create mode 100644 tests/components/flux/tests/nocache_named_slot.blade.php create mode 100644 tests/components/flux/tests/nocache_slot.blade.php create mode 100644 tests/components/flux/tests/simple.blade.php diff --git a/composer.json b/composer.json index 96a8ce8f..bf586a2c 100644 --- a/composer.json +++ b/composer.json @@ -18,11 +18,20 @@ "livewire/livewire": "^3.5.19", "laravel/prompts": "^0.1|^0.2|^0.3" }, + "require-dev": { + "orchestra/testbench": "^9.2 || ^10.0", + "pestphp/pest": "^2" + }, "autoload": { "psr-4": { "Flux\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Flux\\Tests\\": "tests" + } + }, "extra": { "laravel": { "providers": [ @@ -32,5 +41,10 @@ "Flux": "Flux\\Flux" } } + }, + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true + } } } diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php new file mode 100644 index 00000000..5b76932f --- /dev/null +++ b/src/Compiler/ComponentCompiler.php @@ -0,0 +1,87 @@ +'); + } + + public function compile($value) + { + if (! $value) { + return $value; + } + + if (! $this->canCompileComponent($value)) { + return $value; + } + + $value = preg_replace('/(?compileNoCacheMetaComponent($value); + $value = $this->compileNocacheDirective($value); + + return $value; + } + + protected function compileNoCache($content, $ignore) + { + $this->count++; + $replacement = '__FLUX::SWAP_REPLACEMENT::'. Str::random() . $this->count; + + $compiledIgnore = ''; + + if (strlen($ignore) > 0) { + $compiledIgnore = "\Flux\Flux::runtimeCache()->ignore({$ignore});"; + } + + + $swap = <<<'PHP' +addSwap('$replacement', function ($data) { + extract($data); ob_start(); ?>#body#$replacement +PHP; + + return Str::swap([ + '$replacement' => $replacement, + '#body#' => $content, + '#ignore#' => $compiledIgnore, + ], $swap); + } + + protected function compileNoCacheMetaComponent($value) + { + return preg_replace_callback('/]+))?>(.*?)<\/flux:nocache>/s', function ($matches) { + $ignore = ''; + + if ($matches[1]) { + $attributes = $this->getAttributesFromAttributeString($matches[1]); + + if (isset($attributes['use'])) { + $ignore = '['.$attributes['use'].']'; + } + } + + return $this->compileNoCache($matches[2], $ignore); + }, $value); + } + + protected function compileNocacheDirective($value) + { + return preg_replace_callback('/@nocache(?:\((.*?)\))?([\s\S]*?)@endnocache/s', function ($matches) { + return $this->compileNoCache(trim($matches[2]), $matches[1] ?? ''); + }, $value); + } +} \ No newline at end of file diff --git a/src/Compiler/ComponentHashes.php b/src/Compiler/ComponentHashes.php new file mode 100644 index 00000000..ed4841a1 --- /dev/null +++ b/src/Compiler/ComponentHashes.php @@ -0,0 +1,18 @@ +startComponent{$expression}; ?>"; + } + + public static function compileClassComponentOpening(string $component, string $alias, string $data, string $hash) + { + $componentData = $data ?: '[]'; + + return implode("\n", [ + '', + '', + '', + '', + 'all() : [])); ?>', + 'withName('.$alias.'); ?>', + 'shouldRender()): ?>', /* START: RENDER */ + 'startComponent($component->resolveView(), $component->data()); ?>', + ]); + } + + public static function compileEndFluxComponentClass() + { + $hash = ComponentHashes::popHash(); + + return static::normalizeLineEndings(<<key(\$component->componentName, \$__fluxHoistedComponentData['data'], \$__env); + \$__componentRenderData{$hash} = \$__env->fluxComponentData(); + if (isset(\$__fluxCacheKey{$hash})) { \$__fluxCacheKeyOriginal{$hash} = \$__fluxCacheKey{$hash}; } + if (\$__fluxCacheKey{$hash} === null || \Flux\Flux::runtimeCache()->has(\$__fluxCacheKey{$hash}) === false): /* START: CACHE BLOCK */ + + \Flux\Flux::runtimeCache()->startObserving(\$component->componentName); + \$__fluxTmpOutput{$hash} = \$__env->renderFluxComponent(\$__componentRenderData{$hash}); + \Flux\Flux::runtimeCache()->stopObserving(\$component->componentName); + \$__fluxCacheKey{$hash} = \Flux\Flux::runtimeCache()->key(\$component->componentName, \$__fluxHoistedComponentData['data'], \$__env); + + if (\$__fluxCacheKey{$hash} !== null) { + \Flux\Flux::runtimeCache()->put(\$component->componentName, \$__fluxCacheKey{$hash}, \$__fluxTmpOutput{$hash}); + \$__fluxTmpOutput{$hash} = \Flux\Flux::runtimeCache()->swap(\$component->componentName, \$__fluxTmpOutput{$hash}, \$__componentRenderData{$hash}); + } + + echo \$__fluxTmpOutput{$hash}; + unset(\$__fluxTmpOutput{$hash}); + + else: /* ELSE: CACHE BLOCK */ + \$__env->popFluxComponent(); + \$__fluxTmpOutput{$hash} = \Flux\Flux::runtimeCache()->get(\$__fluxCacheKey{$hash}); + \$__fluxTmpOutput{$hash} = \Flux\Flux::runtimeCache()->swap(\$component->componentName, \$__fluxTmpOutput{$hash}, \$__componentRenderData{$hash}); + echo \$__fluxTmpOutput{$hash}; + + unset(\$__fluxTmpOutput{$hash}); + endif; /* END: CACHE BLOCK */ + + endif; /* END: RENDER */ + + + if (isset(\$__fluxCacheKey{$hash})) { unset(\$__fluxCacheKey{$hash}); } + if (isset(\$__fluxCacheKeyOriginal{$hash})) { + \$__fluxCacheKey{$hash} = \$__fluxCacheKeyOriginal{$hash}; + unset(\$__fluxCacheKeyOriginal{$hash}); + } + if (isset(\$__componentRenderData{$hash})) { unset(\$__componentRenderData{$hash}); } + if (isset(\$__fluxHoistedComponentData)) { unset(\$__fluxHoistedComponentData); } + if (isset(\$__attributesOriginal{$hash})) { + \$attributes = \$__attributesOriginal{$hash}; unset(\$__attributesOriginal{$hash}); + } + + if (isset(\$__componentOriginal{$hash})) { + \$component = \$__componentOriginal{$hash}; unset(\$__componentOriginal{$hash}); + } + + if (isset(\$__fluxHoistedComponentDataOriginal{$hash})) { + \$__fluxHoistedComponentData = \$__fluxHoistedComponentDataOriginal{$hash}; unset(\$__fluxHoistedComponentDataOriginal{$hash}); + } +?> +PHP); + } + + public static function compileFluxAware($expression) + { + return static::normalizeLineEndings(" \$__value) { + \$__consumeVariable = is_string(\$__key) ? \$__key : \$__value; + if (is_string (\$__key)) { + \$\$__consumeVariable = \$__env->getConsumableComponentData(\$__key, \$__value); + \Flux\Flux::runtimeCache()->usesVariable(\$___key, \$\$__consumeVariable, \$__value); + } else { + \$\$__consumeVariable = \$__env->getConsumableComponentData(\$__value); + \Flux\Flux::runtimeCache()->usesVariable(\$__value, \$\$__consumeVariable); + } +} ?>"); + } +} \ No newline at end of file diff --git a/src/Compiler/TagCompiler.php b/src/Compiler/TagCompiler.php new file mode 100644 index 00000000..2be4f3f2 --- /dev/null +++ b/src/Compiler/TagCompiler.php @@ -0,0 +1,229 @@ +##BEGIN-COMPONENT-CLASS##@fluxComponent('{$class}', 'flux::' . {$component}, [ + 'view' => (app()->version() >= 12 ? hash('xxh128', 'flux') : md5('flux')) . '::' . {$component}, + 'data' => \$__env->getCurrentComponentData(), +]) +withAttributes(\$attributes->getAttributes()); ?>"; + } + + return $this->fluxComponentString($component, $attributes); + } + + protected function fluxComponentString(string $component, array $attributes) + { + $class = $this->componentClass($component); + + [$data, $attributes] = $this->partitionDataAndAttributes($class, $attributes); + + $data = $data->mapWithKeys(function ($value, $key) { + return [Str::camel($key) => $value]; + }); + + // If the component doesn't exist as a class, we'll assume it's a class-less + // component and pass the component as a view parameter to the data so it + // can be accessed within the component and we can render out the view. + if (! class_exists($class)) { + $view = Str::startsWith($component, 'mail::') + ? "\$__env->getContainer()->make(Illuminate\\View\\Factory::class)->make('{$component}')" + : "'$class'"; + + $parameters = [ + 'view' => $view, + 'data' => '['.$this->attributesToString($data->all(), $escapeBound = false).']', + ]; + + $class = AnonymousComponent::class; + } else { + $parameters = $data->all(); + } + + return "##BEGIN-COMPONENT-CLASS##@fluxComponent('{$class}', '{$component}', [".$this->attributesToString($parameters, $escapeBound = false).'] ) + +except(\\'.$class.'::ignoredParameterNames()); ?> + +withAttributes(['.$this->fluxAttributesToString($attributes->all(), $escapeAttributes = $class !== DynamicComponent::class).']); ?>'; + } + + protected function fluxAttributesToString(array $attributes, $escapeBound = true) + { + return (new Collection($attributes)) + ->map(function (string $value, string $attribute) use ($escapeBound) { + $propName = Str::camel($attribute); + // Use this retrieval fallback behavior to avoid invoking methods more than needed. + $retrieval = "\$__fluxHoistedComponentData['data']['{$propName}'] ?? \$__fluxHoistedComponentData['data']['{$attribute}'] ?? {$value}"; + + return $escapeBound && isset($this->boundAttributes[$attribute]) && $value !== 'true' && ! is_numeric($value) + ? "'{$attribute}' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute({$retrieval})" + : "'{$attribute}' => {$retrieval}"; + }) + ->implode(','); + } + + /** + * Compile the opening tags within the given string. + * + * @param string $value + * @return string + * + * @throws \InvalidArgumentException + */ + protected function compileOpeningTags(string $value) + { + $pattern = "/ + < + \s* + flux[\:]([\w\-\:\.]*) + (? + (?: + \s+ + (?: + (?: + @(?:class)(\( (?: (?>[^()]+) | (?-1) )* \)) + ) + | + (?: + @(?:style)(\( (?: (?>[^()]+) | (?-1) )* \)) + ) + | + (?: + \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\} + ) + | + (?: + (\:\\\$)(\w+) + ) + | + (?: + [\w\-:.@%]+ + ( + = + (?: + \\\"[^\\\"]*\\\" + | + \'[^\']*\' + | + [^\'\\\"=<>]+ + ) + )? + ) + ) + )* + \s* + ) + (? + /x"; + + return preg_replace_callback($pattern, function (array $matches) { + $this->boundAttributes = []; + + $attributes = $this->getAttributesFromAttributeString($matches['attributes']); + + return $this->componentString('flux::'.$matches[1], $attributes); + }, $value); + } + + /** + * Compile the self-closing tags within the given string. + * + * @param string $value + * @return string + * + * @throws \InvalidArgumentException + */ + protected function compileSelfClosingTags(string $value) + { + $pattern = "/ + < + \s* + flux[\:]([\w\-\:\.]*) + \s* + (? + (?: + \s+ + (?: + (?: + @(?:class)(\( (?: (?>[^()]+) | (?-1) )* \)) + ) + | + (?: + @(?:style)(\( (?: (?>[^()]+) | (?-1) )* \)) + ) + | + (?: + \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\} + ) + | + (?: + (\:\\\$)(\w+) + ) + | + (?: + [\w\-:.@%]+ + ( + = + (?: + \\\"[^\\\"]*\\\" + | + \'[^\']*\' + | + [^\'\\\"=<>]+ + ) + )? + ) + ) + )* + \s* + ) + \/> + /x"; + + return preg_replace_callback($pattern, function (array $matches) { + $this->boundAttributes = []; + + $attributes = $this->getAttributesFromAttributeString($matches['attributes']); + + // Support inline "slot" attributes... + if (isset($attributes['slot'])) { + $slot = $attributes['slot']; + + unset($attributes['slot']); + + return '@slot('.$slot.') ' . $this->componentString('flux::'.$matches[1], $attributes)."\n@endFluxComponentClass##END-COMPONENT-CLASS##" . ' @endslot'; + } + + return $this->componentString('flux::'.$matches[1], $attributes)."\n@endFluxComponentClass##END-COMPONENT-CLASS##"; + }, $value); + } + + /** + * Compile the closing tags within the given string. + * + * @param string $value + * @return string + */ + protected function compileClosingTags(string $value) + { + return preg_replace("/<\/\s*flux[\:][\w\-\:\.]*\s*>/", ' @endFluxComponentClass##END-COMPONENT-CLASS##', $value); + } +} diff --git a/src/FluxComponentCache.php b/src/FluxComponentCache.php new file mode 100644 index 00000000..e549e60d --- /dev/null +++ b/src/FluxComponentCache.php @@ -0,0 +1,178 @@ +observedComponents[$component])) { + return true; + } + + if (! isset($this->cacheableComponents[$component])) { + return true; + } + + return false; + } + + public function key($component, $data, $env) + { + if ($this->shouldSkipComponent($component)) { + return null; + } + + $cacheData = []; + + foreach ($data as $k => $v) { + // Ignore data that is likely internal state. + if (Str::startsWith($k, '__')) { + continue; + } + + // Skip the default slot. + if ($k == 'slot') { + continue; + } + + $cacheData[$k] = $v; + } + + ksort($cacheData); + + $observedComponent = $this->observedComponents[$component]; + $ignoreKeys = $observedComponent['ignore'] ?? []; + $uses = $observedComponent['uses'] ?? []; + + if (count($uses) > 0) { + foreach ($uses as $variableName => $details) { + if (isset($cacheData[$variableName])) { + continue; + } + + // Get the data from the stack so we + // can use it as part of the key + $cacheData[$variableName] = $env->getConsumableComponentData( + $variableName, $details[1] + ); + } + } + + if (count($ignoreKeys) > 0) { + $cacheData = array_diff_key($cacheData, $ignoreKeys); + } + + return $component . '|' .serialize($cacheData); + } + + public function startObserving(string $componentName) + { + $this->observingStack[] = [ + 'component' => $componentName, + 'cacheable' => false, + 'ignore' => [], + 'uses' => [], + ]; + } + + public function stopObserving(string $componentName) + { + $lastObserved = array_pop($this->observingStack); + $lastObserved['ignore'] = array_flip($lastObserved['ignore']); + + if ($lastObserved['cacheable']) { + $this->observedComponents[$componentName] = $lastObserved; + $this->cacheableComponents[$componentName] = true; + } else { + // Don't need a ton of extra information here + // Just need to know we've seen it before + $this->observedComponents[$componentName] = 1; + } + } + + public function ignore($keys) + { + $keys = Arr::wrap($keys); + + $this->observingStack[array_key_last($this->observingStack)]['ignore'] = $keys; + } + + public function usesVariable(string $name, $currentValue, $default = null) + { + $this->observingStack[array_key_last($this->observingStack)]['uses'][$name] = [$currentValue, $default]; + } + + public function isCacheable() + { + $lastKey = array_key_last($this->observingStack); + $lastObserved = $this->observingStack[$lastKey]; + $lastObserved['cacheable'] = true; + + $this->observingStack[$lastKey] = $lastObserved; + } + + public function has($key) + { + return array_key_exists($key, $this->items); + } + + public function put($component, $key, $result) + { + if ($this->shouldSkipComponent($component)) { + return; + } + + $this->items[$key] = $result; + } + + public function get($key) + { + return $this->items[$key]; + } + + public function addSwap($replacement, $callback) + { + $component = $this->observingStack[array_key_last($this->observingStack)]['component']; + + if (! array_key_exists($component, $this->swaps)) { + $this->swaps[$component] = []; + } + + $this->swaps[$component][$replacement] = $callback; + } + + public function swap($component, $value, $data) + { + if (! isset($this->swaps[$component])) { + return $value; + } + + foreach ($this->swaps[$component] as $replacement => $callback) { + $value = str_replace($replacement, $callback($data), $value); + } + + return $value; + } +} \ No newline at end of file diff --git a/src/FluxManager.php b/src/FluxManager.php index 07c9f18b..b2b6fcbc 100644 --- a/src/FluxManager.php +++ b/src/FluxManager.php @@ -15,8 +15,13 @@ class FluxManager public $hasRenderedAssets = false; + /** @var FluxComponentCache */ + protected $runtimeCache = null; + public function boot() { + $this->runtimeCache = new FluxComponentCache; + on('flush-state', function () { $this->hasRenderedAssets = false; }); @@ -24,6 +29,16 @@ public function boot() $this->bootComponents(); } + public function runtimeCache() + { + return $this->runtimeCache; + } + + public function cache() + { + $this->runtimeCache()->isCacheable(); + } + public function ensurePro() { if (! InstalledVersions::isInstalled('livewire/flux-pro')) { diff --git a/src/FluxServiceProvider.php b/src/FluxServiceProvider.php index 2e7cb024..7eb4b21d 100644 --- a/src/FluxServiceProvider.php +++ b/src/FluxServiceProvider.php @@ -2,10 +2,16 @@ namespace Flux; +use Flux\Compiler\ComponentCompiler; +use Flux\Compiler\FluxComponentDirectives; +use Flux\Compiler\TagCompiler; +use Illuminate\Contracts\Support\Htmlable; +use Illuminate\Contracts\View\View; use Illuminate\View\ComponentAttributeBag; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Arr; +use Illuminate\View\ComponentSlot; class FluxServiceProvider extends ServiceProvider { @@ -45,14 +51,24 @@ public function bootComponentPath() public function bootTagCompiler() { - $compiler = new FluxTagCompiler( + app('blade.compiler')->directive('fluxComponent', fn ($expression) => FluxComponentDirectives::compileFluxComponentClass($expression)); + app('blade.compiler')->directive('endFluxComponentClass', fn () => FluxComponentDirectives::compileEndFluxComponentClass()); + app('blade.compiler')->directive('fluxAware', fn ($expression) => FluxComponentDirectives::compileFluxAware($expression)); + + $compiler = new TagCompiler( app('blade.compiler')->getClassComponentAliases(), app('blade.compiler')->getClassComponentNamespaces(), app('blade.compiler') ); + $componentCompiler = new ComponentCompiler; + app()->bind('flux.compiler', fn () => $compiler); + app('blade.compiler')->prepareStringsForCompilationUsing(function ($in) use ($componentCompiler) { + return $componentCompiler->compile($in); + }); + app('blade.compiler')->precompiler(function ($in) use ($compiler) { return $compiler->compile($in); }); @@ -64,6 +80,62 @@ public function bootMacros() return $this->currentComponentData; }); + app('view')::macro('startFluxComponent', function ($view, array $data = []) { + $this->componentStack[] = $view; + + $this->componentData[$this->currentComponent()] = $data; + $this->slots[$this->currentComponent()] = []; + }); + + app('view')::macro('registerFluxComponentSlot', function ($name, $attributes, $callback) { + last($this->componentStack); + + $this->slots[$this->currentComponent()][$name] = new LazyComponentSlot($callback, $attributes); + }); + + app('view')::macro('fluxComponentData', function () { + + $defaultSlot = new ComponentSlot(trim(ob_get_clean())); + + $slots = array_merge([ + '__default' => $defaultSlot, + ], $this->slots[count($this->componentStack) - 1]); + + return array_merge( + $this->componentData[count($this->componentStack) - 1], + ['slot' => $defaultSlot], + $this->slots[count($this->componentStack) - 1], + ['__laravel_slots' => $slots] + ); + }); + + app('view')::macro('popFluxComponent', function () { + array_pop($this->componentStack); + }); + + app('view')::macro('renderFluxComponent', function ($data) { + $view = array_pop($this->componentStack); + + $this->currentComponentData = array_merge( + $previousComponentData = $this->currentComponentData, + $data + ); + + try { + $view = value($view, $data); + + if ($view instanceof View) { + return $view->with($data)->render(); + } elseif ($view instanceof Htmlable) { + return $view->toHtml(); + } else { + return $this->make($view, $data)->render(); + } + } finally { + $this->currentComponentData = $previousComponentData; + } + }); + ComponentAttributeBag::macro('pluck', function ($key) { $result = $this->get($key); diff --git a/src/FluxTagCompiler.php b/src/FluxTagCompiler.php index 99815066..4a8e64c2 100644 --- a/src/FluxTagCompiler.php +++ b/src/FluxTagCompiler.php @@ -2,7 +2,10 @@ namespace Flux; +use Illuminate\Support\Str; +use Illuminate\View\AnonymousComponent; use Illuminate\View\Compilers\ComponentTagCompiler; +use Illuminate\View\DynamicComponent; class FluxTagCompiler extends ComponentTagCompiler { @@ -22,7 +25,42 @@ public function componentString(string $component, array $attributes) withAttributes(\$attributes->getAttributes()); ?>"; } - return parent::componentString($component, $attributes); + return $this->fluxComponentString($component, $attributes); + } + + protected function fluxComponentString(string $component, array $attributes) + { + $class = $this->componentClass($component); + + [$data, $attributes] = $this->partitionDataAndAttributes($class, $attributes); + + $data = $data->mapWithKeys(function ($value, $key) { + return [Str::camel($key) => $value]; + }); + + // If the component doesn't exist as a class, we'll assume it's a class-less + // component and pass the component as a view parameter to the data so it + // can be accessed within the component and we can render out the view. + if (! class_exists($class)) { + $view = Str::startsWith($component, 'mail::') + ? "\$__env->getContainer()->make(Illuminate\\View\\Factory::class)->make('{$component}')" + : "'$class'"; + + $parameters = [ + 'view' => $view, + 'data' => '['.$this->attributesToString($data->all(), $escapeBound = false).']', + ]; + + $class = AnonymousComponent::class; + } else { + $parameters = $data->all(); + } + + return "##BEGIN-COMPONENT-CLASS##@fluxComponent('{$class}', '{$component}', [".$this->attributesToString($parameters, $escapeBound = false).']) + +except(\\'.$class.'::ignoredParameterNames()); ?> + +withAttributes(['.$this->attributesToString($attributes->all(), $escapeAttributes = $class !== DynamicComponent::class).']); ?>'; } /** diff --git a/tests/Feature/CacheTest.php b/tests/Feature/CacheTest.php new file mode 100644 index 00000000..b9733271 --- /dev/null +++ b/tests/Feature/CacheTest.php @@ -0,0 +1,146 @@ +{{ $i }}| +@endfor +BLADE; + + $this->assertSame('0|0|', $this->render($flux)); +}); + +test('attributes can bust the cache', function () { + $flux = <<<'BLADE' +@for ($i = 0; $i < 2; $i++) + {{ $i }} + {{ $i }} +@endfor + +i +i +i +i2 +BLADE; + + $expected = <<<'HTML' +
0
0
0
0
+
0
0
i
i
+HTML; + + $this->assertSame($expected, $this->render($flux)); +}); + +test('nocache directive with variables can exclude items from the cache', function () { + $flux = <<<'BLADE' +@for ($i = 0; $i < 5; $i++) +{{ $i }} +@endfor +BLADE; + + $expected = <<<'EXP' +The Value: 0 +Slot: 0The Value: 1 +Slot: 0The Value: 2 +Slot: 0The Value: 3 +Slot: 0The Value: 4 +Slot: 0 +EXP; + + $this->assertSame($expected, $this->render($flux)); +}); + +test('nocache component with variables can exclude items from the cache', function () { + $flux = <<<'BLADE' +@for ($i = 0; $i < 5; $i++) +{{ $i }} +@endfor +BLADE; + + $expected = <<<'EXP' +The Value: 0 +Slot: 0The Value: 1 +Slot: 0The Value: 2 +Slot: 0The Value: 3 +Slot: 0The Value: 4 +Slot: 0 +EXP; + + $this->assertSame($expected, $this->render($flux)); +}); + +test('nocache can be applied to slot contents', function () { + $flux = <<<'BLADE' +@for ($i = 0; $i < 3; $i++) +Slot: {{ $i }} +@endfor +BLADE; + + $counter = new \Flux\Tests\TestCounter; + + $result = $this->render($flux, ['counter' => $counter]); + + $this->assertSame(1, $counter->count); + $this->assertSame('Slot: 0Slot: 0Slot: 0', $result); +}); + +test('nocache can be applied to named slots', function () { + $flux = <<<'BLADE' + + + A slot! + + + + Another slot! + +BLADE; + + $counter = new \Flux\Tests\TestCounter; + + $result = $this->render($flux, ['counter' => $counter]); + + $this->assertSame(2, $counter->count); + + $this->assertStringContainsString('Was just a prop: bell', $result); + $this->assertStringContainsString('A slot!', $result); + $this->assertStringContainsString('Another slot!', $result); +}); + +test('multiple nocache regions can be declared', function () { + $flux = <<<'BLADE' +@for ($i = 0; $i < 3; $i++) +Slot 1: {{ $i }} +Slot 2: {{ $i }} +@endfor +BLADE; + + $counter = new \Flux\Tests\TestCounter; + + $result = $this->render($flux, ['counter' => $counter]); + + $expected = <<<'HTML' + +HTML; + + $this->assertSame($expected, $result); + $this->assertSame(1, $counter->count); +}); diff --git a/tests/Feature/ComponentDataTest.php b/tests/Feature/ComponentDataTest.php new file mode 100644 index 00000000..7ed0f6a5 --- /dev/null +++ b/tests/Feature/ComponentDataTest.php @@ -0,0 +1,19 @@ +withAttributes([...]) does not call our methods twice. + + $flux = <<<'BLADE' +The Content +BLADE; + + $counter = new \Flux\Tests\TestCounter; + + $result = $this->render($flux, ['counter' => $counter]); + + $this->assertSame(1, $counter->count); + $this->assertSame( + '
The Content
', + $result + ); +}); diff --git a/tests/Feature/GeneralComponentsTest.php b/tests/Feature/GeneralComponentsTest.php new file mode 100644 index 00000000..2acc93de --- /dev/null +++ b/tests/Feature/GeneralComponentsTest.php @@ -0,0 +1,80 @@ +assertSame( + 'Simple Output', + $this->render(''), + 'Colon: Self closing tag' + ); + + $this->assertSame( + 'Simple Output', + $this->render(''), + 'Colon: Tag pair without slot contents' + ); + + $this->assertSame( + 'Simple Output', + $this->render(' '), + 'Colon: Tag pair with useless slot content' + ); +}); + +test('flux component whitespace behavior matches blade', function () { + $flux = <<<'BLADE' + + +The Slot Contents + + +BLADE; + + $blade = <<<'BLADE' + + +The Slot Contents + + +BLADE; + + $this->assertSame($this->render($blade), $this->render($flux)); +}); + +test('escaped blade is preserved', function () { + $flux = <<<'BLADE' +$@{{ $i }}.00 +BLADE; + + $this->assertSame('
${{ $i }}.00
', $this->render($flux)); +}); + +test('literal replacements are not replaced', function () { + // Excessive nesting is to ensure that are not + // incorrectly applied to nested buffers + $flux = <<<'BLADE' + +start:a + +start:b + +start:c + +inner: {{ $title }} + +end:c + +end:b + +end:a + +BLADE; + + $expected = <<<'HTML' +
start:a +
start:b +
start:c +
inner: The Title
end:c
end:b
end:a
+HTML; + + $this->assertSame($expected, $this->render($flux, ['title' => 'The Title'])); +}); diff --git a/tests/Feature/InlineNamedSlotTest.php b/tests/Feature/InlineNamedSlotTest.php new file mode 100644 index 00000000..1e0dff04 --- /dev/null +++ b/tests/Feature/InlineNamedSlotTest.php @@ -0,0 +1,20 @@ + + + + + Main Content + +BLADE; + + $expected = <<<'HTML' +
Component A Content
+
Main Content
+
Component B Content
+HTML; + + $this->assertSame($expected, $this->render($flux)); +}); diff --git a/tests/Feature/NamedSlotTest.php b/tests/Feature/NamedSlotTest.php new file mode 100644 index 00000000..02e8d831 --- /dev/null +++ b/tests/Feature/NamedSlotTest.php @@ -0,0 +1,157 @@ + + The Header Content + The Footer Content + + The Main Content + +BLADE; + + $expected = <<<'HTML' +
The Header Content
+
The Main Content
+
The Footer Content
+HTML; + + $this->assertSame($expected, $this->render($blade)); +}); + +test('attribute bags are applied to named slots', function () { + $flux = <<<'BLADE' + + The Header Content + The Footer Content + + The Main Content + +BLADE; + + $expected = <<<'HTML' +
The Header Content
+
The Main Content
+
The Footer Content
+HTML; + + $this->assertSame($expected, $this->render($flux)); +}); + +test('blade named slot behavior', function () { + $blade = <<<'BLADE' + + Leading Content + + The Header Content + + Middle Content + + The Footer Content + + Trailing Content + +BLADE; + + $flux = <<<'BLADE' + + Leading Content + + The Header Content + + Middle Content + + The Footer Content + + Trailing Content + +BLADE; + + // Using squish to normalize the whitespace left by the Blade compiler. + $actualFlux = Str::squish($this->render($flux)); + $actualBlade = Str::squish($this->render($blade)); + + $this->assertStringContainsString('Leading Content', $actualFlux); + $this->assertStringContainsString('Middle Content', $actualFlux); + $this->assertStringContainsString('Trailing Content', $actualFlux); + + $this->assertStringContainsString('
The Footer Content
', $actualFlux); + $this->assertStringContainsString('
The Header Content
', $actualFlux); + + $this->assertSame($actualBlade, $actualFlux); +}); + +test('nested component slots with similarly named named slots do not get confused', function () { + $flux = <<<'BLADE' + + + Start of Inner Header #1 + + + Header: Inner Header Slot #1 + Header: Inner Footer Slot #1 + + Start of Inner Nested Content + + Header: Inner Header Slot #2 + Header: Inner Footer Slot #2 + + Start of Inner Nested Content #2! + + Header: Inner Header Slot #3 + Header: Inner Footer Slot #3 + + Inner Header Content #3 + + + + End of Inner Nested Content + + + End of Inner Header # 1 + + + + Start of Inner Footer #4 + + + Footer: Inner Header Slot #4 + Footer: Inner Footer Slot #4 + + Inner Footer Content #4 + + + End of Inner Footer + + + Main Content + +BLADE; + + $expected = <<<'HTML' +
Start of Inner Header #1 + +
Header: Inner Header Slot #1
+
Start of Inner Nested Content +
Header: Inner Header Slot #2
+
Start of Inner Nested Content #2! +
Header: Inner Header Slot #3
+
Inner Header Content #3
+
Header: Inner Footer Slot #3
+
Header: Inner Footer Slot #2
+ End of Inner Nested Content
+
Header: Inner Footer Slot #1
+ End of Inner Header # 1
+
Main Content
+
Start of Inner Footer #4 + +
Footer: Inner Header Slot #4
+
Inner Footer Content #4
+
Footer: Inner Footer Slot #4
+ End of Inner Footer
+HTML; + + $this->assertSame($expected, $this->render($flux)); +}); diff --git a/tests/Feature/SlotDataTest.php b/tests/Feature/SlotDataTest.php new file mode 100644 index 00000000..2aee7601 --- /dev/null +++ b/tests/Feature/SlotDataTest.php @@ -0,0 +1,46 @@ + + Component: {{ $component->componentName }} + +BLADE; + + $this->assertSame( + '
Component: flux::tests.default_slot
', + $this->render($flux) + ); +}); + +test('named slots receive component instance', function () { + $blade = <<<'BLADE' + + {{ $component->componentName }} + {{ $component->componentName }} + + {{ $component->componentName }} + +BLADE; + + $expected = <<<'HTML' +
flux::tests.named_slots
+
flux::tests.named_slots
+
flux::tests.named_slots
+HTML; + + $this->assertSame($expected, $this->render($blade)); +}); + +test('slot content is not retained across multiple renders', function () { + $flux = <<<'BLADE' +@for ($i = 0; $i < 3; $i++) +Render: {{ $i }} +@endfor +BLADE; + + $this->assertSame( + '
Render: 0
Render: 1
Render: 2
', + $this->render($flux) + ); +}); diff --git a/tests/FluxTestCase.php b/tests/FluxTestCase.php new file mode 100644 index 00000000..c6071984 --- /dev/null +++ b/tests/FluxTestCase.php @@ -0,0 +1,34 @@ +in('Unit', 'Feature'); diff --git a/tests/TestCounter.php b/tests/TestCounter.php new file mode 100644 index 00000000..98874d4e --- /dev/null +++ b/tests/TestCounter.php @@ -0,0 +1,13 @@ +count; + } +} \ No newline at end of file diff --git a/tests/components/blade/default_slot.blade.php b/tests/components/blade/default_slot.blade.php new file mode 100644 index 00000000..a1b03134 --- /dev/null +++ b/tests/components/blade/default_slot.blade.php @@ -0,0 +1,3 @@ +@props(['simpleProp' => 'value']) + +
{{ $slot }}
\ No newline at end of file diff --git a/tests/components/blade/named_slots.blade.php b/tests/components/blade/named_slots.blade.php new file mode 100644 index 00000000..46881aff --- /dev/null +++ b/tests/components/blade/named_slots.blade.php @@ -0,0 +1,3 @@ +
attributes }}>{{ $header }}
+
{{ $slot }}
+
attributes }}>{{ $footer }}
\ No newline at end of file diff --git a/tests/components/flux/tests/attribute_cache.blade.php b/tests/components/flux/tests/attribute_cache.blade.php new file mode 100644 index 00000000..55c37bff --- /dev/null +++ b/tests/components/flux/tests/attribute_cache.blade.php @@ -0,0 +1,2 @@ + +
{{ $slot }}
\ No newline at end of file diff --git a/tests/components/flux/tests/component_a.blade.php b/tests/components/flux/tests/component_a.blade.php new file mode 100644 index 00000000..bc8428be --- /dev/null +++ b/tests/components/flux/tests/component_a.blade.php @@ -0,0 +1 @@ +Component A Content \ No newline at end of file diff --git a/tests/components/flux/tests/component_b.blade.php b/tests/components/flux/tests/component_b.blade.php new file mode 100644 index 00000000..f8ef130d --- /dev/null +++ b/tests/components/flux/tests/component_b.blade.php @@ -0,0 +1 @@ +Component B Content \ No newline at end of file diff --git a/tests/components/flux/tests/default_slot.blade.php b/tests/components/flux/tests/default_slot.blade.php new file mode 100644 index 00000000..408dfa1f --- /dev/null +++ b/tests/components/flux/tests/default_slot.blade.php @@ -0,0 +1,3 @@ +@props(['simpleProp' => 'value']) + +
{{ $slot }}
\ No newline at end of file diff --git a/tests/components/flux/tests/full_cache.blade.php b/tests/components/flux/tests/full_cache.blade.php new file mode 100644 index 00000000..d03a4af3 --- /dev/null +++ b/tests/components/flux/tests/full_cache.blade.php @@ -0,0 +1,3 @@ + + +{{ $slot }} \ No newline at end of file diff --git a/tests/components/flux/tests/multiple_nocache.blade.php b/tests/components/flux/tests/multiple_nocache.blade.php new file mode 100644 index 00000000..9934cc03 --- /dev/null +++ b/tests/components/flux/tests/multiple_nocache.blade.php @@ -0,0 +1,15 @@ + +@props([ + 'value' => null, + 'counter', +]) + +@php($counter->increment()) + + \ No newline at end of file diff --git a/tests/components/flux/tests/named_slots.blade.php b/tests/components/flux/tests/named_slots.blade.php new file mode 100644 index 00000000..46881aff --- /dev/null +++ b/tests/components/flux/tests/named_slots.blade.php @@ -0,0 +1,3 @@ +
attributes }}>{{ $header }}
+
{{ $slot }}
+
attributes }}>{{ $footer }}
\ No newline at end of file diff --git a/tests/components/flux/tests/nocache_component_use.blade.php b/tests/components/flux/tests/nocache_component_use.blade.php new file mode 100644 index 00000000..09fc40d5 --- /dev/null +++ b/tests/components/flux/tests/nocache_component_use.blade.php @@ -0,0 +1,7 @@ + +@props([ + 'value' => null, +]) + +The Value: {{ $value }} +Slot: {{ $slot }} \ No newline at end of file diff --git a/tests/components/flux/tests/nocache_directive.blade.php b/tests/components/flux/tests/nocache_directive.blade.php new file mode 100644 index 00000000..33e97986 --- /dev/null +++ b/tests/components/flux/tests/nocache_directive.blade.php @@ -0,0 +1,10 @@ + +@props([ + 'value' => null, +]) + +@nocache(['value']) + +The Value: {{ $value }} +@endnocache +Slot: {{ $slot }} \ No newline at end of file diff --git a/tests/components/flux/tests/nocache_named_slot.blade.php b/tests/components/flux/tests/nocache_named_slot.blade.php new file mode 100644 index 00000000..8f971c6e --- /dev/null +++ b/tests/components/flux/tests/nocache_named_slot.blade.php @@ -0,0 +1,16 @@ + +@props([ + 'counter', + 'icon' => null, +]) + +@php($counter->increment()) + + + +Was just a prop: {{ $icon }} + +{{ $icon }} + + + diff --git a/tests/components/flux/tests/nocache_slot.blade.php b/tests/components/flux/tests/nocache_slot.blade.php new file mode 100644 index 00000000..eed3c9b7 --- /dev/null +++ b/tests/components/flux/tests/nocache_slot.blade.php @@ -0,0 +1,6 @@ + +@props(['counter']) + +@php($counter->increment()) + +{{ $slot }} \ No newline at end of file diff --git a/tests/components/flux/tests/simple.blade.php b/tests/components/flux/tests/simple.blade.php new file mode 100644 index 00000000..1747f099 --- /dev/null +++ b/tests/components/flux/tests/simple.blade.php @@ -0,0 +1 @@ +Simple Output \ No newline at end of file From 7de0311c1e96a4f89e3709fe710efa52dbbc8edb Mon Sep 17 00:00:00 2001 From: John Koster Date: Tue, 15 Apr 2025 13:22:44 -0500 Subject: [PATCH 02/46] More tests, some little tweaks --- src/Compiler/ComponentCompiler.php | 6 ++++- src/Compiler/FluxComponentDirectives.php | 2 +- src/FluxServiceProvider.php | 11 +++++++++- tests/Feature/CacheTest.php | 22 +++++++++++++++++++ tests/components/flux/tests/wrapper.blade.php | 7 ++++++ .../flux/tests/wrapper_inner.blade.php | 8 +++++++ 6 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 tests/components/flux/tests/wrapper.blade.php create mode 100644 tests/components/flux/tests/wrapper_inner.blade.php diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index 5b76932f..8fcd1974 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -70,7 +70,11 @@ protected function compileNoCacheMetaComponent($value) $attributes = $this->getAttributesFromAttributeString($matches[1]); if (isset($attributes['use'])) { - $ignore = '['.$attributes['use'].']'; + $variables = str(mb_substr($attributes['use'], 1, -1)) + ->explode(',') + ->map(fn ($var) => "'{$var}'") + ->implode(', '); + $ignore = '['.$variables.']'; } } diff --git a/src/Compiler/FluxComponentDirectives.php b/src/Compiler/FluxComponentDirectives.php index c2a0574b..75619e1c 100644 --- a/src/Compiler/FluxComponentDirectives.php +++ b/src/Compiler/FluxComponentDirectives.php @@ -54,9 +54,9 @@ public static function compileEndFluxComponentClass() return static::normalizeLineEndings(<<key(\$component->componentName, \$__fluxHoistedComponentData['data'], \$__env); \$__componentRenderData{$hash} = \$__env->fluxComponentData(); - if (isset(\$__fluxCacheKey{$hash})) { \$__fluxCacheKeyOriginal{$hash} = \$__fluxCacheKey{$hash}; } if (\$__fluxCacheKey{$hash} === null || \Flux\Flux::runtimeCache()->has(\$__fluxCacheKey{$hash}) === false): /* START: CACHE BLOCK */ \Flux\Flux::runtimeCache()->startObserving(\$component->componentName); diff --git a/src/FluxServiceProvider.php b/src/FluxServiceProvider.php index 7eb4b21d..1da77a72 100644 --- a/src/FluxServiceProvider.php +++ b/src/FluxServiceProvider.php @@ -15,8 +15,11 @@ class FluxServiceProvider extends ServiceProvider { + protected $useCachingCompiler = true; + public function register(): void { + $this->app->alias(FluxManager::class, 'flux'); $this->app->singleton(FluxManager::class); @@ -55,7 +58,13 @@ public function bootTagCompiler() app('blade.compiler')->directive('endFluxComponentClass', fn () => FluxComponentDirectives::compileEndFluxComponentClass()); app('blade.compiler')->directive('fluxAware', fn ($expression) => FluxComponentDirectives::compileFluxAware($expression)); - $compiler = new TagCompiler( + $compilerClass = FluxTagCompiler::class; + + if ($this->useCachingCompiler) { + $compilerClass = TagCompiler::class; + } + + $compiler = new $compilerClass( app('blade.compiler')->getClassComponentAliases(), app('blade.compiler')->getClassComponentNamespaces(), app('blade.compiler') diff --git a/tests/Feature/CacheTest.php b/tests/Feature/CacheTest.php index b9733271..79b1578a 100644 --- a/tests/Feature/CacheTest.php +++ b/tests/Feature/CacheTest.php @@ -144,3 +144,25 @@ $this->assertSame($expected, $result); $this->assertSame(1, $counter->count); }); + +test('inner components do not force caching of wrapper components', function () { + $flux = <<<'BLADE' +@for ($i = 0; $i < 10; $i++) + + |{{ $i }} + +@endfor +BLADE; + + $wrapperCounter = new \Flux\Tests\TestCounter; + $innerCounter = new \Flux\Tests\TestCounter; + + $result = $this->render($flux, [ + 'wrapperCounter' => $wrapperCounter, + 'innerCounter' => $innerCounter, + ]); + + $this->assertStringContainsString('|9', $result); + $this->assertSame(1, $innerCounter->count); + $this->assertSame(10, $wrapperCounter->count); +}); diff --git a/tests/components/flux/tests/wrapper.blade.php b/tests/components/flux/tests/wrapper.blade.php new file mode 100644 index 00000000..b7ab2082 --- /dev/null +++ b/tests/components/flux/tests/wrapper.blade.php @@ -0,0 +1,7 @@ +@props([ + 'counter', +]) + +@php($counter->increment()) + +{{ $slot }} \ No newline at end of file diff --git a/tests/components/flux/tests/wrapper_inner.blade.php b/tests/components/flux/tests/wrapper_inner.blade.php new file mode 100644 index 00000000..36468d00 --- /dev/null +++ b/tests/components/flux/tests/wrapper_inner.blade.php @@ -0,0 +1,8 @@ + +@props([ + 'counter', +]) + +@php($counter->increment()) + +{{ $slot }} \ No newline at end of file From db19251d2b5609e3b2dcd81ce1f439409f8d99af Mon Sep 17 00:00:00 2001 From: John Koster Date: Tue, 15 Apr 2025 14:26:53 -0500 Subject: [PATCH 03/46] Update FluxComponentCache.php --- src/FluxComponentCache.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/FluxComponentCache.php b/src/FluxComponentCache.php index e549e60d..5efee4ac 100644 --- a/src/FluxComponentCache.php +++ b/src/FluxComponentCache.php @@ -4,6 +4,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Str; +use Illuminate\View\ComponentAttributeBag; class FluxComponentCache { @@ -82,6 +83,19 @@ public function key($component, $data, $env) if (count($ignoreKeys) > 0) { $cacheData = array_diff_key($cacheData, $ignoreKeys); + + // If we find an attribute bag, we will want to exclude + // our ignored values from that as well. If we do + // not do this, the ignored values will sneak + // into the cache key this way, usually in + // components that are nested in others + foreach ($cacheData as $k => $v) { + if (! $v instanceof ComponentAttributeBag) { + continue; + } + + $cacheData[$k] = $v->except(array_keys($ignoreKeys)); + } } return $component . '|' .serialize($cacheData); From ad31b0bf14b91d314f2b73063c3fe9bf76bb115d Mon Sep 17 00:00:00 2001 From: John Koster Date: Wed, 23 Apr 2025 18:10:20 -0500 Subject: [PATCH 04/46] Simplify Revert back to "normal" behavior, just to reduce the amount of custom code and behavior --- src/Compiler/TagCompiler.php | 17 +---------------- tests/Feature/ComponentDataTest.php | 19 ------------------- 2 files changed, 1 insertion(+), 35 deletions(-) delete mode 100644 tests/Feature/ComponentDataTest.php diff --git a/src/Compiler/TagCompiler.php b/src/Compiler/TagCompiler.php index 2be4f3f2..55b3b6f1 100644 --- a/src/Compiler/TagCompiler.php +++ b/src/Compiler/TagCompiler.php @@ -61,22 +61,7 @@ protected function fluxComponentString(string $component, array $attributes) except(\\'.$class.'::ignoredParameterNames()); ?> -withAttributes(['.$this->fluxAttributesToString($attributes->all(), $escapeAttributes = $class !== DynamicComponent::class).']); ?>'; - } - - protected function fluxAttributesToString(array $attributes, $escapeBound = true) - { - return (new Collection($attributes)) - ->map(function (string $value, string $attribute) use ($escapeBound) { - $propName = Str::camel($attribute); - // Use this retrieval fallback behavior to avoid invoking methods more than needed. - $retrieval = "\$__fluxHoistedComponentData['data']['{$propName}'] ?? \$__fluxHoistedComponentData['data']['{$attribute}'] ?? {$value}"; - - return $escapeBound && isset($this->boundAttributes[$attribute]) && $value !== 'true' && ! is_numeric($value) - ? "'{$attribute}' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute({$retrieval})" - : "'{$attribute}' => {$retrieval}"; - }) - ->implode(','); +withAttributes(['.$this->attributesToString($attributes->all(), $escapeAttributes = $class !== DynamicComponent::class).']); ?>'; } /** diff --git a/tests/Feature/ComponentDataTest.php b/tests/Feature/ComponentDataTest.php deleted file mode 100644 index 7ed0f6a5..00000000 --- a/tests/Feature/ComponentDataTest.php +++ /dev/null @@ -1,19 +0,0 @@ -withAttributes([...]) does not call our methods twice. - - $flux = <<<'BLADE' -The Content -BLADE; - - $counter = new \Flux\Tests\TestCounter; - - $result = $this->render($flux, ['counter' => $counter]); - - $this->assertSame(1, $counter->count); - $this->assertSame( - '
The Content
', - $result - ); -}); From c9d19d6025dd43efc8469b35ba0508d7c0296d7c Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 26 Apr 2025 10:10:07 -0500 Subject: [PATCH 05/46] Cleanup --- src/Compiler/ComponentCompiler.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index 8fcd1974..1fae8607 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -7,8 +7,6 @@ class ComponentCompiler extends ComponentTagCompiler { - protected $count = 0; - public function canCompileComponent($value) { return Str::startsWith(ltrim($value), ''); @@ -34,8 +32,7 @@ public function compile($value) protected function compileNoCache($content, $ignore) { - $this->count++; - $replacement = '__FLUX::SWAP_REPLACEMENT::'. Str::random() . $this->count; + $replacement = '__FLUX::SWAP_REPLACEMENT::'. Str::random(); $compiledIgnore = ''; From 86d7e1ae0a3b5e950a8e0cc7188bc319d974a0cb Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 26 Apr 2025 10:17:28 -0500 Subject: [PATCH 06/46] Rename methods for happiness --- src/Compiler/ComponentCompiler.php | 6 ++--- src/Compiler/FluxComponentDirectives.php | 22 +++++++++---------- src/FluxManager.php | 12 +++++----- .../flux/tests/attribute_cache.blade.php | 2 +- .../flux/tests/full_cache.blade.php | 2 +- .../flux/tests/multiple_nocache.blade.php | 2 +- .../tests/nocache_component_use.blade.php | 2 +- .../flux/tests/nocache_directive.blade.php | 2 +- .../flux/tests/nocache_named_slot.blade.php | 2 +- .../flux/tests/nocache_slot.blade.php | 2 +- .../flux/tests/wrapper_inner.blade.php | 2 +- 11 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index 1fae8607..f18953a3 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -9,7 +9,7 @@ class ComponentCompiler extends ComponentTagCompiler { public function canCompileComponent($value) { - return Str::startsWith(ltrim($value), ''); + return Str::startsWith(ltrim($value), ''); } public function compile($value) @@ -37,14 +37,14 @@ protected function compileNoCache($content, $ignore) $compiledIgnore = ''; if (strlen($ignore) > 0) { - $compiledIgnore = "\Flux\Flux::runtimeCache()->ignore({$ignore});"; + $compiledIgnore = "\Flux\Flux::cache()->ignore({$ignore});"; } $swap = <<<'PHP' addSwap('$replacement', function ($data) { + \Flux\Flux::cache()->addSwap('$replacement', function ($data) { extract($data); ob_start(); ?>#body#key(\$component->componentName, \$__fluxHoistedComponentData['data'], \$__env); + \$__fluxCacheKey{$hash} = \Flux\Flux::cache()->key(\$component->componentName, \$__fluxHoistedComponentData['data'], \$__env); \$__componentRenderData{$hash} = \$__env->fluxComponentData(); - if (\$__fluxCacheKey{$hash} === null || \Flux\Flux::runtimeCache()->has(\$__fluxCacheKey{$hash}) === false): /* START: CACHE BLOCK */ + if (\$__fluxCacheKey{$hash} === null || \Flux\Flux::cache()->has(\$__fluxCacheKey{$hash}) === false): /* START: CACHE BLOCK */ - \Flux\Flux::runtimeCache()->startObserving(\$component->componentName); + \Flux\Flux::cache()->startObserving(\$component->componentName); \$__fluxTmpOutput{$hash} = \$__env->renderFluxComponent(\$__componentRenderData{$hash}); - \Flux\Flux::runtimeCache()->stopObserving(\$component->componentName); - \$__fluxCacheKey{$hash} = \Flux\Flux::runtimeCache()->key(\$component->componentName, \$__fluxHoistedComponentData['data'], \$__env); + \Flux\Flux::cache()->stopObserving(\$component->componentName); + \$__fluxCacheKey{$hash} = \Flux\Flux::cache()->key(\$component->componentName, \$__fluxHoistedComponentData['data'], \$__env); if (\$__fluxCacheKey{$hash} !== null) { - \Flux\Flux::runtimeCache()->put(\$component->componentName, \$__fluxCacheKey{$hash}, \$__fluxTmpOutput{$hash}); - \$__fluxTmpOutput{$hash} = \Flux\Flux::runtimeCache()->swap(\$component->componentName, \$__fluxTmpOutput{$hash}, \$__componentRenderData{$hash}); + \Flux\Flux::cache()->put(\$component->componentName, \$__fluxCacheKey{$hash}, \$__fluxTmpOutput{$hash}); + \$__fluxTmpOutput{$hash} = \Flux\Flux::cache()->swap(\$component->componentName, \$__fluxTmpOutput{$hash}, \$__componentRenderData{$hash}); } echo \$__fluxTmpOutput{$hash}; @@ -74,8 +74,8 @@ public static function compileEndFluxComponentClass() else: /* ELSE: CACHE BLOCK */ \$__env->popFluxComponent(); - \$__fluxTmpOutput{$hash} = \Flux\Flux::runtimeCache()->get(\$__fluxCacheKey{$hash}); - \$__fluxTmpOutput{$hash} = \Flux\Flux::runtimeCache()->swap(\$component->componentName, \$__fluxTmpOutput{$hash}, \$__componentRenderData{$hash}); + \$__fluxTmpOutput{$hash} = \Flux\Flux::cache()->get(\$__fluxCacheKey{$hash}); + \$__fluxTmpOutput{$hash} = \Flux\Flux::cache()->swap(\$component->componentName, \$__fluxTmpOutput{$hash}, \$__componentRenderData{$hash}); echo \$__fluxTmpOutput{$hash}; unset(\$__fluxTmpOutput{$hash}); @@ -112,10 +112,10 @@ public static function compileFluxAware($expression) \$__consumeVariable = is_string(\$__key) ? \$__key : \$__value; if (is_string (\$__key)) { \$\$__consumeVariable = \$__env->getConsumableComponentData(\$__key, \$__value); - \Flux\Flux::runtimeCache()->usesVariable(\$___key, \$\$__consumeVariable, \$__value); + \Flux\Flux::cache()->usesVariable(\$___key, \$\$__consumeVariable, \$__value); } else { \$\$__consumeVariable = \$__env->getConsumableComponentData(\$__value); - \Flux\Flux::runtimeCache()->usesVariable(\$__value, \$\$__consumeVariable); + \Flux\Flux::cache()->usesVariable(\$__value, \$\$__consumeVariable); } } ?>"); } diff --git a/src/FluxManager.php b/src/FluxManager.php index b2b6fcbc..345bad77 100644 --- a/src/FluxManager.php +++ b/src/FluxManager.php @@ -16,11 +16,11 @@ class FluxManager public $hasRenderedAssets = false; /** @var FluxComponentCache */ - protected $runtimeCache = null; + protected $cache = null; public function boot() { - $this->runtimeCache = new FluxComponentCache; + $this->cache = new FluxComponentCache; on('flush-state', function () { $this->hasRenderedAssets = false; @@ -29,14 +29,14 @@ public function boot() $this->bootComponents(); } - public function runtimeCache() + public function cache() { - return $this->runtimeCache; + return $this->cache; } - public function cache() + public function shouldCache() { - $this->runtimeCache()->isCacheable(); + $this->cache()->isCacheable(); } public function ensurePro() diff --git a/tests/components/flux/tests/attribute_cache.blade.php b/tests/components/flux/tests/attribute_cache.blade.php index 55c37bff..7134fc16 100644 --- a/tests/components/flux/tests/attribute_cache.blade.php +++ b/tests/components/flux/tests/attribute_cache.blade.php @@ -1,2 +1,2 @@ - +
{{ $slot }}
\ No newline at end of file diff --git a/tests/components/flux/tests/full_cache.blade.php b/tests/components/flux/tests/full_cache.blade.php index d03a4af3..7bb7bcdc 100644 --- a/tests/components/flux/tests/full_cache.blade.php +++ b/tests/components/flux/tests/full_cache.blade.php @@ -1,3 +1,3 @@ - + {{ $slot }} \ No newline at end of file diff --git a/tests/components/flux/tests/multiple_nocache.blade.php b/tests/components/flux/tests/multiple_nocache.blade.php index 9934cc03..7a4d1abb 100644 --- a/tests/components/flux/tests/multiple_nocache.blade.php +++ b/tests/components/flux/tests/multiple_nocache.blade.php @@ -1,4 +1,4 @@ - + @props([ 'value' => null, 'counter', diff --git a/tests/components/flux/tests/nocache_component_use.blade.php b/tests/components/flux/tests/nocache_component_use.blade.php index 09fc40d5..f44557c0 100644 --- a/tests/components/flux/tests/nocache_component_use.blade.php +++ b/tests/components/flux/tests/nocache_component_use.blade.php @@ -1,4 +1,4 @@ - + @props([ 'value' => null, ]) diff --git a/tests/components/flux/tests/nocache_directive.blade.php b/tests/components/flux/tests/nocache_directive.blade.php index 33e97986..a325f97b 100644 --- a/tests/components/flux/tests/nocache_directive.blade.php +++ b/tests/components/flux/tests/nocache_directive.blade.php @@ -1,4 +1,4 @@ - + @props([ 'value' => null, ]) diff --git a/tests/components/flux/tests/nocache_named_slot.blade.php b/tests/components/flux/tests/nocache_named_slot.blade.php index 8f971c6e..74079c82 100644 --- a/tests/components/flux/tests/nocache_named_slot.blade.php +++ b/tests/components/flux/tests/nocache_named_slot.blade.php @@ -1,4 +1,4 @@ - + @props([ 'counter', 'icon' => null, diff --git a/tests/components/flux/tests/nocache_slot.blade.php b/tests/components/flux/tests/nocache_slot.blade.php index eed3c9b7..70ba35b3 100644 --- a/tests/components/flux/tests/nocache_slot.blade.php +++ b/tests/components/flux/tests/nocache_slot.blade.php @@ -1,4 +1,4 @@ - + @props(['counter']) @php($counter->increment()) diff --git a/tests/components/flux/tests/wrapper_inner.blade.php b/tests/components/flux/tests/wrapper_inner.blade.php index 36468d00..a5722614 100644 --- a/tests/components/flux/tests/wrapper_inner.blade.php +++ b/tests/components/flux/tests/wrapper_inner.blade.php @@ -1,4 +1,4 @@ - + @props([ 'counter', ]) From 0939c15d065d80a341e556e1518648a1149a7379 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 26 Apr 2025 10:19:36 -0500 Subject: [PATCH 07/46] Refactor PHP block to cache directive-like thing --- src/Compiler/ComponentCompiler.php | 3 ++- tests/components/flux/tests/attribute_cache.blade.php | 2 +- tests/components/flux/tests/full_cache.blade.php | 2 +- tests/components/flux/tests/multiple_nocache.blade.php | 2 +- tests/components/flux/tests/nocache_component_use.blade.php | 2 +- tests/components/flux/tests/nocache_directive.blade.php | 2 +- tests/components/flux/tests/nocache_named_slot.blade.php | 2 +- tests/components/flux/tests/nocache_slot.blade.php | 2 +- tests/components/flux/tests/wrapper_inner.blade.php | 2 +- 9 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index f18953a3..f0e279ca 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -9,7 +9,7 @@ class ComponentCompiler extends ComponentTagCompiler { public function canCompileComponent($value) { - return Str::startsWith(ltrim($value), ''); + return Str::startsWith(ltrim($value), '@cached'); } public function compile($value) @@ -22,6 +22,7 @@ public function compile($value) return $value; } + $value = preg_replace('/(?', $value); $value = preg_replace('/(?compileNoCacheMetaComponent($value); diff --git a/tests/components/flux/tests/attribute_cache.blade.php b/tests/components/flux/tests/attribute_cache.blade.php index 7134fc16..89a09002 100644 --- a/tests/components/flux/tests/attribute_cache.blade.php +++ b/tests/components/flux/tests/attribute_cache.blade.php @@ -1,2 +1,2 @@ - +@cached
{{ $slot }}
\ No newline at end of file diff --git a/tests/components/flux/tests/full_cache.blade.php b/tests/components/flux/tests/full_cache.blade.php index 7bb7bcdc..4bf6de4c 100644 --- a/tests/components/flux/tests/full_cache.blade.php +++ b/tests/components/flux/tests/full_cache.blade.php @@ -1,3 +1,3 @@ - +@cached {{ $slot }} \ No newline at end of file diff --git a/tests/components/flux/tests/multiple_nocache.blade.php b/tests/components/flux/tests/multiple_nocache.blade.php index 7a4d1abb..0a8060c0 100644 --- a/tests/components/flux/tests/multiple_nocache.blade.php +++ b/tests/components/flux/tests/multiple_nocache.blade.php @@ -1,4 +1,4 @@ - +@cached @props([ 'value' => null, 'counter', diff --git a/tests/components/flux/tests/nocache_component_use.blade.php b/tests/components/flux/tests/nocache_component_use.blade.php index f44557c0..edafed81 100644 --- a/tests/components/flux/tests/nocache_component_use.blade.php +++ b/tests/components/flux/tests/nocache_component_use.blade.php @@ -1,4 +1,4 @@ - +@cached @props([ 'value' => null, ]) diff --git a/tests/components/flux/tests/nocache_directive.blade.php b/tests/components/flux/tests/nocache_directive.blade.php index a325f97b..0f3af394 100644 --- a/tests/components/flux/tests/nocache_directive.blade.php +++ b/tests/components/flux/tests/nocache_directive.blade.php @@ -1,4 +1,4 @@ - +@cached @props([ 'value' => null, ]) diff --git a/tests/components/flux/tests/nocache_named_slot.blade.php b/tests/components/flux/tests/nocache_named_slot.blade.php index 74079c82..4fce4090 100644 --- a/tests/components/flux/tests/nocache_named_slot.blade.php +++ b/tests/components/flux/tests/nocache_named_slot.blade.php @@ -1,4 +1,4 @@ - +@cached @props([ 'counter', 'icon' => null, diff --git a/tests/components/flux/tests/nocache_slot.blade.php b/tests/components/flux/tests/nocache_slot.blade.php index 70ba35b3..edd7c9b7 100644 --- a/tests/components/flux/tests/nocache_slot.blade.php +++ b/tests/components/flux/tests/nocache_slot.blade.php @@ -1,4 +1,4 @@ - +@cached @props(['counter']) @php($counter->increment()) diff --git a/tests/components/flux/tests/wrapper_inner.blade.php b/tests/components/flux/tests/wrapper_inner.blade.php index a5722614..79d298f1 100644 --- a/tests/components/flux/tests/wrapper_inner.blade.php +++ b/tests/components/flux/tests/wrapper_inner.blade.php @@ -1,4 +1,4 @@ - +@cached @props([ 'counter', ]) From 18bd054702e462ae2085810c9369ef53c32005e4 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 26 Apr 2025 10:23:14 -0500 Subject: [PATCH 08/46] Make it clearer what this is for --- src/Compiler/ComponentCompiler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index f0e279ca..391e0393 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -7,7 +7,7 @@ class ComponentCompiler extends ComponentTagCompiler { - public function canCompileComponent($value) + public function isFluxComponent($value) { return Str::startsWith(ltrim($value), '@cached'); } @@ -18,7 +18,7 @@ public function compile($value) return $value; } - if (! $this->canCompileComponent($value)) { + if (! $this->isFluxComponent($value)) { return $value; } From aa217736a038207819047eb59acfb86328781af9 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 26 Apr 2025 10:30:38 -0500 Subject: [PATCH 09/46] Rename ignore to exclude --- src/Compiler/ComponentCompiler.php | 16 ++++++++-------- src/FluxComponentCache.php | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index 391e0393..7cda3a68 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -31,14 +31,14 @@ public function compile($value) return $value; } - protected function compileNoCache($content, $ignore) + protected function compileNoCache($content, $excludeExpression) { $replacement = '__FLUX::SWAP_REPLACEMENT::'. Str::random(); - $compiledIgnore = ''; + $comopiledExclude = ''; - if (strlen($ignore) > 0) { - $compiledIgnore = "\Flux\Flux::cache()->ignore({$ignore});"; + if (strlen($excludeExpression) > 0) { + $comopiledExclude = "\Flux\Flux::cache()->exclude({$excludeExpression});"; } @@ -55,14 +55,14 @@ protected function compileNoCache($content, $ignore) return Str::swap([ '$replacement' => $replacement, '#body#' => $content, - '#ignore#' => $compiledIgnore, + '#ignore#' => $comopiledExclude, ], $swap); } protected function compileNoCacheMetaComponent($value) { return preg_replace_callback('/]+))?>(.*?)<\/flux:nocache>/s', function ($matches) { - $ignore = ''; + $excludeExpression = ''; if ($matches[1]) { $attributes = $this->getAttributesFromAttributeString($matches[1]); @@ -72,11 +72,11 @@ protected function compileNoCacheMetaComponent($value) ->explode(',') ->map(fn ($var) => "'{$var}'") ->implode(', '); - $ignore = '['.$variables.']'; + $excludeExpression = '['.$variables.']'; } } - return $this->compileNoCache($matches[2], $ignore); + return $this->compileNoCache($matches[2], $excludeExpression); }, $value); } diff --git a/src/FluxComponentCache.php b/src/FluxComponentCache.php index 5efee4ac..32696c01 100644 --- a/src/FluxComponentCache.php +++ b/src/FluxComponentCache.php @@ -64,7 +64,7 @@ public function key($component, $data, $env) ksort($cacheData); $observedComponent = $this->observedComponents[$component]; - $ignoreKeys = $observedComponent['ignore'] ?? []; + $ignoreKeys = $observedComponent['exclude'] ?? []; $uses = $observedComponent['uses'] ?? []; if (count($uses) > 0) { @@ -106,7 +106,7 @@ public function startObserving(string $componentName) $this->observingStack[] = [ 'component' => $componentName, 'cacheable' => false, - 'ignore' => [], + 'exclude' => [], 'uses' => [], ]; } @@ -114,7 +114,7 @@ public function startObserving(string $componentName) public function stopObserving(string $componentName) { $lastObserved = array_pop($this->observingStack); - $lastObserved['ignore'] = array_flip($lastObserved['ignore']); + $lastObserved['exclude'] = array_flip($lastObserved['exclude']); if ($lastObserved['cacheable']) { $this->observedComponents[$componentName] = $lastObserved; @@ -126,11 +126,11 @@ public function stopObserving(string $componentName) } } - public function ignore($keys) + public function exclude($keys) { $keys = Arr::wrap($keys); - $this->observingStack[array_key_last($this->observingStack)]['ignore'] = $keys; + $this->observingStack[array_key_last($this->observingStack)]['exclude'] = $keys; } public function usesVariable(string $name, $currentValue, $default = null) From 498fa710583d662aadade38c93c928be710ed97f Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 26 Apr 2025 10:31:30 -0500 Subject: [PATCH 10/46] Rename to align more closely with the Blade feature this works with --- src/FluxComponentCache.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/FluxComponentCache.php b/src/FluxComponentCache.php index 32696c01..766c2402 100644 --- a/src/FluxComponentCache.php +++ b/src/FluxComponentCache.php @@ -65,7 +65,7 @@ public function key($component, $data, $env) $observedComponent = $this->observedComponents[$component]; $ignoreKeys = $observedComponent['exclude'] ?? []; - $uses = $observedComponent['uses'] ?? []; + $uses = $observedComponent['aware'] ?? []; if (count($uses) > 0) { foreach ($uses as $variableName => $details) { @@ -107,7 +107,7 @@ public function startObserving(string $componentName) 'component' => $componentName, 'cacheable' => false, 'exclude' => [], - 'uses' => [], + 'aware' => [], ]; } @@ -135,7 +135,7 @@ public function exclude($keys) public function usesVariable(string $name, $currentValue, $default = null) { - $this->observingStack[array_key_last($this->observingStack)]['uses'][$name] = [$currentValue, $default]; + $this->observingStack[array_key_last($this->observingStack)]['aware'][$name] = [$currentValue, $default]; } public function isCacheable() From 38faa8aa0e4ea8a5ddee593a3e634c831fdea2e2 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 26 Apr 2025 10:52:33 -0500 Subject: [PATCH 11/46] Typo --- src/Compiler/ComponentCompiler.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index 7cda3a68..3cd468ce 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -35,10 +35,10 @@ protected function compileNoCache($content, $excludeExpression) { $replacement = '__FLUX::SWAP_REPLACEMENT::'. Str::random(); - $comopiledExclude = ''; + $compiledExclude = ''; if (strlen($excludeExpression) > 0) { - $comopiledExclude = "\Flux\Flux::cache()->exclude({$excludeExpression});"; + $compiledExclude = "\Flux\Flux::cache()->exclude({$excludeExpression});"; } @@ -55,7 +55,7 @@ protected function compileNoCache($content, $excludeExpression) return Str::swap([ '$replacement' => $replacement, '#body#' => $content, - '#ignore#' => $comopiledExclude, + '#ignore#' => $compiledExclude, ], $swap); } From 8ff4de0e074ec7d0de7d4bd42bd2f033b664b0e5 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 26 Apr 2025 12:41:50 -0500 Subject: [PATCH 12/46] Process props in cache callbacks --- src/Compiler/ComponentCompiler.php | 48 ++++++++++++++++----- src/Compiler/FluxComponentDirectives.php | 33 ++++++++++++++ src/FluxComponentCache.php | 19 +++++++- src/FluxServiceProvider.php | 1 + tests/Feature/PropsTest.php | 22 ++++++++++ tests/components/flux/tests/props.blade.php | 8 ++++ 6 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 tests/Feature/PropsTest.php create mode 100644 tests/components/flux/tests/props.blade.php diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index 3cd468ce..9a419a84 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -2,6 +2,7 @@ namespace Flux\Compiler; +use Flux\Flux; use Illuminate\Support\Str; use Illuminate\View\Compilers\ComponentTagCompiler; @@ -23,6 +24,7 @@ public function compile($value) } $value = preg_replace('/(?', $value); + $value = preg_replace('/(?compileNoCacheMetaComponent($value); @@ -41,22 +43,48 @@ protected function compileNoCache($content, $excludeExpression) $compiledExclude = "\Flux\Flux::cache()->exclude({$excludeExpression});"; } + $component = Flux::cache()->currentComponent(); - $swap = <<<'PHP' + return <<addSwap('$replacement', function ($data) { - extract($data); ob_start(); ?>#body#addSwap('$replacement', function (\$data) { + extract(\$data); + \$attributes ??= new \\Illuminate\\View\\ComponentAttributeBag; + + \$__newAttributes = []; + [\$__propNames, \$__originalPropValues] = \Flux\Flux::cache()->componentProps('$component'); + + foreach (\$attributes->all() as \$__key => \$__value) { + if (in_array(\$__key, \$__propNames)) { + \$\$__key = \$\$__key ?? \$__value; + } else { + \$__newAttributes[\$__key] = \$__value; + } + } + + \$attributes = new \Illuminate\View\ComponentAttributeBag(\$__newAttributes); + unset(\$__propNames); + unset(\$__newAttributes); + + foreach (array_filter(\$__originalPropValues, 'is_string', ARRAY_FILTER_USE_KEY) as \$__key => \$__value) { + \$\$__key = \$\$__key ?? \$__value; + } + unset(\$__originalPropValues); + + \$__defined_vars = get_defined_vars(); + + foreach (\$attributes->all() as \$__key => \$__value) { + if (array_key_exists(\$__key, \$__defined_vars)) unset(\$\$__key); + } + + unset(\$__defined_vars); + ob_start(); +?>$content$replacement PHP; - - return Str::swap([ - '$replacement' => $replacement, - '#body#' => $content, - '#ignore#' => $compiledExclude, - ], $swap); } protected function compileNoCacheMetaComponent($value) diff --git a/src/Compiler/FluxComponentDirectives.php b/src/Compiler/FluxComponentDirectives.php index a8814e01..b42fb4a8 100644 --- a/src/Compiler/FluxComponentDirectives.php +++ b/src/Compiler/FluxComponentDirectives.php @@ -119,4 +119,37 @@ public static function compileFluxAware($expression) } } ?>"); } + + public static function compileFluxProps($expression) + { + return "props(\$__propNames, $expression); + +foreach (\$attributes->all() as \$__key => \$__value) { + if (in_array(\$__key, \$__propNames)) { + \$\$__key = \$\$__key ?? \$__value; + } else { + \$__newAttributes[\$__key] = \$__value; + } +} + +\$attributes = new \Illuminate\View\ComponentAttributeBag(\$__newAttributes); +unset(\$__propNames); +unset(\$__newAttributes); + +foreach (array_filter({$expression}, 'is_string', ARRAY_FILTER_USE_KEY) as \$__key => \$__value) { + \$\$__key = \$\$__key ?? \$__value; +} + +\$__defined_vars = get_defined_vars(); + +foreach (\$attributes->all() as \$__key => \$__value) { + if (array_key_exists(\$__key, \$__defined_vars)) unset(\$\$__key); +} + +unset(\$__defined_vars); ?>"; + } } \ No newline at end of file diff --git a/src/FluxComponentCache.php b/src/FluxComponentCache.php index 766c2402..fab9dc89 100644 --- a/src/FluxComponentCache.php +++ b/src/FluxComponentCache.php @@ -22,6 +22,8 @@ class FluxComponentCache protected $observingStack = []; + protected $componentProps = []; + protected $items = []; protected $swaps = []; @@ -126,6 +128,11 @@ public function stopObserving(string $componentName) } } + public function currentComponent() + { + return $this->observingStack[array_key_last($this->observingStack)]['component']; + } + public function exclude($keys) { $keys = Arr::wrap($keys); @@ -133,6 +140,16 @@ public function exclude($keys) $this->observingStack[array_key_last($this->observingStack)]['exclude'] = $keys; } + public function props($props, $values) + { + $this->componentProps[$this->currentComponent()] = [$props, $values]; + } + + public function componentProps($component) + { + return $this->componentProps[$component] ?? [[], []]; + } + public function usesVariable(string $name, $currentValue, $default = null) { $this->observingStack[array_key_last($this->observingStack)]['aware'][$name] = [$currentValue, $default]; @@ -168,7 +185,7 @@ public function get($key) public function addSwap($replacement, $callback) { - $component = $this->observingStack[array_key_last($this->observingStack)]['component']; + $component = $this->currentComponent(); if (! array_key_exists($component, $this->swaps)) { $this->swaps[$component] = []; diff --git a/src/FluxServiceProvider.php b/src/FluxServiceProvider.php index 1da77a72..c097266c 100644 --- a/src/FluxServiceProvider.php +++ b/src/FluxServiceProvider.php @@ -57,6 +57,7 @@ public function bootTagCompiler() app('blade.compiler')->directive('fluxComponent', fn ($expression) => FluxComponentDirectives::compileFluxComponentClass($expression)); app('blade.compiler')->directive('endFluxComponentClass', fn () => FluxComponentDirectives::compileEndFluxComponentClass()); app('blade.compiler')->directive('fluxAware', fn ($expression) => FluxComponentDirectives::compileFluxAware($expression)); + app('blade.compiler')->directive('fluxProps', fn ($expression) => FluxComponentDirectives::compileFluxProps($expression)); $compilerClass = FluxTagCompiler::class; diff --git a/tests/Feature/PropsTest.php b/tests/Feature/PropsTest.php new file mode 100644 index 00000000..bf1c7eae --- /dev/null +++ b/tests/Feature/PropsTest.php @@ -0,0 +1,22 @@ + + +BLADE; + + $expected = <<<'EXPECTED' +Title: Title 1 +Attributes: class="mt-1 mt-2" +Title: Title 2 +Attributes: class="mt-1 mt-2" + +EXPECTED; + + $this->assertSame($expected, $this->render($flux)); + + // Ensure props are still available after things have been cached once. + $this->assertSame($expected, $this->render($flux)); + $this->assertSame($expected, $this->render($flux)); +}); \ No newline at end of file diff --git a/tests/components/flux/tests/props.blade.php b/tests/components/flux/tests/props.blade.php new file mode 100644 index 00000000..62abaa3e --- /dev/null +++ b/tests/components/flux/tests/props.blade.php @@ -0,0 +1,8 @@ +@cached + +@props(['title']) + + +Title: {{ $title ?? 'nope' }} +Attributes: {{ $attributes }} + \ No newline at end of file From 8215fe0e5f2fe3de5bbd6c5ce07b189e281e8369 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 26 Apr 2025 12:48:53 -0500 Subject: [PATCH 13/46] Rename nocache to uncache --- src/Compiler/ComponentCompiler.php | 18 +++++------ tests/Feature/CacheTest.php | 32 +++++++++---------- ....blade.php => multiple_uncached.blade.php} | 6 ++-- .../tests/nocache_component_use.blade.php | 7 ---- tests/components/flux/tests/props.blade.php | 4 +-- .../tests/uncached_component_use.blade.php | 7 ++++ ...blade.php => uncached_directive.blade.php} | 4 +-- ...lade.php => uncached_named_slot.blade.php} | 4 +-- ...slot.blade.php => uncached_slot.blade.php} | 0 .../flux/tests/wrapper_inner.blade.php | 2 +- 10 files changed, 42 insertions(+), 42 deletions(-) rename tests/components/flux/tests/{multiple_nocache.blade.php => multiple_uncached.blade.php} (70%) delete mode 100644 tests/components/flux/tests/nocache_component_use.blade.php create mode 100644 tests/components/flux/tests/uncached_component_use.blade.php rename tests/components/flux/tests/{nocache_directive.blade.php => uncached_directive.blade.php} (71%) rename tests/components/flux/tests/{nocache_named_slot.blade.php => uncached_named_slot.blade.php} (86%) rename tests/components/flux/tests/{nocache_slot.blade.php => uncached_slot.blade.php} (100%) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index 9a419a84..7ec7d718 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -27,13 +27,13 @@ public function compile($value) $value = preg_replace('/(?compileNoCacheMetaComponent($value); - $value = $this->compileNocacheDirective($value); + $value = $this->compileUncachedComponent($value); + $value = $this->compileUncachedDirective($value); return $value; } - protected function compileNoCache($content, $excludeExpression) + protected function compileUncached($content, $excludeExpression) { $replacement = '__FLUX::SWAP_REPLACEMENT::'. Str::random(); @@ -87,9 +87,9 @@ protected function compileNoCache($content, $excludeExpression) PHP; } - protected function compileNoCacheMetaComponent($value) + protected function compileUncachedComponent($value) { - return preg_replace_callback('/]+))?>(.*?)<\/flux:nocache>/s', function ($matches) { + return preg_replace_callback('/]+))?>(.*?)<\/flux:uncached>/s', function ($matches) { $excludeExpression = ''; if ($matches[1]) { @@ -104,14 +104,14 @@ protected function compileNoCacheMetaComponent($value) } } - return $this->compileNoCache($matches[2], $excludeExpression); + return $this->compileUncached($matches[2], $excludeExpression); }, $value); } - protected function compileNocacheDirective($value) + protected function compileUncachedDirective($value) { - return preg_replace_callback('/@nocache(?:\((.*?)\))?([\s\S]*?)@endnocache/s', function ($matches) { - return $this->compileNoCache(trim($matches[2]), $matches[1] ?? ''); + return preg_replace_callback('/@uncached(?:\((.*?)\))?([\s\S]*?)@enduncached/s', function ($matches) { + return $this->compileUncached(trim($matches[2]), $matches[1] ?? ''); }, $value); } } \ No newline at end of file diff --git a/tests/Feature/CacheTest.php b/tests/Feature/CacheTest.php index 79b1578a..d02dab5b 100644 --- a/tests/Feature/CacheTest.php +++ b/tests/Feature/CacheTest.php @@ -1,6 +1,6 @@ {{ $i }}| @@ -31,10 +31,10 @@ $this->assertSame($expected, $this->render($flux)); }); -test('nocache directive with variables can exclude items from the cache', function () { +test('uncached directive with variables can exclude items from the cache', function () { $flux = <<<'BLADE' @for ($i = 0; $i < 5; $i++) -{{ $i }} +{{ $i }} @endfor BLADE; @@ -50,10 +50,10 @@ $this->assertSame($expected, $this->render($flux)); }); -test('nocache component with variables can exclude items from the cache', function () { +test('uncached component with variables can exclude items from the cache', function () { $flux = <<<'BLADE' @for ($i = 0; $i < 5; $i++) -{{ $i }} +{{ $i }} @endfor BLADE; @@ -69,10 +69,10 @@ $this->assertSame($expected, $this->render($flux)); }); -test('nocache can be applied to slot contents', function () { +test('uncached can be applied to slot contents', function () { $flux = <<<'BLADE' @for ($i = 0; $i < 3; $i++) -Slot: {{ $i }} +Slot: {{ $i }} @endfor BLADE; @@ -84,16 +84,16 @@ $this->assertSame('Slot: 0Slot: 0Slot: 0', $result); }); -test('nocache can be applied to named slots', function () { +test('uncached can be applied to named slots', function () { $flux = <<<'BLADE' - - + + A slot! - + - + Another slot! - + BLADE; $counter = new \Flux\Tests\TestCounter; @@ -107,11 +107,11 @@ $this->assertStringContainsString('Another slot!', $result); }); -test('multiple nocache regions can be declared', function () { +test('multiple uncached regions can be declared', function () { $flux = <<<'BLADE' @for ($i = 0; $i < 3; $i++) -Slot 1: {{ $i }} -Slot 2: {{ $i }} +Slot 1: {{ $i }} +Slot 2: {{ $i }} @endfor BLADE; diff --git a/tests/components/flux/tests/multiple_nocache.blade.php b/tests/components/flux/tests/multiple_uncached.blade.php similarity index 70% rename from tests/components/flux/tests/multiple_nocache.blade.php rename to tests/components/flux/tests/multiple_uncached.blade.php index 0a8060c0..1cb7ece2 100644 --- a/tests/components/flux/tests/multiple_nocache.blade.php +++ b/tests/components/flux/tests/multiple_uncached.blade.php @@ -8,8 +8,8 @@ \ No newline at end of file + @enduncached +>{{ $slot }} \ No newline at end of file diff --git a/tests/components/flux/tests/nocache_component_use.blade.php b/tests/components/flux/tests/nocache_component_use.blade.php deleted file mode 100644 index edafed81..00000000 --- a/tests/components/flux/tests/nocache_component_use.blade.php +++ /dev/null @@ -1,7 +0,0 @@ -@cached -@props([ - 'value' => null, -]) - -The Value: {{ $value }} -Slot: {{ $slot }} \ No newline at end of file diff --git a/tests/components/flux/tests/props.blade.php b/tests/components/flux/tests/props.blade.php index 62abaa3e..c97b6af2 100644 --- a/tests/components/flux/tests/props.blade.php +++ b/tests/components/flux/tests/props.blade.php @@ -2,7 +2,7 @@ @props(['title']) - + Title: {{ $title ?? 'nope' }} Attributes: {{ $attributes }} - \ No newline at end of file + \ No newline at end of file diff --git a/tests/components/flux/tests/uncached_component_use.blade.php b/tests/components/flux/tests/uncached_component_use.blade.php new file mode 100644 index 00000000..e1212328 --- /dev/null +++ b/tests/components/flux/tests/uncached_component_use.blade.php @@ -0,0 +1,7 @@ +@cached +@props([ + 'value' => null, +]) + +The Value: {{ $value }} +Slot: {{ $slot }} \ No newline at end of file diff --git a/tests/components/flux/tests/nocache_directive.blade.php b/tests/components/flux/tests/uncached_directive.blade.php similarity index 71% rename from tests/components/flux/tests/nocache_directive.blade.php rename to tests/components/flux/tests/uncached_directive.blade.php index 0f3af394..469146c9 100644 --- a/tests/components/flux/tests/nocache_directive.blade.php +++ b/tests/components/flux/tests/uncached_directive.blade.php @@ -3,8 +3,8 @@ 'value' => null, ]) -@nocache(['value']) +@uncached(['value']) The Value: {{ $value }} -@endnocache +@enduncached Slot: {{ $slot }} \ No newline at end of file diff --git a/tests/components/flux/tests/nocache_named_slot.blade.php b/tests/components/flux/tests/uncached_named_slot.blade.php similarity index 86% rename from tests/components/flux/tests/nocache_named_slot.blade.php rename to tests/components/flux/tests/uncached_named_slot.blade.php index 4fce4090..ff1aee8a 100644 --- a/tests/components/flux/tests/nocache_named_slot.blade.php +++ b/tests/components/flux/tests/uncached_named_slot.blade.php @@ -6,11 +6,11 @@ @php($counter->increment()) - + Was just a prop: {{ $icon }} {{ $icon }} - + diff --git a/tests/components/flux/tests/nocache_slot.blade.php b/tests/components/flux/tests/uncached_slot.blade.php similarity index 100% rename from tests/components/flux/tests/nocache_slot.blade.php rename to tests/components/flux/tests/uncached_slot.blade.php diff --git a/tests/components/flux/tests/wrapper_inner.blade.php b/tests/components/flux/tests/wrapper_inner.blade.php index 79d298f1..267f2685 100644 --- a/tests/components/flux/tests/wrapper_inner.blade.php +++ b/tests/components/flux/tests/wrapper_inner.blade.php @@ -5,4 +5,4 @@ @php($counter->increment()) -{{ $slot }} \ No newline at end of file +{{ $slot }} \ No newline at end of file From 53327a5fb7b037e02ba31bdfb1190395b83bbbef Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 26 Apr 2025 12:50:29 -0500 Subject: [PATCH 14/46] Rename "use" to "exclude" for consistency --- src/Compiler/ComponentCompiler.php | 4 ++-- tests/Feature/CacheTest.php | 2 +- .../flux/tests/uncached_component_exclude.blade.php | 7 +++++++ .../components/flux/tests/uncached_component_use.blade.php | 7 ------- 4 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 tests/components/flux/tests/uncached_component_exclude.blade.php delete mode 100644 tests/components/flux/tests/uncached_component_use.blade.php diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index 7ec7d718..cbc36510 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -95,8 +95,8 @@ protected function compileUncachedComponent($value) if ($matches[1]) { $attributes = $this->getAttributesFromAttributeString($matches[1]); - if (isset($attributes['use'])) { - $variables = str(mb_substr($attributes['use'], 1, -1)) + if (isset($attributes['exclude'])) { + $variables = str(mb_substr($attributes['exclude'], 1, -1)) ->explode(',') ->map(fn ($var) => "'{$var}'") ->implode(', '); diff --git a/tests/Feature/CacheTest.php b/tests/Feature/CacheTest.php index d02dab5b..400b8997 100644 --- a/tests/Feature/CacheTest.php +++ b/tests/Feature/CacheTest.php @@ -53,7 +53,7 @@ test('uncached component with variables can exclude items from the cache', function () { $flux = <<<'BLADE' @for ($i = 0; $i < 5; $i++) -{{ $i }} +{{ $i }} @endfor BLADE; diff --git a/tests/components/flux/tests/uncached_component_exclude.blade.php b/tests/components/flux/tests/uncached_component_exclude.blade.php new file mode 100644 index 00000000..5ef5045a --- /dev/null +++ b/tests/components/flux/tests/uncached_component_exclude.blade.php @@ -0,0 +1,7 @@ +@cached +@props([ + 'value' => null, +]) + +The Value: {{ $value }} +Slot: {{ $slot }} \ No newline at end of file diff --git a/tests/components/flux/tests/uncached_component_use.blade.php b/tests/components/flux/tests/uncached_component_use.blade.php deleted file mode 100644 index e1212328..00000000 --- a/tests/components/flux/tests/uncached_component_use.blade.php +++ /dev/null @@ -1,7 +0,0 @@ -@cached -@props([ - 'value' => null, -]) - -The Value: {{ $value }} -Slot: {{ $slot }} \ No newline at end of file From 50c3e9262f6210c0cffbbc8823205878cef02d25 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 26 Apr 2025 15:35:07 -0500 Subject: [PATCH 15/46] Adds a flux "optimized" compiler flag/directive --- src/Compiler/ComponentCompiler.php | 63 +++++++++++++++++++++++- src/Compiler/FluxComponentDirectives.php | 10 ++-- src/FluxComponentCache.php | 16 ++++++ src/FluxManager.php | 5 ++ src/Support/StrUtil.php | 11 +++++ 5 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 src/Support/StrUtil.php diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index cbc36510..3cbc00ad 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -3,6 +3,7 @@ namespace Flux\Compiler; use Flux\Flux; +use Flux\Support\StrUtil; use Illuminate\Support\Str; use Illuminate\View\Compilers\ComponentTagCompiler; @@ -10,7 +11,63 @@ class ComponentCompiler extends ComponentTagCompiler { public function isFluxComponent($value) { - return Str::startsWith(ltrim($value), '@cached'); + return Str::startsWith(ltrim($value), ['@cached', '@optimized']); + } + + protected function compileOptimizedComponent($value) + { + $value = StrUtil::normalizeLineEndings($value); + + // If the Flux component contains @aware or @props, we need + // to make sure we insert our @uncached directive pair + // after them. Otherwise, we will be missing info + // later on, which will lead to state errors + // + // This error presents itself as "undefined array key index ''" + $value = ltrim(preg_replace('/(?', $value)); + preg_match_all('/(?substr(0, $lastMatchOffset) // Limit the search space to the start of the last important directive. + ->substrCount("\n"); // Find the 0-indexed line number of the directive. + + // Now that we have the line our directive starts on, we need to find the ending of it. + $lines = explode("\n", $value); + $endLine = null; + + for ($i = $startLine; $i < count($lines); $i++) { + $line = (string) str($lines[$i]) + ->squish(); // Collapse the whitespace to make finding our ending easier. + + if (Str::endsWith($line, ['])', '] )'])) { + $endLine = $i; + break; + } + } + + if ($endLine === null) { + return $value; + } + + // Insert the beginning of our uncached directive. + array_splice( + $lines, + $endLine, 1, + [ $lines[$endLine], '@uncached' ] + ); + + $lines[] = '@enduncached'; + + return implode("\n", $lines); } public function compile($value) @@ -23,6 +80,10 @@ public function compile($value) return $value; } + if (Str::startsWith(ltrim($value), '@optimized')) { + $value = $this->compileOptimizedComponent($value); + } + $value = preg_replace('/(?', $value); $value = preg_replace('/(? \$__value) { + return StrUtil::normalizeLineEndings(" \$__value) { \$__consumeVariable = is_string(\$__key) ? \$__key : \$__value; if (is_string (\$__key)) { \$\$__consumeVariable = \$__env->getConsumableComponentData(\$__key, \$__value); diff --git a/src/FluxComponentCache.php b/src/FluxComponentCache.php index fab9dc89..b51309b3 100644 --- a/src/FluxComponentCache.php +++ b/src/FluxComponentCache.php @@ -15,6 +15,8 @@ class FluxComponentCache */ protected $cacheableComponents = []; + protected $optimizedComponents = []; + /** * @var array */ @@ -43,6 +45,13 @@ protected function shouldSkipComponent($component) public function key($component, $data, $env) { + // Optimized components will share the same key + // The component's contents will always be + // re-evaluated, so we don't need data + if (array_key_exists($component, $this->optimizedComponents)) { + return $component; + } + if ($this->shouldSkipComponent($component)) { return null; } @@ -155,6 +164,13 @@ public function usesVariable(string $name, $currentValue, $default = null) $this->observingStack[array_key_last($this->observingStack)]['aware'][$name] = [$currentValue, $default]; } + public function isOptimized() + { + $this->isCacheable(); + + $this->optimizedComponents[$this->currentComponent()] = true; + } + public function isCacheable() { $lastKey = array_key_last($this->observingStack); diff --git a/src/FluxManager.php b/src/FluxManager.php index 345bad77..2b171021 100644 --- a/src/FluxManager.php +++ b/src/FluxManager.php @@ -39,6 +39,11 @@ public function shouldCache() $this->cache()->isCacheable(); } + public function shouldOptimize() + { + $this->cache()->isOptimized(); + } + public function ensurePro() { if (! InstalledVersions::isInstalled('livewire/flux-pro')) { diff --git a/src/Support/StrUtil.php b/src/Support/StrUtil.php new file mode 100644 index 00000000..97a0b110 --- /dev/null +++ b/src/Support/StrUtil.php @@ -0,0 +1,11 @@ + Date: Sat, 26 Apr 2025 15:51:40 -0500 Subject: [PATCH 16/46] Ensure the environment is always available --- src/Compiler/ComponentCompiler.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index 3cbc00ad..cbb219f8 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -111,6 +111,7 @@ protected function compileUncached($content, $excludeExpression) $compiledExclude \Flux\Flux::cache()->addSwap('$replacement', function (\$data) { extract(\$data); + \$__env = \$__env ?? view(); \$attributes ??= new \\Illuminate\\View\\ComponentAttributeBag; \$__newAttributes = []; From b8f04a4fe0515307cc1a5790dfb72175d59352c0 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 26 Apr 2025 16:07:01 -0500 Subject: [PATCH 17/46] Host any leading content If we have some code before the the first aware/props, it's probably important for adjusting state before those things evaluate. An example of this is the Avatar component. Instead of having to rethink how all of that works, this change simply hoists that content so the component does not need to be reworked in order to use "optimized" --- src/Compiler/ComponentCompiler.php | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index cbb219f8..26821a13 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -9,6 +9,8 @@ class ComponentCompiler extends ComponentTagCompiler { + protected $hoistedContent = ''; + public function isFluxComponent($value) { return Str::startsWith(ltrim($value), ['@cached', '@optimized']); @@ -16,6 +18,8 @@ public function isFluxComponent($value) protected function compileOptimizedComponent($value) { + $this->hoistedContent = ''; + $value = StrUtil::normalizeLineEndings($value); // If the Flux component contains @aware or @props, we need @@ -24,15 +28,23 @@ protected function compileOptimizedComponent($value) // later on, which will lead to state errors // // This error presents itself as "undefined array key index ''" - $value = ltrim(preg_replace('/(?', $value)); + $value = ltrim(preg_replace('/(?\n@uncached\n$value\n@enduncached"; } $matches = $matches[1]; + // Get the offset of the first interesting directive. + $firstMatchOffset = $matches[0][1]; + + // Find anything interesting we may want to hoist + $this->hoistedContent = (string) str($value) + ->substr(0, $firstMatchOffset - 1) + ->trim(); + // Get the offset of the last directive we are interested in $lastMatchOffset = $matches[array_key_last($matches)][1]; @@ -67,6 +79,9 @@ protected function compileOptimizedComponent($value) $lines[] = '@enduncached'; + // Add our shouldOptimize call. + array_unshift($lines, ''); + return implode("\n", $lines); } @@ -114,6 +129,8 @@ protected function compileUncached($content, $excludeExpression) \$__env = \$__env ?? view(); \$attributes ??= new \\Illuminate\\View\\ComponentAttributeBag; +?>{$this->hoistedContent}componentProps('$component'); From 37a6456fa25c9353348b98631d64be48af90410b Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 26 Apr 2025 19:25:35 -0500 Subject: [PATCH 18/46] Yeet all this Thought of a better way to do this --- src/Compiler/ComponentCompiler.php | 107 ++--------------------- src/Compiler/FluxComponentDirectives.php | 9 +- src/Support/StrUtil.php | 11 --- 3 files changed, 15 insertions(+), 112 deletions(-) delete mode 100644 src/Support/StrUtil.php diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index 26821a13..b4ba7a27 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -3,13 +3,12 @@ namespace Flux\Compiler; use Flux\Flux; -use Flux\Support\StrUtil; use Illuminate\Support\Str; use Illuminate\View\Compilers\ComponentTagCompiler; class ComponentCompiler extends ComponentTagCompiler { - protected $hoistedContent = ''; + protected $isOptimized = false; public function isFluxComponent($value) { @@ -18,71 +17,11 @@ public function isFluxComponent($value) protected function compileOptimizedComponent($value) { - $this->hoistedContent = ''; + $this->isOptimized = true; - $value = StrUtil::normalizeLineEndings($value); - - // If the Flux component contains @aware or @props, we need - // to make sure we insert our @uncached directive pair - // after them. Otherwise, we will be missing info - // later on, which will lead to state errors - // - // This error presents itself as "undefined array key index ''" $value = ltrim(preg_replace('/(?\n@uncached\n$value\n@enduncached"; - } - - $matches = $matches[1]; - - // Get the offset of the first interesting directive. - $firstMatchOffset = $matches[0][1]; - - // Find anything interesting we may want to hoist - $this->hoistedContent = (string) str($value) - ->substr(0, $firstMatchOffset - 1) - ->trim(); - - // Get the offset of the last directive we are interested in - $lastMatchOffset = $matches[array_key_last($matches)][1]; - - $startLine = str($value) - ->substr(0, $lastMatchOffset) // Limit the search space to the start of the last important directive. - ->substrCount("\n"); // Find the 0-indexed line number of the directive. - - // Now that we have the line our directive starts on, we need to find the ending of it. - $lines = explode("\n", $value); - $endLine = null; - - for ($i = $startLine; $i < count($lines); $i++) { - $line = (string) str($lines[$i]) - ->squish(); // Collapse the whitespace to make finding our ending easier. - - if (Str::endsWith($line, ['])', '] )'])) { - $endLine = $i; - break; - } - } - if ($endLine === null) { - return $value; - } - - // Insert the beginning of our uncached directive. - array_splice( - $lines, - $endLine, 1, - [ $lines[$endLine], '@uncached' ] - ); - - $lines[] = '@enduncached'; - - // Add our shouldOptimize call. - array_unshift($lines, ''); - - return implode("\n", $lines); + return "\n@uncached\n$value\n@enduncached"; } public function compile($value) @@ -100,8 +39,11 @@ public function compile($value) } $value = preg_replace('/(?', $value); - $value = preg_replace('/(?isOptimized) { + $value = preg_replace('/(?compileUncachedComponent($value); $value = $this->compileUncachedDirective($value); @@ -119,45 +61,12 @@ protected function compileUncached($content, $excludeExpression) $compiledExclude = "\Flux\Flux::cache()->exclude({$excludeExpression});"; } - $component = Flux::cache()->currentComponent(); - return <<addSwap('$replacement', function (\$data) { extract(\$data); \$__env = \$__env ?? view(); - \$attributes ??= new \\Illuminate\\View\\ComponentAttributeBag; - -?>{$this->hoistedContent}componentProps('$component'); - - foreach (\$attributes->all() as \$__key => \$__value) { - if (in_array(\$__key, \$__propNames)) { - \$\$__key = \$\$__key ?? \$__value; - } else { - \$__newAttributes[\$__key] = \$__value; - } - } - - \$attributes = new \Illuminate\View\ComponentAttributeBag(\$__newAttributes); - unset(\$__propNames); - unset(\$__newAttributes); - - foreach (array_filter(\$__originalPropValues, 'is_string', ARRAY_FILTER_USE_KEY) as \$__key => \$__value) { - \$\$__key = \$\$__key ?? \$__value; - } - unset(\$__originalPropValues); - - \$__defined_vars = get_defined_vars(); - - foreach (\$attributes->all() as \$__key => \$__value) { - if (array_key_exists(\$__key, \$__defined_vars)) unset(\$\$__key); - } - - unset(\$__defined_vars); ob_start(); ?>$content \$__value) { + return static::normalizeLineEndings(" \$__value) { \$__consumeVariable = is_string(\$__key) ? \$__key : \$__value; if (is_string (\$__key)) { \$\$__consumeVariable = \$__env->getConsumableComponentData(\$__key, \$__value); diff --git a/src/Support/StrUtil.php b/src/Support/StrUtil.php deleted file mode 100644 index 97a0b110..00000000 --- a/src/Support/StrUtil.php +++ /dev/null @@ -1,11 +0,0 @@ - Date: Sat, 26 Apr 2025 22:04:30 -0500 Subject: [PATCH 19/46] Make it less annoying to swap between versions --- src/Compiler/ComponentCompiler.php | 19 +++++++++++++++++++ src/FluxManager.php | 12 ++++++++++++ src/FluxServiceProvider.php | 4 ++-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index b4ba7a27..ac7a5375 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -24,6 +24,17 @@ protected function compileOptimizedComponent($value) return "\n@uncached\n$value\n@enduncached"; } + protected function removeCacheFeatures($value) + { + $value = preg_replace('/(?compileUncachedComponent($value); + $value = $this->compileUncachedDirective($value); + + return $value; + } + public function compile($value) { if (! $value) { @@ -34,6 +45,10 @@ public function compile($value) return $value; } + if (! Flux::cacheEnabled()) { + return $this->removeCacheFeatures($value); + } + if (Str::startsWith(ltrim($value), '@optimized')) { $value = $this->compileOptimizedComponent($value); } @@ -53,6 +68,10 @@ public function compile($value) protected function compileUncached($content, $excludeExpression) { + if (! Flux::cacheEnabled()) { + return $content; + } + $replacement = '__FLUX::SWAP_REPLACEMENT::'. Str::random(); $compiledExclude = ''; diff --git a/src/FluxManager.php b/src/FluxManager.php index 2b171021..7be4b14f 100644 --- a/src/FluxManager.php +++ b/src/FluxManager.php @@ -18,6 +18,13 @@ class FluxManager /** @var FluxComponentCache */ protected $cache = null; + protected $cacheEnabled = false; + + public function __construct($cacheEnabled) + { + $this->cacheEnabled = $cacheEnabled; + } + public function boot() { $this->cache = new FluxComponentCache; @@ -44,6 +51,11 @@ public function shouldOptimize() $this->cache()->isOptimized(); } + public function cacheEnabled() + { + return $this->cacheEnabled; + } + public function ensurePro() { if (! InstalledVersions::isInstalled('livewire/flux-pro')) { diff --git a/src/FluxServiceProvider.php b/src/FluxServiceProvider.php index c097266c..627a70de 100644 --- a/src/FluxServiceProvider.php +++ b/src/FluxServiceProvider.php @@ -15,14 +15,14 @@ class FluxServiceProvider extends ServiceProvider { - protected $useCachingCompiler = true; + protected $useCachingCompiler = false; public function register(): void { $this->app->alias(FluxManager::class, 'flux'); - $this->app->singleton(FluxManager::class); + $this->app->singleton(FluxManager::class, fn () => new FluxManager($this->useCachingCompiler)); $loader = \Illuminate\Foundation\AliasLoader::getInstance(); $loader->alias('Flux', \Flux\Flux::class); From 34aade703af98de5c26ea3a6e08e573383c8ce16 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 26 Apr 2025 22:04:46 -0500 Subject: [PATCH 20/46] Update ComponentCompiler.php --- src/Compiler/ComponentCompiler.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index ac7a5375..ef1f2e80 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -8,7 +8,7 @@ class ComponentCompiler extends ComponentTagCompiler { - protected $isOptimized = false; + protected $isOptimizedComponent = false; public function isFluxComponent($value) { @@ -17,7 +17,7 @@ public function isFluxComponent($value) protected function compileOptimizedComponent($value) { - $this->isOptimized = true; + $this->isOptimizedComponent = true; $value = ltrim(preg_replace('/(?', $value); - if (! $this->isOptimized) { + if (! $this->isOptimizedComponent) { $value = preg_replace('/(? Date: Sun, 27 Apr 2025 12:05:34 -0500 Subject: [PATCH 21/46] Revert some changes Only really needing that value in component compiler for now --- src/Compiler/ComponentCompiler.php | 7 +++++-- src/FluxManager.php | 12 ------------ src/FluxServiceProvider.php | 5 +++-- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index ef1f2e80..177314dd 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -4,12 +4,15 @@ use Flux\Flux; use Illuminate\Support\Str; +use Illuminate\View\Compilers\BladeCompiler; use Illuminate\View\Compilers\ComponentTagCompiler; class ComponentCompiler extends ComponentTagCompiler { protected $isOptimizedComponent = false; + public $outputOptimizations = false; + public function isFluxComponent($value) { return Str::startsWith(ltrim($value), ['@cached', '@optimized']); @@ -45,7 +48,7 @@ public function compile($value) return $value; } - if (! Flux::cacheEnabled()) { + if (! $this->outputOptimizations) { return $this->removeCacheFeatures($value); } @@ -68,7 +71,7 @@ public function compile($value) protected function compileUncached($content, $excludeExpression) { - if (! Flux::cacheEnabled()) { + if (! $this->outputOptimizations) { return $content; } diff --git a/src/FluxManager.php b/src/FluxManager.php index 7be4b14f..2b171021 100644 --- a/src/FluxManager.php +++ b/src/FluxManager.php @@ -18,13 +18,6 @@ class FluxManager /** @var FluxComponentCache */ protected $cache = null; - protected $cacheEnabled = false; - - public function __construct($cacheEnabled) - { - $this->cacheEnabled = $cacheEnabled; - } - public function boot() { $this->cache = new FluxComponentCache; @@ -51,11 +44,6 @@ public function shouldOptimize() $this->cache()->isOptimized(); } - public function cacheEnabled() - { - return $this->cacheEnabled; - } - public function ensurePro() { if (! InstalledVersions::isInstalled('livewire/flux-pro')) { diff --git a/src/FluxServiceProvider.php b/src/FluxServiceProvider.php index 627a70de..ff32ce99 100644 --- a/src/FluxServiceProvider.php +++ b/src/FluxServiceProvider.php @@ -15,14 +15,14 @@ class FluxServiceProvider extends ServiceProvider { - protected $useCachingCompiler = false; + protected $useCachingCompiler = true; public function register(): void { $this->app->alias(FluxManager::class, 'flux'); - $this->app->singleton(FluxManager::class, fn () => new FluxManager($this->useCachingCompiler)); + $this->app->singleton(FluxManager::class); $loader = \Illuminate\Foundation\AliasLoader::getInstance(); $loader->alias('Flux', \Flux\Flux::class); @@ -72,6 +72,7 @@ public function bootTagCompiler() ); $componentCompiler = new ComponentCompiler; + $componentCompiler->outputOptimizations = $this->useCachingCompiler; app()->bind('flux.compiler', fn () => $compiler); From c88ea54e1479e78d96f77b105d4b60a632086269 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 27 Apr 2025 13:49:50 -0500 Subject: [PATCH 22/46] Update ComponentCompiler.php --- src/Compiler/ComponentCompiler.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index 177314dd..0a99976f 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -27,17 +27,22 @@ protected function compileOptimizedComponent($value) return "\n@uncached\n$value\n@enduncached"; } - protected function removeCacheFeatures($value) + protected function compileComponent($value) { - $value = preg_replace('/(?compileUncachedComponent($value); $value = $this->compileUncachedDirective($value); return $value; } + protected function removeCacheFeatures($value) + { + $value = preg_replace('/(?compileComponent($value); + } + public function compile($value) { if (! $value) { @@ -63,10 +68,7 @@ public function compile($value) $value = preg_replace('/(?compileUncachedComponent($value); - $value = $this->compileUncachedDirective($value); - - return $value; + return $this->compileComponent($value); } protected function compileUncached($content, $excludeExpression) From 816bbac9c7c70ec5b8196cd3a66f12e36e70e9d0 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 27 Apr 2025 14:35:59 -0500 Subject: [PATCH 23/46] Going to change approach on this --- src/Compiler/FluxComponentDirectives.php | 33 ------------------------ src/FluxComponentCache.php | 12 --------- src/FluxServiceProvider.php | 1 - 3 files changed, 46 deletions(-) diff --git a/src/Compiler/FluxComponentDirectives.php b/src/Compiler/FluxComponentDirectives.php index 55536964..ce0257c1 100644 --- a/src/Compiler/FluxComponentDirectives.php +++ b/src/Compiler/FluxComponentDirectives.php @@ -120,37 +120,4 @@ public static function compileFluxAware($expression) } } ?>"); } - - public static function compileFluxProps($expression) - { - return "props(\$__propNames, $expression); - -foreach (\$attributes->all() as \$__key => \$__value) { - if (in_array(\$__key, \$__propNames)) { - \$\$__key = \$\$__key ?? \$__value; - } else { - \$__newAttributes[\$__key] = \$__value; - } -} - -\$attributes = new \Illuminate\View\ComponentAttributeBag(\$__newAttributes); -unset(\$__propNames); -unset(\$__newAttributes); - -foreach (array_filter({$expression}, 'is_string', ARRAY_FILTER_USE_KEY) as \$__key => \$__value) { - \$\$__key = \$\$__key ?? \$__value; -} - -\$__defined_vars = get_defined_vars(); - -foreach (\$attributes->all() as \$__key => \$__value) { - if (array_key_exists(\$__key, \$__defined_vars)) unset(\$\$__key); -} - -unset(\$__defined_vars); ?>"; - } } \ No newline at end of file diff --git a/src/FluxComponentCache.php b/src/FluxComponentCache.php index b51309b3..edb1f16d 100644 --- a/src/FluxComponentCache.php +++ b/src/FluxComponentCache.php @@ -24,8 +24,6 @@ class FluxComponentCache protected $observingStack = []; - protected $componentProps = []; - protected $items = []; protected $swaps = []; @@ -149,16 +147,6 @@ public function exclude($keys) $this->observingStack[array_key_last($this->observingStack)]['exclude'] = $keys; } - public function props($props, $values) - { - $this->componentProps[$this->currentComponent()] = [$props, $values]; - } - - public function componentProps($component) - { - return $this->componentProps[$component] ?? [[], []]; - } - public function usesVariable(string $name, $currentValue, $default = null) { $this->observingStack[array_key_last($this->observingStack)]['aware'][$name] = [$currentValue, $default]; diff --git a/src/FluxServiceProvider.php b/src/FluxServiceProvider.php index ff32ce99..5b22348f 100644 --- a/src/FluxServiceProvider.php +++ b/src/FluxServiceProvider.php @@ -57,7 +57,6 @@ public function bootTagCompiler() app('blade.compiler')->directive('fluxComponent', fn ($expression) => FluxComponentDirectives::compileFluxComponentClass($expression)); app('blade.compiler')->directive('endFluxComponentClass', fn () => FluxComponentDirectives::compileEndFluxComponentClass()); app('blade.compiler')->directive('fluxAware', fn ($expression) => FluxComponentDirectives::compileFluxAware($expression)); - app('blade.compiler')->directive('fluxProps', fn ($expression) => FluxComponentDirectives::compileFluxProps($expression)); $compilerClass = FluxTagCompiler::class; From ccdbed3b98c15f34b143f5f3977c10fcdfdeba7a Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 27 Apr 2025 15:44:58 -0500 Subject: [PATCH 24/46] Much simpler --- src/Compiler/TagCompiler.php | 205 +---------------------------------- src/FluxTagCompiler.php | 55 ++-------- 2 files changed, 16 insertions(+), 244 deletions(-) diff --git a/src/Compiler/TagCompiler.php b/src/Compiler/TagCompiler.php index 55b3b6f1..6a9f7306 100644 --- a/src/Compiler/TagCompiler.php +++ b/src/Compiler/TagCompiler.php @@ -2,213 +2,16 @@ namespace Flux\Compiler; +use Flux\FluxTagCompiler; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Illuminate\View\AnonymousComponent; use Illuminate\View\Compilers\ComponentTagCompiler; use Illuminate\View\DynamicComponent; -class TagCompiler extends ComponentTagCompiler +class TagCompiler extends FluxTagCompiler { - public function componentString(string $component, array $attributes) - { - // A component that forwards all data, attributes, and named slots to another component... - if ($component === 'flux::delegate-component') { - $component = $attributes['component']; + protected $componentDirective = 'fluxComponent'; - $class = \Illuminate\View\AnonymousComponent::class; - - // Laravel 12+ uses xxh128 hashing for views https://github.com/laravel/framework/pull/52301... - return "##BEGIN-COMPONENT-CLASS##@fluxComponent('{$class}', 'flux::' . {$component}, [ - 'view' => (app()->version() >= 12 ? hash('xxh128', 'flux') : md5('flux')) . '::' . {$component}, - 'data' => \$__env->getCurrentComponentData(), -]) -withAttributes(\$attributes->getAttributes()); ?>"; - } - - return $this->fluxComponentString($component, $attributes); - } - - protected function fluxComponentString(string $component, array $attributes) - { - $class = $this->componentClass($component); - - [$data, $attributes] = $this->partitionDataAndAttributes($class, $attributes); - - $data = $data->mapWithKeys(function ($value, $key) { - return [Str::camel($key) => $value]; - }); - - // If the component doesn't exist as a class, we'll assume it's a class-less - // component and pass the component as a view parameter to the data so it - // can be accessed within the component and we can render out the view. - if (! class_exists($class)) { - $view = Str::startsWith($component, 'mail::') - ? "\$__env->getContainer()->make(Illuminate\\View\\Factory::class)->make('{$component}')" - : "'$class'"; - - $parameters = [ - 'view' => $view, - 'data' => '['.$this->attributesToString($data->all(), $escapeBound = false).']', - ]; - - $class = AnonymousComponent::class; - } else { - $parameters = $data->all(); - } - - return "##BEGIN-COMPONENT-CLASS##@fluxComponent('{$class}', '{$component}', [".$this->attributesToString($parameters, $escapeBound = false).'] ) - -except(\\'.$class.'::ignoredParameterNames()); ?> - -withAttributes(['.$this->attributesToString($attributes->all(), $escapeAttributes = $class !== DynamicComponent::class).']); ?>'; - } - - /** - * Compile the opening tags within the given string. - * - * @param string $value - * @return string - * - * @throws \InvalidArgumentException - */ - protected function compileOpeningTags(string $value) - { - $pattern = "/ - < - \s* - flux[\:]([\w\-\:\.]*) - (? - (?: - \s+ - (?: - (?: - @(?:class)(\( (?: (?>[^()]+) | (?-1) )* \)) - ) - | - (?: - @(?:style)(\( (?: (?>[^()]+) | (?-1) )* \)) - ) - | - (?: - \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\} - ) - | - (?: - (\:\\\$)(\w+) - ) - | - (?: - [\w\-:.@%]+ - ( - = - (?: - \\\"[^\\\"]*\\\" - | - \'[^\']*\' - | - [^\'\\\"=<>]+ - ) - )? - ) - ) - )* - \s* - ) - (? - /x"; - - return preg_replace_callback($pattern, function (array $matches) { - $this->boundAttributes = []; - - $attributes = $this->getAttributesFromAttributeString($matches['attributes']); - - return $this->componentString('flux::'.$matches[1], $attributes); - }, $value); - } - - /** - * Compile the self-closing tags within the given string. - * - * @param string $value - * @return string - * - * @throws \InvalidArgumentException - */ - protected function compileSelfClosingTags(string $value) - { - $pattern = "/ - < - \s* - flux[\:]([\w\-\:\.]*) - \s* - (? - (?: - \s+ - (?: - (?: - @(?:class)(\( (?: (?>[^()]+) | (?-1) )* \)) - ) - | - (?: - @(?:style)(\( (?: (?>[^()]+) | (?-1) )* \)) - ) - | - (?: - \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\} - ) - | - (?: - (\:\\\$)(\w+) - ) - | - (?: - [\w\-:.@%]+ - ( - = - (?: - \\\"[^\\\"]*\\\" - | - \'[^\']*\' - | - [^\'\\\"=<>]+ - ) - )? - ) - ) - )* - \s* - ) - \/> - /x"; - - return preg_replace_callback($pattern, function (array $matches) { - $this->boundAttributes = []; - - $attributes = $this->getAttributesFromAttributeString($matches['attributes']); - - // Support inline "slot" attributes... - if (isset($attributes['slot'])) { - $slot = $attributes['slot']; - - unset($attributes['slot']); - - return '@slot('.$slot.') ' . $this->componentString('flux::'.$matches[1], $attributes)."\n@endFluxComponentClass##END-COMPONENT-CLASS##" . ' @endslot'; - } - - return $this->componentString('flux::'.$matches[1], $attributes)."\n@endFluxComponentClass##END-COMPONENT-CLASS##"; - }, $value); - } - - /** - * Compile the closing tags within the given string. - * - * @param string $value - * @return string - */ - protected function compileClosingTags(string $value) - { - return preg_replace("/<\/\s*flux[\:][\w\-\:\.]*\s*>/", ' @endFluxComponentClass##END-COMPONENT-CLASS##', $value); - } + protected $endComponentDirective = 'endFluxComponentClass'; } diff --git a/src/FluxTagCompiler.php b/src/FluxTagCompiler.php index 4a8e64c2..fe9d4b45 100644 --- a/src/FluxTagCompiler.php +++ b/src/FluxTagCompiler.php @@ -2,13 +2,13 @@ namespace Flux; -use Illuminate\Support\Str; -use Illuminate\View\AnonymousComponent; use Illuminate\View\Compilers\ComponentTagCompiler; -use Illuminate\View\DynamicComponent; class FluxTagCompiler extends ComponentTagCompiler { + protected $componentDirective = 'component'; + protected $endComponentDirective = 'endComponentClass'; + public function componentString(string $component, array $attributes) { // A component that forwards all data, attributes, and named slots to another component... @@ -18,49 +18,18 @@ public function componentString(string $component, array $attributes) $class = \Illuminate\View\AnonymousComponent::class; // Laravel 12+ uses xxh128 hashing for views https://github.com/laravel/framework/pull/52301... - return "##BEGIN-COMPONENT-CLASS##@component('{$class}', 'flux::' . {$component}, [ + return "##BEGIN-COMPONENT-CLASS##@{$this->componentDirective}('{$class}', 'flux::' . {$component}, [ 'view' => (app()->version() >= 12 ? hash('xxh128', 'flux') : md5('flux')) . '::' . {$component}, 'data' => \$__env->getCurrentComponentData(), ]) withAttributes(\$attributes->getAttributes()); ?>"; } - return $this->fluxComponentString($component, $attributes); - } - - protected function fluxComponentString(string $component, array $attributes) - { - $class = $this->componentClass($component); - - [$data, $attributes] = $this->partitionDataAndAttributes($class, $attributes); - - $data = $data->mapWithKeys(function ($value, $key) { - return [Str::camel($key) => $value]; - }); - - // If the component doesn't exist as a class, we'll assume it's a class-less - // component and pass the component as a view parameter to the data so it - // can be accessed within the component and we can render out the view. - if (! class_exists($class)) { - $view = Str::startsWith($component, 'mail::') - ? "\$__env->getContainer()->make(Illuminate\\View\\Factory::class)->make('{$component}')" - : "'$class'"; - - $parameters = [ - 'view' => $view, - 'data' => '['.$this->attributesToString($data->all(), $escapeBound = false).']', - ]; - - $class = AnonymousComponent::class; - } else { - $parameters = $data->all(); - } - - return "##BEGIN-COMPONENT-CLASS##@fluxComponent('{$class}', '{$component}', [".$this->attributesToString($parameters, $escapeBound = false).']) - -except(\\'.$class.'::ignoredParameterNames()); ?> - -withAttributes(['.$this->attributesToString($attributes->all(), $escapeAttributes = $class !== DynamicComponent::class).']); ?>'; + return str_replace( + '##BEGIN-COMPONENT-CLASS##@component(', + '##BEGIN-COMPONENT-CLASS##@'.$this->componentDirective.'(', + parent::componentString($component, $attributes) + ); } /** @@ -193,10 +162,10 @@ protected function compileSelfClosingTags(string $value) unset($attributes['slot']); - return '@slot('.$slot.') ' . $this->componentString('flux::'.$matches[1], $attributes)."\n@endComponentClass##END-COMPONENT-CLASS##" . ' @endslot'; + return '@slot('.$slot.') ' . $this->componentString('flux::'.$matches[1], $attributes)."\n@{$this->endComponentDirective}##END-COMPONENT-CLASS##" . ' @endslot'; } - return $this->componentString('flux::'.$matches[1], $attributes)."\n@endComponentClass##END-COMPONENT-CLASS##"; + return $this->componentString('flux::'.$matches[1], $attributes)."\n@{$this->endComponentDirective}##END-COMPONENT-CLASS##"; }, $value); } @@ -208,6 +177,6 @@ protected function compileSelfClosingTags(string $value) */ protected function compileClosingTags(string $value) { - return preg_replace("/<\/\s*flux[\:][\w\-\:\.]*\s*>/", ' @endComponentClass##END-COMPONENT-CLASS##', $value); + return preg_replace("/<\/\s*flux[\:][\w\-\:\.]*\s*>/", ' @'.$this->endComponentDirective.'##END-COMPONENT-CLASS##', $value); } } From d4945905ef5ec5098272bb655e565e16740d5687 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 27 Apr 2025 19:35:13 -0500 Subject: [PATCH 25/46] Internal infra to run a "setup" function This will be used to process aware/props/etc., even for cached components. This change currently requires manual demarking like: ``` @setup .... @endsetup ``` Some of this can be done automatically later --- src/Compiler/FluxComponentDirectives.php | 2 ++ src/FluxComponentCache.php | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/Compiler/FluxComponentDirectives.php b/src/Compiler/FluxComponentDirectives.php index ce0257c1..85accf04 100644 --- a/src/Compiler/FluxComponentDirectives.php +++ b/src/Compiler/FluxComponentDirectives.php @@ -67,6 +67,7 @@ public static function compileEndFluxComponentClass() if (\$__fluxCacheKey{$hash} !== null) { \Flux\Flux::cache()->put(\$component->componentName, \$__fluxCacheKey{$hash}, \$__fluxTmpOutput{$hash}); + \$__componentRenderData{$hash} = \Flux\Flux::cache()->runComponentSetup(\$component->componentName, \$__componentRenderData{$hash}); \$__fluxTmpOutput{$hash} = \Flux\Flux::cache()->swap(\$component->componentName, \$__fluxTmpOutput{$hash}, \$__componentRenderData{$hash}); } @@ -76,6 +77,7 @@ public static function compileEndFluxComponentClass() else: /* ELSE: CACHE BLOCK */ \$__env->popFluxComponent(); \$__fluxTmpOutput{$hash} = \Flux\Flux::cache()->get(\$__fluxCacheKey{$hash}); + \$__componentRenderData{$hash} = \Flux\Flux::cache()->runComponentSetup(\$component->componentName, \$__componentRenderData{$hash}); \$__fluxTmpOutput{$hash} = \Flux\Flux::cache()->swap(\$component->componentName, \$__fluxTmpOutput{$hash}, \$__componentRenderData{$hash}); echo \$__fluxTmpOutput{$hash}; diff --git a/src/FluxComponentCache.php b/src/FluxComponentCache.php index edb1f16d..c63cbfc8 100644 --- a/src/FluxComponentCache.php +++ b/src/FluxComponentCache.php @@ -28,6 +28,8 @@ class FluxComponentCache protected $swaps = []; + protected $setupFunctions = []; + protected function shouldSkipComponent($component) { if (! isset($this->observedComponents[$component])) { @@ -210,4 +212,25 @@ public function swap($component, $value, $data) return $value; } + + public function registerSetup($callback) + { + $component = $this->currentComponent(); + + $this->setupFunctions[$component] = $callback; + } + + public function runCurrentComponentSetup($data) + { + return $this->runComponentSetup($this->currentComponent(), $data); + } + + public function runComponentSetup($component, $data) + { + if (! array_key_exists($component, $this->setupFunctions)) { + return $data; + } + + return $this->setupFunctions[$component]($data); + } } \ No newline at end of file From 455611f72d6a67fea6c4ebd474c08f09d63742be Mon Sep 17 00:00:00 2001 From: John Koster Date: Sun, 27 Apr 2025 21:29:53 -0500 Subject: [PATCH 26/46] Add compiler support for setup --- src/Compiler/ComponentCompiler.php | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index 0a99976f..fa56c27d 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -29,6 +29,7 @@ protected function compileOptimizedComponent($value) protected function compileComponent($value) { + $value = $this->compileSetupDirective($value); $value = $this->compileUncachedComponent($value); $value = $this->compileUncachedDirective($value); @@ -64,13 +65,29 @@ public function compile($value) $value = preg_replace('/(?', $value); if (! $this->isOptimizedComponent) { - $value = preg_replace('/(?compileComponent($value); } + protected function compileSetup($content) + { + return <<registerSetup(function (\$__tmpComponentData) { + extract(\$__tmpComponentData); + \$__env = \$__env ?? view(); + ?>{$content}runCurrentComponentSetup(get_defined_vars())); +?> +PHP; + + } + protected function compileUncached($content, $excludeExpression) { if (! $this->outputOptimizations) { @@ -120,6 +137,13 @@ protected function compileUncachedComponent($value) }, $value); } + protected function compileSetupDirective($value) + { + return preg_replace_callback('/@setup(?:\((.*?)\))?([\s\S]*?)@endsetup/s', function ($matches) { + return $this->compileSetup(trim($matches[2])); + }, $value); + } + protected function compileUncachedDirective($value) { return preg_replace_callback('/@uncached(?:\((.*?)\))?([\s\S]*?)@enduncached/s', function ($matches) { From 800b1874da278119a9f06a34841407a9d53f6a00 Mon Sep 17 00:00:00 2001 From: John Koster Date: Thu, 1 May 2025 19:09:06 -0500 Subject: [PATCH 27/46] Makes optimized a bit smarter --- src/Compiler/ComponentCompiler.php | 26 ++++- src/Compiler/OptimizedComponentCompiler.php | 95 +++++++++++++++++++ tests/Feature/PropsTest.php | 63 ++++++++++++ .../flux/tests/optimized_props.blade.php | 6 ++ .../optimized_props_custom_setup.blade.php | 8 ++ .../tests/optimized_props_multiline.blade.php | 14 +++ tests/components/flux/tests/props.blade.php | 2 + 7 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 src/Compiler/OptimizedComponentCompiler.php create mode 100644 tests/components/flux/tests/optimized_props.blade.php create mode 100644 tests/components/flux/tests/optimized_props_custom_setup.blade.php create mode 100644 tests/components/flux/tests/optimized_props_multiline.blade.php diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index fa56c27d..c36a1594 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -22,13 +22,12 @@ protected function compileOptimizedComponent($value) { $this->isOptimizedComponent = true; - $value = ltrim(preg_replace('/(?\n@uncached\n$value\n@enduncached"; + return OptimizedComponentCompiler::compile($value); } protected function compileComponent($value) { + $value = $this->compileSetupComponent($value); $value = $this->compileSetupDirective($value); $value = $this->compileUncachedComponent($value); $value = $this->compileUncachedDirective($value); @@ -58,7 +57,9 @@ public function compile($value) return $this->removeCacheFeatures($value); } - if (Str::startsWith(ltrim($value), '@optimized')) { + $value = ltrim($value); + + if (Str::startsWith($value, '@optimized')) { $value = $this->compileOptimizedComponent($value); } @@ -116,8 +117,23 @@ protected function compileUncached($content, $excludeExpression) PHP; } + protected function compileSetupComponent($value) + { + if (! $this->outputOptimizations) { + return $value; + } + + return preg_replace_callback('/]+))?>(.*?)<\/flux:setup>/s', function ($matches) { + return $this->compileSetup(trim($matches[2])); + }, $value); + } + protected function compileUncachedComponent($value) { + if (! $this->outputOptimizations) { + return $value; + } + return preg_replace_callback('/]+))?>(.*?)<\/flux:uncached>/s', function ($matches) { $excludeExpression = ''; @@ -133,7 +149,7 @@ protected function compileUncachedComponent($value) } } - return $this->compileUncached($matches[2], $excludeExpression); + return $this->compileUncached(trim($matches[2]), $excludeExpression); }, $value); } diff --git a/src/Compiler/OptimizedComponentCompiler.php b/src/Compiler/OptimizedComponentCompiler.php new file mode 100644 index 00000000..a0b68864 --- /dev/null +++ b/src/Compiler/OptimizedComponentCompiler.php @@ -0,0 +1,95 @@ +\n@uncached\n$value\n@enduncached"; + } + + public static function compile($value) + { + if (Str::contains($value, ['@endsetup', '')) { + $search = ''; + } + + $value = ltrim(preg_replace('/(?".$value."\n@enduncached"; + } + + if (! Str::contains($value, ['@props', '@aware'])) { + return self::compileSimpleOptimized($value); + } + + $directive = '@optimized'; + + $originalValue = $value; + + $value = str($value) + ->replaceMatches('/\r\n|\r|\n/', "\n") + ->substr(strlen($directive)); + + preg_match_all( + '/(?substr(0, $lastDirective[1]) + ->substrCount("\n"); + + // Find ending line number. + $endingLineNumber = null; + + $lines = $value->split('/\r\n|\r|\n/')->all(); + for ($i = $startingLineNumber; $i < count($lines); $i++) { + $line = Str::squish($lines[$i]); + + if (Str::endsWith($line, ['])', '] )'])) { + $endingLineNumber = $i; + break; + } + } + + if ($endingLineNumber === null) { + return $originalValue; + } + + // Insert our endsetup and uncached directives. + array_splice($lines, $endingLineNumber, 1, [ + $lines[$endingLineNumber], + '@endsetup', + '@uncached', + ]); + + // Insert our leading directives. + array_unshift($lines, '', '@setup'); + + $lines[] = '@enduncached'; + + return implode("\n", $lines); + } +} \ No newline at end of file diff --git a/tests/Feature/PropsTest.php b/tests/Feature/PropsTest.php index bf1c7eae..a4b7a853 100644 --- a/tests/Feature/PropsTest.php +++ b/tests/Feature/PropsTest.php @@ -3,6 +3,7 @@ test('props can be used on cached instances', function () { $flux = <<<'BLADE' + BLADE; @@ -11,7 +12,69 @@ Attributes: class="mt-1 mt-2" Title: Title 2 Attributes: class="mt-1 mt-2" +EXPECTED; + + $this->assertSame($expected, $this->render($flux)); + + // Ensure props are still available after things have been cached once. + $this->assertSame($expected, $this->render($flux)); + $this->assertSame($expected, $this->render($flux)); +}); + +test('props can be used on optimized instances', function () { + $flux = <<<'BLADE' + + + +BLADE; + + $expected = <<<'EXPECTED' +Title: Title 1 +Attributes: class="mt-1 mt-2" +Title: Title 2 +Attributes: class="mt-1 mt-2" +EXPECTED; + $this->assertSame($expected, $this->render($flux)); + + // Ensure props are still available after things have been cached once. + $this->assertSame($expected, $this->render($flux)); + $this->assertSame($expected, $this->render($flux)); +}); + +test('props that spans multiple lines can be compiled and used on optimized instances', function () { + $flux = <<<'BLADE' + + + +BLADE; + + $expected = <<<'EXPECTED' +Title: Title 1 +Attributes: class="mt-1 mt-2" +Title: Title 2 +Attributes: class="mt-1 mt-2" +EXPECTED; + + $this->assertSame($expected, $this->render($flux)); + + // Ensure props are still available after things have been cached once. + $this->assertSame($expected, $this->render($flux)); + $this->assertSame($expected, $this->render($flux)); +}); + +test('test custom setup block can be used on optimized instances', function () { + $flux = <<<'BLADE' + + + +BLADE; + + $expected = <<<'EXPECTED' +Title: Title 1 +Attributes: class="mt-1 mt-2" +Title: Title 2 +Attributes: class="mt-1 mt-2" EXPECTED; $this->assertSame($expected, $this->render($flux)); diff --git a/tests/components/flux/tests/optimized_props.blade.php b/tests/components/flux/tests/optimized_props.blade.php new file mode 100644 index 00000000..f1fa6098 --- /dev/null +++ b/tests/components/flux/tests/optimized_props.blade.php @@ -0,0 +1,6 @@ +@optimized + +@props(['title']) + +Title: {{ $title ?? 'nope' }} +Attributes: {{ $attributes }} \ No newline at end of file diff --git a/tests/components/flux/tests/optimized_props_custom_setup.blade.php b/tests/components/flux/tests/optimized_props_custom_setup.blade.php new file mode 100644 index 00000000..ae015975 --- /dev/null +++ b/tests/components/flux/tests/optimized_props_custom_setup.blade.php @@ -0,0 +1,8 @@ +@optimized + + + @props(['title']) + + +Title: {{ $title ?? 'nope' }} +Attributes: {{ $attributes }} \ No newline at end of file diff --git a/tests/components/flux/tests/optimized_props_multiline.blade.php b/tests/components/flux/tests/optimized_props_multiline.blade.php new file mode 100644 index 00000000..b975b1d3 --- /dev/null +++ b/tests/components/flux/tests/optimized_props_multiline.blade.php @@ -0,0 +1,14 @@ +@optimized + +@props(['title' + + + + + + + +] ) + +Title: {{ $title ?? 'nope' }} +Attributes: {{ $attributes }} \ No newline at end of file diff --git a/tests/components/flux/tests/props.blade.php b/tests/components/flux/tests/props.blade.php index c97b6af2..1e241f25 100644 --- a/tests/components/flux/tests/props.blade.php +++ b/tests/components/flux/tests/props.blade.php @@ -1,6 +1,8 @@ @cached +@setup @props(['title']) +@endsetup Title: {{ $title ?? 'nope' }} From a90e052ee648abf7c4534dab4dcb54f3bf09f189 Mon Sep 17 00:00:00 2001 From: John Koster Date: Thu, 1 May 2025 21:06:32 -0500 Subject: [PATCH 28/46] No longer needed --- src/FluxServiceProvider.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/FluxServiceProvider.php b/src/FluxServiceProvider.php index 5b22348f..b8c17218 100644 --- a/src/FluxServiceProvider.php +++ b/src/FluxServiceProvider.php @@ -97,12 +97,6 @@ public function bootMacros() $this->slots[$this->currentComponent()] = []; }); - app('view')::macro('registerFluxComponentSlot', function ($name, $attributes, $callback) { - last($this->componentStack); - - $this->slots[$this->currentComponent()][$name] = new LazyComponentSlot($callback, $attributes); - }); - app('view')::macro('fluxComponentData', function () { $defaultSlot = new ComponentSlot(trim(ob_get_clean())); From 30137ecfd7f7f3a41baccb12263c80d12043d0b4 Mon Sep 17 00:00:00 2001 From: John Koster Date: Thu, 1 May 2025 21:09:34 -0500 Subject: [PATCH 29/46] Only register/boot when enabled --- src/FluxServiceProvider.php | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/FluxServiceProvider.php b/src/FluxServiceProvider.php index b8c17218..e85031e6 100644 --- a/src/FluxServiceProvider.php +++ b/src/FluxServiceProvider.php @@ -34,6 +34,11 @@ public function boot(): void $this->bootTagCompiler(); $this->bootMacros(); + if ($this->useCachingCompiler) { + $this->bootCacheCompilerDirectives(); + $this->bootCacheCompilerMacros(); + } + app('livewire')->propertySynthesizer(DateRangeSynth::class); AssetManager::boot(); @@ -54,10 +59,6 @@ public function bootComponentPath() public function bootTagCompiler() { - app('blade.compiler')->directive('fluxComponent', fn ($expression) => FluxComponentDirectives::compileFluxComponentClass($expression)); - app('blade.compiler')->directive('endFluxComponentClass', fn () => FluxComponentDirectives::compileEndFluxComponentClass()); - app('blade.compiler')->directive('fluxAware', fn ($expression) => FluxComponentDirectives::compileFluxAware($expression)); - $compilerClass = FluxTagCompiler::class; if ($this->useCachingCompiler) { @@ -84,12 +85,30 @@ public function bootTagCompiler() }); } + protected function bootCacheCompilerDirectives() + { + app('blade.compiler')->directive('fluxComponent', fn ($expression) => FluxComponentDirectives::compileFluxComponentClass($expression)); + app('blade.compiler')->directive('endFluxComponentClass', fn () => FluxComponentDirectives::compileEndFluxComponentClass()); + app('blade.compiler')->directive('fluxAware', fn ($expression) => FluxComponentDirectives::compileFluxAware($expression)); + } + public function bootMacros() { app('view')::macro('getCurrentComponentData', function () { return $this->currentComponentData; }); + ComponentAttributeBag::macro('pluck', function ($key) { + $result = $this->get($key); + + unset($this->attributes[$key]); + + return $result; + }); + } + + protected function bootCacheCompilerMacros() + { app('view')::macro('startFluxComponent', function ($view, array $data = []) { $this->componentStack[] = $view; @@ -139,14 +158,6 @@ public function bootMacros() $this->currentComponentData = $previousComponentData; } }); - - ComponentAttributeBag::macro('pluck', function ($key) { - $result = $this->get($key); - - unset($this->attributes[$key]); - - return $result; - }); } public function bootCommands() From 89152b184f3b056009c88269498cb53e6f383bc6 Mon Sep 17 00:00:00 2001 From: John Koster Date: Thu, 1 May 2025 21:10:03 -0500 Subject: [PATCH 30/46] Update ComponentCompiler.php --- src/Compiler/ComponentCompiler.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index c36a1594..5e4f44bf 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -86,7 +86,6 @@ protected function compileSetup($content) extract(\Flux\Flux::cache()->runCurrentComponentSetup(get_defined_vars())); ?> PHP; - } protected function compileUncached($content, $excludeExpression) From c494d672f396e0027288b1a2985d18638b627b64 Mon Sep 17 00:00:00 2001 From: John Koster Date: Thu, 1 May 2025 21:14:02 -0500 Subject: [PATCH 31/46] Convert to real directive --- src/Compiler/ComponentCompiler.php | 2 -- src/Compiler/FluxComponentDirectives.php | 11 +++++++++++ src/FluxServiceProvider.php | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index 5e4f44bf..eadaa150 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -63,8 +63,6 @@ public function compile($value) $value = $this->compileOptimizedComponent($value); } - $value = preg_replace('/(?', $value); - if (! $this->isOptimizedComponent) { $value = preg_replace('/(?'; + } + public static function compileFluxAware($expression) { return static::normalizeLineEndings(" \$__value) { diff --git a/src/FluxServiceProvider.php b/src/FluxServiceProvider.php index e85031e6..b34b9c37 100644 --- a/src/FluxServiceProvider.php +++ b/src/FluxServiceProvider.php @@ -90,6 +90,7 @@ protected function bootCacheCompilerDirectives() app('blade.compiler')->directive('fluxComponent', fn ($expression) => FluxComponentDirectives::compileFluxComponentClass($expression)); app('blade.compiler')->directive('endFluxComponentClass', fn () => FluxComponentDirectives::compileEndFluxComponentClass()); app('blade.compiler')->directive('fluxAware', fn ($expression) => FluxComponentDirectives::compileFluxAware($expression)); + app('blade.compiler')->directive('cached', fn ($expression) => FluxComponentDirectives::compileCached($expression)); } public function bootMacros() From 2ceb5b5e6f439ce6b918b3d8ed711a284c9b1ba9 Mon Sep 17 00:00:00 2001 From: John Koster Date: Fri, 2 May 2025 19:25:24 -0500 Subject: [PATCH 32/46] Add support for ignored attributes + tests --- src/Compiler/FluxComponentDirectives.php | 17 ++++- src/FluxComponentCache.php | 75 ++++++++++++++++++- tests/Feature/IgnoredAttributesTest.php | 60 +++++++++++++++ .../flux/tests/ignored_attributes.blade.php | 7 ++ .../tests/ignored_attributes_output.blade.php | 9 +++ 5 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 tests/Feature/IgnoredAttributesTest.php create mode 100644 tests/components/flux/tests/ignored_attributes.blade.php create mode 100644 tests/components/flux/tests/ignored_attributes_output.blade.php diff --git a/src/Compiler/FluxComponentDirectives.php b/src/Compiler/FluxComponentDirectives.php index 22231880..65ceec1d 100644 --- a/src/Compiler/FluxComponentDirectives.php +++ b/src/Compiler/FluxComponentDirectives.php @@ -117,7 +117,22 @@ public static function compileCached($expression) $expression = trim(Str::substr($expression, 1, -1)); } - return ''; + if (! $expression) { + return ''; + } + + return <<ignoreAttributes(\$__fluxCacheOptions['except'], get_defined_vars()); + } + + unset(\$__fluxCacheOptions); +?> +PHP; } public static function compileFluxAware($expression) diff --git a/src/FluxComponentCache.php b/src/FluxComponentCache.php index c63cbfc8..52fac38d 100644 --- a/src/FluxComponentCache.php +++ b/src/FluxComponentCache.php @@ -26,10 +26,19 @@ class FluxComponentCache protected $items = []; + protected $attributeSwaps = []; + protected $swaps = []; protected $setupFunctions = []; + protected $attributeReplacementValue = ''; + + public function __construct() + { + $this->attributeReplacementValue = '__FLUX:ATTRIBUTE'.Str::random(); + } + protected function shouldSkipComponent($component) { if (! isset($this->observedComponents[$component])) { @@ -170,6 +179,34 @@ public function isCacheable() $this->observingStack[$lastKey] = $lastObserved; } + public function ignoreAttributes($ignore, $data) + { + $ignore = Arr::wrap($ignore); + $attributes = clone ($data['attributes'] ?? new ComponentAttributeBag); + $excludeKeys = []; + + foreach ($ignore as $key => $v) { + if (! is_string($key)) { + $replacement = '__FLUX:ATTRIBUTE_REPLACEMENT'.Str::random(); + $excludeKeys[] = $v; + $attributes[$v] = $replacement; + $this->addAttributeSwap($replacement, $v, function ($attribute, $data) { + $attributes = $data['attributes'] ?? null; + + if (! $attributes) { + return $this->attributeReplacementValue; + } + + return $attributes->get($attribute) ?? $this->attributeReplacementValue; + }); + } + } + + $this->exclude($excludeKeys); + + return $attributes; + } + public function has($key) { return array_key_exists($key, $this->items); @@ -189,6 +226,17 @@ public function get($key) return $this->items[$key]; } + public function addAttributeSwap($replacement, $attribute, $callback) + { + $component = $this->currentComponent(); + + if (! array_key_exists($component, $this->attributeSwaps)) { + $this->attributeSwaps[$component] = []; + } + + $this->attributeSwaps[$component][$replacement] = [$attribute, $callback]; + } + public function addSwap($replacement, $callback) { $component = $this->currentComponent(); @@ -202,14 +250,33 @@ public function addSwap($replacement, $callback) public function swap($component, $value, $data) { - if (! isset($this->swaps[$component])) { - return $value; + if (isset($this->swaps[$component])) { + foreach ($this->swaps[$component] as $replacement => $callback) { + $value = str_replace($replacement, $callback($data), $value); + } } - foreach ($this->swaps[$component] as $replacement => $callback) { - $value = str_replace($replacement, $callback($data), $value); + if (isset($this->attributeSwaps[$component])) { + $emptyReplacements = []; + + foreach ($this->attributeSwaps[$component] as $replacement => $attributeDetails) { + [$attribute, $callback] = $attributeDetails; + $result = call_user_func_array($callback, [$attribute, $data]); + + if ($result === true) { + $result = $attribute === 'x-data' || str_starts_with($attribute, 'wire:') ? '' : $attribute; + } + + $value = str_replace('="'.$replacement, '="'.$result, $value); + $value = str_replace($replacement, e($result), $value); + + $emptyReplacements[' '.$attribute.'="'.$this->attributeReplacementValue.'"'] = ''; + } + + $value = Str::swap($emptyReplacements, $value); } + return $value; } diff --git a/tests/Feature/IgnoredAttributesTest.php b/tests/Feature/IgnoredAttributesTest.php new file mode 100644 index 00000000..834f32e9 --- /dev/null +++ b/tests/Feature/IgnoredAttributesTest.php @@ -0,0 +1,60 @@ + + + + + +BLADE; + + + $counter = new \Flux\Tests\TestCounter; + + $expected = <<<'HTML' +
+HTML; + + $this->assertSame($expected, $this->render($flux, ['counter' => $counter])); + + $this->assertSame(1, $counter->count); +}); + +test('attributes can be output in arbitrary places', function () { + $flux = <<<'BLADE' +@foreach ($values as $value) + +@endforeach +BLADE; + + $counter = new \Flux\Tests\TestCounter; + + $data = [ + 'values' => [ + 'one', + 'two', + 'three', + 'four', + '<>', + ], + 'counter' => $counter, + ]; + + $expected = <<<'HTML' +
+ one +
+ two +
+ three +
+ four +
+ &lt;&gt; +
+HTML; + + $this->assertSame($expected, $this->render($flux, $data)); + $this->assertSame(1, $counter->count); +}); diff --git a/tests/components/flux/tests/ignored_attributes.blade.php b/tests/components/flux/tests/ignored_attributes.blade.php new file mode 100644 index 00000000..52272fda --- /dev/null +++ b/tests/components/flux/tests/ignored_attributes.blade.php @@ -0,0 +1,7 @@ +@cached(['except' => 'wire:key']) + +@props(['counter']) + +@php($counter->increment()) + +
\ No newline at end of file diff --git a/tests/components/flux/tests/ignored_attributes_output.blade.php b/tests/components/flux/tests/ignored_attributes_output.blade.php new file mode 100644 index 00000000..b7e3fd73 --- /dev/null +++ b/tests/components/flux/tests/ignored_attributes_output.blade.php @@ -0,0 +1,9 @@ +@cached(['except' => 'value']) + +@props(['counter']) + +@php($counter->increment()) + +
+ {{ $attributes->get('value') }} +
\ No newline at end of file From 08e68703de6e7bb77af5202ccb2a565ac7850f78 Mon Sep 17 00:00:00 2001 From: John Koster Date: Fri, 2 May 2025 19:37:12 -0500 Subject: [PATCH 33/46] Update OptimizedComponentCompiler.php --- src/Compiler/OptimizedComponentCompiler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compiler/OptimizedComponentCompiler.php b/src/Compiler/OptimizedComponentCompiler.php index a0b68864..a646c624 100644 --- a/src/Compiler/OptimizedComponentCompiler.php +++ b/src/Compiler/OptimizedComponentCompiler.php @@ -75,7 +75,7 @@ public static function compile($value) } if ($endingLineNumber === null) { - return $originalValue; + return mb_substr($originalValue, strlen($directive)); } // Insert our endsetup and uncached directives. From 558271942baad57e22dc2adaa223471f9abc4068 Mon Sep 17 00:00:00 2001 From: John Koster Date: Fri, 2 May 2025 19:37:27 -0500 Subject: [PATCH 34/46] Add coverage to ensure extra newlines don't get added --- tests/Feature/GeneralComponentsTest.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Feature/GeneralComponentsTest.php b/tests/Feature/GeneralComponentsTest.php index 2acc93de..23e762d7 100644 --- a/tests/Feature/GeneralComponentsTest.php +++ b/tests/Feature/GeneralComponentsTest.php @@ -78,3 +78,16 @@ $this->assertSame($expected, $this->render($flux, ['title' => 'The Title'])); }); + +test('optimized components do not add extra newlines at the end', function () { + $flux = <<<'BLADE' + +BLADE; + + $expected = <<<'HTML' +Title: The Title +Attributes: class="mt-4" +HTML; + + $this->assertSame($expected, $this->render($flux)); +}); From c7ccd01141793be4f5e0218f13ce9552795ecf8b Mon Sep 17 00:00:00 2001 From: John Koster Date: Fri, 2 May 2025 19:47:30 -0500 Subject: [PATCH 35/46] Update detection, some simple cleanup --- src/Compiler/OptimizedComponentCompiler.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Compiler/OptimizedComponentCompiler.php b/src/Compiler/OptimizedComponentCompiler.php index a646c624..3ff1e09b 100644 --- a/src/Compiler/OptimizedComponentCompiler.php +++ b/src/Compiler/OptimizedComponentCompiler.php @@ -46,7 +46,7 @@ public static function compile($value) ->substr(strlen($directive)); preg_match_all( - '/(?split('/\r\n|\r|\n/')->all(); + $lines = $value->split('/\n/')->all(); + for ($i = $startingLineNumber; $i < count($lines); $i++) { $line = Str::squish($lines[$i]); From dcf44178a40c81ee8940cb1887ddb617b814c6b4 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 3 May 2025 10:30:06 -0500 Subject: [PATCH 36/46] Help prevent undefined array key "" --- src/FluxComponentCache.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/FluxComponentCache.php b/src/FluxComponentCache.php index 52fac38d..b5945605 100644 --- a/src/FluxComponentCache.php +++ b/src/FluxComponentCache.php @@ -148,6 +148,10 @@ public function stopObserving(string $componentName) public function currentComponent() { + if (count($this->observingStack) === 0) { + return null; + } + return $this->observingStack[array_key_last($this->observingStack)]['component']; } @@ -165,9 +169,13 @@ public function usesVariable(string $name, $currentValue, $default = null) public function isOptimized() { + if (! $component = $this->currentComponent()) { + return; + } + $this->isCacheable(); - $this->optimizedComponents[$this->currentComponent()] = true; + $this->optimizedComponents[$component] = true; } public function isCacheable() From 026cee5fbb5166b0f948dbc7a8e6693fe76ad215 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 3 May 2025 11:55:43 -0500 Subject: [PATCH 37/46] Cleanup --- tests/Feature/PropsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/PropsTest.php b/tests/Feature/PropsTest.php index a4b7a853..a5cef9bd 100644 --- a/tests/Feature/PropsTest.php +++ b/tests/Feature/PropsTest.php @@ -63,7 +63,7 @@ $this->assertSame($expected, $this->render($flux)); }); -test('test custom setup block can be used on optimized instances', function () { +test('custom setup block can be used on optimized instances', function () { $flux = <<<'BLADE' From 695fa4fd60d5f7fbfcd27708c11effe81ebd6ae0 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 3 May 2025 11:57:49 -0500 Subject: [PATCH 38/46] Tweaks for morph markers Leave the original content alone temporarily so Livewire can get in there and insert its markers the way it wants to. If things are wrapped in the uncached callbacks too early, Livewire's patterns may not match correctly inside attributes, etc. --- src/Compiler/ComponentCompiler.php | 18 +++++++++++--- src/Compiler/LivewirePrecompiler.php | 36 ++++++++++++++++++++++++++++ src/FluxServiceProvider.php | 11 +++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 src/Compiler/LivewirePrecompiler.php diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index eadaa150..c4b629a4 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -13,6 +13,8 @@ class ComponentCompiler extends ComponentTagCompiler public $outputOptimizations = false; + protected $uncachedBuffer = []; + public function isFluxComponent($value) { return Str::startsWith(ltrim($value), ['@cached', '@optimized']); @@ -32,6 +34,10 @@ protected function compileComponent($value) $value = $this->compileUncachedComponent($value); $value = $this->compileUncachedDirective($value); + $value .= implode("\n", $this->uncachedBuffer); + + $this->uncachedBuffer = []; + return $value; } @@ -100,17 +106,23 @@ protected function compileUncached($content, $excludeExpression) $compiledExclude = "\Flux\Flux::cache()->exclude({$excludeExpression});"; } - return <<uncachedBuffer[] = <<addSwap('$replacement', function (\$data) { extract(\$data); \$__env = \$__env ?? view(); ob_start(); -?>$content[FLUX_SWAP:COMPILED $replacement]$replacement +?> +PHP; + + return << $replacementMarker, + $compileMarker => $compiled, + ], $template); + } + + return $template; + } +} \ No newline at end of file diff --git a/src/FluxServiceProvider.php b/src/FluxServiceProvider.php index b34b9c37..c812e94e 100644 --- a/src/FluxServiceProvider.php +++ b/src/FluxServiceProvider.php @@ -4,6 +4,7 @@ use Flux\Compiler\ComponentCompiler; use Flux\Compiler\FluxComponentDirectives; +use Flux\Compiler\LivewirePrecompiler; use Flux\Compiler\TagCompiler; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Contracts\View\View; @@ -37,6 +38,16 @@ public function boot(): void if ($this->useCachingCompiler) { $this->bootCacheCompilerDirectives(); $this->bootCacheCompilerMacros(); + + app('livewire')->precompiler(function ($template) { + return LivewirePrecompiler::compile($template); + }); + + // We also need to register one with Blade to handle the + // times we are not inside the Livewire life-cycle. + app('blade.compiler')->precompiler(function ($template) { + return LivewirePrecompiler::compile($template); + }); } app('livewire')->propertySynthesizer(DateRangeSynth::class); From 3e3115374626b66faa7a6ebdf31ddad30dac3348 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 3 May 2025 12:29:45 -0500 Subject: [PATCH 39/46] Improve compilation when disabled --- src/Compiler/ComponentCompiler.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index c4b629a4..ff9674be 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -78,6 +78,10 @@ public function compile($value) protected function compileSetup($content) { + if (! $this->outputOptimizations) { + return $content; + } + return <<registerSetup(function (\$__tmpComponentData) { @@ -128,10 +132,6 @@ protected function compileUncached($content, $excludeExpression) protected function compileSetupComponent($value) { - if (! $this->outputOptimizations) { - return $value; - } - return preg_replace_callback('/]+))?>(.*?)<\/flux:setup>/s', function ($matches) { return $this->compileSetup(trim($matches[2])); }, $value); @@ -139,10 +139,6 @@ protected function compileSetupComponent($value) protected function compileUncachedComponent($value) { - if (! $this->outputOptimizations) { - return $value; - } - return preg_replace_callback('/]+))?>(.*?)<\/flux:uncached>/s', function ($matches) { $excludeExpression = ''; From 07c24e6a243265e0c04250a0dde478b2a7bd045c Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 3 May 2025 14:09:50 -0500 Subject: [PATCH 40/46] Keep markers in the same relative position Prevents pushing markers to the end of the compiled document by putting the swap placeholder back where it was extracted. Consistent newlines --- src/Compiler/ComponentCompiler.php | 19 ++---------- src/Compiler/LivewirePrecompiler.php | 21 +++++++++---- tests/Feature/CacheTest.php | 44 +++++++++++++--------------- 3 files changed, 37 insertions(+), 47 deletions(-) diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index ff9674be..7b7d77ab 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -34,7 +34,7 @@ protected function compileComponent($value) $value = $this->compileUncachedComponent($value); $value = $this->compileUncachedDirective($value); - $value .= implode("\n", $this->uncachedBuffer); + $value .= implode('', $this->uncachedBuffer); $this->uncachedBuffer = []; @@ -104,25 +104,10 @@ protected function compileUncached($content, $excludeExpression) $replacement = '__FLUX::SWAP_REPLACEMENT::'. Str::random(); - $compiledExclude = ''; - if (strlen($excludeExpression) > 0) { - $compiledExclude = "\Flux\Flux::cache()->exclude({$excludeExpression});"; + $this->uncachedBuffer[] = "exclude({$excludeExpression}); ?>"; } - $this->uncachedBuffer[] = <<addSwap('$replacement', function (\$data) { - extract(\$data); - \$__env = \$__env ?? view(); - ob_start(); -?>[FLUX_SWAP:COMPILED $replacement] -PHP; - return << $replacementMarker, - $compileMarker => $compiled, - ], $template); + // It's important that there is no whitespace between the replacement and the PHP tag. + $swap = <<addSwap('$replacement', function (\$data) { + extract(\$data); + \$__env = \$__env ?? view(); + ob_start(); +?>$compiled +PHP; + + $template = str_replace($original, $swap, $template); } return $template; diff --git a/tests/Feature/CacheTest.php b/tests/Feature/CacheTest.php index 400b8997..fdefa713 100644 --- a/tests/Feature/CacheTest.php +++ b/tests/Feature/CacheTest.php @@ -35,16 +35,17 @@ $flux = <<<'BLADE' @for ($i = 0; $i < 5; $i++) {{ $i }} + @endfor BLADE; $expected = <<<'EXP' -The Value: 0 -Slot: 0The Value: 1 -Slot: 0The Value: 2 -Slot: 0The Value: 3 -Slot: 0The Value: 4 -Slot: 0 +The Value: 0Slot: 0 +The Value: 1Slot: 0 +The Value: 2Slot: 0 +The Value: 3Slot: 0 +The Value: 4Slot: 0 + EXP; $this->assertSame($expected, $this->render($flux)); @@ -54,16 +55,17 @@ $flux = <<<'BLADE' @for ($i = 0; $i < 5; $i++) {{ $i }} + @endfor BLADE; $expected = <<<'EXP' -The Value: 0 -Slot: 0The Value: 1 -Slot: 0The Value: 2 -Slot: 0The Value: 3 -Slot: 0The Value: 4 -Slot: 0 +The Value: 0Slot: 0 +The Value: 1Slot: 0 +The Value: 2Slot: 0 +The Value: 3Slot: 0 +The Value: 4Slot: 0 + EXP; $this->assertSame($expected, $this->render($flux)); @@ -122,23 +124,17 @@ $expected = <<<'HTML' + value="value-2" wire:key="value-2" >Slot 2: 2 HTML; $this->assertSame($expected, $result); From ff3ab52a1cd96db9c0a49b442db715a2d0aa4116 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 3 May 2025 14:13:05 -0500 Subject: [PATCH 41/46] =?UTF-8?q?Remove=20extra=20helper=20=F0=9F=A4=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Compiler/FluxComponentDirectives.php | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Compiler/FluxComponentDirectives.php b/src/Compiler/FluxComponentDirectives.php index 65ceec1d..5da7a63b 100644 --- a/src/Compiler/FluxComponentDirectives.php +++ b/src/Compiler/FluxComponentDirectives.php @@ -8,11 +8,6 @@ class FluxComponentDirectives { - public static function normalizeLineEndings($value) - { - return str_replace(["\r\n", "\r"], "\n", $value); - } - public static function compileFluxComponentClass($expression) { [$component, $alias, $data] = str_contains($expression, ',') @@ -52,7 +47,7 @@ public static function compileEndFluxComponentClass() { $hash = ComponentHashes::popHash(); - return static::normalizeLineEndings(<< -PHP); +PHP; } public static function compileCached($expression) @@ -137,7 +132,7 @@ public static function compileCached($expression) public static function compileFluxAware($expression) { - return static::normalizeLineEndings(" \$__value) { + return " \$__value) { \$__consumeVariable = is_string(\$__key) ? \$__key : \$__value; if (is_string (\$__key)) { \$\$__consumeVariable = \$__env->getConsumableComponentData(\$__key, \$__value); @@ -146,6 +141,6 @@ public static function compileFluxAware($expression) \$\$__consumeVariable = \$__env->getConsumableComponentData(\$__value); \Flux\Flux::cache()->usesVariable(\$__value, \$\$__consumeVariable); } -} ?>"); +} ?>"; } } \ No newline at end of file From d19f9f28ba9117f291ccce35b32173f6c2f0e082 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 3 May 2025 15:10:27 -0500 Subject: [PATCH 42/46] Some cleanup, remove hoisted tmp var --- src/Compiler/FluxComponentDirectives.php | 28 +++++++++++------------- src/FluxComponentCache.php | 5 +++++ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/Compiler/FluxComponentDirectives.php b/src/Compiler/FluxComponentDirectives.php index 5da7a63b..d4adc2fb 100644 --- a/src/Compiler/FluxComponentDirectives.php +++ b/src/Compiler/FluxComponentDirectives.php @@ -34,9 +34,7 @@ public static function compileClassComponentOpening(string $component, string $a return implode("\n", [ '', '', - '', - '', - 'all() : [])); ?>', + 'all() : [])); ?>', 'withName('.$alias.'); ?>', 'shouldRender()): ?>', /* START: RENDER */ 'startComponent($component->resolveView(), $component->data()); ?>', @@ -49,21 +47,23 @@ public static function compileEndFluxComponentClass() return <<key(\$component->componentName, \$__fluxHoistedComponentData['data'], \$__env); \$__componentRenderData{$hash} = \$__env->fluxComponentData(); + \$__fluxCacheKey{$hash} = \Flux\Flux::cache()->key(\$component->componentName, \$__componentRenderData{$hash}, \$__env); if (\$__fluxCacheKey{$hash} === null || \Flux\Flux::cache()->has(\$__fluxCacheKey{$hash}) === false): /* START: CACHE BLOCK */ \Flux\Flux::cache()->startObserving(\$component->componentName); \$__fluxTmpOutput{$hash} = \$__env->renderFluxComponent(\$__componentRenderData{$hash}); \Flux\Flux::cache()->stopObserving(\$component->componentName); - \$__fluxCacheKey{$hash} = \Flux\Flux::cache()->key(\$component->componentName, \$__fluxHoistedComponentData['data'], \$__env); + \$__fluxCacheKey{$hash} = \Flux\Flux::cache()->key(\$component->componentName, \$__componentRenderData{$hash}, \$__env); if (\$__fluxCacheKey{$hash} !== null) { \Flux\Flux::cache()->put(\$component->componentName, \$__fluxCacheKey{$hash}, \$__fluxTmpOutput{$hash}); - \$__componentRenderData{$hash} = \Flux\Flux::cache()->runComponentSetup(\$component->componentName, \$__componentRenderData{$hash}); - \$__fluxTmpOutput{$hash} = \Flux\Flux::cache()->swap(\$component->componentName, \$__fluxTmpOutput{$hash}, \$__componentRenderData{$hash}); + \$__fluxTmpOutput{$hash} = \Flux\Flux::cache()->swap( + \$component->componentName, + \$__fluxTmpOutput{$hash}, + \Flux\Flux::cache()->runComponentSetup(\$component->componentName, \$__componentRenderData{$hash}) + ); } echo \$__fluxTmpOutput{$hash}; @@ -72,8 +72,11 @@ public static function compileEndFluxComponentClass() else: /* ELSE: CACHE BLOCK */ \$__env->popFluxComponent(); \$__fluxTmpOutput{$hash} = \Flux\Flux::cache()->get(\$__fluxCacheKey{$hash}); - \$__componentRenderData{$hash} = \Flux\Flux::cache()->runComponentSetup(\$component->componentName, \$__componentRenderData{$hash}); - \$__fluxTmpOutput{$hash} = \Flux\Flux::cache()->swap(\$component->componentName, \$__fluxTmpOutput{$hash}, \$__componentRenderData{$hash}); + \$__fluxTmpOutput{$hash} = \Flux\Flux::cache()->swap( + \$component->componentName, + \$__fluxTmpOutput{$hash}, + \Flux\Flux::cache()->runComponentSetup(\$component->componentName, \$__componentRenderData{$hash}) + ); echo \$__fluxTmpOutput{$hash}; unset(\$__fluxTmpOutput{$hash}); @@ -88,7 +91,6 @@ public static function compileEndFluxComponentClass() unset(\$__fluxCacheKeyOriginal{$hash}); } if (isset(\$__componentRenderData{$hash})) { unset(\$__componentRenderData{$hash}); } - if (isset(\$__fluxHoistedComponentData)) { unset(\$__fluxHoistedComponentData); } if (isset(\$__attributesOriginal{$hash})) { \$attributes = \$__attributesOriginal{$hash}; unset(\$__attributesOriginal{$hash}); } @@ -96,10 +98,6 @@ public static function compileEndFluxComponentClass() if (isset(\$__componentOriginal{$hash})) { \$component = \$__componentOriginal{$hash}; unset(\$__componentOriginal{$hash}); } - - if (isset(\$__fluxHoistedComponentDataOriginal{$hash})) { - \$__fluxHoistedComponentData = \$__fluxHoistedComponentDataOriginal{$hash}; unset(\$__fluxHoistedComponentDataOriginal{$hash}); - } ?> PHP; } diff --git a/src/FluxComponentCache.php b/src/FluxComponentCache.php index b5945605..2a472bbf 100644 --- a/src/FluxComponentCache.php +++ b/src/FluxComponentCache.php @@ -5,6 +5,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Str; use Illuminate\View\ComponentAttributeBag; +use Illuminate\View\ComponentSlot; class FluxComponentCache { @@ -68,6 +69,10 @@ public function key($component, $data, $env) $cacheData = []; foreach ($data as $k => $v) { + if ($v instanceof ComponentSlot) { + continue; + } + // Ignore data that is likely internal state. if (Str::startsWith($k, '__')) { continue; From 7ca9705edcc0341b96f63941ddf81c00183aee2b Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 3 May 2025 15:25:19 -0500 Subject: [PATCH 43/46] Tidy up --- src/Compiler/FluxComponentDirectives.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Compiler/FluxComponentDirectives.php b/src/Compiler/FluxComponentDirectives.php index d4adc2fb..fb9b310f 100644 --- a/src/Compiler/FluxComponentDirectives.php +++ b/src/Compiler/FluxComponentDirectives.php @@ -71,10 +71,9 @@ public static function compileEndFluxComponentClass() else: /* ELSE: CACHE BLOCK */ \$__env->popFluxComponent(); - \$__fluxTmpOutput{$hash} = \Flux\Flux::cache()->get(\$__fluxCacheKey{$hash}); \$__fluxTmpOutput{$hash} = \Flux\Flux::cache()->swap( \$component->componentName, - \$__fluxTmpOutput{$hash}, + \Flux\Flux::cache()->get(\$__fluxCacheKey{$hash}), \Flux\Flux::cache()->runComponentSetup(\$component->componentName, \$__componentRenderData{$hash}) ); echo \$__fluxTmpOutput{$hash}; From 25f17a1cd95034f5baf4d624298151b5b99db577 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 3 May 2025 18:11:31 -0500 Subject: [PATCH 44/46] Update IgnoredAttributesTest.php --- tests/Feature/IgnoredAttributesTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/IgnoredAttributesTest.php b/tests/Feature/IgnoredAttributesTest.php index 834f32e9..b5e7a7c1 100644 --- a/tests/Feature/IgnoredAttributesTest.php +++ b/tests/Feature/IgnoredAttributesTest.php @@ -1,6 +1,6 @@ From dfef32dac702da783ce6ce865318180b503f06be Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 3 May 2025 18:13:18 -0500 Subject: [PATCH 45/46] Add coverage for multiple ignored attributes --- tests/Feature/IgnoredAttributesTest.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/Feature/IgnoredAttributesTest.php b/tests/Feature/IgnoredAttributesTest.php index b5e7a7c1..b2888fbb 100644 --- a/tests/Feature/IgnoredAttributesTest.php +++ b/tests/Feature/IgnoredAttributesTest.php @@ -21,6 +21,27 @@ $this->assertSame(1, $counter->count); }); +test('multiple attributes can be ignored', function () { + $flux = <<<'BLADE' + + + + + +BLADE; + + + $counter = new \Flux\Tests\TestCounter; + + $expected = <<<'HTML' +
+HTML; + + $this->assertSame($expected, $this->render($flux, ['counter' => $counter])); + + $this->assertSame(1, $counter->count); +})->only(); + test('attributes can be output in arbitrary places', function () { $flux = <<<'BLADE' @foreach ($values as $value) From 06124bc37e7afc5eb87850f376fd9897d9e54df9 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 3 May 2025 18:23:44 -0500 Subject: [PATCH 46/46] Create multiple_ignored_attributes.blade.php --- .../flux/tests/multiple_ignored_attributes.blade.php | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tests/components/flux/tests/multiple_ignored_attributes.blade.php diff --git a/tests/components/flux/tests/multiple_ignored_attributes.blade.php b/tests/components/flux/tests/multiple_ignored_attributes.blade.php new file mode 100644 index 00000000..0a6d7575 --- /dev/null +++ b/tests/components/flux/tests/multiple_ignored_attributes.blade.php @@ -0,0 +1,7 @@ +@cached(['except' => ['wire:key', 'key']]) + +@props(['counter']) + +@php($counter->increment()) + +
\ No newline at end of file