Using Custom Eloquent Casts in Laravel
How custom eloquent casts work
Any object implementing the new CastsAttributes
contract provided by Laravel can now be used in the $casts
property on your model. When accessing properties on your model, Eloquent will first check if there's a custom cast to transform the value with before returning the value to you.
Keep in mind that your cast will be called on every single get and set operation on the model, so consider caching intensive operations.
Creating Custom Casts
A popular feature suggestion for Laravel has been allowing selective encryption of model properties, and with custom casts this is super simple to implement.
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class EncryptCast implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return decrypt($value);
}
public function set($model, $key, $value, $attributes)
{
return [$key => encrypt($value)];
}
}
In your model, you can then assign a property to the cast we just created.
class TestModel extends Model
{
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'secret' => EncryptCast::class,
];
}
Now that we've set it up, let's test it out! Because the encrypt
/decrypt
functions serialize input, you can store pretty much anything (but you should probably stick to the simple built-in types).
$model = new TestModel();
$model->secret = 'Hello World'; // Plain text value
// Encrypted value (which will be saved to the database)
// Raw Value: eyJpdiI685InV4Q25ZN0FZUW5YSEZkRCtZSGlVXC9BPT0iLCJ2Y...
$model->getAttributes()['secret'];
$model->save(); // Save & reload the model from the DB
$model->fresh();
echo $model->secret; // Hello World
Because custom casts are just objects, they can be as simple or complex as required. There's a couple of additional features in the implementation, including "Inbound" casts (that only cast set values) and the ability to accept config from the deceleration in the model's $casts
.
For example, we could limit strings to a configurable length, but only when setting values (therefore any existing values would remain the same length).
class LimitCaster implements CastsInboundAttributes
{
public function __construct($length = 25)
{
$this->length = $length;
}
public function set($model, $key, $value, $attributes)
{
return [$key => Str::limit((string) $value, $this->length)];
}
}
In the model, separate the class name of the cast and the parameters with a colon.
class TestModel extends Model
{
protected $casts = [
'limited_str' => LimitCaster::class . ':10', // Limit to 10 characters
];
}
Value Object Casting
You are not limited to casting values to primitive types. You may also cast values to objects. Defining custom casts that cast values to objects is very similar to casting to primitive types; however, the set
method should return an array of key / value pairs that will be used to set raw, storable values on the model.
As an example, we will define a custom cast class that casts multiple model values into a single Address
value object. We will assume the Address
value has two public properties: lineOne
and lineTwo
:
<?php
namespace App\Casts;
use App\Models\Address as AddressModel;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use InvalidArgumentException;
class Address implements CastsAttributes
{
/**
* Cast the given value.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return \App\Models\Address
*/
public function get($model, $key, $value, $attributes)
{
return new AddressModel(
$attributes['address_line_one'],
$attributes['address_line_two']
);
}
/**
* Prepare the given value for storage.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param \App\Models\Address $value
* @param array $attributes
* @return array
*/
public function set($model, $key, $value, $attributes)
{
if (! $value instanceof AddressModel) {
throw new InvalidArgumentException('The given value is not an Address instance.');
}
return [
'address_line_one' => $value->lineOne,
'address_line_two' => $value->lineTwo,
];
}
}
When casting to value objects, any changes made to the value object will automatically be synced back to the model before the model is saved:
$user = App\Models\User::find(1);
$user->address->lineOne = 'Updated Address Value';
$user->save();
Got any questions? Feel free to comment below and I'll do my best to answer them!