From d6635dcb2301e9e8d3c1343cbaee660af588825a Mon Sep 17 00:00:00 2001 From: John Koster Date: Fri, 31 Jan 2025 09:30:28 -0600 Subject: [PATCH 1/9] Add support for cache attribute --- src/Cache/CacheAttributeParser.php | 118 +++++++++++++ src/Cache/CacheProperties.php | 13 ++ src/Compiler/ComponentState.php | 3 + .../Concerns/AppliesCompilerParams.php | 12 ++ src/Compiler/Concerns/CompilesCache.php | 120 ++++++++++++++ .../Concerns/CompilesForwardedAttributes.php | 6 + src/Compiler/TemplateCompiler.php | 13 +- tests/Cache/CacheAttributeTest.php | 144 ++++++++++++++++ tests/Cache/CacheParserTest.php | 156 ++++++++++++++++++ .../cache_attribute/basic.blade.php | 4 + 10 files changed, 588 insertions(+), 1 deletion(-) create mode 100644 src/Cache/CacheAttributeParser.php create mode 100644 src/Cache/CacheProperties.php create mode 100644 src/Compiler/Concerns/CompilesCache.php create mode 100644 tests/Cache/CacheAttributeTest.php create mode 100644 tests/Cache/CacheParserTest.php create mode 100644 tests/resources/components/cache_attribute/basic.blade.php diff --git a/src/Cache/CacheAttributeParser.php b/src/Cache/CacheAttributeParser.php new file mode 100644 index 0000000..a459402 --- /dev/null +++ b/src/Cache/CacheAttributeParser.php @@ -0,0 +1,118 @@ +getDefaultDriver(); + } + + public static function parseCacheString(string $cache): CacheProperties + { + if (! Str::startsWith($cache, 'cache.') && $cache !== 'cache') { + $cache = 'cache.'.$cache; + } + + $parts = explode('.', $cache); + + $extraArgs = []; + $store = static::getStore($parts); + $duration = static::getDuration($parts); + + if (is_array($duration)) { + $extraArgs = $duration[1]; + $duration = $duration[0]; + } + + return new CacheProperties( + $duration, + $store, + $extraArgs + ); + } +} diff --git a/src/Cache/CacheProperties.php b/src/Cache/CacheProperties.php new file mode 100644 index 0000000..ed46a7e --- /dev/null +++ b/src/Cache/CacheProperties.php @@ -0,0 +1,13 @@ +isCacheParam($param)) { + return true; + } + if (in_array($param->type, $this->invalidCompilerParamTypes, true)) { return false; } @@ -39,6 +43,14 @@ protected function applyCompilerParameters(ComponentState $component, array $com return; } + $cacheParam = collect($compilerParams) + ->where(fn (ParameterNode $param) => $this->isCacheParam($param)) + ->first(); + + if ($cacheParam) { + $this->applyCacheParam($component, $cacheParam); + } + $compilerParams = collect($compilerParams) ->mapWithKeys(function (ParameterNode $param) { if (! $this->isValidCompilerParam($param)) { diff --git a/src/Compiler/Concerns/CompilesCache.php b/src/Compiler/Concerns/CompilesCache.php new file mode 100644 index 0000000..9836353 --- /dev/null +++ b/src/Compiler/Concerns/CompilesCache.php @@ -0,0 +1,120 @@ +materializedName === '#cache' || Str::startsWith($param->materializedName, '#cache.'); + } + + protected function applyCacheParam(ComponentState $component, ParameterNode $cacheParam): void + { + $cacheString = $cacheParam->materializedName; + + if (Str::startsWith($cacheString, '#')) { + $cacheString = ltrim($cacheString, '#'); + } + + $cacheProperties = CacheAttributeParser::parseCacheString($cacheString); + + if (is_array($cacheProperties->duration)) { + $now = now()->clone(); + $expires = $now->clone() + ->addYears($cacheProperties->duration[0]) + ->addMonths($cacheProperties->duration[1]) + ->addWeeks($cacheProperties->duration[2]) + ->addDays($cacheProperties->duration[3]) + ->addHours($cacheProperties->duration[4]) + ->addMinutes($cacheProperties->duration[5]) + ->addSeconds($cacheProperties->duration[6]); + + $cacheProperties->duration = $now->diffInSeconds($expires); + } + + // TODO: INteroplated variables test. + if ($cacheParam->type == ParameterType::DynamicVariable) { + $cacheProperties->key = $cacheParam->value; + } else { + $cacheProperties->key = $cacheParam->valueNode->content; + } + + $component->cacheProperties = $cacheProperties; + } + + protected function compileCache(string $compiledComponent): string + { + if ($this->activeComponent->cacheProperties->duration === 'flexible') { + $cacheStub = <<<'PHP' +store('#store#')->flexible($__cacheKeyVarSuffix, [$fresh, $stale], function () use ($__cacheTmpVarsVarSuffix) { +extract($__cacheTmpVarsVarSuffix); +ob_start(); +?>#compiled# +PHP; + + $cacheStub = Str::swap([ + '$fresh' => $this->activeComponent->cacheProperties->args[0], + '$stale' => $this->activeComponent->cacheProperties->args[1], + ], $cacheStub); + + } else { + $cacheStub = <<<'PHP' +store('#store#')->has($__cacheKeyVarSuffix)) { + echo cache()->store('#store#')->get($__cacheKeyVarSuffix); + unset($__cacheKeyVarSuffix); +} else { ob_start(); +?>#compiled# +PHP; + + if ($this->activeComponent->cacheProperties->duration === 'forever') { + $cacheMethod = <<<'PHP' +cache()->store('#store#')->forever($__cacheKeyVarSuffix, $__cacheResultVarSuffix); +PHP; + } else { + $cacheMethod = <<<'PHP' +cache()->store('#store#')->put($__cacheKeyVarSuffix, $__cacheResultVarSuffix, '#ttl#'); +PHP; + + $cacheMethod = Str::swap([ + "'#ttl#'" => $this->activeComponent->cacheProperties->duration, + ], $cacheMethod); + } + + $cacheStub = Str::swap([ + '#cacheMethod#' => $cacheMethod, + ], $cacheStub); + } + + return Str::swap([ + 'VarSuffix' => Utils::makeRandomString(), + '#store#' => $this->activeComponent->cacheProperties->store, + "'#key#'" => $this->activeComponent->cacheProperties->key, + '#compiled#' => $compiledComponent, + ], $cacheStub); + } +} diff --git a/src/Compiler/Concerns/CompilesForwardedAttributes.php b/src/Compiler/Concerns/CompilesForwardedAttributes.php index b30f44e..4eb37cb 100644 --- a/src/Compiler/Concerns/CompilesForwardedAttributes.php +++ b/src/Compiler/Concerns/CompilesForwardedAttributes.php @@ -16,6 +16,12 @@ protected function filterParameters(ComponentNode $node): array $paramsToKeep = []; foreach ($node->parameters as $param) { + if ($this->isCacheParam($param)) { + $compilerParams[] = $param; + + continue; + } + if (Str::startsWith($param->name, '##')) { $param->name = mb_substr($param->name, 1); $paramsToKeep[] = $param; diff --git a/src/Compiler/TemplateCompiler.php b/src/Compiler/TemplateCompiler.php index da8bd08..e91159a 100644 --- a/src/Compiler/TemplateCompiler.php +++ b/src/Compiler/TemplateCompiler.php @@ -15,6 +15,7 @@ use Stillat\BladeParser\Parser\DocumentParser; use Stillat\Dagger\Compiler\Concerns\AppliesCompilerParams; use Stillat\Dagger\Compiler\Concerns\CompilesBasicComponents; +use Stillat\Dagger\Compiler\Concerns\CompilesCache; use Stillat\Dagger\Compiler\Concerns\CompilesCompilerDirectives; use Stillat\Dagger\Compiler\Concerns\CompilesComponentDetails; use Stillat\Dagger\Compiler\Concerns\CompilesDynamicComponents; @@ -40,6 +41,7 @@ final class TemplateCompiler use AppliesCompilerParams, CompilesBasicComponents, + CompilesCache, CompilesCompilerDirectives, CompilesComponentDetails, CompilesDynamicComponents, @@ -564,7 +566,7 @@ protected function compileNodes(array $nodes): string $compiledComponentTemplate = Str::swap($swapVars, $compiledComponentTemplate); $compiledComponentTemplate = $this->compileExceptions($compiledComponentTemplate); - $compiled .= $this->storeComponentBlock($compiledComponentTemplate); + $compiled .= $this->finalizeCompiledComponent($compiledComponentTemplate); $this->stopCompilingComponent(); } @@ -578,6 +580,15 @@ protected function compileNodes(array $nodes): string return $compiled; } + protected function finalizeCompiledComponent(string $compiled): string + { + if ($this->activeComponent->cacheProperties != null) { + $compiled = $this->compileCache($compiled); + } + + return $this->storeComponentBlock($compiled); + } + public function cleanup(): void { $this->componentParser->getComponentCache()->clear(); diff --git a/tests/Cache/CacheAttributeTest.php b/tests/Cache/CacheAttributeTest.php new file mode 100644 index 0000000..2ed9a4c --- /dev/null +++ b/tests/Cache/CacheAttributeTest.php @@ -0,0 +1,144 @@ + +@endfor +BLADE; + + $expected = <<<'EXPECTED' +Title: The Title +Var: 1 + +Title: The Title +Var: 1 + +Title: The Title +Var: 1 + +Title: The Title +Var: 1 + +Title: The Title +Var: 1 +EXPECTED; + + $this->assertSame($expected, $this->render($template)); + $this->assertSame($expected, $this->render($template)); + $this->assertSame($expected, $this->render($template)); +}); + +test('forever cache attribute', function () { + $template = <<<'BLADE' +@for ($i = 0; $i < 5; $i++) + +@endfor +BLADE; + + $expected = <<<'EXPECTED' +Title: The Title2 +Var: 1 + +Title: The Title2 +Var: 1 + +Title: The Title2 +Var: 1 + +Title: The Title2 +Var: 1 + +Title: The Title2 +Var: 1 +EXPECTED; + + $this->assertSame($expected, $this->render($template)); + $this->assertSame($expected, $this->render($template)); +}); + +test('flexible cache attribute', function () { + $template = <<<'BLADE' +@for ($i = 0; $i < 5; $i++) + +@endfor +BLADE; + + $expected = <<<'EXPECTED' +Title: The Title3 +Var: 1 + +Title: The Title3 +Var: 1 + +Title: The Title3 +Var: 1 + +Title: The Title3 +Var: 1 + +Title: The Title3 +Var: 1 +EXPECTED; + + $compiled = $this->compile($template); + + $this->assertStringContainsString( + "echo cache()->store('array')->flexible(", + $compiled + ); + + $this->assertStringContainsString( + ', [10, 20], function ()', + $compiled, + ); + + $this->assertSame($expected, $this->render($template)); + $this->assertSame($expected, $this->render($template)); + $this->assertSame($expected, $this->render($template)); +}); + +test('dynamic cache keys', function () { + $template = <<<'BLADE' +@for ($i = 0; $i < 5; $i++) + +@endfor +BLADE; + + $expected = <<<'EXPECTED' +Title: The Title4 +Var: 1 + +Title: The Title4 +Var: 2 + +Title: The Title4 +Var: 3 + +Title: The Title4 +Var: 4 + +Title: The Title4 +Var: 5 +EXPECTED; + + $this->assertSame($expected, $this->render($template)); + $this->assertSame($expected, $this->render($template)); + $this->assertSame($expected, $this->render($template)); +}); diff --git a/tests/Cache/CacheParserTest.php b/tests/Cache/CacheParserTest.php new file mode 100644 index 0000000..d7b07e5 --- /dev/null +++ b/tests/Cache/CacheParserTest.php @@ -0,0 +1,156 @@ +parseCacheString($cacheString); + + expect($parseResults->duration)->toBe($expectedDuration) + ->and($parseResults->store)->toBe($expectedStore) + ->and($parseResults->args)->toBe($expectedExtraParams); +})->with([ + ['cache', ['forever', 'array', []]], + ['cache.forever', ['forever', 'array', []]], + ['cache.forever.array', ['forever', 'array', []]], + ['forever.array1', ['forever', 'array1', []]], + ['forever.array1', ['forever', 'array1', []]], + ['flexible:5:10', ['flexible', 'array', ['5', '10']]], + ['cache.flexible:5:10', ['flexible', 'array', ['5', '10']]], + ['cache.flexible:15:150', ['flexible', 'array', ['15', '150']]], + ['cache.100', ['100', 'array', []]], + ['100', ['100', 'array', []]], + ['100.file', ['100', 'file', []]], + ['cache.100.file', ['100', 'file', []]], + ['42', ['42', 'array', []]], + ['42.file', ['42', 'file', []]], + ['cache.42.file', ['42', 'file', []]], + [ + 'cache.1y.file', + [ + [1, 0, 0, 0, 0, 0, 0], + 'file', + [], + ], + ], + [ + 'cache.2mo.file', + [ + [0, 2, 0, 0, 0, 0, 0], + 'file', + [], + ], + ], + [ + 'cache.3w.file', + [ + [0, 0, 3, 0, 0, 0, 0], + 'file', + [], + ], + ], + [ + 'cache.4d.file', + [ + [0, 0, 0, 4, 0, 0, 0], + 'file', + [], + ], + ], + [ + 'cache.5h.file', + [ + [0, 0, 0, 0, 5, 0, 0], + 'file', + [], + ], + ], + [ + 'cache.6m.file', + [ + [0, 0, 0, 0, 0, 6, 0], + 'file', + [], + ], + ], + [ + 'cache.7s.file', + [ + [0, 0, 0, 0, 0, 0, 7], + 'file', + [], + ], + ], + [ + 'cache.1y2mo.file', + [ + [1, 2, 0, 0, 0, 0, 0], + 'file', + [], + ], + ], + [ + 'cache.1y2mo3w.file', + [ + [1, 2, 3, 0, 0, 0, 0], + 'file', + [], + ], + ], + [ + 'cache.1y2mo3w4d.file', + [ + [1, 2, 3, 4, 0, 0, 0], + 'file', + [], + ], + ], + [ + 'cache.1y2mo3w4d5h.file', + [ + [1, 2, 3, 4, 5, 0, 0], + 'file', + [], + ], + ], + [ + 'cache.1y2mo3w4d5h6m.file', + [ + [1, 2, 3, 4, 5, 6, 0], + 'file', + [], + ], + ], + [ + 'cache.1y2mo3w4d5h6m7s.file', + [ + [1, 2, 3, 4, 5, 6, 7], + 'file', + [], + ], + ], + [ + 'cache.1d2h3m4s.file', + [ + [0, 0, 0, 1, 2, 3, 4], + 'file', + [], + ], + ], + [ + '1y2mo3w4d5h6m7s.database', + [ + [1, 2, 3, 4, 5, 6, 7], + 'database', + [], + ], + ], +]); diff --git a/tests/resources/components/cache_attribute/basic.blade.php b/tests/resources/components/cache_attribute/basic.blade.php new file mode 100644 index 0000000..d8e6ce2 --- /dev/null +++ b/tests/resources/components/cache_attribute/basic.blade.php @@ -0,0 +1,4 @@ +@props(['title']) + +Title: {{ $title }} +Var: {{ \Stillat\Dagger\Tests\StaticTestHelpers::counter() }} \ No newline at end of file From 8350f41455c081b0959ba3aa19ef36330fa348e1 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 8 Feb 2025 12:04:57 -0600 Subject: [PATCH 2/9] Update composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f44ddbe..a68b6c1 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } }, "require": { - "laravel/framework": "^11.9", + "laravel/framework": "^11.23", "stillat/blade-parser": "^1.10.3", "nikic/php-parser": "^5", "symfony/var-exporter": "^6.0" From a2fb3b614e88d8181bf39fece9a00b1c60d9d38a Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 8 Feb 2025 12:11:45 -0600 Subject: [PATCH 3/9] Update CacheAttributeTest.php --- tests/Cache/CacheAttributeTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Cache/CacheAttributeTest.php b/tests/Cache/CacheAttributeTest.php index 2ed9a4c..509d07c 100644 --- a/tests/Cache/CacheAttributeTest.php +++ b/tests/Cache/CacheAttributeTest.php @@ -109,7 +109,7 @@ $this->assertSame($expected, $this->render($template)); $this->assertSame($expected, $this->render($template)); $this->assertSame($expected, $this->render($template)); -}); +})->skip(); test('dynamic cache keys', function () { $template = <<<'BLADE' From ca301714c9de73ad6b73ef31b9802a8871883bdf Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 8 Feb 2025 12:13:19 -0600 Subject: [PATCH 4/9] Update composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a68b6c1..16fb4d4 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } }, "require": { - "laravel/framework": "^11.23", + "laravel/framework": "^11.24", "stillat/blade-parser": "^1.10.3", "nikic/php-parser": "^5", "symfony/var-exporter": "^6.0" From e02e010a42812cb4ab246c57a80339ca94ad5fe7 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 8 Feb 2025 12:19:15 -0600 Subject: [PATCH 5/9] Test/dep updates. --- composer.json | 4 ++-- tests/Cache/CacheAttributeTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 16fb4d4..746f5f7 100644 --- a/composer.json +++ b/composer.json @@ -15,13 +15,13 @@ } }, "require": { - "laravel/framework": "^11.24", + "laravel/framework": "^11.23", "stillat/blade-parser": "^1.10.3", "nikic/php-parser": "^5", "symfony/var-exporter": "^6.0" }, "require-dev": { - "orchestra/testbench": "^9.0", + "orchestra/testbench": "^9.2", "brianium/paratest": "*", "pestphp/pest": "^2", "laravel/pint": "^1.13", diff --git a/tests/Cache/CacheAttributeTest.php b/tests/Cache/CacheAttributeTest.php index 509d07c..2ed9a4c 100644 --- a/tests/Cache/CacheAttributeTest.php +++ b/tests/Cache/CacheAttributeTest.php @@ -109,7 +109,7 @@ $this->assertSame($expected, $this->render($template)); $this->assertSame($expected, $this->render($template)); $this->assertSame($expected, $this->render($template)); -})->skip(); +}); test('dynamic cache keys', function () { $template = <<<'BLADE' From b96a3f0db45e46a676444b17ddb8b07da957ca97 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 8 Feb 2025 13:08:52 -0600 Subject: [PATCH 6/9] Some docs --- README.md | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/README.md b/README.md index ca6d866..db95df4 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ The main visual difference when working with Dagger components is the use of the - [Shorthand Validation Rules](#shorthand-validation-rules) - [Compiler Attributes](#compiler-attributes) - [Escaping Compiler Attributes](#escaping-compiler-attributes) +- [Caching Components](#caching-components) + - [Dynamic Cache Keys](#dynamic-cache-keys) + - [Specifying the Cache Store](#specifying-the-cache-store) + - [Stale While Revalidate/Flexible Caching](#stale-while-revalidate-flexible-cache) - [Component Name](#component-name) - [Component Depth](#component-depth) - [Attribute Forwarding](#attribute-forwarding) @@ -658,6 +662,97 @@ In general, you should avoid using props or attributes beginning with `#` as the - `#cache` - `#precompile` +## Caching Components + +You may cache the output of any Dagger component using the `#cache` compiler attribute. This attribute utilizes Laravel's [Cache](https://laravel.com/docs/cache) feature, and provides ways to customize cache keys, expirations, and the cache store. + +For example, imagine we had a report component that we'd like to cache: + +```blade + + +@php + // Some expensive report logic. +@endphp +``` + +Instead of manually capturing output, or adding caching in other locations, we can simply cache the output of our component call like so: + +```blade + +``` + +Now, the output of the component will be cached forever using the `the-cache-key` string as the cache key. + +We may also specify a different time-to-live by specifying the number of seconds the cached output is valid: + +```blade + +``` + +You may also use a shorthand notation to calculate the time-to-live in seconds. For example, if we'd like to have the cache expire ten minutes from the time the component was first rendered we could use the value `10m`: + +```blade + +``` + +Alternatively, we could also have the cache expire in 1 hour, 15 minutes, and 32 seconds: + +```blade + +``` + +The total number of seconds is calculated dynamically by adding the desired "date parts" to the current time and *then* calculating the number of seconds to use. + +The following table lists the possible suffixes that may be used: + +| Suffix | Description | Example | +|---|---|---| +| y | Year | 1y | +| mo | Month | 1mo | +| w | Week | 1w | +| d | Day | 2d | +| h | Hour | 1h | +| m | Minute | 30m | +| s | Seconds | 15s | + +### Dynamic Cache Keys + +You may create dynamic cache keys by prefixing the `#cache` attribute with the `:` character: + +```blade + +``` + +### Specifying the Cache Store + +You may use a specific cache store by providing the desired cache store's name as the final modifier to the `#cache` attribute. + +The following examples would cache the output for 30 seconds on different cache stores: + +```blade + + + +``` + +### Stale While Revalidate (Flexible Cache) + +You may leverage Laravel's [stale-while-revalidate pattern implementation](https://laravel.com/docs/11.x/cache#swr) using the `flexible` cache modifier. This modifier accepts two values: the number of seconds the cache is considered fresh, and the second value determines how long the cached contents can be served as stale before recalculation is necessary. + +```blade + +``` + ## Component Name You may access the name of the current component through the `name` property on the component instance: From 36c774eaaa8ba74482e7ab702209a98052a5ef96 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 8 Feb 2025 13:19:51 -0600 Subject: [PATCH 7/9] Some extra coverage --- src/Compiler/Concerns/CompilesCache.php | 1 - tests/Cache/CacheAttributeTest.php | 32 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Compiler/Concerns/CompilesCache.php b/src/Compiler/Concerns/CompilesCache.php index 9836353..db02dd3 100644 --- a/src/Compiler/Concerns/CompilesCache.php +++ b/src/Compiler/Concerns/CompilesCache.php @@ -40,7 +40,6 @@ protected function applyCacheParam(ComponentState $component, ParameterNode $cac $cacheProperties->duration = $now->diffInSeconds($expires); } - // TODO: INteroplated variables test. if ($cacheParam->type == ParameterType::DynamicVariable) { $cacheProperties->key = $cacheParam->value; } else { diff --git a/tests/Cache/CacheAttributeTest.php b/tests/Cache/CacheAttributeTest.php index 2ed9a4c..796fc11 100644 --- a/tests/Cache/CacheAttributeTest.php +++ b/tests/Cache/CacheAttributeTest.php @@ -142,3 +142,35 @@ $this->assertSame($expected, $this->render($template)); $this->assertSame($expected, $this->render($template)); }); + +test('dynamic cache keys using interpolation', function () { + $template = <<<'BLADE' +@for ($i = 0; $i < 5; $i++) + +@endfor +BLADE; + + $expected = <<<'EXPECTED' +Title: The Title4 +Var: 1 + +Title: The Title4 +Var: 2 + +Title: The Title4 +Var: 3 + +Title: The Title4 +Var: 4 + +Title: The Title4 +Var: 5 +EXPECTED; + + $this->assertSame($expected, $this->render($template)); + $this->assertSame($expected, $this->render($template)); + $this->assertSame($expected, $this->render($template)); +}); From 586575a959f3c5ce2c5902c7a943a1b3aa88ce80 Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 8 Feb 2025 13:21:57 -0600 Subject: [PATCH 8/9] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09b2e4a..0571fff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +- Adds a `#cache` compiler attribute, which may be used to cache the results of any Dagger component +- Bumps the minimum Laravel version to `11.23`, for `Cache::flexible` support + ## [v1.0.6](https://github.com/Stillat/dagger/compare/v1.0.5...v1.0.6) - 2025-01-31 - Corrects an issue where Blade stack compilation results in array index errors From b5fb64a6bf6896e75cf2c77dcfc8ccfe7986851b Mon Sep 17 00:00:00 2001 From: John Koster Date: Sat, 8 Feb 2025 15:04:08 -0600 Subject: [PATCH 9/9] Improves compilation of custom functions declared within a component's template --- CHANGELOG.md | 4 ++ src/Compiler/ComponentCompiler.php | 2 + .../ComponentStages/RewriteFunctions.php | 27 +++++++ src/Parser/Visitors/FindFunctionsVisitor.php | 25 +++++++ src/Parser/Visitors/RenameFunctionVisitor.php | 70 +++++++++++++++++++ tests/Compiler/CustomFunctionsTest.php | 22 ++++++ .../components/functions/declared.blade.php | 9 +++ 7 files changed, 159 insertions(+) create mode 100644 src/Compiler/ComponentStages/RewriteFunctions.php create mode 100644 src/Parser/Visitors/FindFunctionsVisitor.php create mode 100644 src/Parser/Visitors/RenameFunctionVisitor.php create mode 100644 tests/Compiler/CustomFunctionsTest.php create mode 100644 tests/resources/components/functions/declared.blade.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 09b2e4a..6116e4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Improves compilation of custom functions declared within a component's template + ## [v1.0.6](https://github.com/Stillat/dagger/compare/v1.0.5...v1.0.6) - 2025-01-31 - Corrects an issue where Blade stack compilation results in array index errors diff --git a/src/Compiler/ComponentCompiler.php b/src/Compiler/ComponentCompiler.php index 5417ff8..e19e984 100644 --- a/src/Compiler/ComponentCompiler.php +++ b/src/Compiler/ComponentCompiler.php @@ -8,6 +8,7 @@ use Stillat\Dagger\Compiler\ComponentStages\ExtractsRenderCalls; use Stillat\Dagger\Compiler\ComponentStages\RemoveUseStatements; use Stillat\Dagger\Compiler\ComponentStages\ResolveNamespaces; +use Stillat\Dagger\Compiler\ComponentStages\RewriteFunctions; use Stillat\Dagger\Compiler\Concerns\CompilesPhp; use Stillat\Dagger\Parser\PhpParser; @@ -42,6 +43,7 @@ public function compile(string $component): string ResolveNamespaces::class, RemoveUseStatements::class, new ExtractsRenderCalls($this), + RewriteFunctions::class, ]) ->thenReturn(); diff --git a/src/Compiler/ComponentStages/RewriteFunctions.php b/src/Compiler/ComponentStages/RewriteFunctions.php new file mode 100644 index 0000000..a168e38 --- /dev/null +++ b/src/Compiler/ComponentStages/RewriteFunctions.php @@ -0,0 +1,27 @@ +addVisitor($finder); + $traverser->traverse($ast); + + $traverser->removeVisitor($finder); + + $modifyFunctionsVisitor = new RenameFunctionVisitor($finder->getFunctionNames()); + $traverser->addVisitor($modifyFunctionsVisitor); + + return $next($traverser->traverse($ast)); + } +} diff --git a/src/Parser/Visitors/FindFunctionsVisitor.php b/src/Parser/Visitors/FindFunctionsVisitor.php new file mode 100644 index 0000000..65ac01b --- /dev/null +++ b/src/Parser/Visitors/FindFunctionsVisitor.php @@ -0,0 +1,25 @@ +functionNames[] = $node->name->toString(); + } + + public function getFunctionNames(): array + { + return $this->functionNames; + } +} diff --git a/src/Parser/Visitors/RenameFunctionVisitor.php b/src/Parser/Visitors/RenameFunctionVisitor.php new file mode 100644 index 0000000..de494af --- /dev/null +++ b/src/Parser/Visitors/RenameFunctionVisitor.php @@ -0,0 +1,70 @@ +functionNames = $this->buildFunctionNameMap($functionNames); + } + + protected function buildFunctionNameMap(array $functionNames): array + { + return collect($functionNames) + ->mapWithKeys(function ($name) { + return [$name => $name.'_'.Utils::makeRandomString()]; + }) + ->all(); + } + + public function enterNode(Node $node) + { + if (! $node instanceof Node\Expr\FuncCall || ! $node->name instanceof Node\Name) { + return; + } + + $functionName = $node->name->toString(); + + if (! isset($this->functionNames[$functionName])) { + return; + } + + $node->name = new Node\Name($this->functionNames[$functionName]); + } + + public function leaveNode(Node $node) + { + if (! $node instanceof Node\Stmt\Function_) { + return null; + } + + $functionName = $node->name->toString(); + + if (! isset($this->functionNames[$functionName])) { + return null; + } + + $newFunctionName = $this->functionNames[$functionName]; + + $node->name = new Node\Identifier($newFunctionName); + + return new Node\Stmt\If_( + new Node\Expr\BooleanNot( + new Node\Expr\FuncCall( + new Node\Name('function_exists'), + [new Node\Arg(new Node\Scalar\String_($newFunctionName))] + ) + ), + [ + 'stmts' => [$node], + ] + ); + } +} diff --git a/tests/Compiler/CustomFunctionsTest.php b/tests/Compiler/CustomFunctionsTest.php new file mode 100644 index 0000000..6bbf743 --- /dev/null +++ b/tests/Compiler/CustomFunctionsTest.php @@ -0,0 +1,22 @@ + +@endfor +BLADE; + + $expected = <<<'EXPECTED' +THE TITLE: 0THE TITLE: 1THE TITLE: 2THE TITLE: 3THE TITLE: 4 +EXPECTED; + + $this->assertSame( + $expected, + $this->render($template) + ); +}); diff --git a/tests/resources/components/functions/declared.blade.php b/tests/resources/components/functions/declared.blade.php new file mode 100644 index 0000000..1935017 --- /dev/null +++ b/tests/resources/components/functions/declared.blade.php @@ -0,0 +1,9 @@ +props(['title'])->trimOutput(); + +function toUpper($value) { + return mb_strtoupper($value); +} +?> + +{{ toUpper($title) }}