Laravel Eloquent and Value Objects, Mutators and Accessors

January 31, 2016

Value objects are small objects, like money, strings, dates or date ranges. Their equality is based on state, not on identity; two value objects are equal when they have the same value.

Here is an example of a value object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Price
{
private $amount;
public static function fromAmount($amount)
{
if(!is_int($amount)) {
throw new \InvalidArgumentException('Amount must be an integer');
}
$price = new static;
$price->amount = $amount;
return $price;
}
public function getAmount()
{
return $this->amount;
}
public function add(Price $other)
{
return Price::fromAmount($this->amount + $other->amount);
}
}

The value object encapsulates behavior and communicates intent.

Value objects should also be immutable. If you want a different value, create a new object. You can thus share value objects without worrying about one part of the application affecting the other. If you add one Price to another, you get a completely new object.

Eloquent

Using value objects together with Eloquent can be a bit tricky. There are several implementations that result in unwanted behavior: value objects getting wrapped in other value objects, rules not being enforced, and limitations being set on the use of value objects that consist of more than one field.

To work around this we must first understand how Eloquent behaves. You should be familiar with Eloquent’s accessors and mutators and be aware of the following:

Saving properties to the database

In order for a model’s attribute to be saved to the database it must be stored in the Eloquent model’s $attributes array. The setAttribute() method is responsible for saving attributes to this array.

There are many ways to create a new Eloquent model instance, but setAttribute() is eventually called in almost every case: The constructor, create(), fill(), firstOrCreate() — all of these methods ultimately call setAttribute() on each of your attributes.

setAttribute() is also “magically” called when you directly set the attribute on the model’s instance, e.g. $product->price = 1000, if the property hasn’t been explicitly defined on the model, or isn’t visible in the current scope.

To illustrate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace App;
use Illuminate\Database\Eloquent\Model;
class Product extends Model {
public $price;
}
// ---
// The price will be saved to the database:
$product = new App\Product(['price' => 5000]);
$product->save();
// The price will NOT be saved to the database:
$product = new App\Product;
$product->price = 1000; // Circumventing Eloquent's magic because $price is public
$product->save();

Accessors and mutators

The mutator setPriceAttribute() is called when attempting to set the value of the price attribute on the model: e.g. $model->price = 1000;.

The accessor getPriceAttribute() is called when attempting to retrieve the value of price: e.g. echo $model->price;.

However, the mutator/accessor will not be called if the property is explicitly defined on the model and is visible in the current scope. For example, if the property is defined as public, or if it’s protected or private and referenced within the class itself, the accessor/mutator will not be invoked.

Example

With this in mind let’s create an example Eloquent model.

We will still access the attributes directly, like $model->price, instead of using setters and getters. We will also add a named constructor (addProduct()) just for fun.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Product extends Illuminate\Database\Eloquent\Model
{
// Database fields:
// title, price_amount
protected $fillable = ['title', 'price'];
protected $casts = ['price_amount' => 'integer'];
public static function addProduct($title, Price $price)
{
$product = new static;
$product->price = $price;
$product->title = $title;
$product->save();
return $product;
}
public function setPriceAmountAttribute($price)
{
// Not allowed, throw exception
// We shouldn't set Price's internal attributes directly
}
public function setPriceAttribute(Price $price)
{
// Set the price attributes (there's only one in this example: price_amount)
$this->attributes['price_amount'] = $price->getAmount();
}
public function getPriceAttribute()
{
// price_amount is cast as an integer
return Price::fromAmount($this->price_amount);
}
}

We could also store the $price value object directly on our model.

1
protected $price;

If you make it public, any user of your class will be able to bypass the logic that maps the price fields to the database (only price_amount in this case). Doing $product->price = 'blah'; will not trigger the mutator.

However, making it protected makes $price invisible from the outside and forces Eloquent to call the mutator instead. But the mutator will not be called if you set the price within the class itself. In this case you will need to manually call the mutator setPriceAttribute() or create a setter method.

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Product extends Model
{
protected $fillable = ['title', 'price'];
protected $casts = ['price_amount' => 'integer'];
protected $price; // Our value object
public static function addProduct($title, Price $price)
{
$product = new static;
$product->setPrice($price);
$product->title = $title;
$product->save();
return $product;
}
public function setPriceAmountAttribute($price)
{
// Not allowed, throw exception
// We shouldn't set Price's internal attributes directly
}
public function setPriceAttribute(Price $price)
{
$this->setPrice($price);
}
public function setPrice(Price $price)
{
$this->attributes['price_amount'] = $price->getAmount();
$this->price = $price;
}
public function getPriceAttribute()
{
if(is_object($this->price)) {
return $this->price;
}
return Price::fromAmount($this->price_amount);
}
}

You can create additional enforcements if you’d like. Just remember that there are still Eloquent methods that let you bypass the value object (e.g hydrate()).

It’s not a pretty solution. When using Eloquent, your “model” is tied to the persistence layer and there’s no way around it. This is how the Active Record Pattern works. If you decide to use it you should either embrace it or consider something else.