Introduction
We all write code like this repeatedly:
// Converting amount from cents to currency — repeated everywhere
$price = $product->price / 100;
$formatted = number_format($price, 2) . ' USD';
This logic gets duplicated across dozens of files. Change the formatting logic once — update twenty places. Custom Casts solve this at the root by turning any attribute into a full PHP object with its own encapsulated logic.
01. What is a Custom Cast?
A Custom Cast in Laravel is a class that tells an Eloquent Model: "when you retrieve this attribute from the database, don't return it raw — transform it into a PHP object." And when saving, that object automatically converts back to the appropriate storage value.
This feature has existed since Laravel 8, yet many developers still aren't using it.
02. Practical Example: Money Value Object
We'll build a complete pricing system. Prices are stored in the database as integers (cents) but we want to work with them as objects.
Step 1: Create the Value Object
// app/ValueObjects/Money.php
namespace App\ValueObjects;
final readonly class Money
{
public function __construct(
public readonly int $amount, // stored in cents in the database
public readonly string $currency = 'EGP',
) {}
/** The value in base currency unit (for display) */
public function value(): float
{
return $this->amount / 100;
}
/** Formatted string ready for the UI */
public function formatted(): string
{
return number_format($this->value(), 2) . ' ' . $this->currency;
}
/** Add two amounts together */
public function add(Money $other): static
{
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException('Cannot add two different currencies.');
}
return new static($this->amount + $other->amount, $this->currency);
}
/** Apply a percentage discount */
public function discountBy(float $percentage): static
{
$discounted = (int) round($this->amount * (1 - $percentage / 100));
return new static($discounted, $this->currency);
}
/** Compare two amounts */
public function isGreaterThan(Money $other): bool
{
return $this->amount > $other->amount;
}
/** Create from a float value directly */
public static function fromFloat(float $value, string $currency = 'EGP'): static
{
return new static((int) round($value * 100), $currency);
}
}
Step 2: Create the Cast Class
php artisan make:cast MoneyCast
// 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 __construct(
private readonly string $currency = 'EGP'
) {}
/**
* Database → Money object
*/
public function get(Model $model, string $key, mixed $value, array $attributes): Money
{
return new Money(
amount: (int) $value,
currency: $this->currency,
);
}
/**
* Money object → Database (integer)
*/
public function set(Model $model, string $key, mixed $value, array $attributes): int
{
return $value instanceof Money
? $value->amount
: (int) $value;
}
}
Step 3: Attach to the Model
// app/Models/Product.php
namespace App\Models;
use App\Casts\MoneyCast;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $fillable = ['name', 'price', 'original_price'];
protected function casts(): array
{
return [
// Pass the currency as a parameter to the Cast
'price' => MoneyCast::class . ':EGP',
'original_price' => MoneyCast::class . ':EGP',
];
}
}
Step 4: Using It in Code
Before Custom Cast — repeated logic:
// ❌ Old approach — repeated and unsafe
$product = Product::find(1);
$price = $product->price / 100;
$formatted = number_format($price, 2) . ' EGP';
// Same thing repeated somewhere else in the codebase...
$discounted = $product->price * 0.9 / 100;
After Custom Cast — clean and smart code:
// ✅ New approach — clean and safe
$product = Product::find(1);
// $product->price is now a Money object, not an integer!
$product->price->formatted(); // "1,500.00 EGP"
$product->price->value(); // 1500.00
$product->price->discountBy(10)->formatted(); // "1,350.00 EGP"
// Add two amounts
$total = $product->price->add($product->original_price);
$total->formatted(); // "3,000.00 EGP"
// Comparison
if ($product->price->isGreaterThan(Money::fromFloat(1000))) {
// Product costs more than 1000
}
// Saving to the database works normally
$product->price = Money::fromFloat(2000.00);
$product->save(); // stores 200000 in the database automatically
03. Second Example: Coordinate Cast
Instead of storing latitude and longitude in two separate columns and calculating distances manually everywhere — create a single object that encapsulates location logic.
// app/ValueObjects/Coordinate.php
namespace App\ValueObjects;
final readonly class Coordinate
{
public function __construct(
public readonly float $latitude,
public readonly float $longitude,
) {}
/** Calculate distance in kilometers (Haversine Formula) */
public function distanceTo(Coordinate $other): float
{
$earthRadius = 6371;
$latDiff = deg2rad($other->latitude - $this->latitude);
$lonDiff = deg2rad($other->longitude - $this->longitude);
$a = sin($latDiff / 2) ** 2
+ cos(deg2rad($this->latitude))
* cos(deg2rad($other->latitude))
* sin($lonDiff / 2) ** 2;
return $earthRadius * 2 * atan2(sqrt($a), sqrt(1 - $a));
}
/** Convert to JSON for storage */
public function toJson(): string
{
return json_encode([
'lat' => $this->latitude,
'lng' => $this->longitude,
]);
}
public static function fromJson(string $json): static
{
$data = json_decode($json, true);
return new static($data['lat'], $data['lng']);
}
}
// app/Casts/CoordinateCast.php
namespace App\Casts;
use App\ValueObjects\Coordinate;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class CoordinateCast implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): Coordinate
{
return Coordinate::fromJson($value);
}
public function set(Model $model, string $key, mixed $value, array $attributes): string
{
return $value instanceof Coordinate
? $value->toJson()
: $value;
}
}
// app/Models/Branch.php
class Branch extends Model
{
protected function casts(): array
{
return [
'location' => CoordinateCast::class,
];
}
}
// Usage:
$branch = Branch::find(1);
$customer = new Coordinate(30.0444, 31.2357); // Cairo coordinates
$distance = $branch->location->distanceTo($customer);
// $distance = 2.4 km — no logic outside the object
04. Inbound Cast — Validation on Save Only
Sometimes you only need to process a value on the way in (saving), not on the way out (reading). For that, use an Inbound Cast.
// app/Casts/Uppercase.php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
use Illuminate\Database\Eloquent\Model;
class Uppercase implements CastsInboundAttributes
{
/** Runs on save only — converts the value to uppercase */
public function set(Model $model, string $key, mixed $value, array $attributes): string
{
return strtoupper($value);
}
}
// Usage in the Model:
protected function casts(): array
{
return [
'country_code' => Uppercase::class,
];
}
// Result:
$user->country_code = 'eg';
$user->save();
// 'EG' is stored in the database automatically
05. Testing Custom Casts
Since each Cast is an independent class, testing becomes fast and straightforward:
namespace Tests\Unit\ValueObjects;
use App\ValueObjects\Money;
use PHPUnit\Framework\TestCase;
class MoneyTest extends TestCase
{
public function test_formatted_output_is_correct(): void
{
$money = new Money(150000, 'EGP');
$this->assertEquals('1,500.00 EGP', $money->formatted());
}
public function test_discount_is_applied_correctly(): void
{
$money = new Money(100000); // 1000 units
$discounted = $money->discountBy(10);
$this->assertEquals(90000, $discounted->amount);
$this->assertEquals('900.00 EGP', $discounted->formatted());
}
public function test_adding_different_currencies_throws(): void
{
$this->expectException(\InvalidArgumentException::class);
(new Money(1000, 'EGP'))->add(new Money(1000, 'USD'));
}
public function test_from_float_converts_correctly(): void
{
$money = Money::fromFloat(99.99);
$this->assertEquals(9999, $money->amount);
}
}
06. Recommended Folder Structure
app/
├── Casts/
│ ├── MoneyCast.php
│ ├── CoordinateCast.php
│ └── Uppercase.php
├── ValueObjects/
│ ├── Money.php
│ └── Coordinate.php
└── Models/
├── Product.php ← uses MoneyCast
└── Branch.php ← uses CoordinateCast
Summary
Custom Casts combined with Value Objects are among the most powerful code design tools in Laravel. They eliminate duplicated transformation logic across your codebase entirely. Every Model attribute gains its own "brain" — it knows how to display itself, operate on itself, and validate itself — leading to code that is genuinely cleaner, safer, and a joy to test.