Harris Raftopoulos (Senior Software Engineer, Ulobby / Staff Writer, Laravel News) co-organises the Laravel Greece meetup. He gave versions of this talk at Laracon India 2026 and Laravel Live Japan 2026, where he also released Laravel Aegis on stage. The talk is built around the "primitive obsession" anti-pattern and how value objects fix it.
Value objects - the core pattern
Primitive obsession is passing raw strings, integers, and other primitives around an application and trusting they are valid. A plain string is not an email. An integer is not money. When those assumptions are wrong, bugs surface far from where the bad data entered the system.
A value object solves this by making validation part of construction. The
constructor either produces a valid instance or throws. Once you hold an Email
object, you know it is a real email address. Invalid values cannot propagate
because they never become objects in the first place.
The secondary benefit is that related behaviour moves out of the model and into
the object itself. An Email value object can have a domain() method. A
Money object can have currency-aware arithmetic. This debloats the Eloquent
model and makes the domain logic findable.
A properly written value object is a final readonly class with validation in
the constructor, normalisation, an equals() method, and a Castable
implementation so Eloquent can store and retrieve it. Written by hand, this runs
to around 70 lines per class.
References:
- Aegis for Laravel - Laravel News
- Optimizing Eloquent: Accessor Caching and Value Objects - Harris on Medium
- harris21/laravel-value-objects-examples
Laravel Aegis
Laravel Aegis is Harris's package that scaffolds the boilerplate for a value object from a single Artisan command.
composer require harrisrafto/laravel-aegis
php artisan make:value-object Email \
--rule=email \
--normalize=lower \
--method=domain:string \
--cast=Order.email
This generates three things. First, app/Domain/ValueObjects/Email.php, a
final readonly class with validation, normalisation, Stringable,
JsonSerializable, the Eloquent Castable block, and a stub for the domain()
method. Second, a test stub in tests/Unit/EmailTest.php using Pest if it is
installed, otherwise PHPUnit. Third, it patches the model's casts() method
directly, adding 'email' => Email::class.
Validation in FormRequests
Laravel Aegis registers a valueObject macro on Rule so the value object
validates itself in a FormRequest:
use Illuminate\Validation\Rule;
public function rules(): array
{
return [
'email' => ['required', Rule::valueObject(Email::class)],
];
}
Add the ResolvesValueObjects trait to the FormRequest and pull the validated
instance straight out in the controller:
$email = $request->valueObject('email'); // Email instance, already validated
Scanning an existing codebase
php artisan vo:scan
Aegis walks Eloquent models and migrations, identifies columns matching common
patterns (email, url, uuid, country_code, slug, ip, status, money), and prints
the exact make:value-object command to run for each. It outputs a coverage
percentage showing what proportion of candidate columns are already wrapped.
Requirements
- PHP 8.3+
- Laravel 13
References:
filter_var
filter_var is a native PHP function for validating and sanitising common data
types without a library. It is likely referenced in the talk as the underlying
mechanism for validating emails, URLs, and IPs inside value objects.
Common validation filters:
filter_var($value, FILTER_VALIDATE_EMAIL); // email
filter_var($value, FILTER_VALIDATE_URL); // URL
filter_var($value, FILTER_VALIDATE_IP); // IP address (IPv4 or IPv6)
filter_var($value, FILTER_VALIDATE_INT); // integer, optionally with range
filter_var($value, FILTER_VALIDATE_BOOLEAN); // boolean
Sanitise before validating where needed:
$email = filter_var($raw, FILTER_SANITIZE_EMAIL);
$valid = filter_var($email, FILTER_VALIDATE_EMAIL);
IP validation supports flags to restrict to IPv4, IPv6, or exclude private/reserved ranges:
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE);
filter_var returns the filtered value on success or false on failure. It is
a reasonable building block for value object constructors where the input type
maps to one of the supported filters.
References:
Zed editor - bonus tooling
Zed is a code editor written in Rust, built for performance. It is noticeably faster than VS Code for large projects and has built-in multiplayer collaboration. Available on macOS, Linux, and Windows.
PHP and Laravel support comes via extensions:
- The official Laravel extension adds go-to-definition for views, routes, config keys, translations, env values, Blade components, Livewire references, middleware, and container bindings. It also includes autocomplete for model properties, query-chain methods, and Blade variables, plus diagnostics for missing views and invalid column references.
- Laravel Pint can be configured to run on save via the language server formatter settings.
- The community
zed-for-laravelproject provides a one-command installer bundling Pint formatting, 200+ snippets (including Pest, Filament, Inertia), and pre-configured Artisan tasks.
As of March 2026, Tinkerwell added Zed to its list of supported editors.
References:
- Community Laravel Extension for Zed - Laravel News
- Zed Laravel extension - zed.dev
- zed-for-laravel - GitHub
- Moving from PHPStorm to Zed for Laravel - Freek Van der Herten
Tinkerwell - bonus tooling
Tinkerwell is a standalone code runner for PHP and Laravel by Beyond Code. It is
php artisan tinker with a proper editor interface, autocompletion, inline
output, and remote connection support.
Key features:
- Run code locally within any Laravel project context without creating routes or test files
- Connect remotely to production via SSH, Docker, Laravel Vapor, or Laravel Cloud to run Eloquent queries and one-off scripts without deploying files
- Output-aware display - renders emails, tables, and object graphs depending on what the code returns
- AI code completion and Conversational Mode added in v5 (July 2025), plus an MCP server so Claude Code, Cursor, and similar tools can execute code through Tinkerwell
- Saved snippets and history
Current version as of the talk is v5, which supports Laravel 13 and PHP 8.3+.
References:
Notes from Laravel Live UK 2026.