Skip to content

Commit

Permalink
Merge branch 'main' into compile-time-rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnathonKoster committed Feb 8, 2025
2 parents 62848ff + 2087141 commit c86abb1
Show file tree
Hide file tree
Showing 25 changed files with 1,053 additions and 12 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
## Unreleased

- Adds a new "Compile Time Rendering" system, which can render components at compile time and inline the static output
- 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
99 changes: 95 additions & 4 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 @@ -112,10 +116,6 @@ Dagger components are also interopable with Blade components, and will add thems

This is due to the View Manifest. The Dagger compiler and runtime will store which component files were used to create the final output in a JSON file, which is later used for cache-invalidation. The Dagger compiler inlines component templates, which prevents typical file-based cache invalidation from working; the View Manifest solves that problem.

### Are circular component hierarchies supported?

A circular component hierarchy is one where Component A includes Component B, which might conditionally include Component A again. Because the compiler inlines components, circular components are not supported and may result in infinite loops.

### Why build all of this?

Because I wanted to.
Expand Down Expand Up @@ -662,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
<!-- /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\ComponentOptions;
use Stillat\Dagger\Support\Utils;

Expand Down Expand Up @@ -78,6 +79,8 @@ class ComponentState
*/
public bool $isCtrEligible = false;

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 c86abb1

Please sign in to comment.