I am writing a Trait for Laravel eloquent models that enables "promotion" of input elements during create and update. The base trait looks like this:
<?php
namespace AppTraits;
use IlluminateDatabaseEloquentBuilder;
trait PromotesInputsDuringCreation {
public static function doPromotion($attrs) {
if (isset(static::$promotions) && !empty(static::$promotions)){
foreach (static::$promotions as $target => $path) {
$pathParts = explode('.', $path);
$targetData = $attrs;
foreach ($pathParts as $part){
if (is_null($targetData)) {
break;
}
$targetData = is_object($targetData) ? $targetData->$part : $targetData[$part];
}
if(!is_null($targetData)) {
$attrs[$target] = $targetData;
}
}
}
return $attrs;
}
The models that implement the trait would define a $promotions
array that looks something like this:
public static $promotions = [
'loan_status_end_date' => 'loan_status.end_date',
'loan_status_type' => 'loan_status.type'
];
The goal of this trait is to accept inputs with nested arrays/objects full of data, and to selectively promote some of that data to new top-level keys. This enables easier sorting/searching at the database level once it is saved.
The issue I am having is that no solution I have tried "does it all"
First solution: The boot method
Problem: Boot method callbacks receive an already instantiated model which does not contain the raw data passed into ::create() / ::updateOrCreate() / similar methods. (Or, if it does, I am not aware how to access it. Please let me know if I am dumb)
Example Code:
public static function bootPromotesInputsDuringCreation()
{
static::creating(function ($model) {
//$model->getDirty() only gets attributes that exactly match data keys
$model->attributes = array_intersect_key(self::doPromotion($model->getDirty()), $model->attributes);
});
static::updating(function ($model) {
//$model->getDirty() only gets attributes that will change in the update
$model->attributes = array_intersect_key(self::doPromotion($model->getDirty()), $model->attributes);
});
}
Second solution: Overriding creation methods
Problem: The promotion works, but the following creation method skips model casting and other laravel niceties I rely on. All attempts to use static::create()
or parent::create()
with the promoted data create infinite loops.
Example Code:
public static function create($data)
{
return (new static)->newQuery()->create(self::doPromotion($data));
}
public static function updateOrCreate($where, $data)
{
$data = self::doPromotion($data);
if ((new static)->newQuery()->where($where)->exists()) {
// this skips $casts and other model events :(
return (new static)->newQuery()->where($where)->update($data);
}
return (new static)->newQuery()->create([...$where, ...$data]);
}
It really feels like I am inches away from making this trait work. Anyone see what I am missing? Could be as simple as finding a way to get the raw creation/update input inside the boot method or calling create/update in the overridden methods in a different way that invokes those laravel niceties.
Thanks in advance.
2
Answers
The solution I found was to override the
fill()
method found inIlluminateDatabaseEloquentModel
with an exact copy on my trait that simply adds the call todoPromotions()
before continuing its usual processing.Id like this to be cleaner and more resilient to changes in
IlluminateDatabaseEloquentModel
over time, but for now this works well. Please comment any improvements in readability or elegance. The code is below.I think using a global observer is better for this
than you can create this
PromotesInputsObserver
and implement yourcreating
andupdating
events. But sincegetDirty
is looking for dirty attributes and yourloan_status
is not a property of your model, it will not show up as a dirty attribute. Yourloan_status
is never being compared.also rather that defining
promotions
as a static property of the model and than checking if that property exists, it’s easier to define it as a methodpublic function promotions(): array
and implement it on your trait asreturn [];
. Than each model can overwrite this method with it’s own array but you don’t need to worry if that function exists on the model or not.