php - Using $casts for array of objects

I've got a following class:

class Account extends Model
{
    protected $casts = [
        'industries' => 'json',
    ];

    public function getIndustriesAttribute($value = null)
    {
        return collect(json_decode($value, true))->map(function ($industry) {
            return new Industry($industry);
        });
    }
}

with industries being a json field in the database.

The above code does what I'm aiming to do - if given field is accessed, the value from given field will be json_decoded and then transformed into array of Industry. When this model is saved, industries will be saved as json per $casts.

What I'd like to do is get rid of getIndustriesAttribute and make Laravel do a cast of my json to array of objects - ideally my code would look like this:

class Account extends Model
{
    protected $casts = [
        'industries' => Industry::class.'[]',
    ];
}

Of course this doesn't work, but it gives an idea what I'd like to happen - an array of objects should be json_encode / json_decoded (Industry is a plain object so it doesn't need to be an serialized).

As a workaround I've written this castable:

<?php

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

trait ArrayableCast
{
    public static function castUsing(array $arguments)
    {
        if (in_array('[]', $arguments)) {
            return new class implements CastsAttributes {
                public function get($model, $key, $value, $attributes)
                {
                    return collect(json_decode($value, true))->map(function ($item) {
                        return new (static::class)($item);
                    })->all();
                }
        
                public function set($model, $key, $value, $attributes)
                {
                    return json_encode(collect($value)->map(function ($item) {
                        return $item->toArray();
                    })->all());
                }
            };
        }

        return new class implements CastsAttributes {
            public function get($model, $key, $value, $attributes)
            {
                return new (static::class)(json_decode($value, true));
            }
    
            public function set($model, $key, $value, $attributes)
            {
                return json_encode($value->toArray());
            }
        };
    }
}

which can be used per

protected $casts = [
   'industries' => Industry::class.':[]',
];

but I'd appreciate if there was a native Laravel of handling this scenario.

Answer

Solution:

You could use my library, it has an example of how to cast to/from arrays with Laravel custom casts - https://github.com/morrislaptop/laravel-popo-caster#2-configure-your-eloquent-attribute-to-cast-to-it

Answer

Solution:

If you're adamant about casting your attribute this way, then you should check out Custom Casts.

Otherwise I would recommend that you check out the comment by @Bodhi regarding using a one-to-many relation.

Here is what Custom Casts might look like for you:

app/Casts/Industries.php

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class Industries implements CastsAttributes
{
    /**
     * Cast the given value.
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @param  string  $key
     * @param  mixed  $value
     * @param  array  $attributes
     * @return array
     */
    public function get($model, $key, $value, $attributes)
    {
        return collect(json_decode($value, true))->map(function ($industry) {
            return new Industry($industry);
        });
    }

    /**
     * Prepare the given value for storage.
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @param  string  $key
     * @param  array  $value
     * @param  array  $attributes
     * @return string
     */
    public function set($model, $key, $value, $attributes)
    {
        return json_encode($value);
    }
}

app/Models/Account.php

<?php

use App\Casts\Industries;

class Account extends Model
{
    protected $casts = [
        'industries' => Industries::class,
    ];
}

Source