Last active
June 5, 2020 02:42
-
-
Save danrichards/de22efa41b533d81fa1ec15f42cf64f5 to your computer and use it in GitHub Desktop.
Laravel Base Model with some simple caching mechanisms using attribute mutators.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App; | |
use App\Utils\Data; | |
use App\Utils\Str; | |
use Cache; | |
use Closure; | |
use DateTime; | |
use DB; | |
use Illuminate\Database\Eloquent\Model as BaseModel; | |
use Illuminate\Notifications\Notifiable; | |
/** | |
* Class Model | |
* | |
* Please discuss any changes to this Class with Dan before making them. | |
* | |
* @method static \Illuminate\Database\Eloquent\Builder whereMorphedBy(\Illuminate\Database\Eloquent\Model|string $model, string $morph = null) | |
* @method static \Illuminate\Database\Eloquent\Builder whereMorph(\Illuminate\Database\Eloquent\Model $model, string $morph = null) | |
* @method static \Illuminate\Database\Eloquent\Builder whereMorphNull(string $morph = null) | |
* @method static \Illuminate\Database\Eloquent\Builder whereMorphNotNull(string $morph = null) | |
* @method static \Illuminate\Database\Eloquent\Builder joinExists(string $table) | |
*/ | |
abstract class Model extends BaseModel | |
{ | |
use LogsExceptions, | |
ActionTracking, | |
NotifiableSupplement, | |
Notifiable { | |
notify as parent_notify; | |
} | |
/** | |
* Laravel's getMutatedAttributes() method will not fetch mutators that are | |
* within a trait. We need to preserve mutators (for fields) saved to the | |
* attributes property. | |
* | |
* @see \App\Utils\Data::withMutatedAttributes() | |
* @var array $trait_mutators | |
*/ | |
protected static $trait_mutators_with_fields = [ | |
'value' | |
]; | |
/** | |
* 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 | |
* @param null $id | |
*/ | |
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); | |
} | |
/** | |
* Set an individual model attribute. No checking is done. | |
* | |
* @param string $key | |
* @param mixed $value | |
* @param bool $sync | |
* @return $this | |
*/ | |
public function setRawAttribute($key, $value, $sync = false) | |
{ | |
$this->attributes[$key] = $value; | |
if ($sync) { | |
$this->syncOriginal(); | |
} | |
return $this; | |
} | |
/** | |
* @param $query | |
* @param Model $model | |
* @param null $related_field | |
* @return \Illuminate\Database\Eloquent\Builder | |
*/ | |
public function scopeWhereModel($query, Model $model, $related_field = null) | |
{ | |
$model_class = get_class($model); | |
$model_id = $model->getKey(); | |
if (empty($related_field)) { | |
$exploded = explode('\\', $model_class); | |
$related_field = array_pop($exploded); | |
$related_field = strtolower(Str::snake(Str::singular($related_field))); | |
} | |
return $query->where("{$related_field}_id", $model_id); | |
} | |
/** | |
* @param $query | |
* @param Model|string $model_or_class | |
* @param null $morph | |
* @return \Illuminate\Database\Eloquent\Builder | |
*/ | |
public function scopeWhereMorphedBy($query, $model_or_class, $morph = null) | |
{ | |
$model_class = is_object($model_or_class) | |
? get_class($model_or_class) | |
: $model_or_class; | |
if (empty($morph)) { | |
$exploded = explode('\\', $model_class); | |
$morph = array_pop($exploded); | |
$morph = strtolower(Str::snake(Str::singular($morph))); | |
} | |
return $query->where("{$morph}_type", Str::rawClass($model_class)); | |
} | |
/** | |
* @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)) { | |
$exploded = explode('\\', $model_class); | |
$morph = array_pop($exploded); | |
$morph = strtolower(Str::snake(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"); | |
} | |
/** | |
* @param null $morph | |
* @return array | |
*/ | |
public function compact($morph = null) | |
{ | |
if (is_null($morph)) { | |
$model_class = get_class($this); | |
$exploded = explode('\\', $model_class); | |
$morph = array_pop($exploded); | |
$morph = strtolower(Str::snake(Str::singular($morph))); | |
} | |
return [ | |
"{$morph}_type" => get_class($this), | |
"{$morph}_id" => $this->getKey() | |
]; | |
} | |
/** | |
* @param $instance | |
* @return void | |
*/ | |
public function notify($instance) | |
{ | |
if ( config(sprintf("notifications.%s.enabled", get_class($instance))) ) { | |
$this->parent_notify($instance); | |
} | |
} | |
/** | |
* @param \Illuminate\Database\Eloquent\Builder $query | |
* @param static|\Illuminate\Database\Eloquent\Model|string $table | |
* @return bool | |
*/ | |
public function scopeJoinExists($query, $table) | |
{ | |
if (is_object($table)) { | |
$table = get_class($table); | |
} elseif (class_exists($table)) { | |
$model = new $table; | |
/** @var static $table */ | |
$table = $model->getTable(); | |
} | |
$joins = $query->getQuery()->joins ?: []; | |
foreach ($joins as $j) { | |
if ($j->table == $table) { | |
return true; | |
} | |
} | |
return false; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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