Claude-skill-registry acc-create-value-object
Generates DDD Value Objects for PHP 8.5. Creates immutable, self-validating objects with equality comparison. Includes unit tests.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/acc-create-value-object" ~/.claude/skills/majiayu000-claude-skill-registry-acc-create-value-object && rm -rf "$T"
manifest:
skills/data/acc-create-value-object/SKILL.mdsource content
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:
method compares valuesequals() - 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 | | |
| Money amount | | |
| Order identifier | | |
| Physical address | | |
| Phone number | | |
| Date range | | |
Usage
To generate a Value Object, provide:
- Name (e.g., "Email", "CustomerId")
- Bounded Context (e.g., "Order", "Customer")
- Validation rules
- Any special methods needed