Skip to content

Instantly share code, notes, and snippets.

@danrichards
Last active June 5, 2020 02:42
Show Gist options
  • Save danrichards/de22efa41b533d81fa1ec15f42cf64f5 to your computer and use it in GitHub Desktop.
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 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;
}
}
<?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