stougeiro / valueobject
A lightweight Value Object foundation for PHP
Fund package maintenance!
Requires
- php: >=8.2
Requires (Dev)
- pestphp/pest: ^3.8
- phpstan/phpstan: ^2.2
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-17 05:34:53 UTC
README
ValueObject
A lightweight and flexible foundation for creating Value Objects in PHP.
This package provides a minimalistic interface and an extensible abstract class that help developers build semantic value objects that encourage immutability, with optional validation and a clean factory pattern.
✨ Features
-
Named constructors: Each value object defines its own typed factory methods (e.g.
fromString(),fromInt()), ensuring full type safety and IDE support. -
Immutability: Uses
readonlyproperties (PHP 8.2+) to ensure value objects cannot be modified after creation. -
Optional validation:
isValid()
Developers decide when and how to validate the underlying value. -
Semantic comparison:
equals(ValueObjectInterface $other)
Compares viatoArray()representation, ensuring structural equality with type safety. -
Consistent value access:
value()
Returns the underlying primitive or structured value. -
Hash support:
hashCode()
Deterministic hash based ontoArray()for use as array keys or in collections. -
Universal serialization:
toArray()
Returns array representation, works with JSON, XML, and other formats. -
Change detection:
diff(ValueObjectInterface $other)
Shows what changed between two value objects. -
String representation:
toString()and__toString()
Ensures every value object can be safely cast to a string. -
Minimalistic and extensible
The interface stays small and expressive, while concrete classes define their own semantics and factories.
🔍 How Comparisons Work
Both equals() and diff() compare value objects using their toArray() representation.
This approach ensures:
- Full structural equality: Not just raw value, but the complete semantic representation
- Type safety:
equals()requires same class viainstanceof static - Consistent behavior: Hash, JSON, XML, and comparisons all use the same array structure
$email1 = Email::fromString('user@example.com'); $email2 = Email::fromString('USER@EXAMPLE.COM'); // Both normalize to the same toArray() representation $email1->toArray(); // ['email' => 'user@example.com', 'user' => 'user', 'domain' => 'example.com'] $email2->toArray(); // ['email' => 'user@example.com', 'user' => 'user', 'domain' => 'example.com'] $email1->equals($email2); // true
The hashCode() method also uses toArray(), ensuring consistency when value objects are used as array keys or in collections.
📦 Installation
Install via Composer:
composer require stougeiro/valueobject
🚀 Usage
Defining a Value Object
use STDW\ValueObject\ValueObjectAbstracted; final class Email extends ValueObjectAbstracted { private function __construct(private readonly string $email) {} public static function fromString(string $email): static { $instance = new self(strtolower(trim($email))); if ( ! $instance->isValid()) { throw new \InvalidArgumentException("Invalid email: {$email}"); } return $instance; } public function value(): string { return $this->email; } public function isValid(): bool { return filter_var($this->email, FILTER_VALIDATE_EMAIL) !== false; } public function user(): string { return explode('@', $this->email)[0]; } public function domain(): string { return explode('@', $this->email)[1]; } public function toString(): string { return $this->email; } /** @return array<string, mixed> */ public function toArray(): array { return [ 'email' => $this->email, 'user' => $this->user(), 'domain' => $this->domain(), ]; } }
Creating Instances
Named constructors are defined by you, with full type safety:
$email = Email::fromString('User@Example.com'); $email->value(); // "user@example.com" $email->toString(); // "user@example.com" (string) $email; // "user@example.com"
Comparison
$a = Email::fromString('user@example.com'); $b = Email::fromString('USER@EXAMPLE.COM'); $c = Email::fromString('other@example.com'); $a->equals($b); // true — same values after normalization $a->equals($c); // false — different values
Serialization
// Array $email->toArray(); // ['email' => 'user@example.com', 'user' => 'user', 'domain' => 'example.com'] // JSON json_encode($email->toArray()); // '{"email":"user@example.com","user":"user","domain":"example.com"}' // As array key $emails = [$email->hashCode() => $email];
Change Detection
$current = Email::fromString('user@example.com'); $updated = Email::fromString('new@domain.com'); $current->diff($updated); // ['email' => 'new@domain.com', 'user' => 'new', 'domain' => 'domain.com'] // Empty when no changes $current->diff(Email::fromString('user@example.com')); // []
Domain-Specific Accessors
Define your own semantic methods:
$email = Email::fromString('user@example.com'); $email->user(); // "user" $email->domain(); // "example.com"
🧠 Why?
Value Objects are a core building block in domain‑driven design and clean architecture. They encapsulate meaning, enforce structure, and prevent primitive obsession — ensuring that values carry behavior and validation instead of floating loosely through the system.
This package aims to provide a simple, expressive and unobtrusive foundation for building your own Value Objects without unnecessary boilerplate.
🤝 Contributions
Contributions are welcome. Feel free to open issues or submit pull requests.