Skip to content

Commit

Permalink
Merge branch 'main' into circular-component-compilation
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnathonKoster authored Feb 8, 2025
2 parents 37db81a + e8af95b commit d9dbe42
Show file tree
Hide file tree
Showing 19 changed files with 874 additions and 3 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
## Unreleased

- Adds compiler support for circular component references, like nested comment threads
- 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
- 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

Expand Down
95 changes: 95 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -654,6 +658,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
<!-- /resources/dagger/views/report.blade.php -->
@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
<c-report #cache="the-cache-key" />
```

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
<c-report #cache.300="the-cache-key" />
```

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
<c-report
#cache.10m="the-cache-key"
/>
```

Alternatively, we could also have the cache expire in 1 hour, 15 minutes, and 32 seconds:

```blade
<c-report
#cache=1h15m32s="the-cache-key"
/>
```

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
<c-profile
:$user
:#cache.forever="'user-profile'.$user->id"
/>
```

### 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
<c-first_component #cache.300.array="first-key" />
<c-second_component #cache.300.file="second-key" />
```

### 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
<c-report
#cache.flexible:5:10="the-cache-key"
/>
```

## Component Name

You may access the name of the current component through the `name` property on the component instance:
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
}
},
"require": {
"laravel/framework": "^11.9",
"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",
Expand Down
118 changes: 118 additions & 0 deletions src/Cache/CacheAttributeParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

namespace Stillat\Dagger\Cache;

use Illuminate\Support\Str;

class CacheAttributeParser
{
protected static function parseDurationIntoParts(string $duration): int|array
{
$duration = strtolower(trim($duration));

$pattern = '/(\d+)(y|mo|w|d|h|m|s)/';

preg_match_all($pattern, $duration, $matches, PREG_SET_ORDER);

$years = 0;
$months = 0;
$weeks = 0;
$days = 0;
$hours = 0;
$minutes = 0;
$seconds = 0;

foreach ($matches as $match) {
$value = (int) $match[1];
$unit = $match[2];

switch ($unit) {
case 'y':
$years += $value;
break;
case 'mo':
$months += $value;
break;
case 'w':
$weeks += $value;
break;
case 'd':
$days += $value;
break;
case 'h':
$hours += $value;
break;
case 'm':
$minutes += $value;
break;
case 's':
$seconds += $value;
break;
}
}

return [
$years,
$months,
$weeks,
$days,
$hours,
$minutes,
$seconds,
];
}

protected static function getDuration(array $parts): string|array
{
$duration = trim($parts[1] ?? 'forever');

if ($duration === 'forever') {
return $duration;
}

if (Str::contains($duration, ':')) {
$durationParts = explode(':', $duration);
$duration = array_shift($durationParts);

return [
$duration,
array_values($durationParts),
];
}

if (is_numeric($duration)) {
return $duration;
}

return [static::parseDurationIntoParts($duration), []];
}

protected static function getStore(array $parts)
{
return $parts[2] ?? cache()->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
);
}
}
13 changes: 13 additions & 0 deletions src/Cache/CacheProperties.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Stillat\Dagger\Cache;

class CacheProperties
{
public function __construct(
public string|array $duration,
public string $store,
public array $args = [],
public string $key = '',
) {}
}
2 changes: 2 additions & 0 deletions src/Compiler/ComponentCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -42,6 +43,7 @@ public function compile(string $component): string
ResolveNamespaces::class,
RemoveUseStatements::class,
new ExtractsRenderCalls($this),
RewriteFunctions::class,
])
->thenReturn();

Expand Down
27 changes: 27 additions & 0 deletions src/Compiler/ComponentStages/RewriteFunctions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Stillat\Dagger\Compiler\ComponentStages;

use Closure;
use PhpParser\NodeTraverser;
use Stillat\Dagger\Parser\Visitors\FindFunctionsVisitor;
use Stillat\Dagger\Parser\Visitors\RenameFunctionVisitor;

class RewriteFunctions extends AbstractStage
{
public function handle($ast, Closure $next)
{
$traverser = new NodeTraverser;

$finder = new FindFunctionsVisitor;
$traverser->addVisitor($finder);
$traverser->traverse($ast);

$traverser->removeVisitor($finder);

$modifyFunctionsVisitor = new RenameFunctionVisitor($finder->getFunctionNames());
$traverser->addVisitor($modifyFunctionsVisitor);

return $next($traverser->traverse($ast));
}
}
3 changes: 3 additions & 0 deletions src/Compiler/ComponentState.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\View\ComponentAttributeBag;
use InvalidArgumentException;
use Stillat\BladeParser\Nodes\Components\ComponentNode;
use Stillat\Dagger\Cache\CacheProperties;
use Stillat\Dagger\Support\Utils;

class ComponentState
Expand Down Expand Up @@ -64,6 +65,8 @@ class ComponentState

public int $lineOffset = 0;

public ?CacheProperties $cacheProperties = null;

public function __construct(
public ?ComponentNode $node,
public string $varSuffix,
Expand Down
12 changes: 12 additions & 0 deletions src/Compiler/Concerns/AppliesCompilerParams.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ trait AppliesCompilerParams

protected function isValidCompilerParam(ParameterNode $param): bool
{
if ($this->isCacheParam($param)) {
return true;
}

if (in_array($param->type, $this->invalidCompilerParamTypes, true)) {
return false;
}
Expand All @@ -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)) {
Expand Down
Loading

0 comments on commit d9dbe42

Please sign in to comment.