Jose Jimenez
Software Architect & Developer
> >

Define your own Laravel 7 Custom Eloquent Casts

Published in Laravel on May 11, 2020

As of version 7, we can now declare our own Eloquent cast types. Let's assume we have a database JSON column that stores a user's address, by using Eloquent cast types you can always ensure that column retrieves and stores your data in that strict format.

Let's assume we have a JSON column called "address_data" on your users table and we wanted to store the minimum amount of data, but in a way to standarize it for future use.

Let's start by making our casts class

php artisan make:cast AddressData

You will now have a file app/Casts/AddressData.php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class AddressData 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 $value;
    }

    /**
     * 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 $value;
    }
}

Let's start by adding a DEFAULT_DATA constant variable, I do this to more easily see the structure of my column, and to work more easily with it in the future.

public const DEFAULT_DATA = [
    'name'     => '',
    'address1' => '',
    'address2' => '',
    'city'     => '',
    'state'    => '',
    'country'  => '',
];

Next let's update our get() method, this will grab the data from the database, and merge it with our default data, allowing us return a standard format each and every time.

public function get($model, $key, $value, $attributes)
{
    if (!is_null($value)) {
        return array_merge(self::DEFAULT_DATA, json_decode($value, true));
    }
    return self::DEFAULT_DATA;
}

Lastly let's update our set() method, to easily store only the data defined in our DEFAULT data, we will use the array only helper, to store values defined by our default data, this will ensure only that data exists in our system.

// Import class
use Illuminate\Support\Arr;

// Set method
public function set($model, $key, $value, $attributes)
{
    return json_encode(Arr::only($value, array_keys(self::DEFAULT_DATA)));
}

Our complete file should look like this:

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Arr;

class AddressData implements CastsAttributes
{
    public const DEFAULT_DATA = [
        'name'     => '',
        'address1' => '',
        'address2' => '',
        'city'     => '',
        'state'    => '',
        'country'  => '',
    ];

    /**
     * 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)
    {
        if (!is_null($value)) {
            return array_merge(self::DEFAULT_DATA, json_decode($value, true));
        }
        return self::DEFAULT_DATA;
    }

    /**
     * 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(Arr::only($value, array_keys(self::DEFAULT_DATA)));
    }
}

We now update our User model to add our new casts class that we created.

// Import our Eloquent Casts AddressData Class

use App\Casts\AddressData;

// Update our casts variable

/**
 * The attributes that should be cast to native types.
 *
 * @var array
 */
protected $casts = [
    'address_data'        => AddressData::class,
];

You can now safely update and store only the data you need

$user = User::first();

$address = [
    'name' => 'Jane Doe',
    'address1' => '123 Fake St',
];

$user->address_data = $address;
$user->save();

dd($user->address_data);

// Will output

array:6 [
  "name" => "Jane Doe"
  "address1" => "123 Fake St"
  "address2" => ""
  "city" => ""
  "state" => ""
  "country" => ""
]

As you can see in the above example, we only stored 2 pieces of information, name and address1, however when we retrieved through ->address_data our casts class was able to easily format and return the data in the expected format that we needed, and only with the data that was filled in.