Two Laravel devs that won't disappear on you. Finally! Hire Joel and Aaron from No Compromises.

Aegis for Laravel: Scaffolding and Validation Helpers for Value Objects

Published on by

Aegis for Laravel: Scaffolding and Validation Helpers for Value Objects image

I just released Aegis for Laravel on stage at Laravel Live Japan 2026. It's a package I built around a pattern I keep coming back to: Value Objects. They quietly remove a whole class of bugs that primitives cause, but writing them by hand gets tedious fast. Aegis writes the tedious part for you.

The Problem

A string isn't an email until something validates it. An int isn't money until something tags it with a currency. We pass these raw primitives around our apps and trust they're correct, then lose an afternoon to the one place they weren't.

A Value Object fixes that. Its constructor takes the input and either produces a valid instance or throws. Once you're holding an Email, it's a real email. Bad values never reach the rest of the system.

The catch is the boilerplate. A Value Object that does its job properly runs about 70 lines of PHP. A final readonly class. Validation in the constructor. Normalization. An equals() method. The Castable block with get, set, and compare so Eloquent can store it. Typing all of that for every string a team wants to harden costs more than the string was costing you. So people skip it, and the primitives stay.

Aegis writes those lines for you. One Artisan command produces the class with everything wired up, plus a test stub. You fill in the methods that belong to your domain.

Installation

composer require harrisrafto/laravel-aegis

The service provider registers itself through Laravel's package auto-discovery.

If you want to change the namespace for generated Value Objects, publish the config:

php artisan vendor:publish --tag=aegis-config

Scaffolding a Value Object

Here's the command for an Email:

php artisan make:value-object Email \
--rule=email \
--normalize=lower \
--method=domain:string \
--cast=Order.email

That generates three things.

First, app/Domain/ValueObjects/Email.php. It's final readonly, validated, and normalized. It implements Stringable and JsonSerializable so it behaves at the edges of your app, and it carries the Castable block for Eloquent. The domain(): string method you asked for is there as an empty stub, waiting for your logic.

Second, tests/Unit/EmailTest.php. If Pest is in your vendor/, Aegis writes a Pest it()->todo(). If it isn't, you get a PHPUnit markTestIncomplete() case instead. Either way the test exists and is asking to be filled in.

Third, it patches app/Models/Order.php, adding 'email' => Email::class to the model's casts() method. It preserves your existing indentation, and it's safe to run again. The cast gets added once.

Validating With the Same Value Object

The Value Object already knows what a valid email is. So you validate with it directly instead of repeating the rules in a FormRequest:

use Illuminate\Validation\Rule;
 
public function rules(): array
{
return [
'email' => ['required', Rule::valueObject(Email::class)],
];
}

Aegis registers valueObject as a macro on Illuminate\Validation\Rule. The call site reads like any other built-in rule, right next to Rule::in() or Rule::unique().

Resolving the Validated Instance

Once validation passes, you usually want the object, not the raw string. Add the ResolvesValueObjects trait to your FormRequest:

use HarrisRafto\Aegis\Concerns\ResolvesValueObjects;
 
class StoreUserRequest extends FormRequest
{
use ResolvesValueObjects;
 
public function rules(): array
{
return [
'email' => ['required', Rule::valueObject(Email::class)],
];
}
}

Then pull the instance out in your controller:

$email = $request->valueObject('email'); // an Email instance, already validated

The object that validated the input is the one you carry forward, so there's no second copy of the truth to keep in sync.

Finding Candidates in Your Codebase

If you're adding Value Objects to an app that already exists, the hard part is knowing where to start. Run:

php artisan vo:scan

Aegis walks your Eloquent models and migrations, flags column names that match common patterns (email, url, uuid, country_code, slug, ip, status, money), and prints the exact make:value-object command for each one:

app/Models/Customer.php
· billing_email → php artisan make:value-object Email --rule=email --normalize=lower --cast=Customer.billing_email
· country_code → php artisan make:value-object CountryCode --rule=regex:/^[A-Z]{2}$/ --normalize=upper --cast=Customer.country_code
· monthly_amount_cents candidate — Money column, see cknow/laravel-money
 
