| name | acc-create-value-object |
| description | Generates DDD Value Objects for PHP 8.5. Creates immutable, self-validating objects with equality comparison. Includes unit tests. |
Value Object Generator
Generate DDD-compliant Value Objects with validation, equality, and tests.
Value Object Characteristics
- Immutable:
final readonly class
- Self-validating: Validates in constructor
- Equality by value:
equals() method compares values
- No identity: No ID, compared by content
- Encapsulates concept: Represents domain concept
Template
<?php
declare(strict_types=1);
namespace Domain\{BoundedContext}\ValueObject;
use Domain\{BoundedContext}\Exception\Invalid{Name}Exception;
final readonly class {Name}
{
public function __construct(
public {type} $value
) {
{validation}
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
public function __toString(): string
{
return (string) $this->value;
}
}
Test Template
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\{BoundedContext}\ValueObject;
use Domain\{BoundedContext}\ValueObject\{Name};
use Domain\{BoundedContext}\Exception\Invalid{Name}Exception;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
#[Group('unit')]
#[CoversClass({Name}::class)]
final class {Name}Test extends TestCase
{
public function testCreatesWithValidValue(): void
{
$vo = new {Name}({validValue});
self::assertSame({validValue}, ->value);
}
{
->(Invalid{Name}::);
{Name}({invalidValue});
}
{
= {Name}({validValue});
= {Name}({validValue});
= {Name}({differentValue});
::(->());
::(->());
}
{
= {Name}({validValue});
::({expectedString}, () );
}
}
Common Value Objects
Email
final readonly class Email
{
public function __construct(
public string $value
) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidEmailException($value);
}
}
public function equals(self $other): bool
{
return strtolower($this->value) === strtolower($other->value);
}
public function domain(): string
{
return substr($this->value, strpos($this->value, '@') + 1);
}
public function __toString(): string
{
return $this->value;
}
}
Money
final readonly class Money
{
public function __construct(
public int $cents,
public string $currency
) {
if ($cents < 0) {
throw new InvalidMoneyException('Amount cannot be negative');
}
if (strlen($currency) !== 3) {
throw new InvalidMoneyException('Currency must be 3 characters');
}
}
public static function zero(string $currency): self
{
return new self(0, $currency);
}
public static function fromFloat(float $amount, string $currency): self
{
(() ( * ), );
}
{
->();
(->cents + ->cents, ->currency);
}
{
->();
(->cents - ->cents, ->currency);
}
{
(->cents * , ->currency);
}
{
->();
->cents > ->cents;
}
{
->cents === ;
}
{
->cents > ;
}
{
->cents === ->cents && ->currency === ->currency;
}
{
(->cents / , ) . . ->currency;
}
{
(->currency !== ->currency) {
(->currency, ->currency);
}
}
}
UUID-based ID
final readonly class OrderId
{
public function __construct(
public string $value
) {
if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $value)) {
throw new InvalidOrderIdException($value);
}
}
public static function generate(): self
{
return new self(self::uuid4());
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
public function __toString(): string
{
return $this->value;
}
private static {
= ();
[] = (([]) & | );
[] = (([]) & | );
(, ((), ));
}
}
Address (Composite)
final readonly class Address
{
public function __construct(
public string $street,
public string $city,
public string $postalCode,
public string $country
) {
if (empty(trim($street))) {
throw new InvalidAddressException('Street cannot be empty');
}
if (empty(trim($city))) {
throw new InvalidAddressException('City cannot be empty');
}
if (strlen($country) !== 2) {
throw new InvalidAddressException('Country must be ISO 3166-1 alpha-2');
}
}
public function equals(self $other):
{
->street === ->street
&& ->city === ->city
&& ->postalCode === ->postalCode
&& ->country === ->country;
}
{
;
}
}
DateRange
final readonly class DateRange
{
public function __construct(
public DateTimeImmutable $start,
public DateTimeImmutable $end
) {
if ($end < $start) {
throw new InvalidDateRangeException('End date must be after start date');
}
}
public function contains(DateTimeImmutable $date): bool
{
return $date >= $this->start && $date <= $this->end;
}
public function overlaps(self $other): bool
{
return $this->start <= $other->end && $this->end >= $other->start;
}
public function days(): int
{
return (int) ->start->(->end)->days;
}
{
->start == ->start && ->end == ->end;
}
}
Generation Instructions
When asked to create a Value Object:
- Identify the concept being modeled
- Determine validation rules from domain requirements
- Choose appropriate type (string, int, composite)
- Add domain-specific methods if needed
- Generate corresponding test with valid/invalid cases
Naming Conventions
| Concept | Class Name | Exception |
|---|
| Email address | Email | InvalidEmailException |
| Money amount | Money | InvalidMoneyException |
| Order identifier | OrderId | InvalidOrderIdException |
| Physical address | Address | InvalidAddressException |
| Phone number | Phone | InvalidPhoneException |
| Date range | DateRange | InvalidDateRangeException |
Usage
To generate a Value Object, provide:
- Name (e.g., "Email", "CustomerId")
- Bounded Context (e.g., "Order", "Customer")
- Validation rules
- Any special methods needed