acc-create-value-object
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}, $vo->value);
}
public function testThrowsExceptionForInvalidValue(): void
{
$this->expectException(Invalid{Name}Exception::class);
new {Name}({invalidValue});
}
public function testEquality(): void
{
$vo1 = new {Name}({validValue});
$vo2 = new {Name}({validValue});
$vo3 = new {Name}({differentValue});
self::assertTrue($vo1->equals($vo2));
self::assertFalse($vo1->equals($vo3));
}
public function testToString(): void
{
$vo = new {Name}({validValue});
self::assertSame({expectedString}, (string) $vo);
}
}
Common Value Objects
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
{
return new self((int) round($amount * 100), $currency);
}
public function add(self $other): self
{
$this->assertSameCurrency($other);
return new self($this->cents + $other->cents, $this->currency);
}
public function subtract(self $other): self
{
$this->assertSameCurrency($other);
return new self($this->cents - $other->cents, $this->currency);
}
public function multiply(int $factor): self
{
return new self($this->cents * $factor, $this->currency);
}
public function isGreaterThan(self $other): bool
{
$this->assertSameCurrency($other);
return $this->cents > $other->cents;
}
public function isZero(): bool
{
return $this->cents === 0;
}
public function isPositive(): bool
{
return $this->cents > 0;
}
public function equals(self $other): bool
{
return $this->cents === $other->cents && $this->currency === $other->currency;
}
public function format(): string
{
return number_format($this->cents / 100, 2) . ' ' . $this->currency;
}
private function assertSameCurrency(self $other): void
{
if ($this->currency !== $other->currency) {
throw new CurrencyMismatchException($this->currency, $other->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 function uuid4(): string
{
$data = random_bytes(16);
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
}
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): bool
{
return $this->street === $other->street
&& $this->city === $other->city
&& $this->postalCode === $other->postalCode
&& $this->country === $other->country;
}
public function format(): string
{
return "{$this->street}\n{$this->postalCode} {$this->city}\n{$this->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) $this->start->diff($this->end)->days;
}
public function equals(self $other): bool
{
return $this->start == $other->start && $this->end == $other->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
More from dykyi-roman/awesome-claude-code
psr-overview-knowledge
PHP Standards Recommendations (PSR) overview knowledge base. Provides comprehensive reference for all accepted PSRs including PSR-1,3,4,6,7,11,12,13,14,15,16,17,18,20. Use for PSR selection decisions and compliance audits.
22detect-code-smells
Detects code smells in PHP codebases. Identifies God Class, Feature Envy, Data Clumps, Long Parameter List, Long Method, Primitive Obsession, Message Chains, Inappropriate Intimacy. Generates actionable reports with refactoring recommendations.
15clean-arch-knowledge
Clean Architecture knowledge base. Provides patterns, antipatterns, and PHP-specific guidelines for Clean Architecture and Hexagonal Architecture audits.
15ddd-knowledge
DDD architecture knowledge base. Provides patterns, antipatterns, and PHP-specific guidelines for Domain-Driven Design audits.
14testing-knowledge
Testing knowledge base for PHP 8.4 projects. Provides testing pyramid, AAA pattern, naming conventions, isolation principles, DDD testing guidelines, and PHPUnit patterns.
12bug-root-cause-finder
Root cause analysis methods for PHP bugs. Provides 5 Whys technique, fault tree analysis, git bisect guidance, and stack trace parsing.
12