PHP

Implementing Custom Eloquent Casts for Value Objects

Extend Laravel Eloquent's casting system by creating custom cast classes, allowing you to map complex database columns (like currency amounts or geographic coordinates) to PHP value objects.

<?php
// 1. Create a Value Object (e.g., app/ValueObjects/Money.php)
namespace App\ValueObjects;

use InvalidArgumentException;

class Money
{
    public function __construct(
        public float $amount,
        public string $currency = 'USD'
    ) {
        if ($amount < 0) {
            throw new InvalidArgumentException("Amount cannot be negative.");
        }
    }

    public function formatted(): string
    {
        return $this->currency . ' ' . number_format($this->amount, 2);
    }

    public function equals(Money $other): bool
    {
        return $this->amount === $other->amount && $this->currency === $other->currency;
    }
}

// 2. Create a Custom Cast Class (e.g., app/Casts/MoneyCast.php)
namespace App\Casts;

use App\ValueObjects\Money;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;

class MoneyCast implements CastsAttributes
{
    public function get(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        // Assuming the database stores the amount as a float/decimal
        // and currency might be in another column or fixed.
        // For simplicity, we'll assume 'price' column stores amount.
        // If currency is also dynamic, it would be passed via $attributes.
        return new Money((float) $value, $attributes['currency'] ?? 'USD');
    }

    public function set(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        if (!$value instanceof Money) {
            throw new InvalidArgumentException('The given value is not a Money instance.');
        }

        // When saving, store just the amount (float) in the 'price' column.
        // If currency was dynamic, you'd set that too.
        return $value->amount;
    }
}

// 3. Use the Custom Cast in your Eloquent Model (e.g., app/Models/Product.php)
namespace App\Models;

use App\Casts\MoneyCast; // Don't forget to import
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'price', 'currency'];

    protected $casts = [
        'price' => MoneyCast::class, // Apply the custom cast
    ];
}

// Example Usage in a controller or route
use App\Models\Product;
use App\ValueObjects\Money;

// Create a product
$product = Product::create([
    'name' => 'Fancy Gadget',
    'price' => 99.99, // Stored as float, cast to Money on retrieval
    'currency' => 'EUR',
]);

// Retrieve and interact with the Money object
$retrievedProduct = Product::find($product->id);
echo $retrievedProduct->price->formatted(); // Output: EUR 99.99

// Update the product's price using a Money object
$retrievedProduct->price = new Money(120.50, 'GBP');
$retrievedProduct->save(); // Mutator handles saving just the amount

$updatedProduct = Product::find($product->id);
echo $updatedProduct->price->formatted(); // Output: GBP 120.50 (if currency column was updated too)
How it works: Custom Eloquent casts allow you to define how a database column's raw value is converted to a custom PHP object (value object) when retrieved and how that object is serialized back into a database-compatible format when saved. This snippet demonstrates creating a `Money` value object and a `MoneyCast` class. The `get` method in the cast deserializes the database value into a `Money` object, while the `set` method serializes the `Money` object back into a float for storage. This pattern improves code maintainability and enforces data integrity by centralizing type conversion and validation logic.

Need help integrating this into your project?

Our team of expert developers can help you build your custom application from scratch.

Hire DigitalCodeLabs