Skip to content

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