-
Notifications
You must be signed in to change notification settings - Fork 28
[Proposal] Implement custom & nested casting for Eloquent Model (WIP) #1354
Description
I had some cases when i need to cast some attributes to a custom type or even with nested casts.
So, i've been thinkering with an easy and useable way to acheive this.
THE ZONDA IDEA
This is the feature we love to have in our daily life:
<?php namespace App\Models;
use App\Models\Casts\SettingProperties;
use Illuminate\Database\Eloquent\Model;
/**
* @property-read \App\Models\Casts\SettingProperties properties
*/
class Setting extends Model
{
protected $fillable = ['properties'];
protected $casts = [
'properties' => SettingProperties::class
];
}And i have the properties column in the settings table with the following value.
{
"has_zonda": 1,
"acquired_at": {
"date": "2011-06-09 12:00:00.000000",
"timezone": "UTC"
}
}So we can access them like the following example:
$setting->properties->has_zonda;
$setting->properties['has_zonda'];
$setting->properties->acquired_at;
$setting->properties['acquired_at'];And if you're asking what it looks like the SettingProperties ?
<?php namespace App\Models\Casts;
/**
* @property-read boolean has_zonda
* @property-read \Illuminate\Support\Carbon acquired_at
*/
class SettingProperties extends AttributeCaster
{
protected $casts = [
'has_zonda' => 'boolean',
'acquired_at' => Carbon::class,
];
protected $selfCast = 'fluent';
}The Carbon class is also an attribute caster, but it will allows you to handle the custom casts:
<?php namespace App\Models\Casts;
use Illuminate\Support\Carbon as IlluminateCarbon;
class Carbon extends AttributeCaster
{
public function handle($value)
{
return new IlluminateCarbon($value['date'], $value['timezone']);
}
}About the AttributeCaster class, it's still WIP:
<?php namespace App\Models\Casts;
use Illuminate\Support\Fluent;
/**
* We can refactor this to a new Trait or something similar.
* Something like `Illuminate\Database\Eloquent\Concerns\CastAttributes` and use it inside HasAttributes
*/
abstract class AttributeCaster
{
protected $casts = [];
protected $selfCast = null; // or 'fluent', 'collection
public function handle($value)
{
$value = (new static)->castAttributes($value);
return $this->castSelfAttribute($value);
}
protected function castAttributes($values)
{
$data = is_array($values) ? $values : json_decode($values, true);
if ( ! empty($this->casts)) {
foreach ($this->casts as $key => $type) {
$data[$key] = $this->castAttribute($type, $data[$key]);
}
}
return $data;
}
protected function castAttribute($type, $value)
{
if (is_null($value))
return $value;
if (
array_key_exists($type, $this->casts) &&
is_a($type, self::class, true) // Replace it with an interface ?!
) {
return (new $type)->handle($value);
}
switch ($type) {
// Same casts as default (integer, bool, json ...)
case 'boolean':
case 'bool':
return boolval($value);
// ...
default:
return $value;
}
}
protected function castSelfAttribute($value)
{
switch ($this->selfCast) {
case 'fluent':
return new Fluent($value);
case 'collection':
return collect($value);
default:
return $value;
}
}
}To Reproduce
This is the full Setting model class with overridden default cast attribute:
<?php namespace App\Models;
use App\Models\Casts\AttributeCaster;
use App\Models\Casts\SettingProperties;
use Illuminate\Database\Eloquent\Model;
/**
* @property-read \App\Models\Casts\SettingProperties properties
*/
class Setting extends Model
{
protected $fillable = ['properties'];
protected $casts = [
'properties' => SettingProperties::class
];
/**
* Overridden method
*/
public function castAttribute($key, $value)
{
if (is_null($value)) {
return $value;
}
if (
array_key_exists($key, $this->casts) &&
is_a($this->casts[$key], AttributeCaster::class, true)
) {
return (new $this->casts[$key])->handle($value);
}
return parent::castAttribute($key, $value);
}
}And create/copy the other classes with their respective directories/namespaces:
App\Models\Casts\AttributeCasterApp\Models\Casts\SettingPropertiesApp\Models\Casts\Carbon
And finally in your web.php routes:
Route::get('test', function () {
$setting = new \App\Models\Setting([
'properties' => '{"has_zonda": 1, "acquired_at": {"date": "2011-06-09 12:00:00.000000", "timezone": "UTC"}}'
]);
dd(
$setting->toArray(),
$setting->properties->has_zonda,
$setting->properties['has_zonda'],
$setting->properties->acquired_at,
$setting->properties['acquired_at']
);
});Related: