September 3, 2023 —John Koster
This article is part of a series. If you'd like to read a different part first, check out the following links:
In this section, we will enhance our cache system by introducing the ability to categorize or "label" our cached responses. We can efficiently invalidate multiple related responses by using these cache labels. Additionally, we'll set up new event listeners that will automatically invalidate cached responses, such as when we create new collection entries using the Statamic Control Panel.
The idea behind our label system will be to leverage our paths
array, like when we implemented our global cache invalidation. Our labels will support a label "namespace" and the actual label. The namespace will be an overarching label category, and these will correspond to larger content categories like "asset," "collection," "taxonomy," etc.; we can think of the label itself as the handle of a collection, for example.
To help keep things organized, we will store our cache labels inside a labels
directory within our existing hybrid-cache
directory. We will update our cache's service provider to create this automatically.
In app/HybridCache/Providers/HybridCacheServiceProvider.php
:
1<?php 2 3namespace App\HybridCache\Providers; 4 5use App\HybridCache\Commands\InvalidateAll; 6use App\HybridCache\Facades\HybridCache; 7use App\HybridCache\Manager; 8use Illuminate\Cookie\Middleware\EncryptCookies; 9use Illuminate\Support\ServiceProvider;10use Illuminate\View\View;11 12class HybridCacheServiceProvider extends ServiceProvider13{14 // ...15 16 public function boot()17 {18 $cacheStoragePath = storage_path('hybrid-cache');19 $labelsPath = $cacheStoragePath.'/labels'; 20 $globalInvalidationPath = $cacheStoragePath.'/global-invalidation';21 22 if (! file_exists($cacheStoragePath)) {23 mkdir(storage_path('hybrid-cache'), 0755, true);24 }25 26 if (! file_exists($labelsPath)) { 27 mkdir($labelsPath, 0755, true);28 }29 30 if (! file_exists($globalInvalidationPath)) {31 touch($globalInvalidationPath);32 }33 34 view()->composer('*', function (View $view) {35 HybridCache::registerViewPath($view->getPath());36 });37 38 if ($this->app->runningInConsole()) {39 $this->commands(40 InvalidateAll::class,41 );42 }43 }44}
The changes to our cache manager will seem rather extensive, but they all help achieve the same goal: keep track of the unique label namespaces and labels for the current request, with a few helper methods sprinkled in to get these details later.
In app/HybridCache/Manager.php
:
1<?php 2 3namespace App\HybridCache; 4 5class Manager 6{ 7 // ... 8 9 protected array $labelNamespaces = []; 10 11 protected array $labels = [];12 13 public function __construct()14 {15 self::$instance = $this;16 17 $configurationPath = realpath(__DIR__.'/../../config/hybrid-cache.php');18 19 if ($configurationPath) {20 $this->configuration = require $configurationPath;21 }22 }23 24 public function invalidateCacheLabel(string $namespace, ?string $label): void 25 {26 if ($label == null) {27 $this->invalidateLabelNamespace($namespace);28 29 return;30 }31 32 $this->invalidateLabel($namespace, $label);33 }34 35 public function invalidateLabelNamespace(string $namespace): void36 {37 touch(storage_path('hybrid-cache/labels/'.$namespace));38 }39 40 public function invalidateLabel(string $namespace, string $label): void41 {42 touch($this->labelPath($namespace, $label));43 }44 45 public function label(string $namespace, string $label = null): void46 {47 if (! in_array($namespace, $this->labelNamespaces)) {48 $this->labelNamespaces[] = $namespace;49 }50 51 if ($label) {52 $newLabel = $this->labelName($namespace, $label);53 54 if (! in_array($newLabel, $this->labels)) {55 $this->labels[] = $newLabel;56 }57 }58 }59 60 public function labelName(string $namespace, string $label): string61 {62 return $namespace.'__'.$label;63 }64 65 public function labelPath(string $namespace, string $label): string66 {67 return storage_path('hybrid-cache/labels/'.$this->labelName($namespace, $label));68 }69 70 public function getLabels(): array71 {72 return $this->labels;73 }74 75 public function getLabelNamespaces(): array76 {77 return $this->labelNamespaces;78 }79 80 // ...81}
And the corresponding changes to our facade.
In app/HybridCache/Facades/HybridCache.php
:
1<?php 2 3namespace App\HybridCache\Facades; 4 5use App\HybridCache\Manager; 6use Illuminate\Support\Facades\Facade; 7 8/** 9 * @method static bool canHandle()10 * @method static void sendCachedResponse()11 * @method static string|null getCacheFileName()12 * @method static void registerViewPath(string $path)13 * @method static void registerEntryId(string $id)14 * @method static void registerAssetPath(string $path)15 * @method static void registerTermId(string $id)16 * @method static void registerGlobalPath(string $path)17 * @method static array getCacheData()18 * @method static bool canCache()19 * @method static void abandonCache()20 * @method static array getCacheableStatusCodes()21 * @method static bool getIgnoreQueryString()22 * @method static bool isCacheBypassed()23 * @method static void setExpiration(mixed $expiration)24 * @method static mixed getExpiration()25 * @method static void invalidateAll()26 * @method static void invalidateCacheLabel(string $namespace, ?string $label) 27 * @method static void invalidateLabelNamespace(string $namespace) 28 * @method static void invalidateLabel(string $namespace, string $label) 29 * @method static void label(string $namespace, ?string $label = null) 30 * @method static string labelName(string $namespace, string $label) 31 * @method static string labelPath(string $namespace, string $label) 32 * @method static array getLabels() 33 * @method static array getLabelNamespaces() 34 * @method35 *36 * @see \App\HybridCache\Facades\HybridCache37 */38class HybridCache extends Facade39{40 protected static function getFacadeAccessor()41 {42 return Manager::class;43 }44}
Before we add our cache labels, let us update our ResponsePreparedListener
implementation to store the paths of any labels or namespaces.
In app/HybridCache/Listeners/ResponsePreparedListener.php
:
1<?php 2 3namespace App\HybridCache\Listeners; 4 5use App\HybridCache\Facades\HybridCache; 6use Illuminate\Routing\Events\ResponsePrepared; 7use Illuminate\Support\Facades\Auth; 8use Statamic\Facades\Entry; 9use Statamic\Facades\Term;10 11class ResponsePreparedListener12{13 public function handle(ResponsePrepared $event)14 {15 // ...16 17 $responseDependencies = HybridCache::getCacheData();18 19 $paths = [20 storage_path('hybrid-cache/global-invalidation'),21 ];22 23 $cacheLabels = array_merge(HybridCache::getLabels(), HybridCache::getLabelNamespaces()); 24 25 foreach ($cacheLabels as $label) {26 $labelPath = storage_path('hybrid-cache/labels/'.$label);27 28 if (! file_exists($labelPath)) {29 touch($labelPath);30 }31 32 $paths[] = $labelPath;33 }34 35 $paths = array_merge($paths, $responseDependencies['viewPaths']);36 $paths = array_merge($paths, $responseDependencies['globalPaths']);37 38 // ...39 40 file_put_contents($cacheFileName, json_encode($cacheData));41 }42}
As we've done in many other situations, we are constructing paths to our cache's storage directory and adding them to our cached response. Later, if we were to update either a namespace or label using PHP's touch
function, our cached content would invalidate itself.
In this section, we will adapt some core Statamic tags to suit our cache management needs better, specifically focusing on populating our cache namespaces and labels. Luckily, if we create a custom Antlers tag in our project that shares a handle with a core Statamic tag, our custom tag takes precedence. We'll leverage this feature to collect data whenever certain tags are activated. For instance, we'll be able to identify all collections queried on a particular page.
In app/Tags/Collection.php
:
1<?php 2 3namespace App\Tags; 4 5use App\HybridCache\Facades\HybridCache; 6use Illuminate\Pagination\LengthAwarePaginator; 7use Statamic\Tags\Collection\Collection as StatamicCollection; 8 9class Collection extends StatamicCollection10{11 protected function output($items)12 {13 $outputItems = $items;14 15 if ($outputItems instanceof LengthAwarePaginator) {16 $outputItems = $outputItems->items();17 }18 19 collect($outputItems)->map(function ($item) {20 return $item->collection()->handle();21 })->unique()->each(function ($collection) {22 HybridCache::label('collection', $collection);23 });24 25 return parent::output($items);26 }27}
We iterate each output item between lines 19 and 23 and retrieve the unique collection handles. Once we have the unique handles, we use the string collection
as the namespace and each collection's handle as the cache label.
After manually deleting any existing cache files and visiting the homepage, we should be able to locate the corresponding JSON file within our storage/hybrid-cache
directory. Opening this up, we can see quite a few new entries within the paths
array:
1{ 2 "expires": null, 3 "paths": { 4 "\\/storage\/hybrid-cache\/global-invalidation": 1693606948, 5 "\\/storage\/hybrid-cache\/labels\/collection__articles": 1693606949, 6 "\\/storage\/hybrid-cache\/labels\/collection": 1693606949, 7 "\\/resources\/views\/home.antlers.html": 1693238231, 8 "\\/resources\/views\/layout.antlers.html": 1693408555, 9 "\\/resources\/views\/\/_nav.antlers.html": 1692813969,10 "\\/resources\/views\/\/_footer.antlers.html": 1692813969,11 "\\/content\/globals\/settings.yaml": 1692813969,12 "\\/content\/collections\/articles\/1994-07-05.magic.md": 1692813969,13 "\\/content\/collections\/articles\/1996-08-16.nectar.md": 1692813969,14 "\\/content\/collections\/articles\/1996-11-18.dance.md": 1692816329,15 "\\/content\/collections\/pages\/about.md": 1692813969,16 "\\/content\/collections\/pages\/articles.md": 1692813969,17 "\\/content\/collections\/pages\/topics.md": 1692813969,18 "\\/content\/collections\/pages\/home.md": 1693235304,19 "\\/public\/assets\/site\/social-icons\/.meta\/twitter.svg.yaml": 1692814047,20 "\\/public\/assets\/site\/social-icons\/.meta\/github.svg.yaml": 1692814047,21 "\\/public\/assets\/site\/social-icons\/.meta\/email.svg.yaml": 169281404722 },23 "headers": {24 "cache-control": [25 "no-cache, private"26 ],27 "content-type": [28 "text\/html; charset=UTF-8"29 ]30 }31}
Additionally, if we were to look inside our hybrid-cache/labels
directory, we would also see two new files created: collection
and collection_articles
. Later, we will create new event listeners to invalidate these namespaces and labels for us. Before we do, we will override Statamic's taxonomy
tag to do something similar.
Statamic's taxonomy
tag utilizes the Statamic\Tags\Taxonomy\Terms
helper class to help determine what terms should used when returning results. We will need to override this class to add some helper methods to retrieve the taxonomy and collection handles used by the taxonomy
tag.
In app/Tags/Terms.php
:
1<?php 2 3namespace App\Tags; 4 5use Statamic\Tags\Taxonomy\Terms as StatamicTerms; 6 7class Terms extends StatamicTerms 8{ 9 public function getTaxonomies()10 {11 return $this->taxonomies->map(function ($taxonomy) {12 return $taxonomy->handle();13 })->all();14 }15 16 public function getCollections()17 {18 return $this->collections->map(function ($collection) {19 return $collection->handle();20 })->all();21 }22}
Using our new Terms
class, we can now override Statamic's taxonomy
tag to help populate cache labels for us:
In app/Tags/Taxonomy.php
:
1<?php 2 3namespace App\Tags; 4 5use App\HybridCache\Facades\HybridCache; 6use Statamic\Tags\Taxonomy\Taxonomy as StatamicTaxonomy; 7 8class Taxonomy extends StatamicTaxonomy 9{10 public function index()11 {12 $terms = new Terms($this->params);13 14 foreach ($terms->getTaxonomies() as $term) {15 HybridCache::touchLabel('taxonomy', $term);16 }17 18 foreach ($terms->getCollections() as $collection) {19 HybridCache::touchLabel('collection', $collection);20 }21 22 return parent::index();23 }24}
When we visit the Cool Writings /topics
page and find the cached JSON file, we can see that the taxonomy handles are now being added to the paths
array:
1{ 2 "expires": null, 3 "paths": { 4 "\\/storage\/hybrid-cache\/global-invalidation": 1693607887, 5 "\\/storage\/hybrid-cache\/labels\/taxonomy__topics": 1693607887, 6 "\\/storage\/hybrid-cache\/labels\/taxonomy": 1693607887, 7 "\\/resources\/views\/topics\/index.antlers.html": 1692813969, 8 "\\/resources\/views\/layout.antlers.html": 1693408555, 9 "\\/resources\/views\/\/_nav.antlers.html": 1692813969,10 "\\/resources\/views\/\/_footer.antlers.html": 1692813969,11 "\\/content\/globals\/settings.yaml": 1692813969,12 "\\/content\/collections\/pages\/about.md": 1692813969,13 "\\/content\/collections\/pages\/articles.md": 1692813969,14 "\\/content\/collections\/pages\/topics.md": 1692813969,15 "\\/content\/collections\/pages\/home.md": 1693235304,16 "\\/content\/taxonomies\/topics\/sneakers.yaml": 1692813969,17 "\\/content\/taxonomies\/topics\/soda.yaml": 1692813969,18 "\\/content\/taxonomies\/topics\/dance.yaml": 1692813969,19 "\\/public\/assets\/site\/social-icons\/.meta\/twitter.svg.yaml": 1692814047,20 "\\/public\/assets\/site\/social-icons\/.meta\/github.svg.yaml": 1692814047,21 "\\/public\/assets\/site\/social-icons\/.meta\/email.svg.yaml": 169281404722 },23 "headers": {24 "cache-control": [25 "no-cache, private"26 ],27 "content-type": [28 "text\/html; charset=UTF-8"29 ]30 }31}
Now that our cached responses include labels, we can begin the process of invalidating them using new event listeners. We'll be listening for the following events, all of which are triggered by Statamic's Control Panel:
Statamic\Events\EntryCreated
: Triggered when a new entry (like a blog post or an article) is added in Statamic. This event can be used to ensure that new content gets reflected immediately across the website.Statamic\Events\EntryDeleted
: Triggered when an existing entry is removed. By listening to this, we can ensure that the deleted content doesn't appear in the cache, maintaining the site's relevance.Statamic\Events\EntrySaved
: Triggered when an entry gets saved, which could be after creating a new entry or updating an existing one. This is useful to refresh the cache whenever content updates are made.Statamic\Events\TaxonomyCreated
: This event is fired takes place when a new taxonomy (a way to categorize or label content) is introduced. By monitoring this, we can ensure any pages listing all taxonomies can be invalidated.Statamic\Events\TaxonomyDeleted
: Triggered when a taxonomy is removed. This ensures that any references to the deleted taxonomy don't remain cached.Statamic\Events\TaxonomySaved
: Fired whenever a taxonomy is saved, either after its creation or its update. This keeps cached taxonomies up-to-date with any changes.Statamic\Events\TermCreated
: Takes place when a new term within a taxonomy is created.Statamic\Events\TermDeleted
: Triggered when a term within a taxonomy gets deleted.Statamic\Events\TermSaved
: Fires every time a term gets saved, be it post-creation or after an update.By tapping into these events, we can invalidate our stored cache namespaces and labels. We'll start by implementing our EntryCreatedListener
.
In app/HybridCache/Listeners/EntryCreatedListener.php
:
1<?php 2 3namespace App\HybridCache\Listeners; 4 5use App\HybridCache\Facades\HybridCache; 6use Statamic\Events\EntryCreated; 7 8class EntryCreatedListener 9{10 public function handle(EntryCreated $event)11 {12 HybridCache::invalidateLabelNamespace('collection');13 HybridCache::invalidateLabel('collection', $event->entry->collection()->handle());14 }15}
Our listener uses the data from Statamic's events to invalidate both the collection namespace and the specific label tied to that collection. This action ensures the system invalidates any cached pages associated with these label paths. The remaining listener implementations will look similar.
In app/HybridCache/Listeners/EntryDeletedListener.php
:
1<?php 2 3namespace App\HybridCache\Listeners; 4 5use App\HybridCache\Facades\HybridCache; 6use Statamic\Events\EntryDeleted; 7 8class EntryDeletedListener 9{10 public function handle(EntryDeleted $event)11 {12 HybridCache::invalidateLabelNamespace('collection');13 HybridCache::invalidateLabel('collection', $event->entry->collection()->handle());14 }15}
In app/HybridCache/Listeners/EntrySavedListener.php
:
1<?php 2 3namespace App\HybridCache\Listeners; 4 5use App\HybridCache\Facades\HybridCache; 6use Statamic\Events\EntrySaved; 7 8class EntrySavedListener 9{10 public function handle(EntrySaved $event)11 {12 HybridCache::invalidateLabelNamespace('collection');13 HybridCache::invalidateLabel('collection', $event->entry->collection()->handle());14 }15}
In app/HybridCache/Listeners/TaxonomyCreatedListener.php
:
1<?php 2 3namespace App\HybridCache\Listeners; 4 5use App\HybridCache\Facades\HybridCache; 6use Statamic\Events\TaxonomyCreated; 7 8class TaxonomyCreatedListener 9{10 public function handle(TaxonomyCreated $event)11 {12 HybridCache::invalidateLabelNamespace('taxonomy');13 HybridCache::invalidateLabel('taxonomy', $event->taxonomy->handle());14 }15}
In app/HybridCache/Listeners/TaxonomyDeletedListener.php
:
1<?php 2 3namespace App\HybridCache\Listeners; 4 5use App\HybridCache\Facades\HybridCache; 6use Statamic\Events\TaxonomyDeleted; 7 8class TaxonomyDeletedListener 9{10 public function handle(TaxonomyDeleted $event)11 {12 HybridCache::invalidateLabelNamespace('taxonomy');13 HybridCache::invalidateLabel('taxonomy', $event->taxonomy->handle());14 }15}
In app/HybridCache/Listeners/TaxonomySavedListener.php
:
1<?php 2 3namespace App\HybridCache\Listeners; 4 5use App\HybridCache\Facades\HybridCache; 6use Statamic\Events\TaxonomySaved; 7 8class TaxonomySavedListener 9{10 public function handle(TaxonomySaved $event)11 {12 HybridCache::invalidateLabelNamespace('taxonomy');13 HybridCache::invalidateLabel('taxonomy', $event->taxonomy->handle());14 }15}
In app/HybridCache/Listeners/TermCreatedListener.php
:
1<?php 2 3namespace App\HybridCache\Listeners; 4 5use App\HybridCache\Facades\HybridCache; 6use Statamic\Events\TermCreated; 7 8class TermCreatedListener 9{10 public function handle(TermCreated $event)11 {12 HybridCache::invalidateLabelNamespace('taxonomy');13 HybridCache::invalidateLabel('taxonomy', $event->term->taxonomy()->handle());14 }15}
In app/HybridCache/Listeners/TermDeletedListener.php
:
1<?php 2 3namespace App\HybridCache\Listeners; 4 5use App\HybridCache\Facades\HybridCache; 6use Statamic\Events\TermDeleted; 7 8class TermDeletedListener 9{10 public function handle(TermDeleted $event)11 {12 HybridCache::invalidateLabelNamespace('taxonomy');13 HybridCache::invalidateLabel('taxonomy', $event->term->taxonomy()->handle());14 }15}
In app/HybridCache/Listeners/TermSavedListener.php
:
1<?php 2 3namespace App\HybridCache\Listeners; 4 5use App\HybridCache\Facades\HybridCache; 6use Statamic\Events\TermSaved; 7 8class TermSavedListener 9{10 public function handle(TermSaved $event)11 {12 HybridCache::invalidateLabelNamespace('taxonomy');13 HybridCache::invalidateLabel('taxonomy', $event->term->taxonomy()->handle());14 }15}
We can now update our cache's event service provider for the last time to register our new listeners.
In app/HybridCache/Providers/HybridCacheEventServiceProvider.php
:
1<?php 2 3namespace App\HybridCache\Providers; 4 5use App\HybridCache\Listeners\EntryCreatedListener; 6use App\HybridCache\Listeners\EntryDeletedListener; 7use App\HybridCache\Listeners\EntrySavedListener; 8use App\HybridCache\Listeners\LoggedOutListener; 9use App\HybridCache\Listeners\LoginSuccessListener;10use App\HybridCache\Listeners\ResponsePreparedListener;11use App\HybridCache\Listeners\TaxonomyCreatedListener; 12use App\HybridCache\Listeners\TaxonomyDeletedListener;13use App\HybridCache\Listeners\TaxonomySavedListener;14use App\HybridCache\Listeners\TermCreatedListener;15use App\HybridCache\Listeners\TermDeletedListener;16use App\HybridCache\Listeners\TermSavedListener;17use Illuminate\Auth\Events\Login;18use Illuminate\Auth\Events\Logout;19use Illuminate\Foundation\Support\Providers\EventServiceProvider;20use Illuminate\Routing\Events\ResponsePrepared;21use Statamic\Events\EntryCreated; 22use Statamic\Events\EntryDeleted;23use Statamic\Events\EntrySaved;24use Statamic\Events\TaxonomyCreated;25use Statamic\Events\TaxonomyDeleted;26use Statamic\Events\TaxonomySaved;27use Statamic\Events\TermCreated;28use Statamic\Events\TermDeleted;29use Statamic\Events\TermSaved;30 31class HybridCacheEventServiceProvider extends EventServiceProvider32{33 /**34 * The event to listener mappings for the application.35 *36 * @var array<class-string, array<int, class-string>>37 */38 protected $listen = [39 ResponsePrepared::class => [40 ResponsePreparedListener::class,41 ],42 Login::class => [43 LoginSuccessListener::class,44 ],45 Logout::class => [46 LoggedOutListener::class,47 ],48 TermSaved::class => [ 49 TermSavedListener::class,50 ],51 TermCreated::class => [52 TermCreatedListener::class,53 ],54 TermDeleted::class => [55 TermDeletedListener::class,56 ],57 TaxonomyCreated::class => [58 TaxonomyCreatedListener::class,59 ],60 TaxonomySaved::class => [61 TaxonomySavedListener::class,62 ],63 TaxonomyDeleted::class => [64 TaxonomyDeletedListener::class,65 ],66 EntrySaved::class => [67 EntrySavedListener::class,68 ],69 EntryCreated::class => [70 EntryCreatedListener::class,71 ],72 EntryDeleted::class => [73 EntryDeletedListener::class,74 ],75 ];76}
In this section, we will update our custom Antlers tag to allow for adding arbitrary cache namespaces and labels from our templates.
In app/Tags/HybridCache.php
:
1<?php 2 3namespace App\Tags; 4 5use App\HybridCache\Facades\HybridCache as Cache; 6use Carbon\Carbon; 7use Statamic\Tags\Tags; 8 9class HybridCache extends Tags10{11 protected static $handle = 'hybrid_cache';12 13 // ...14 15 public function label() 16 {17 foreach ($this->params as $paramName => $value) {18 if (! is_string($value)) {19 continue;20 }21 22 Cache::label($paramName, $value);23 }24 }25}
With our updated Antlers tag, we can now add arbitrary cache namespaces and labels from inside our templates:
1{{2 hybrid_cache:label label_one="value"3 label_two="value"4}}
In the next section we will to implement another custom Artisan command that will let us easily invalidate these custom cache namespaces and labels.
Now that we can specify arbitrary cache namespaces and labels, we will create another Artisan command to help invalidate our cache whenever we want.
In app/HybridCache/Commands/InvalidateLabel.php
:
1<?php 2 3namespace App\HybridCache\Commands; 4 5use App\HybridCache\Facades\HybridCache; 6use Illuminate\Console\Command; 7 8class InvalidateLabel extends Command 9{10 protected $signature = 'hybrid-cache:invalidate-label {namespace} {label?}';11 12 public function __invoke(): void13 {14 $namespace = $this->argument('namespace');15 $label = $this->argument('label');16 17 $this->info("Invalidating label: {$label}...");18 HybridCache::invalidateCacheLabel($namespace, $label);19 $this->info("Invalidating label: {$label}... done!");20 }21}
Like before, we will need to update our cache's service provider to make our command available to us.
In app/HybridCache/Providers/HybridCacheServiceProvider.php
:
1<?php 2 3namespace App\HybridCache\Providers; 4 5use App\HybridCache\Commands\InvalidateAll; 6use App\HybridCache\Commands\InvalidateLabel; 7use App\HybridCache\Facades\HybridCache; 8use App\HybridCache\Manager; 9use Illuminate\Cookie\Middleware\EncryptCookies;10use Illuminate\Support\ServiceProvider;11use Illuminate\View\View;12 13class HybridCacheServiceProvider extends ServiceProvider14{15 // ...16 17 public function boot()18 {19 // ...20 21 if ($this->app->runningInConsole()) {22 $this->commands(23 InvalidateAll::class,24 InvalidateLabel::class, 25 );26 }27 }28}
With our new Artisan command, we could now invalidate all cached pages that have the collection
namespace:
1php artisan hybrid-cache:invalidate-label collection
Alternatively, we could invalidate all cached pages that reference the articles
collection like so:
1php artisan hybrid-cache:invalidate-label collection articles
Get the PDF version on LeanPub
Grab the example code on GitHub
Proceed to Creating a Hybrid Cache System for Statamic: Part Six
∎
The following amazing people help support this site and my open source projects ♥️
If you're interesting in supporting my work and want to show up on this list, check out my GitHub Sponsors Profile.