#laravel#clean-code#php#oop
Custom Eloquent Casts & Value Objects in Laravel
"Clean up Laravel Eloquent models with custom Casts and domain-driven Value Objects."
•By huud•
~2 min read0 views
Custom Eloquent Casts & Value Objects in Laravel
Storing complex structures (such as multi-currency amounts, JSON address payloads, or geo-coordinates) directly on Eloquent models often results in bloated models filled with repetitive accessors and mutators.
With Custom Casts, raw database values can be cast directly to immutable Value Objects.
1. Creating the Value Object
Create an immutable Value Object, for example for monetary values:
namespace App\ValueObjects;
use InvalidArgumentException;
readonly class Money
{
public function __construct(
public int $amountInCents,
public string $currency = 'USD'
) {
if ($this->amountInCents < 0) {
throw new InvalidArgumentException('Amount cannot be negative.');
}
}
public function formatted(): string
{
return '$' . number_format($this->amountInCents / 100, 2);
}
}
2. Implementing the Custom Cast
Implement Illuminate\Contracts\Database\Eloquent\CastsAttributes:
namespace App\Casts;
use App\ValueObjects\Money;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;
class MoneyCast implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): ?Money
{
if ($value === null) {
return null;
}
return new Money(
(int) $value,
$attributes['currency'] ?? 'USD'
);
}
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
if ($value === null) {
return null;
}
if (! $value instanceof Money) {
throw new InvalidArgumentException('The given value is not a Money instance.');
}
return $value->amountInCents;
}
}
3. Registering the Cast on the Model
Use Laravel 11's method-based casts() definition:
namespace App\Models;
use App\Casts\MoneyCast;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected function casts(): array
{
return [
'price' => MoneyCast::class,
];
}
}
4. Usage
// Persisting data
$product = new Product();
$product->name = 'Mechanical Keyboard';
$product->currency = 'USD';
$product->price = new Money(15000, 'USD'); // $150.00
$product->save();
// Retrieving data
$product = Product::find(1);
echo $product->price->formatted(); // "$150.00"
Key Benefits
- Type Safety: Enforces domain constraints rather than passing untyped primitives.
- Encapsulation: Domain calculations and formatting live inside the Value Object.
- Clean Models: Keeps Eloquent models concise and free of accessor clutter.
About the Author
huud
@huud
Systems architect and software engineer building high-performance distributed platforms.