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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ mix-manifest.json
package-lock.json
yarn.lock
.idea/*
.claude/*
66 changes: 66 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

```bash
# Run all tests
vendor/bin/pest

# Run a single test file
vendor/bin/pest tests/Unit/TrackerTest.php

# Run a specific test by name
vendor/bin/pest --filter "test name"

# Lint (Laravel Pint)
vendor/bin/pint

# Build CP assets
npm run build

# Dev CP assets
npm run dev
```

## Architecture

This is a Statamic addon (`thoughtco/statamic-cache-tracker`) that tracks which content items are rendered on each page, then automatically clears those pages from the static cache when content is updated.

### Core Flow

1. **Tracking** — `Http/Middleware/CacheTracker` runs on every GET request. It hooks into Statamic's augmentation lifecycle (via `Asset::hook`, `Entry::hook`, `LocalizedTerm::hook`, etc.) to collect "tags" for each content item rendered during the request. After a successful response, it stores a `URL → [tags]` mapping in cache.

2. **Invalidation** — `Listeners/Subscriber` subscribes to Statamic save/delete events (entries, assets, terms, navs, forms, globals). When content changes, it dispatches `Jobs/InvalidateTags` which calls `Tracker::invalidate($tags)` to find all URLs containing those tags and clear them from the static cache.

3. **Tag format** — Tags follow patterns like `asset:{id}`, `collection:{handle}:{id}`, `term:{id}`, `form:{handle}`, `nav:{handle}`, `global:{handle}`. Wildcard matching is supported via `*` prefix notation (e.g., `collection:blog:*`).

### Key Classes

- **`Tracker/Manager`** — Core logic. Stores `URL → tags[]` in cache under key `tracker::urls`. Handles `add`, `get`, `invalidate`, and `flush`. Tag matching supports wildcards.
- **`Http/Middleware/CacheTracker`** — Hooks into Statamic augmentation to collect tags during rendering. Also fires `TrackContentTags` event for custom integrations.
- **`Listeners/Subscriber`** — Maps Statamic events to tag generation and queues invalidation.
- **`Facades/Tracker`** — Facade for `Manager` (bound as `cache-tracker`).

### Extending

Custom tag sources can be registered via `Tracker::addAdditionalTracker()`, which accepts a closure or class with an `__invoke(Request $request, array &$tags)` signature and runs in a pipeline.

The `TrackContentTags` event can be fired from custom code to inject additional tags into the current request's tracking.

### CP Features

- **Utility** — "Cache Tracker" utility in the CP for manually clearing URLs or wildcards.
- **Bulk Actions** — "View Cache Tags" and "Clear Cache" actions available on entry/term listings.
- **Permissions** — `view cache tracker tags` and `clear cache tracker tags`.

### Testing Notes

Tests use Pest with an `AddonTestCase` base that:
- Enables static caching with `half` strategy
- Creates a default `pages` collection with a `home` entry
- Disables stache disk writes
- Uses SQLite in-memory database

Fixture views live in `tests/__fixtures__/resources/views/`.
4 changes: 3 additions & 1 deletion src/Tracker/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ public function remove(string $url)
{
$this->invalidateUrls([$url]);

$this->cacheStore()->forget(md5($url));
$storeData = $this->all();
unset($storeData[md5($url)]);
$this->cacheStore()->forever($this->cacheKey, $storeData);
}
}
31 changes: 31 additions & 0 deletions tests/Unit/TrackerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,37 @@ public function it_flushes()
Event::assertDispatched(UrlInvalidated::class);
}

#[Test]
public function it_removes_a_url()
{
Tracker::add('/page1', ['products:1']);
Tracker::add('/page2', ['products:2']);

$this->assertCount(2, Tracker::all());

Tracker::remove('/page1');

$this->assertCount(1, Tracker::all());
$this->assertNull(Tracker::get('/page1'));
$this->assertNotNull(Tracker::get('/page2'));
}

#[Test]
public function it_removes_a_url_and_does_not_retrack_on_next_visit()
{
$this->get('/');

$this->assertCount(1, Tracker::all());

Tracker::remove(collect(Tracker::all())->first()['url']);

$this->assertCount(0, Tracker::all());

$this->get('/');

$this->assertCount(1, Tracker::all());
}

#[Test]
public function it_invalidates_by_exact_tag_match()
{
Expand Down
Loading