Fixing Primitive Obsession with Value Objects
Also Known As
value object refactoring
tiny types
micro types
TL;DR
Replacing raw strings, ints, and floats representing domain concepts with dedicated Value Object classes that enforce invariants at construction.
Explanation
Primitive obsession uses raw types where domain concepts belong: string $email, float $price, string $status. Problems: no validation enforced, no type safety distinguishing 'email string' from 'name string', business logic scattered across multiple callers. The fix: create small Value Object classes — Email, Money, OrderStatus — that validate and encapsulate the concept. Constructors throw on invalid input, so a valid VO instance is always well-formed. Methods can be added: $money->add($other), $email->domain(). PHP 8.1 readonly classes make VOs concise. PHPStan recognises distinct types, catching swapped-argument bugs statically. Start with the most-duplicated validation logic and extract outward.
Common Misconception
✗ Value objects are overkill for simple domains. Even in simple domains, a Money class that prevents adding EUR to GBP, or an Email class that validates on construction, catches entire categories of bugs at the type level that string primitives cannot.
Why It Matters
Replacing primitive types with domain-specific value objects makes invalid states unrepresentable and moves validation to the type boundary — no more scattered if(empty($email)) checks across the codebase.
Common Mistakes
- Value objects with setters — they should be immutable; create a new instance for changes.
- Not overriding __toString() for value objects used in string contexts — causes type errors.
- Value object equality checked with == instead of a dedicated equals() method or comparing properties.
- Wrapping every primitive — only wrap primitives that carry domain rules or validation; plain int IDs often do not need wrapping.
Code Examples
✗ Vulnerable
function charge(string $email, float $amount, string $currency): void {
// Which string is which? float can be negative?
}
✓ Fixed
function charge(Email $email, Money $amount): void {
// Email and Money enforce their own invariants
}
Tags
🤝 Adopt this term
£79/year · your link shown here
Added
15 Mar 2026
Edited
22 Mar 2026
Views
29
🤖 AI Guestbook educational data only
|
|
Last 30 days
Agents 0
No pings yet today
No pings yesterday
Amazonbot 8
Perplexity 6
Google 2
Ahrefs 2
Unknown AI 2
ChatGPT 2
Also referenced
How they use it
crawler 20
crawler_json 2
Related categories
⚡
DEV INTEL
Tools & Severity
🟡 Medium
⚙ Fix effort: Medium
⚡ Quick Fix
Start small: pick the most-misused primitive in your codebase (usually email, money, or a status string) and wrap it in a value object — you'll immediately catch bugs at construction time
📦 Applies To
any
web
cli
queue-worker
🔗 Prerequisites
🔍 Detection Hints
Email validation duplicated in 10 places; float for money causing rounding bugs; string status compared across multiple files with no enum
Auto-detectable:
✗ No
phpstan
phpmd
⚠ Related Problems
🤖 AI Agent
Confidence: Medium
False Positives: Medium
✗ Manual fix
Fix: High
Context: File
Tests: Update