From e8262919a3d5e14ef95c4b47e4609d62edbd91f9 Mon Sep 17 00:00:00 2001 From: Bob Vrijland Date: Wed, 29 Jul 2026 15:22:30 +0200 Subject: [PATCH] feat: add parent and referencing-entry rule types Dependencies rendered by a collection's own template previously had no config vocabulary, forcing host apps to subclass the invalidator and override customEntryUrls(). Two new rule types cover that: - ['parent' => true] - ['collection' => 'handle', 'field' => 'field_handle'] Also partitions rules by type before block matching, which fixes an undefined-key warning on any rule without a 'block' key. --- CHANGELOG.md | 108 +++++++++++++++++---------- config/cache_invalidation.php | 20 +++++ src/ContentDependencyInvalidator.php | 80 +++++++++++++++++++- src/PagebuilderDependencyScanner.php | 13 +++- 4 files changed, 174 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a9cdbe..90466b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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. diff --git a/config/cache_invalidation.php b/config/cache_invalidation.php index 215e71a..edd0466 100644 --- a/config/cache_invalidation.php +++ b/config/cache_invalidation.php @@ -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' => [ diff --git a/src/ContentDependencyInvalidator.php b/src/ContentDependencyInvalidator.php index e2045e6..584f3cf 100644 --- a/src/ContentDependencyInvalidator.php +++ b/src/ContentDependencyInvalidator.php @@ -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; @@ -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> $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> $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> $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 diff --git a/src/PagebuilderDependencyScanner.php b/src/PagebuilderDependencyScanner.php index 61fcc40..b9aa07a 100644 --- a/src/PagebuilderDependencyScanner.php +++ b/src/PagebuilderDependencyScanner.php @@ -75,9 +75,11 @@ 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 */ @@ -85,7 +87,10 @@ 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()