Skip to content
This repository was archived by the owner on Jul 16, 2021. It is now read-only.
This repository was archived by the owner on Jul 16, 2021. It is now read-only.

[Proposal] Implement custom & nested casting for Eloquent Model (WIP) #1354

@arcanedev-maroc

Description

@arcanedev-maroc

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\AttributeCaster
  • App\Models\Casts\SettingProperties
  • App\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:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions