Paulund

Laravel Custom Casts

One of the new features in Laravel 7 is the creation of custom eloquent casts.

Previously Laravel has come with a set of basic casts such as - integer, real, float, double, decimal:, string, boolean, object, array, collection, date, datetime, and timestamp.

/**
 * The attributes that should be cast.
 *
 * @var array
 */
 protected $casts = [
     'is_admin' => 'boolean',
 ];

These provide the majority of use cases for allowing you to change the value of the attribute on the way into the database and on the way out.

Alternatively, if you want to change the value of the attributes you can use mutator getter and setter methods. When eloquent attempts to fill the model it will use these mutator attributes to change the value of the attributes on the way in and out of the database.

/**
 * Get the user's first name.
 *
 * @param  string  $value
 * @return void
 */
public function getFirstNameAttribute()
{
    return $this->attributes['first_name'];
}

/**
 * Set the user's first name.
 *
 * @param  string  $value
 * @return void
 */
public function setFirstNameAttribute($value)
{
    $this->attributes['first_name'] = strtolower($value);
}

The downside of this approach is that it populates the model class which can grow larger and harder to manage. It also makes it harder to reuse any of the logic in these mutators. For example if you have an address column on multiple models and want to share the mutation across these multiple models there is no Laravel native way of doing this.

Custom Casts

This new feature allows you to abstract these mutations into a cast class which gives you ability to reuse these casts on multiple columns and models in your application.

To create a custom cast you must create a new class which implements Illuminate\Contracts\Database\Eloquent\CastsAttributes and use this in the $cast property.

/**
 * The attributes that should be cast.
 *
 * @var array
 */
 protected $casts = [
     'is_admin' => IsAdmin::class,
 ];

Eloquent will check if there's a custom cast and change the value using this when storing and getting the values from the database.

To demo this let's create a new custom cast that will save the date time as a UTC timezone rather than the application timezone and get the values from the database in the application timezone.

First create a new class that implements the CastsAttributes.

<?php

namespace CustomCast;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class UtcDateTime implements CastsAttributes
{
    /**
     * @param \Illuminate\Database\Eloquent\Model $model
     * @param string $key
     * @param mixed $value
     * @param array $attributes
     *
     * @return Carbon|mixed
     */
    public function get($model, string $key, $value, array $attributes)
    {

    }

    /**
     * @param \Illuminate\Database\Eloquent\Model $model
     * @param string $key
     * @param mixed $value
     * @param array $attributes
     *
     * @return array|Carbon|string
     */
    public function set($model, string $key, $value, array $attributes)
    {

    }
}

The CastsAttributes requires two methods get and set.

Set is used when saving the model in the database we can take the date value and convert it to UTC.

return $value->copy()->setTimezone('UTC');

The Get method is used when retrieving the value from the database this is when we need to convert the date to the application timezone.

return $value->copy()->setTimezone(config('app.timezone'));

Full Custom Cast

<?php

namespace CustomCast;

use Carbon\Carbon;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class UtcDateTime implements CastsAttributes
{
    /**
     * @param \Illuminate\Database\Eloquent\Model $model
     * @param string $key
     * @param mixed $value
     * @param array $attributes
     *
     * @return Carbon|mixed
     */
    public function get($model, string $key, $value, array $attributes)
    {
        if (is_string($value)) {
            return Carbon::parse($value)->setTimezone(config('app.timezone'));
        }

        return $value->copy()->setTimezone(config('app.timezone'));
    }

    /**
     * @param \Illuminate\Database\Eloquent\Model $model
     * @param string $key
     * @param mixed $value
     * @param array $attributes
     *
     * @return array|Carbon|string
     */
    public function set($model, string $key, $value, array $attributes)
    {
        if (is_string($value)) {
            return Carbon::parse($value)->setTimezone('UTC');
        }
        
        return $value->copy()->setTimezone('UTC');
    }
}