yadddl / value-object
Yet another Value Object library — functional validation (PHP 8.5 pipes) and structural serialize/deserialize
v2.0.1
2026-11-06 17:59 UTC
Requires
- php: ^8.5
- ramsey/uuid: ^4.7
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.16
- pestphp/pest: ^4.1
- vimeo/psalm: ^6.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Yet Another DDD Library — reusable PHP value objects with functional validation (PHP 8.5 pipes) and structural serialize/deserialize in one package.
Requires PHP 8.5+.
composer require yadddl/value-object
Why
- If a value object instance exists, it is valid
- Validation returns
T|ErrorsBag(union types — no Either monad) - Errors accumulate; pipelines do not stop at the first failure
- The serializer does not validate — it calls domain
create()factories - No application-framework dependencies
Inspired by the Value Objects in PHP 8 series, especially the functional approach.
Quick start
Define a value object
use Yadddl\ValueObject\Contracts\ValidatedValue; use Yadddl\ValueObject\Error\ErrorsBag; use Yadddl\ValueObject\Error\Failable; use Yadddl\ValueObject\Error\FailableSuccess; use Yadddl\ValueObject\Validators\IntegerValue; readonly final class Age implements Failable, ValidatedValue { use FailableSuccess; private function __construct(public int $value) {} public static function create(mixed $value): Age|ErrorsBag { $context = $value |> IntegerValue::from(...) |> IntegerValue::min(0, 'Age cannot be negative') |> IntegerValue::max(150, 'Age cannot exceed 150'); return $context->isValid() ? new self($context->getValue()) : $context->getErrors(); } }
Use it
$result = Age::create($_POST['age'] ?? null); if ($result instanceof Age) { echo $result->value; } else { foreach ($result->getMessages() as $message) { echo $message, PHP_EOL; } } // Trusted boundary: $age = Age::create(25)->orFail();
Composite with field errors
$context = MultipleValidationContext::setup( name: Name::create($name), email: Email::create($email), age: Age::create($age), ); return $context->isValid() ? new Person(...$context->getValues()) : $context->getErrors(); // MultipleFieldErrorsBag
Backed enums in the validation pipeline
use Yadddl\ValueObject\Validators\BackedEnumValue; enum ListingStatus: string { case Published = 'published'; case Draft = 'draft'; } $status = BackedEnumValue::create(ListingStatus::class, $raw); // ListingStatus|ErrorsBag — no throw on invalid input $context = MultipleValidationContext::setup( status: $status, // … ); // On success, getValues()['status'] is the enum case (not the backing string)
Serialize / deserialize
use Yadddl\Serializer\StructuralConverter; $converter = StructuralConverter::create(); $converter->toArray($age); // 25 $converter->fromArray(25, Age::class); // Age|ErrorsBag $payload = $converter->toArray($person); // ['name' => …, 'email' => …, 'age' => …] $person = $converter->fromArray($payload, Person::class); // Person|MultipleFieldErrorsBag
Documentation
| Guide | Contents |
|---|---|
| Getting started | Install, first VO, primitives, converter |
| Building value objects | Canonical shape, password-like rules, examples |
| Composites | MultipleValidationContext, form errors |
| Validators | IntegerValue / StringValue / ArrayValue / BackedEnumValue API |
| Errors | DomainError, InvalidField / InvalidEntity, ErrorsBag, orFail(), entry-point mapping |
| Primitives & dates | Index + summary table of all shipped types; Text/Integer/Email/Url/Uuid/Date families |
| Serializer | Conventions, round-trips, nested graphs |
| Migration from 0.8 | Breaking changes from YAVOL / YASL |
| Changelog | Release notes |
Runnable samples live under examples/.
Package layout
Yadddl\ValueObject\… validators, errors, primitives, composites
Yadddl\Serializer\… structural object ↔ array (same Composer package)
Domain code should depend on Yadddl\ValueObject only. Adapters may also use Yadddl\Serializer.
Development
composer install composer test composer test-coverage # Xdebug; enforces 100% coverage composer psalm composer cs-check
License
MIT — see LICENSE.