Value Objects in Laravel 5 with Eloquent Done Right

Update April 25th, 2016: I don’t use Value Objects in Laravel anymore. Here’s why.

One idea of Value Objects is to restrict what a programmer can do within the model. For example, an EmailAddress value object cannot be set to an invalid email address.

The advantage is: Validation and consistency of the domain model.

With Laravel however, there are a few things to think about and it took me a while to figure out a good solution.

tl;dr:

  • Use a Mutator to enforce the type of a Value Object
  • Store your Value Object as native value in your Eloquent model
  • Use an Accessor to create a Value Object from the native value
  • Implement __toString() in your Value Object to allow toArray()
  • Use the JsonSerializable interface to allow toJson() conversion
  • Plus: Do all of this automatically without Mutators and Accessors

Laravel’s Eloquent Model Doesn’t Enforce Property Types

Even though you can set an attribute to be a Value Object like so:

$user = new User;
$user->email = new EmailAddress('hello@example.com');

You can still override it with whatever you want:

$user = new User;
$user->email = 'Banana King';

Not good, because a Value Object by definition is used to keep the model consistent.

Enforce Value Objects in Laravel with a Mutator

One way to enforce a type for a model property in Laravel is to use a Mutator and type-hint the required Value Object:

public function setEmailAttribute(EmailAddress $value) {
    $this->attributes['email'] = $value;
}

Now, you can only do this:

$user = new User([
    'email' => new EmailAddress('hello@example.com')
]);

or this:

$user = new User;
$user->email = new EmailAddress('hello@example.com');

But you cannot set email to anything else but an EmailAddress Value Object.

This works, but there’s still a problem.

Eloquent’s Model Stores Properties as an Array

Internally, Eloquent stores properties in an array called $attributes. They aren’t real properties of a class. This is because Eloquent sits right in between the database and the app. It’s not really an Entity in Domain-driven Design and doesn’t belong only to the Domain Model, but mostly a representation of a database table in PHP.

Laravel's Eloquent Model
Eloquent’s Model sits right in between the app and the database and doesn’t belong to the Domain Model only.

Eloquent’s Model sits right in between the app and the database and doesn’t belong to the Domain Model only.

If I want to create a true Entity, I’d enforce its own invariants by using properties that expect certain types, like so:

class User
{
    private $email;
    public function __construct(EmailAddress $email)
    {
        $this->email = $email;
    }
    // ...
}

Now, the question is: Do we store the Value Object itself in $attributes or do we store a native type like string?

Let’s take a look at what happens if we use the mutator above. At first, it looks like it’s working:

$user = new User;
$user->email = new EmailAddress('hello@example.com');
dd($user->email);

Result:

EmailAddress {#133 ▼
  -value: "hello@example.com"
}

But what happens if I retrieve a user from the database?

$user = User::first();
dd($user->email);

Result:

"hello@example.com"

Now, the attribute isn’t of type EmailAddress anymore.

My first thought was, alright, let’s make an Accessor then:

public function getEmailAttribute($value) {
    return new EmailAddress($value);
}

The problem is: This seems to work, but only if EmailAddress has a __toString() method. The Accessor casts $value to a string and creates a new Value Object. While this might be acceptable, we have an inconsistent model.

Value Object or Native Value?

email in $attributes now can either be a Value Object if it is created within our app, but it can also be a simple string, if Eloquent loads the data from the database.

This is because internally, Eloquent uses setRawAttributes() when fetching data from the DB. Mutators and Accessors are never touched.

This inconsistent internal state can cause side effects.

For example, json_encode() and toArray() yield different results when called on a User we just created or on a User pulled from the DB.

To make it short: $attributes must only store native PHP values to keep its internal state consistent.

The correct Mutator and Accessor in User.php would look like this:

public function setEmailAttribute(EmailAddress $value) {
    $this->attributes['email'] = (string)$value;
}

public function getEmailAttribute($value) {
    return new EmailAddress($value);
}

Arrays and JSON with __toString() and JsonSerializable

If you call $user->toArray(), Laravel will throw an error. Your Value Objects must implement the __toString() method.

