nicolopignatelli/valueobjects
Laravel-friendly PHP value objects library that helps you model domain concepts as immutable, self-validating objects (like Email, Money, Url, etc.). Encourages DDD patterns, clearer APIs, and safer data handling with consistent validation and comparison.
Installation:
composer require nicolopignatelli/valueobjects
Add the service provider to config/app.php:
'providers' => [
// ...
Nicolopignatelli\ValueObjects\ValueObjectsServiceProvider::class,
],
First Use Case:
Define a simple value object (e.g., Email):
use Nicolopignatelli\ValueObjects\ValueObject;
class Email extends ValueObject
{
protected $value;
public function __construct(string $value)
{
$this->validate($value);
$this->value = $value;
}
protected function validate(string $value): void
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException("Invalid email format.");
}
}
public function __toString(): string
{
return $this->value;
}
}
Usage:
$email = new Email('[email protected]');
echo $email; // Outputs: [email protected]
tests folder demonstrates real-world patterns (e.g., Email, Money, Percentage).ValueObject.php for built-in methods like equals(), hashCode(), and immutability enforcement.Immutable Data Modeling: Use value objects to enforce immutability and domain-specific validation. Example:
class UserId extends ValueObject
{
public function __construct(string $value)
{
$this->validate($value);
$this->value = $value;
}
protected function validate(string $value): void
{
if (!preg_match('/^[a-f0-9]{24}$/', $value)) {
throw new \InvalidArgumentException("Invalid MongoDB ID format.");
}
}
}
Domain-Specific Logic:
Encapsulate business rules within value objects. Example for Money:
class Money extends ValueObject
{
public function add(Money $other): Money
{
return new self($this->value + $other->value);
}
public function isGreaterThan(Money $other): bool
{
return $this->value > $other->value;
}
}
Type Safety: Leverage PHP 7.4+ typed properties for stricter contracts:
class Percentage extends ValueObject
{
private float $value;
public function __construct(float $value)
{
$this->validate($value);
$this->value = $value;
}
protected function validate(float $value): void
{
if ($value < 0 || $value > 100) {
throw new \InvalidArgumentException("Percentage must be between 0 and 100.");
}
}
}
Laravel Eloquent: Use value objects as column attributes or accessors:
class User extends Model
{
protected $casts = [
'email' => Email::class,
];
public function getEmailAttribute($value)
{
return new Email($value);
}
}
API Requests: Validate incoming requests with value objects:
use Illuminate\Http\Request;
public function store(Request $request)
{
$email = new Email($request->input('email'));
// Proceed with $email...
}
Collections: Apply value objects to Laravel collections for consistency:
$users = User::all()->map(fn ($user) => new Email($user->email));
DTOs (Data Transfer Objects): Combine value objects into DTOs for API responses:
class UserDTO
{
public function __construct(
public Email $email,
public string $name,
) {}
}
Validation Overhead:
class Email extends ValueObject
{
public static function tryFrom(string $value): ?self
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
return null;
}
return new self($value);
}
}
Serialization Quirks:
serialize or JSON APIs.JsonSerializable or __serialize()/__unserialize():
class Email implements JsonSerializable
{
public function jsonSerialize(): string
{
return $this->value;
}
}
Database Storage:
protected $casts = [
'email' => 'string', // Store raw, hydrate as Email on access
];
Performance with Large Values:
Equality Comparisons:
equals() to handle edge cases (e.g., case sensitivity for strings):
public function equals($other): bool
{
return $other instanceof self && strtolower($this->value) === strtolower($other->value);
}
Hashing:
hashCode() for consistent hashing (critical for collections):
public function hashCode(): int
{
return hash('sha256', $this->value);
}
Logging:
__toString() for readable logs:
class Money
{
public function __toString(): string
{
return sprintf('$%.2f', $this->value);
}
}
Custom Assertions: Extend validation with custom rules:
use Illuminate\Validation\Rule;
class Domain extends ValueObject
{
protected function validate(string $value): void
{
$domain = explode('.', $value);
if (count($domain) < 2) {
throw new \InvalidArgumentException("Invalid domain format.");
}
// Add custom rules (e.g., check against a list of allowed domains)
}
}
Value Object Factories: Create static factories for complex logic:
class Url extends ValueObject
{
public static function fromString(string $url): self
{
$parsed = parse_url($url);
if (!$parsed || empty($parsed['scheme']) || empty($parsed['host'])) {
throw new \InvalidArgumentException("Invalid URL.");
}
return new self($url);
}
}
Laravel Policy Integration: Use value objects in policies for authorization:
class UpdateUserPolicy
{
public function update(User $user, Email $newEmail): bool
{
return $user->email->equals($newEmail);
}
}
Testing: Mock value objects in tests for isolation:
$mockEmail = Mockery::mock(Email::class, ['[email protected]']);
$this->app->instance(Email::class, $mockEmail);
How can I help you explore Laravel packages today?