Value Objects
Value Objects are immutable, self-validating domain primitives that replace primitive types with meaningful domain concepts.
Characteristics
- Immutable: Once created, values cannot change
- Self-validating: Validation occurs at construction
- Equality by value: Two VOs with same values are equal
- No identity: Unlike entities, no unique identifier
Creating Value Objects
String-Based Value Objects
Extend AbstractStringValueObject for text values:
final readonly class Title extends AbstractStringValueObject implements ValueObjectInterface
{
protected static function getExceptionClass(): callable
{
return InvalidTitleException::whenValidationFailed(...);
}
protected function validateRules(string $value): void
{
Assert::that($value)
->notEmpty('Title cannot be empty')
->maxLength(200, 'Title cannot exceed 200 characters');
}
}
Integer-Based Value Objects
Extend AbstractIntValueObject for numeric values:
final readonly class Quantity extends AbstractIntValueObject implements ValueObjectInterface
{
protected static function getExceptionClass(): callable
{
return InvalidQuantityException::whenValidationFailed(...);
}
protected function validateRules(int $value): void
{
Assert::that($value)
->min(1, 'Quantity must be at least 1')
->max(100, 'Quantity cannot exceed 100');
}
}
// Usage
$quantity = Quantity::fromInt(5);
$quantity->getValue(); // 5
Usage
// Create via factory method
$title = Title::fromString('Clean Architecture');
// Compare values
$title1->equals($title2); // true if same value
// Get value
$title->getValue();
(string) $title;
Custom Normalization
Override normalize() for input preprocessing:
protected function normalize(string $value): string
{
return preg_replace('/\D/', '', $value); // Remove non-digits for ISBN
}
Enums as Value Objects
For finite sets of values, use PHP enums with EnumInterface. See Enums for details.
Validation with beberlei/assert
Assert::that($value)
->notEmpty('Cannot be empty')
->email('Must be valid email')
->maxLength(255, 'Too long');
Best Practices
- One concept per Value Object
- Always use factory methods, not direct instantiation
- Normalize input in
normalize() - Use domain-specific exceptions
- Never add setters or mutating methods
See Also
- Clean Code: Avoid Primitive Obsession - Why use Value Objects