Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 69 additions & 39 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,47 @@

All notable changes to this project will be documented in this file.

## [1.1.0] - 2026-07-29

## What's Changed
* fix: close static-cache invalidation gaps by @Bahbv in https://github.com/roxdigital/cache-invalidation/pull/1

## New Contributors
* @Bahbv made their first contribution in https://github.com/roxdigital/cache-invalidation/pull/1

**Full Changelog**: https://github.com/roxdigital/cache-invalidation/compare/v1.0.4...v1.1.0

## [1.0.4] - 2026-07-28
## [1.2.0] - 2026-07-29

### Added

- `FlushStaticCacheOnFormBlueprintSaved` — flushes the entire static cache when a form blueprint is saved, so changed form fields are reflected on every page that embeds the form. Toggle with the `forms_flush_all` config option.
- `StaticCacheFlusher` — single place for full-cache flushes, used by the global/nav flush-all rules as well as form blueprint saves.
Two rule types for `collection_entry_rules`, so dependencies rendered by a
collection's own template no longer need a site-specific invalidator subclass.
Both resolve URLs without the block index, which by design only sees pagebuilder
blocks.

- `['parent' => true]` — clears the saved entry's parent page, for a structured
collection whose parent template lists its children. Opt-in per collection
rather than automatic: in a collection mounted at the site root, a top-level
entry's `parent()` is the root itself, so applying it everywhere would clear
the home page on every save.
- `['collection' => 'handle', 'field' => 'field_handle']` — clears the URL of
every entry in that collection whose field references the saved entry. The
inverse of a block rule; use it for relations rendered by a template, such as
an article detail page showing its author. Walks the named collection on each
save of the source collection.

### Fixed

- Full flushes now go through Statamic's `StaticCache` manager instead of the cacher directly, so they behave exactly like `statamic:static:clear`. Flushing the cacher alone left `nocache::` regions and sessions behind (they are not tracked in the cacher's URL list), and never flushed a dedicated `static_cache` store wholesale. Cached pages — including shared error pages such as the 404 — were already cleared either way; the leak was nocache regions surviving a flush and being restored into freshly rendered pages.

## [1.0.3] - 2026-06-02
- `blockMatchesAnyRule` no longer reads `$rule['block']` unguarded; rules are
partitioned by type first. Previously a rule without a `block` key raised an
undefined-key warning and then silently matched nothing.
- `indexRelevantFields` now only collects `field` from `block` rules. A
`collection` rule's `field` names a field on an entry, not on a block, so
including it kept unnecessary keys in the block index.

### Fixed
- Made gh actions work with node24

## [1.0.2] - 2026-06-02

### Fixed
- Fixed release workflow

## [1.0.1] - 2026-06-02
Backwards compatible: existing rules all carry a `block` key and behave
identically, and both new rule types are inert unless configured.

### Added
- First release
## [1.1.0] - 2026-07-29

## [1.0.0] - 2026-06-02
## What's Changed
* fix: close static-cache invalidation gaps by @Bahbv in https://github.com/roxdigital/cache-invalidation/pull/1

### Added

- `ContentDependencyInvalidator` — replaces Statamic's default invalidator with targeted, pagebuilder-aware cache invalidation.
- `PagebuilderDependencyScanner` — builds and maintains a `url → blocks[]` index in the Laravel cache so invalidation decisions require no live database queries.
- `PagebuilderBlockResolver` — resolves a page entry's block list, expanding `reusable_block` references inline with circular-reference protection.
- `EntryReferenceExtractor` — safely extracts Statamic entry UUIDs from any field value shape (string, array, Collection, augmented Entry).
- Config-driven rules for globals, navigations, collection entries, and taxonomy terms.
- Support for block-type targeting, field-level entry reference matching, and explicit URL lists.
- `customEntryUrls()` extension point for site-specific subclasses.
- Slim index storage — only `type` and rule-referenced fields are persisted, discarding rich-text and other large values.
## New Contributors
* @Bahbv made their first contribution in https://github.com/roxdigital/cache-invalidation/pull/1

