PHP Architect logo

Want to check out an issue? Sign up to receive a special offer.

Working With Value Objects in Laravel 12.x

Posted by on June 2, 2025

If you’re like me, you’re always on the lookout for tools and techniques that can reduce the chances that your code has bugs and make it easier to maintain. Value objects are one of my go-to techniques for making my code easier to maintain, and you can use some of the tools built into Laravel to make working with them a breeze.

In this article, we’ll discuss how to use value objects in Laravel 12.x.

What Are Value Objects?

At a very high level, value objects are used to represent values that have no conceptual identity as objects within our application domain. A value object wraps data and is distinguishable only by its properties, so two value objects with the same properties are considered equal.

new EmailAddress("scott@phparch.com") == new EmailAddress("scott@phparch.com")

Examples of potential value objects include:

  • email addresses
  • phone numbers
  • addresses

Value objects are one of the architectural techniques discussed as part of Domain-Driven Design (DDD). DDD is a software design approach to designing complex application domains based on input from domain experts. That being said, you can use value objects anywhere, even without using the rest of DDD.

When designing value objects, there are three main characteristics they must adhere to.

  1. Structural Equality
  2. Immutability
  3. Self-Validation

We’ve discussed these characteristics at great length in our Value Objects Video/Article so check that out if you have questions but today we’re going to focus on the “Self-Validation” characteristic because it’s what we’ll use to prevent “bad” data from being inserted into our database from our code and pulled out of our database into our code.

Our Email Value Object

Let’s say you need to track a user’s email address and want to use value objects to do so. To do this, you’re going to define the following EmailAddress value object. This example is extremely basic but it’s important to remember that value objects can be just this simple. The important piece is that the EmailAddress class is validating that the string that’s passed to the constructor is a valid email address using the built-in PHP logic.

namespace App\Type;

class EmailAddress
{
    public function __construct(public readonly string $email) {
        if (!filter_var($this->email, FILTER_VALIDATE_EMAIL)) {
            throw new ValueError("{$this->email} is an invalid email address");
        }
    }
}

If you attempt to create a new EmailAddress that has an improperly formatted email string, it will throw an exception:

// Fatal error: Uncaught ValueError: scottphparch.com is an invalid email address 
$email = new EmailAddress("scottphparch.com");

Now you’re set up to make sure you’re always working with valid email addresses, and you don’t need a ton of boilerplate code to see if it’s valid. Now you just need to wire it up in Laravel.

Introducing Custom Casts

If you’ve worked with Laravel long enough, you might know that Laravel provides a way to provide a $casts array (Laravel < 11.x) or a casts() function (Laravel >= 11) in your Eloquent models to define how Eloquent will load the data out of the database.

protected function casts(): array
{
    return [
        "email_verified_at" => "datetime",
    ]
}

Laravel provides a bunch of built-in types like “datetime” and “array,” but you can also create custom casts to work with your data.

This is done by creating a class that will “cast” the data from the database into the form you want and then convert it back into a database-acceptable format.

We’ll use the make:cast command to create the custom cast.

% sail artisan make:cast EmailAddressCast

   INFO  Cast [app/Casts/EmailAddresslCast.php] created successfully. 

This will create a new class like the following:

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;

class EmailAddressCast implements CastsAttributes
{
    /**
     * Cast the given value.
     *
     * @param  array<string, mixed>  $attributes
     */
    public function get(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        return $value;
    }

    /**
     * Prepare the given value for storage.
     *
     * @param  array<string, mixed>  $attributes
     */
    public function set(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        return $value;
    }
}

This class has two functions the get() function which casts a value from the database to the class we want to work with and the set() function which converts the class back to the scaler value for the database.

Because you have already created a class to handle validating the email addresses you can create this very basic implementation (with the boilerplate stripped out):

class EmailAddressCast implements CastsAttributes
{
    public function get(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        return new EmailAddress($value);
    }

    public function set(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        return $value->email;
    }
}

Then you just need to instruct the model to use the cast:

// inside app/Models/User.php
protected function casts(): array
{
    return [
        "email" => \App\Casts\EmailAddressCast::class,
    ]
}

Now you can load up a User in Tinker and it will have loaded the email into an instance of our EmailAddress class.


\App\Models\User::findOrFail(5)->email
= App\Type\EmailAddress {#6811
    +email: "scott@phparch.com",
  }

As a word of warning, Eloquent doesn’t prevent us from setting the email property to any value but it might cause warnings that are ignored.


$user = \App\Models\User::findOrFail(5);
// WARNING: Attempt to read property "email" on string in app/Casts/EmailAddressCast.php on line 28.
$user->email = "scott";

To fix this you’ll want to do a “sanity check” on the data as it’s passed into the `set()` function before you send the property.

public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
if (!is_object($value) || get_class($value) != EmailAddress::class) {
throw new \TypeError("{$key} must be of type " . EmailAddress::class);
}
return $value->email;

}

Then an exception will be thrown.


// TypeError email must be of type App\Type\EmailAddress.
$user->email = "scott";

What You Need to Know

  1. Value objects help us reduce bugs by forcing the data to be valid
  2. We can use value objects with Laravel by creating custom casts
  3. Remember to check the value being set

Tags: