@cm sencond bug isn't possible to dp something like so:
public function setEmailAttribute(EmailAddress $value) {
$this->attributes['email'] = $value->email;
}
Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.
I tried to find useful infos about how to do model validation (or business logic validation) in Laravel and at some point, I stumbled upon Jeff's video about model validation, but he calls it Enforcement, Entities, and Eloquent.
While the video is a decent start, I found some bugs in the code that I want to share with you, together with a better solution.
The code is basically the same for Laravel 4 and 5.
I assume that you just finished writing the code in the video.
You cannot use the proposed accessor for object instances, because an EmailAddress gets wrapped in an EmailAddress. This isn't clear in the video, because Jeff never calls the code in this way. He does this:
Student::first()->email;
But what if you do this instead?
$student = Student::register(
'John Doe',
new EmailAddress('test@test.com')
);
echo $student->email;
The register() method creates a new student and passes an EmailAddress object for the email attribute. Now, if you access this attribute, it gets wrapped in another object. var_dump() shows the following:
object(app\EmailAddress)[150]
private 'email' =>
object(app\EmailAddress)[141]
private 'email' => string 'test@test.com' (length=13)
This is because we assumed that the accessor has to parse a plain value from the database to an actual value object. We didn't assume that we already have an object.
Remember, the video is about enforcing business logic and do model validation. However, you can simply set whatever email address you want, if you use the constructor:
$student = new Student;
$student->name = 'Bart Simpson';
$student->email = 'invalid';
$student->score = 300;
$student->save();
Selecting the rows in SQLite shows:
8|Bart Simpson|invalid|300
To enforce attributes, you have to use a mutator and type-hint the allowed value:
public function setEmailAttribute(EmailAddress $value) {
$this->attributes['email'] = $value;
}
Now, you can never set the attribute email to anything else but an EmailAddress object.
Unfortunately, the first bug remains. Using the accessor wraps an EmailAddress in an EmailAddress.
A simple first idea to solve this could be to change the accessor to return the value object, if it is already an object:
public function getEmailAttribute($email) {
if (is_object($email)) {
return $email;
}
return new EmailAddress($email);
}
Now, if we call this:
Student::first()->email;
we'll get the string value from the database wrapped in an EmailAddress object. If we use an object instance instead (i. e. $student->email), we already have an EmailAddress object and simply return it.
This works, but it doesn't seem to be very robust and a bit inconsequential.
A more robust approach is to work with value objects only when accessing them, but under the hood, a model holds plain/raw values only. Then, it doesn't matter if they come from the DB or if they're instantiated as new objects.
I made an abstract base class which generalizes some of these value objects and offers a raw() method to return the plain value:
abstract class ValueObject {
protected $value;
function __construct($value) {
$this->disallowInvalidValue($value);
$this->value = $value;
}
abstract protected function disallowInvalidValue($value);
public function raw() {
return $this->value;
}
public function __toString() {
return (string)$this->value;
}
}
The new EmailAddress class would be this:
class EmailAddress extends ValueObject {
protected function disallowInvalidValue($value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Email address is invalid.');
}
}
}
And the mutator in Student looks like this:
public function setEmailAttribute(EmailAddress $value) {
$this->attributes['email'] = $value->raw();
}
Now, we have consistent behavior and store attributes internally as plain values and not as value objects. EmailAdress won't be wrapped in another EmailAddress and we still enforce an EmailAddress object with its own rules through the mutator.
You could also implement the raw() method as a trait.
The beautiful thing is that it also works with the constructor and register(), because the mutator will convert the EmailAddress received through the constructor to the "raw" value.
One weird side effect is shown by Jeff in the video around 13:45. He modifies __toString() and suddenly, our integer score gets written as a string to the database.
But Score and EmailAddress how Jeff wrote them don't work without a __toString() method, because down the line, Laravel casts our value object attributes to strings to store them in the database.
Now, we don't have value objects as attributes, but merely "raw" values, which can easily be casted.
__toString() now is only called, if we print the value object received with an accessor, like so:
$student->score; // is converted to string internally when the Score value object is received through the accessor
This could be used to format the score when printed out, but still stores the unmodified score in the DB.
Please or to participate in this conversation.