Scanned 3 models, 16 columns total.
7 commands ready, 2 candidates need your input, 1 already wrapped.
Value Object coverage: 6%.

It reads model $fillable, $casts, and casts() declarations, plus any Schema::create blocks in your migrations. It never touches your database. Pass --json for machine-readable output, --no-cast to drop the cast suggestion from each command, or --path and --migrations-path to point at non-standard directories.

That coverage figure at the bottom is the part I find myself watching. It turns "we should really use Value Objects more" into a number you can actually move.

The Flags

make:value-object takes a handful of flags:

Flag Purpose
--rule=NAME[:ARGS] Validation rule. One of email, url, ip, uuid, alpha_num, alpha, numeric, regex:PATTERN.
--normalize=FN[,FN] Normalization. Compose with commas: lower, upper, trim.
--type=PHP_TYPE Property type. Defaults to string. Also accepts int, float, bool, or a fully qualified class name.
--method=NAME[:RETURN_TYPE] Adds an empty method stub. Repeatable.
--cast=Model.column Wires the cast into app/Models/Model.php. Safe to re-run.
--namespace=NS Overrides the configured default namespace.
--no-test Skips the test stub.
--dry-run Prints the files that would be written or changed without touching disk.
--force Overwrites existing files.

Requirements

  • PHP 8.3+
  • Laravel 13

Links

About the Name

In Greek mythology the aegis was Athena's shield, the thing she carried into battle to deflect what shouldn't reach what it protected. That's exactly what the constructor of a Value Object does: it accepts what's valid and refuses everything else.

The pattern goes back to Eric Evans' Domain-Driven Design and Martin Fowler's Patterns of Enterprise Application Architecture. None of that is new. Aegis just takes care of the typing, so you'll actually reach for it the next time a raw string starts causing trouble.

Harris Raftopoulos photo

Senior Software Engineer • Staff & Educator @ Laravel News • Co-organizer @ Laravel Greece Meetup

Filed in:
Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

image
Tinkerwell

Enjoy coding and debugging in an editor designed for fast feedback and quick iterations. It's like a shell for your application – but with multi-line editing, code completion, and more.

Visit Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $9500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
SerpApi logo

SerpApi

Access real-time search engine results through a simple API—no more scraping headaches! Use it for AI applications, SEO tools, product research, travel information, and more

SerpApi
Harpoon: Next generation time tracking and invoicing logo

Harpoon: Next generation time tracking and invoicing

The next generation time-tracking and billing software that helps your agency plan and forecast a profitable future.

Harpoon: Next generation time tracking and invoicing
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
PhpStorm logo

PhpStorm

The go-to PHP IDE with extensive out-of-the-box support for Laravel and its ecosystem.

PhpStorm
Acquaint Softtech logo

Acquaint Softtech

Acquaint Softtech offers AI-ready Laravel developers who onboard in 48 hours at $3000/Month with no lengthy sales process and a 100 percent money-back guarantee.

Acquaint Softtech
Laravel Cloud logo

Laravel Cloud

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Cloud

The latest

View all →
Playa: Cookie-Based Temporary Players for Laravel image

Playa: Cookie-Based Temporary Players for Laravel

Read article
Scheduler Attributes and Listener Discovery Control in Laravel 13.12.0 image

Scheduler Attributes and Listener Discovery Control in Laravel 13.12.0

Read article
The PHP Foundation Launches an Ecosystem Security Team image

The PHP Foundation Launches an Ecosystem Security Team

Read article
Manage Subscription Plans and Entitlements in Laravel with Laravel Entitlements image

Manage Subscription Plans and Entitlements in Laravel with Laravel Entitlements

Read article
Laravel Fluent Validation: An Object-Oriented Rule Builder image

Laravel Fluent Validation: An Object-Oriented Rule Builder

Read article
Moat: A Security Review for Your GitHub Account image

Moat: A Security Review for Your GitHub Account

Read article