- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Value Objects in PHP: Small Classes That Prevent Big Bugs
About Post
Here's a function signature that looks completely harmless:
public function chargeRent(int $amount, string $currency, string $email): void
Now count the ways to call it wrong. Pass the amount in riyals when it expects dirhams, and the tenant is charged a hundredth of their rent. Pass 'qar' instead of 'QAR'. Swap the currency and the email, which are both strings, and PHP won't blink. Pass an email with a trailing space from a copy-paste, and the receipt never arrives.
Every one of those is a real kind of bug, and the type system can't help because everything is an int or a string. Value objects fix this with almost embarrassingly small classes.
What a value object is
A value object is a small object defined entirely by its values, not by an identity. Two Money objects holding 500 QAR are the same thing, in the same way two identical banknotes are interchangeable. A Tenant, by contrast, is an entity: two tenants with the same name are still two different people.
Good value objects have three properties:
- Always valid. The constructor refuses bad data. If you're holding one, it's correct.
- Immutable. Once created, it never changes. Operations return a new object.
- Compared by value. Equality means "same values", not "same instance".
Modern PHP makes all three easy. Let's build the three I reach for most.
Money
final readonly class Money
{
public function __construct(
public int $amount, // minor units: dirhams, cents
public string $currency,
) {
if (! preg_match('/^[A-Z]{3}$/', $currency)) {
throw new InvalidArgumentException("Bad currency: $currency");
}
}
public function add(Money $other): self
{
if ($other->currency !== $this->currency) {
throw new LogicException('Cannot add different currencies.');
}
return new self($this->amount + $other->amount, $this->currency);
}
public function equals(Money $other): bool
{
return $this->amount === $other->amount
&& $this->currency === $other->currency;
}
}
A few decisions are baked in here, and each one prevents a known bug:
- Integers in minor units. Floats can't represent most decimal amounts exactly, which is how you end up with totals that are off by a fraction. Storing 50000 dirhams instead of 500.00 riyals avoids that entirely.
- Currency travels with the amount. You can't add QAR to USD by accident. The mistake becomes an exception at the exact line where it happened, not a wrong number in a report next month.
readonlyclass. Since PHP 8.2, a readonly class makes every property write-once. Nobody can do$rent->amount = 0somewhere far away.
final readonly class Email
{
public string $value;
public function __construct(string $value)
{
$value = strtolower(trim($value));
if (filter_var($value, FILTER_VALIDATE_EMAIL) === false) {
throw new InvalidArgumentException("Invalid email: $value");
}
$this->value = $value;
}
public function __toString(): string
{
return $this->value;
}
}
The normalisation lives in one place. Trimming and lowercasing happen every time, so "[email protected] " and "[email protected]" can never become two accounts. (Strictly speaking, the part before the @ can be case-sensitive, but almost no real mail server treats it that way. Lowercasing is a pragmatic choice; make it consciously.)
And the signature from the start becomes chargeRent(Money $rent, Email $receiptTo). You can no longer swap the arguments, and you can no longer pass an amount without a currency.
DateRange
Date pairs are where bugs love to hide: a lease that ends before it starts, an "inclusive or exclusive?" argument in every function, overlap checks written slightly differently in three places.
final readonly class DateRange
{
public function __construct(
public CarbonImmutable $start,
public CarbonImmutable $end,
) {
if ($end->lessThan($start)) {
throw new InvalidArgumentException('End is before start.');
}
}
public function contains(CarbonImmutable $date): bool
{
return $date->betweenIncluded($this->start, $this->end);
}
public function overlaps(DateRange $other): bool
{
return $this->start <= $other->end && $other->start <= $this->end;
}
}
Now "can this unit be leased for these dates?" is one well-tested method, $existing->overlaps($requested), instead of a comparison someone retypes from memory.
Why immutability matters so much
Notice CarbonImmutable, not Carbon. That's deliberate. Regular Carbon instances are mutable, which produces this classic bug:
$firstDue = $contract->start_date;
$secondDue = $firstDue->addMonth(); // also changed $firstDue!
With a mutable object, addMonth() changes the original, so the contract's start date silently moves too. Immutable objects make that impossible: addMonth() returns a new instance and leaves the old one alone. If your app is new, consider calling Date::use(CarbonImmutable::class) in a service provider so Eloquent returns immutable dates everywhere.
The same thinking applies to your own value objects. When they're immutable, you can pass them anywhere, cache them, and share them between objects without wondering who might change them. If you want "change one field" methods, return a new object. PHP 8.5 also lets you write them with the new clone($this, ['amount' => $amount]) syntax, inside the class.
The test for a value object: if a concept has rules (a format, a range, a unit, a relationship between two fields), and you've written those rules in more than one place, it wants to be a class.
Plugging them into Eloquent
Value objects become really pleasant when your models hand them to you automatically. A custom cast can even map one object to two columns:
class MoneyCast implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): Money
{
return new Money((int) $attributes["{$key}_amount"], $attributes["{$key}_currency"]);
}
public function set(Model $model, string $key, mixed $value, array $attributes): array
{
return [
"{$key}_amount" => $value->amount,
"{$key}_currency" => $value->currency,
];
}
}
Register it in the model's casts() method with 'rent' => MoneyCast::class, and $contract->rent is now a Money object backed by rent_amount and rent_currency columns. The rest of your code never touches raw integers again. (Simplified: a production version would also handle null values.)
When not to bother
Don't wrap every string. A blog post title with no rules doesn't need a Title class. Value objects earn their place when the concept has validation, units, formatting or behaviour, and when getting it wrong costs something. Money, emails, phone numbers, date ranges, percentages and identifiers like a Commercial Registration number are the usual winners.
Start with one. Money is the best candidate in almost any business app. Once you've seen a currency bug turn into an exception on the exact line that caused it, you'll start spotting the next one everywhere.
Which value object would you add first in your current codebase? And do you have one you've written that saved you from a bug you still remember?

Be first to comment it...