## v1.1.0
**Full Changelog**: https://github.com/roxdigital/cache-invalidation/compare/v1.0.4...v1.1.0

### Fixed

Expand Down Expand Up @@ -85,3 +76,42 @@ All notable changes to this project will be documented in this file.

All changes are backwards compatible: no constructor signatures changed, the new
config key defaults to empty, and reference extraction only ever matches more.

## [1.0.4] - 2026-07-28

### Added

- `FlushStaticCacheOnFormBlueprintSaved` — flushes the entire static cache when a form blueprint is saved, so changed form fields are reflected on every page that embeds the form. Toggle with the `forms_flush_all` config option.
- `StaticCacheFlusher` — single place for full-cache flushes, used by the global/nav flush-all rules as well as form blueprint saves.

### Fixed

- Full flushes now go through Statamic's `StaticCache` manager instead of the cacher directly, so they behave exactly like `statamic:static:clear`. Flushing the cacher alone left `nocache::` regions and sessions behind (they are not tracked in the cacher's URL list), and never flushed a dedicated `static_cache` store wholesale. Cached pages — including shared error pages such as the 404 — were already cleared either way; the leak was nocache regions surviving a flush and being restored into freshly rendered pages.

## [1.0.3] - 2026-06-02

### Fixed
- Made gh actions work with node24

## [1.0.2] - 2026-06-02

### Fixed
- Fixed release workflow

## [1.0.1] - 2026-06-02

### Added
- First release

## [1.0.0] - 2026-06-02

### Added

- `ContentDependencyInvalidator` — replaces Statamic's default invalidator with targeted, pagebuilder-aware cache invalidation.
- `PagebuilderDependencyScanner` — builds and maintains a `url → blocks[]` index in the Laravel cache so invalidation decisions require no live database queries.
- `PagebuilderBlockResolver` — resolves a page entry's block list, expanding `reusable_block` references inline with circular-reference protection.
- `EntryReferenceExtractor` — safely extracts Statamic entry UUIDs from any field value shape (string, array, Collection, augmented Entry).
- Config-driven rules for globals, navigations, collection entries, and taxonomy terms.
- Support for block-type targeting, field-level entry reference matching, and explicit URL lists.
- `customEntryUrls()` extension point for site-specific subclasses.
- Slim index storage — only `type` and rule-referenced fields are persisted, discarding rich-text and other large values.
20 changes: 20 additions & 0 deletions config/cache_invalidation.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,26 @@
| Rule with field: ['block' => 'block_type', 'field' => 'field_handle']
| Flush all cached URLs for a collection: 'collection' => 'all'
|
| The two rules above resolve pages through the block index, so they only
| reach what a pagebuilder block renders. For relations rendered by a
| collection's own template there are two more:
|
| Parent page: ['parent' => true]
| Clears the saved entry's parent page. For a structured collection
| whose parent template lists its children.
| Opt-in per collection, not automatic: in a collection mounted at the
| site root a top-level entry's parent is the root itself, so applying
| this everywhere would clear the home page on every save.
|
| Referencing entries: ['collection' => 'handle', 'field' => 'field_handle']
| Clears the URL of every entry in that collection whose field
| references the saved entry — the inverse of a block rule. Use it when
| the referencing markup is in a template rather than a block, e.g. an
| article detail page rendering its author from the employees
| collection.
| This walks the named collection on each save of the source
| collection, so keep an eye on it for very large collections.
|
*/

'collection_entry_rules' => [
Expand Down
80 changes: 76 additions & 4 deletions src/ContentDependencyInvalidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Illuminate\Support\Collection;
use Statamic\Entries\Entry;
use Statamic\Facades\Entry as EntryFacade;
use Statamic\Globals\Variables;
use Statamic\StaticCaching\Cacher;
use Statamic\StaticCaching\DefaultInvalidator;
Expand Down Expand Up @@ -144,13 +145,84 @@ protected function urlsForEntry(Entry $entry): Collection
return $this->allCachedUrls()->merge($urls);
}