With $user->toJson(), email will be empty:

"email":{}

To make your Value Objects convert to JSON correctly, you should use the JsonSerializable interface and implement jsonSerialize(). An EmailAddress Value Object could look like this:

final class EmailAddress implements JsonSerializable
{
    private $value;

    public function __construct($value)
    {
        $filteredValue = filter_var($value, FILTER_VALIDATE_EMAIL);
        if ($filteredValue === false) {
            throw new \InvalidArgumentException("Invalid argument $value: Not an email address.");
        }
        $this->value = $filteredValue;
    }

    public function getValue()
    {
        return $this->value;
    }

    public function __toString()
    {
        return (string)$this->value;
    }

    public function jsonSerialize() {
        return $this->__toString();
    }
}

Great, how do I automate this such that I don’t have to write hundreds of Mutators and Accessors?

Automatically Convert Properties to Value Objects in Laravel

As far as I know, PHP doesn’t allow to dynamically create methods in classes or objects. You’d use __call() instead. But Eloquent’s model uses method_exists() to determine whether Mutators and Accessors should be called or not.

So, there’s basically no way to dynamically create Mutators and Accessors on the fly.

I stumbled upon this thread on Laracasts and a potential solution. It’s a good start, but doesn’t work all the time:

  • It doesn’t enforce the type of my Value Object. I can still set email to whatever I want.
  • getAttribute() is never called in Eloquent’s toArray() and thus, arrays contain only native PHP types and not Value Objects. But an array should contain the Value Object – after all, I programmatically work with arrays and they’re not a string representation of an object (like JSON).

However, I like the use of a Trait. After browsing the Model.php code for a bit, it became apparent that overriding setAttribute and getAttribute isn’t the best way to do it, because the Eloquent model simply ignores these functions internally in some cases.

After fiddling around, I decided to extend the Attribute Casting functionality of Eloquent instead of getAttribute().

To enforce the type of my Value Objects, I still have to override setAttribute. I’m not entirely happy with that because it feels like this function might change in the future, but it works for now. The entire Trait looks like this:

trait CastsValueObjects
{
    protected function castAttribute($key, $value)
    {
        $castToClass = $this->getValueObjectCastType($key);
        // no Value Object? simply pass this up to the parent
        if (!$castToClass) {
            return parent::castAttribute($key, $value);
        }
        // otherwise create a Value Object
        return $castToClass::fromNative($value);
    }

    public function setAttribute($key, $value)
    {
        $castToClass = $this->getValueObjectCastType($key);
        if (!$castToClass) {
            return parent::setAttribute($key, $value);
        }

        // Enforce type defined in $casts
        if (! ($value instanceof $castToClass)) {
            throw new InvalidArgumentException("Attribute '$key' must be an instance of " . $castToClass);
        }

        // now it has the type, store as native value
        return parent::setAttribute($key, $value->getNativeValue());
    }

    private function getValueObjectCastType($key) {
        $casts = $this->getCasts();
        $castToClass = isset($casts[$key]) ? $casts[$key] : null;
        if (class_exists($castToClass)) {
            return $castToClass;
        }
        return null;
    }
}

It can be used like so:

class User extends Model {
    use CastsValueObjects;
    protected $casts = [
        'email' => EmailAddress::class
    ];
}

No need for Accessors and Mutators anymore.

Please note that my Value Objects implement a ValueObject interface which requires a few implementations. It is loosely based on this package: PHP Value Objects.

I don’t require that package itself, because I don’t need most of the code and the author states that it’s intended for educational purposes. Furthermore, since I also use PHP Enums, I had to implement my own interface:

interface ValueObject {
    public static function fromNative($value);
    public function sameValueAs(ValueObject $object);
    public function __toString();
    public function getNativeValue();
}

I put the entire code in a Gist on GitHub. Why a Gist and no package? Because I don’t want to introduce too many dependencies and you might want to implement your Value Objects in a different way.

There’s also a wrapper for PHP Enum, which I use for my Value Objects, too.

Update: I don’t use Value Objects in Laravel anymore. Here’s why.