Fundamentals 16 min read

Understanding Different Types of Value Objects and Their Implementation in PHP

This article explores simple, complex, and composite value objects in PHP, providing guidelines for their design, validation, immutability, string representation, equality checks, while also discussing alternative error handling approaches such as Either types and union types.

php中文网 Courses
php中文网 Courses
php中文网 Courses
Understanding Different Types of Value Objects and Their Implementation in PHP

Different Types of Value Objects

Value objects can be classified by complexity into three main categories: simple, complex, and composite. Each type serves a specific purpose in a domain model and requires distinct design considerations.

Simple Value Objects

A simple value object encapsulates a single primitive value (e.g., an Age class that stores an integer). It should be immutable, validate its value in the constructor, provide a __toString() method for string conversion, and an equals() method for equality comparison.

Single responsibility – represent one concept.

Immutability – state cannot change after creation.

Validation – ensure the value is always valid.

String representation – implement __toString() .

Equality check – implement equals() .

<code>readonly final class Age {
    public function __construct(public int $value) {
        $this->validate();
    }
    public function validate(): void {
        ($this->value >= 18) or throw InvalidAge::adultRequired($this->value);
        ($this->value <= 120) or throw InvalidAge::matusalem($this->value);
    }
    public function __toString(): string { return (string)$this->value; }
    public function equals(Age $age): bool { return $age->value === $this->value; }
}
</code>

Complex Value Objects

Complex value objects encapsulate multiple related attributes (e.g., a Coordinates class with latitude and longitude). They require structured validation of each attribute and their relationships, as well as meaningful string and equality implementations.

Structured representation of related data.

Comprehensive validation of each field and cross‑field constraints.

Provide __toString() for readable output.

Implement equals() to compare all attributes.

<code>readonly final class Coordinates {
    public function __construct(public float $latitude, public float $longitude) {
        $this->validate();
    }
    private function validate(): void {
        ($this->latitude >= -90 && $this->latitude <= 90) or throw InvalidCoordinates::invalidLatitude($this->latitude);
        ($this->longitude >= -180 && $this->longitude <= 180) or throw InvalidCoordinates::invalidLongitude($this->longitude);
    }
    public function __toString(): string {
        return "Latitude: {$this->latitude}, Longitude: {$this->longitude}";
    }
    public function equals(Coordinates $c): bool {
        return $c->latitude === $this->latitude && $c->longitude === $this->longitude;
    }
}
</code>

Composite Value Objects

A composite value object aggregates several simple or complex value objects into a single cohesive unit (e.g., an Address composed of Street , City , and PostalCode objects). The composite should delegate validation to its components and expose its own string and equality logic.

Combine multiple value objects into a richer structure.

Leverage component validation; additional cross‑component checks if needed.

Implement __toString() and equals() for the whole aggregate.

<code>readonly final class Address {
    public function __construct(public Street $street, public City $city, public PostalCode $postalCode) {}
    public function __toString(): string {
        return "{$this->street}, {$this->city}, {$this->postalCode}";
    }
    public function equals(Address $a): bool {
        return $a->street->equals($this->street)
            && $a->city->equals($this->city)
            && $a->postalCode->equals($this->postalCode);
    }
}
</code>

Factory Methods and Private Constructors

Because PHP lacks constructor overloading, static factory methods are used to create value objects in a controlled way. The constructor is made private, forcing creation through named factories that perform validation before instantiation.

<code>class DateTimeValueObject {
    private DateTimeImmutable $dateTime;
    private function __construct(DateTimeImmutable $dateTime) { $this->dateTime = $dateTime; }
    public static function createFromTimestamp(int $ts): self { /* validate and return */ }
    public static function createFromRFC3339(string $s): self { /* validate and return */ }
    public static function createFromParts(int $y, int $m, int $d, int $h, int $i, int $s, string $tz): self { /* validate and return */ }
    public static function now(): self { return new self(new DateTimeImmutable()); }
    public function getDateTime(): DateTimeImmutable { return $this->dateTime; }
    public function __toString(): string { return $this->dateTime->format(DateTime::RFC3339); }
    public function equals(DateTimeValueObject $other): bool { return $this->dateTime == $other->dateTime; }
}
</code>

Alternative Error‑Handling Strategies

Instead of throwing exceptions, functions can return a boolean, an enum, an Either type, or use PHP 8 union types to represent success or failure.

Either Type

The Either class encapsulates a right (successful) value or a left (error) value, allowing callers to handle both outcomes explicitly.

<code>final class Either {
    private function __construct(private bool $isRight, private mixed $value) {}
    public static function left(mixed $v): self { return new self(false, $v); }
    public static function right(mixed $v): self { return new self(true, $v); }
    public function isRight(): bool { return $this->isRight; }
    public function getValue(): mixed { return $this->value; }
}
</code>

Example usage with an Address factory:

<code>public static function create(string $street, string $city, string $postal): Either {
    try { return Either::right(new Address(new Street($street), new City($city), new PostalCode($postal)));
    } catch (InvalidValue $e) { return Either::left($e); }
}
</code>

Union Types

PHP 8.0 allows a method to declare a return type that is a union of several types, e.g., InvalidValue|Address . Callers can use instanceof to distinguish the result.

<code>public static function create(string $street, string $city, string $postal): InvalidValue|Address {
    try { return new Address(new Street($street), new City($city), new PostalCode($postal)); }
    catch (InvalidValue $e) { return $e; }
}
</code>

Both approaches provide structured error handling; the choice depends on project needs and developer preference.

Domain-Driven DesignPHPerror handlingValue ObjectsimmutabilityFactory Method
php中文网 Courses
Written by

php中文网 Courses

php中文网's platform for the latest courses and technical articles, helping PHP learners advance quickly.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.