Skip to content
Open
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
32 changes: 29 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CI

on:
push:
branches: [main]
branches: [main, v2]
pull_request:
branches: [main]
branches: [main, v2]

jobs:
syntax:
Expand All @@ -22,4 +22,30 @@ jobs:
coverage: none

- name: Check PHP syntax
run: find src config -name '*.php' -print0 | xargs -0 -n1 php -l
run: find src config database tests -name '*.php' -print0 | xargs -0 -n1 php -l

tests:
name: Tests (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'

strategy:
fail-fast: false
matrix:
php: ['8.4', '8.5']

steps:
- uses: actions/checkout@v4

- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: pdo_sqlite, sqlite3
coverage: none

- name: Install dependencies
run: composer update --prefer-dist --no-interaction --no-progress

- name: Run tests
run: vendor/bin/phpunit --no-coverage
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
/.phpunit.cache/
.DS_Store
*.lock
/tests/__fixtures__/dev-null/*
!/tests/__fixtures__/dev-null/.gitkeep
95 changes: 91 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,99 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [2.0.0] - 2026-08-06

Invalidation is now derived from what pages actually read, instead of from rules
describing what they might read. There is no configuration to write.

The 1.x design kept a hand-maintained map of block types and field handles that
had to mirror the templates. It drifted silently — a URL that no longer resolved
invalidated nothing, and a relation rendered outside the `pagebuilder` field was
invisible to the block index by construction. The template already knows what it
renders; v2 observes it rather than restating it.

### Added

- A `url -> tags` dependency graph, recorded while a page renders and written when
it enters the static cache. Works identically on the `half` and `full`
strategies.
- Read recording for entries, terms, globals and forms, hooked at the query
builder, repository and augmentation level rather than in templates.
- Item tags versus list tags: a query pinned to ids records only those items, while
any other query also records its collection or taxonomy, so an entry created
later still invalidates listings that have never seen it.
- Storage drivers: `sqlite` (default, owns its own connection and schema, needs no
database configured), `database` (opt-in, with a migration), and `null` (records
nothing, so every save clears everything).
- A safety net: any cached URL absent from the graph is treated as depending on
everything. Covers pages cached before install, a lost graph, and recorder bugs,
so the failure mode is over-invalidation that heals after one render rather than
a page that stays stale with no symptom.
- `cache-invalidation:why`, `:affected`, `:stats` and `:doctor`. `affected` answers
"what clears if I save this?" before saving — the question the 1.x design could
not be asked. `doctor` exits non-zero when invalidation cannot work, so a broken
environment fails a deploy.
- An `X-Cache-Tags` header behind `CACHE_INVALIDATION_DEBUG`.
- A public API for data this addon cannot observe — an HTTP call, a custom Eloquent
model, a file. `CacheTags::add()` (or `@cachetags(...)`) declares the dependency
where it is rendered, `CacheTags::invalidate()` clears it wherever that data
changes, and `cache-invalidation:clear` does the same from the command line.
Unlike a content save this does not sweep up untracked URLs, so a targeted call
stays targeted right after a deploy.
- A test suite: 74 tests over Testbench, asserting recording through real queries
against real content and invalidation through real save events. Mutation-checked,
and verified to fail against the bugs it covers.

### Changed

- Relicensed from proprietary to the MIT License. Copyright remains with Rox
Digital and the notice must be retained in redistributions, while the licence
disclaims all warranty and liability. `composer.json` now declares `MIT` and a
`LICENSE` file has been added.
- The whole cache is no longer flushed for globals, navigations, form blueprints or
collection trees. URLs are invalidated individually, so `nocache` regions and the
graph survive.
- Navigations are tagged where they render, so one used on a handful of pages clears
only those. A nav in the shared layout still reaches every page, but as a
consequence of where it is used rather than a special case.
- Two kinds of read are excluded from recording, because both made almost every save
clear almost everything. Statamic's URL resolution, which for a structured
collection validates the whole collection tree; and navigation menus, where the
nav is recorded as `nav:{handle}` rather than as an `entry:` tag per menu item.

The navigation exclusion is a deliberate trade-off: renaming a page leaves its
menu label stale on already-cached pages until they clear for another reason, and
saving the navigation clears them. A page save clears where that page is rendered
as content.

Measured on a 215-page site: 31.9 tags per URL where recording everything gave
68.6, and a page save clears a median of 1 URL where before it cleared all of
them. 19 of 25 sampled pages clear 0–5 URLs. Saving a navigation clears all 215,
because all 215 render it.
- Globals invalidate only where they are read. A set rendered in the layout still
reaches every page; one rendered by a single block reaches that block's pages.
- Form blueprint saves clear the pages rendering that form instead of the entire
site.
- The addon now claims Statamic's invalidator when the configured class is one of
its own, not only when the config is null. Sites pin it by name, and a 1.x pin
would otherwise fatal on a class that no longer exists.

### Fixed

- `Invalidator::refresh()` is honoured. `DefaultInvalidator` flips its `$refreshing`
flag before delegating to `invalidate()`, which 1.x overrode without checking, so
`statamic.static_caching.background_recache` hard-purged instead of refreshing.
- Relations rendered outside the `pagebuilder` field — an entry's `author`,
`category`, a hero fieldset, entry links inside Bard — now invalidate. The 1.x
block index only read `$entry->get('pagebuilder')`, and the
`['collection' => …, 'field' => …]` rule existed to patch that hole by walking a
whole collection on every save.

### Removed

- Every rule key: `pagebuilder_collections`, `collection_entry_rules`,
`collection_urls`, `globals_flush_all`, `navs_flush_all`,
`collection_trees_flush_all`, `forms_flush_all`, `global_target_blocks`,
`global_urls`, `taxonomy_target_blocks`, `taxonomy_urls`.
`cache-invalidation:doctor` reports any still present in a published config.
- The block index and its supporting classes, along with `customEntryUrls()`. The
relations that hook existed for are now observed.

## [1.2.0] - 2026-07-29

Expand Down
Loading
Loading