Laravel Value Objects help you eliminate primitive obsession and clean up your domain logic. Learn how to implement them to write safer, more readable code.
I remember staring at a User model two years ago, trying to figure out why I had five different methods checking if an email address was valid or formatted correctly. The logic was scattered across controllers, jobs, and even a few random service classes. I was suffering from "primitive obsession," treating an email address like a simple string when it was actually a core part of my domain.
That’s when I started using Value Objects. If you’re tired of passing around raw strings or integers and wondering if they contain valid data, this guide is for you.
At its core, a Value Object is a small, immutable object that represents a simple entity whose equality is based on its value, not its identity. Think of a Money object or an EmailAddress. If you have two EmailAddress objects with the same string value, they are effectively the same thing.
In the context of Domain Driven Design, Value Objects allow you to group data and behavior together. Instead of asking a controller to validate a string, you ask the Value Object to instantiate itself. If the data is invalid, the object simply refuses to exist.
Let’s look at a concrete example. We'll build an EmailAddress Value Object.
PHPnamespace App\ValueObjects; use InvalidArgumentException; readonly class EmailAddress { public function __construct(public string $value) { if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException("Invalid email format: {$value}"); } } public function domain(): string { return substr(strrchr($this->value, "@"), 1); } }
By using the readonly keyword available since PHP 8.2, we ensure the object is immutable. Once it’s created, the email cannot change. If you need to "change" it, you create a new instance.
When you pass a raw string into a function, you have no guarantee what's inside that string. When you type-hint EmailAddress, you gain a contract. You know that if the code reaches your method, the email is already validated. You don't have to write the same regex or filter_var checks over and over again.
This approach pairs perfectly with Mastering Laravel DTOs: Type-Safe Data Handling for Clean Code, as both patterns aim to move logic out of your controllers and into structured objects.
A common question I get from junior engineers is: "How do I store this in the database?"
You have two main paths:
Here’s a quick look at how to implement a custom cast in Laravel 10+:
PHPnamespace App\Casts; use App\ValueObjects\EmailAddress; use Illuminate\Contracts\Database\Eloquent\CastsAttributes; class EmailAddressCast implements CastsAttributes { public function get($model, $key, $value, $attributes) { return new EmailAddress($value); } public function set($model, $key, $value, $attributes) { return $value instanceof EmailAddress ? $value->value : $value; } }
Now, in your User model, you simply add:
PHPprotected $casts = [ 'email' => EmailAddressCast::class, ];
Suddenly, $user->email isn't just a string anymore. It’s an instance of EmailAddress, and you can call $user->email->domain() anywhere in your application.
I won't lie—there’s a bit of overhead. You’ll have more files to manage. For a simple CRUD app, creating a separate class for a PhoneNumber or ZipCode might feel like overkill. We once tried to wrap every single primitive in a large legacy project, and it slowed down our development speed significantly for about two weeks while we adjusted.
Start small. Use Value Objects for data that carries specific behavior, like currency, coordinates, or status codes. If it’s just a label, a string is probably fine.
If you find yourself needing to configure these objects cleanly, you might want to look into Mastering Laravel Tap Helper for Cleaner Object Configuration to keep your instantiation logic readable.
Not exactly. A Data Transfer Object (DTO) is usually a structure used to move data between layers. A Value Object contains domain logic—it knows how to validate itself and perform operations based on its value.
No. That leads to "over-engineering." Reserve them for values that have constraints or behavior (e.g., EmailAddress, Money, Duration).
In PHP, the performance impact of creating these small objects is negligible for 99% of web applications. The gain in maintainability far outweighs the cost of instantiating a few extra objects per request.
The goal of writing Clean Code isn't to create the most abstract system possible. It's to make your code speak the language of your domain. When you see EmailAddress in a method signature, you know exactly what to expect.
I’m still refining how I handle complex Value Object collections—sometimes it gets tricky when you need to perform calculations on a list of objects—but I haven't looked back since moving away from primitive obsession. Give it a try in your next feature; your future self will thank you when you’re debugging that edge case two months from now.
Master Laravel DTOs to replace messy associative arrays with type-safe objects. Learn how to write cleaner, more maintainable code in your PHP applications.