return $urls
->merge($this->blockRuleUrls((array) $rule, $entry))
->merge($this->parentRuleUrls((array) $rule, $entry))
->merge($this->referencingEntryRuleUrls((array) $rule, $entry));
}

/**
* ['block' => 'x', 'field' => 'y'] — cached pages containing that block.
*
* @param list<array<string, mixed>> $rules
*/
private function blockRuleUrls(array $rules, Entry $entry): Collection
{
$blockRules = array_values(array_filter(
$rules,
fn (array $rule): bool => isset($rule['block']),
));

if ($blockRules === []) {
return collect();
}

$entryId = $entry->id();

return $urls->merge(
$this->pagebuilder->urlsForBlocksMatching(
fn (array $block): bool => $this->blockMatchesAnyRule($block, (array) $rule, $entryId),
),
return $this->pagebuilder->urlsForBlocksMatching(
fn (array $block): bool => $this->blockMatchesAnyRule($block, $blockRules, $entryId),
);
}

/**
* ['parent' => true] — the saved entry's parent page, for a structured
* collection whose parent template renders its children.
*
* Opt-in per collection rather than automatic: in a collection mounted at
* the site root, a top-level entry's parent() is the root itself, so
* applying this everywhere would clear the home page on every save.
*
* @param list<array<string, mixed>> $rules
*/
private function parentRuleUrls(array $rules, Entry $entry): Collection
{
$wantsParent = collect($rules)->contains(
fn (array $rule): bool => ($rule['parent'] ?? false) === true,
);

if (! $wantsParent) {
return collect();
}

$url = $entry->parent()?->absoluteUrl();

return $url ? collect([$url]) : collect();
}

/**
* ['collection' => 'x', 'field' => 'y'] — the URLs of entries in collection
* x whose field y references the saved entry. The inverse of a block rule:
* for relations rendered by a collection's own template rather than by a
* pagebuilder block, so the block index cannot reach them.
*
* @param list<array<string, mixed>> $rules
*/
private function referencingEntryRuleUrls(array $rules, Entry $entry): Collection
{
$entryId = $entry->id();

return collect($rules)
->filter(fn (array $rule): bool => isset($rule['collection'], $rule['field']))
->flatMap(fn (array $rule): array => EntryFacade::whereCollection($rule['collection'])
->filter(fn (Entry $candidate): bool => $this->valueReferencesEntry(
$candidate->get($rule['field']),
$entryId,
))
->map(fn (Entry $candidate): ?string => $candidate->absoluteUrl())
->filter()
->all())
->unique()
->values();
}

protected function urlsForTaxonomyTerm(LocalizedTerm $term): Collection
Expand Down
13 changes: 9 additions & 4 deletions src/PagebuilderDependencyScanner.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,22 @@ private function buildIndex(): array
}

/**
* Returns the field handles that must be kept in the index.
* Only fields referenced in collection_entry_rules are needed;
* global/taxonomy predicates only match on block type.
* Returns the block field handles that must be kept in the index.
*
* Only 'block' rules match against indexed blocks; global and taxonomy
* predicates match on block type alone, and the 'field' of a 'collection'
* rule names a field on an entry rather than on a block.
*
* @return list<string>
*/
private function indexRelevantFields(): array
{
return collect(config('cache_invalidation.collection_entry_rules', []))
->flatMap(fn (mixed $rules): array => is_array($rules)
? collect($rules)->pluck('field')->filter()->all()
? collect($rules)
->filter(fn (mixed $rule): bool => is_array($rule) && isset($rule['block'], $rule['field']))
->pluck('field')
->all()
: []
)
->unique()
Expand Down
Loading