Skip to content

Instantly share code, notes, and snippets.

@danrichards
Last active June 5, 2020 02:42
Show Gist options
  • Select an option

  • Save danrichards/de22efa41b533d81fa1ec15f42cf64f5 to your computer and use it in GitHub Desktop.

Select an option

Save danrichards/de22efa41b533d81fa1ec15f42cf64f5 to your computer and use it in GitHub Desktop.
Laravel Base Model with some simple caching mechanisms using attribute mutators.
<?php
namespace App;
use App\Utils\Data;
use App\Utils\Str;
use Cache;
use Closure;
use Illuminate\Database\Eloquent\Model as BaseModel;
/**
* Class Model
*
* Please discuss any changes to this Class with Dan before making them.
*/
abstract class Model extends BaseModel
{
/**
* If you want to specify the result of a Builder::get() as a property, you
* may do so by specifying a query property. All this does is magically
* call the a method or query scope on your call and hit it with get().
*
* @see \App\User::customers()
*
* e.g. User::find(1)->customers; // \Illuminate\Database\Eloquent\Collection
*
* Works with objects you can call get() on.
*
* e.g. instanceof:
*
* \Illuminate\Database\Query\Builder
*
* To add caching automatically, use the $cache_related property instead.
*
* @var array $query_props */
protected $query_props = [
// 'method_name', // Will not cache
// 'ex_2' => '' // Non-integer will not cache
// 'ex_3' => 30, // Cache for 30 minutes
// 'ex_4' => 0 // Cache forever
];
/**
* Same as query props but more explicit in that there is caching.
*
* e.g. instanceof:
*
* \Illuminate\Database\Query\Builder
* \Illuminate\Database\Query\HasMany
* \Illuminate\Database\Query\HasManyThrough
*
* IMPORTANT: YOU WILL RECEIVE A COLLECTION BACK
*
* Use $cache_attributes for:
*
* \Illuminate\Database\Query\BelongsTo
* \Illuminate\Database\Query\HasOne
*
* @var array
*/
protected $cache_related = [
// 'attr_name' // Cache forever
// 'ex_2' => 0 // Cache forever
// 'ex_3' => 30 // Cache for 30 minutes
];
/**
* Use Laravel's get attribute mutator to conveniently store cached data.
*
* Leverage for simple solutions for Models that do not have Repositories
* already, if you feel like you're doing something dirty, then that means
* you should create a new Repository for your model.
*
* @see https://laravel.com/docs/5.1/eloquent-mutators#accessors-and-mutators
* @var array $cache_attributes */
protected $cache_attributes = [
// 'attr_name' // Cache forever
// 'ex_2' => 0 // Cache forever
// 'ex_3' => 30 // Cache for 30 minutes
];
/**
* Simple attribute caching, for more robust caching, use a Repository
*
* @param string $attribute
* @param null $id
* @return string
*/
private function cacheAttributeKey($attribute, $id = null)
{
$id = $id ?: $this->getKey();
return implode('|', [get_class($this), $id, 'attribute', $attribute]);
}
/**
* Simple method caching, for more robust caching, use a Repository
*
* @param string $related
* @param null $id
* @return string
*/
private function cacheRelatedKey($related, $id = null)
{
$id = $id ?: $this->getKey();
return implode('|', [get_class($this), $id, 'related', $related]);
}
/**
* @param string $attribute
* @param int $minutes
* @param Closure $callback
* @return mixed
*/
private function cacheAttribute($attribute, $minutes, Closure $callback)
{
$dynamic_cache_time = $this->dynamicCacheTime($attribute);
return Cache::remember($this->cacheAttributeKey($attribute), $dynamic_cache_time ?: $minutes, $callback);
}
/**
* @param string $attribute
* @param int $minutes
* @param Closure $callback
* @return mixed
*/
private function cacheRelated($attribute, $minutes, Closure $callback)
{
$dynamic_cache_time = $this->dynamicCacheTime($attribute);
return Cache::remember($this->cacheRelatedKey($attribute), $dynamic_cache_time ?: $minutes, $callback);
}
/**
* @param string|array|null $attribute
*/
public function cacheAttributeBust($attribute = null, $id = null)
{
$attribute = is_null($attribute)
? Data::normalizeAssociative($this->cache_attributes, 0)
: $attribute;
$attribute = is_string($attribute)
? (array) $attribute
: $attribute;
foreach ($attribute as $a) {
Cache::forget($this->cacheAttributeKey($a, $id));
}
}
/**
* @param string|array|null $related
*/
public function cacheRelatedBust($related = null)
{
$related = is_null($related)
? Data::normalizeAssociative($this->cache_related, 0)
: $related;
$related = is_string($related)
? (array) $related
: $related;
foreach ($related as $r) {
Cache::forget($this->cacheRelatedKey($r));
}
}
/**
* @param string $parent_class The Model
* @param string|int $parent_id The Model's primary key value
* @param string $sub_key A classification / category
* @param string $key Cache key name
*/
public static function cacheBust($parent_class, $parent_id, $sub_key, $key)
{
Cache::forget(implode('|', func_get_args()));
}
/**
* In case we want to store a cache time in a config file. For the purposes
* of modifying cache times with our ENV file (not having to do a push)
*
* @param $attribute_or_related
* @return integer
*/
public function dynamicCacheTime($attribute_or_related)
{
$class = get_class($this);
return config("cache.dynamic_model_cache.{$class}.{$attribute_or_related}");
}
/**
* @param string $name
* @return mixed
*/
public function __get($name)
{
$query_props_normalized = Data::normalizeAssociative($this->query_props, 'no-cache');
$cache_attributes_normalized = Data::normalizeAssociative($this->cache_attributes, 0);
$cache_related_normalized = Data::normalizeAssociative($this->cache_related, 0);
/**
* Handle query props.
*/
if (in_array($name, array_keys($query_props_normalized))) {
return call_user_func([$this, $name])->get();
}
/**
* Handle any related models that are cached.
*/
if (in_array($name, array_keys($cache_related_normalized))) {
$minutes = (int) $cache_related_normalized[$name];
return $this->cacheRelated($name, $minutes, function() use($name) {
return call_user_func([$this, $name])->get();
});
}
/**
* Handle and get attribute mutators that are cached.
*/
if (in_array($name, array_keys($cache_attributes_normalized))) {
$minutes = (int) $cache_attributes_normalized[$name];
return $this->cacheAttribute($name, $minutes, function() use($name) {
return $this->getAttribute($name);
});
}
return parent::__get($name);
}
/**
* @param $query
* @param Model $model
* @param null $morph
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeWhereMorph($query, Model $model, $morph = null)
{
$model_class = get_class($model);
$model_id = $model->getKey();
if (! empty($morph)) {
$morph = array_pop(explode('\\', $model_class));
$morph = strtolower(Str::singular($morph));
}
return $query->where("{$morph}_type", Str::rawClass($model_class))
->where("{$morph}_id", $model_id);
}
/**
* @param $query
* @param null $morph
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeWhereMorphNull($query, $morph)
{
return $query->whereNull("{$morph}_type")
->whereNull("{$morph}_id");
}
/**
* @param $query
* @param null $morph
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeWhereMorphNotNull($query, $morph)
{
return $query->whereNotNull("{$morph}_type")
->whereNotNull("{$morph}_id");
}
}
<?php
namespace App\Providers;
use App\Model;
// use App\User;
// use App\Models\Listing;
// use App\Models\Commission;
use Illuminate\Support\ServiceProvider;
/**
* Class ModelCacheServiceProvider
*/
class ModelCacheServiceProvider extends ServiceProvider
{
/**
* Automatically clear caches for related data, cached on Parent models.
*
* @see Model::$cache_related
* @var array $uses_cache_related */
public static $uses_cache_related = [
// [
// 'model' => User::class,
// 'related' => Listing::class,
// 'foreign_key' => 'user_id',
// 'methods' => ['listings'],
// 'events' => ['saved', 'created', 'deleted']
// ]
];
/**
* Automatically clear caches for related data, cached on Parent models.
*
* @see Model::$cache_related
* @var array $uses_cache_attributes */
public static $uses_cache_attributes = [
// [
// 'model' => User::class,
// 'related' => Commission::class,
// 'foreign_key' => 'user_id',
// 'attributes' => ['total_owed'],
// 'events' => ['saved', 'created', 'deleted']
// ]
];
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
// For any usages of "uses cache related"
foreach (static::$uses_cache_related as $purge_event) {
// And for any of the specified model events
$events = $purge_event['events'];
foreach($events as $event) {
$model = $purge_event['model'];
$related = $purge_event['related'];
$foreign_key = $purge_event['foreign_key'];
$methods = $purge_event['methods'];
// Add a listener to the related model to purge the parent's key
call_user_func([$related, $event], function($r) use($model, $foreign_key, $methods) {
$parent_id = $r->{$foreign_key};
// To bust the related caches
foreach ($methods as $method) {
call_user_func_array([$model, 'cacheBust'], [$model, $parent_id, 'related', $method]);
}
});
}
}
// For any usages of "uses cache attributes"
foreach (static::$uses_cache_attributes as $purge_event) {
// And for any of the specified model events
$events = $purge_event['events'];
foreach($events as $event) {
$model = $purge_event['model'];
$related = $purge_event['related'];
$foreign_key = $purge_event['foreign_key'];
$attributes = $purge_event['attributes'];
// Add a listener to the related model to purge the parent's key
call_user_func([$related, $event], function($r) use($model, $foreign_key, $attributes) {
$parent_id = $r->{$foreign_key};
// To bust the related caches
foreach ($attributes as $attribute) {
call_user_func_array([$model, 'cacheBust'], [$model, $parent_id, 'attribute', $attribute]);
}
});
}
}
}
/**
* Register the application services.
*
* @return void
*/
public function register()
{
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment