Laravel Redis Route Binding
I setup Laravel with Redis and made sure all my articles were cached, and still saw a lot of database traffic.
All the route bound parameters get magically injected - but of course behind the scenes these are looked up in the database.
Although these are simple queries that run pretty fast - a lof of them are repeated very frequently so can be efficiently cached.
How to add Cache Logic to the Lookup
The docs have a section on Customizing the Resolution Logic and this is the place to add caching login
This allows you to modify the model lookup - and add caching
Simple Route Binding
For models that have simple binding and can be looked up directly (without dependencies)
add the resolveRouteBinding method to the class
public function resolveRouteBinding($value, $field = null)
{
if ($field == 'code') {
// cache site lookup by code (for API use)
return Cache::tags([$value])->flexible($value, [6000, 12000], function () use ($value) {
return $this->where('code', $value)->firstOrFail();
});
} else {
// CMS uses ID - no caching
return parent::resolveRouteBinding($value, $field);
}
}
In my case I want to cache on the API which uses a site “code” in the route to identify which site teh request is for.
This is fairly high traffic and benefits from caching.
The CMS uses site ID in routes and I’m happy falling back to the default behaviour in this case as current data in teh CMS is more important than speed - and this use case is low traffic.
Scoped Route Binding
My CMS has a hierarchy of parameters : site > category > group > tag
Each is identified by a slug (a url segment) which is unique within it’s parent group but not globally unique.
For example each site can only have one “opinions” category - but this category name can exist in multiple sites.
I use scoped bindings in my routing to facilitate this.
For these models I need to add
public function resolveChildRouteBinding($childType, $value, $field)
{
$site_code = $this->code;
$site_id = $this->id;
if ($childType == 'category' && $field == 'slug') {
return Cache::tags([$site_id])->flexible("$site_code/categories/$value", [6000, 12000], function () use ($site_id, $value) {
return Category::where('site_id', $site_id)->where('slug', $value)->firstOrFail();
});
}
return parent::resolveChildRouteBinding($childType, $value, $field);
}
In the case the Category lookup goes in the Site model (because it is bound to the model)
Note that this causes the lookup to depend on site ID AND category slug.
public function resolveChildRouteBinding($childType, $value, $field)
{
$category = $this;
if ($childType == 'tgroup' && $field == 'slug') {
return Cache::tags([$category->site_id])->flexible("$category->site_id/categories/$category->id/tgroups/$value", [6000, 12000], function () use ($category, $value) {
return Group::where('category_id', $category->id)->where('slug', $value)->firstOrFail();
});
}
return parent::resolveChildRouteBinding($childType, $value, $field);
}
In my Category model I define the lookup for the child (Group)
Note that here I don’t need to specify the site ID because the category ID is globally unique.
In each case I am only caching for API use which uses slugs in the path, the CMS uses IDs and this lookup is not cached.
Conclusion
Now I can make page requests that don’t generate any database requests at all.
The API responses are very fast - which should make teh frontend experience great for users.
Caches get cleared when new articles are saved, or manually through an admin UI.