Claude-skill-registry acc-create-entity
Generates DDD Entities for PHP 8.5. Creates identity-based objects with behavior, state transitions, and invariant protection. 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-entity" ~/.claude/skills/majiayu000-claude-skill-registry-acc-create-entity && rm -rf "$T"
manifest:
skills/data/acc-create-entity/SKILL.mdsource content
Entity Generator
Generate DDD-compliant Entities with identity, behavior, and tests.
Entity Characteristics
- Identity: Has unique identifier (ID)
- Lifecycle: Created, modified, potentially deleted
- Behavior: Contains domain logic (not just data)
- Invariants: Protects business rules
- State transitions: Controlled mutations
- No public setters: State changed via behavior methods
Template
<?php declare(strict_types=1); namespace Domain\{BoundedContext}\Entity; use Domain\{BoundedContext}\ValueObject\{Name}Id; use Domain\{BoundedContext}\Enum\{Name}Status; use Domain\{BoundedContext}\Exception\{Exceptions}; final class {Name} { private {Name}Status $status; private DateTimeImmutable $createdAt; private ?DateTimeImmutable $updatedAt = null; public function __construct( private readonly {Name}Id $id, {constructorProperties} ) { {constructorValidation} $this->status = {Name}Status::default(); $this->createdAt = new DateTimeImmutable(); } public function id(): {Name}Id { return $this->id; } public function status(): {Name}Status { return $this->status; } {behaviorMethods} private function touch(): void { $this->updatedAt = new DateTimeImmutable(); } }
Test Template
<?php declare(strict_types=1); namespace Tests\Unit\Domain\{BoundedContext}\Entity; use Domain\{BoundedContext}\Entity\{Name}; use Domain\{BoundedContext}\ValueObject\{Name}Id; use Domain\{BoundedContext}\Enum\{Name}Status; 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 testCreatesWithValidData(): void { $entity = $this->createEntity(); self::assertInstanceOf({Name}Id::class, $entity->id()); self::assertSame({Name}Status::default(), $entity->status()); } {behaviorTests} private function createEntity(): {Name} { return new {Name}( id: {Name}Id::generate(), {testConstructorArgs} ); } }
Common Entity Patterns
Order Entity
<?php declare(strict_types=1); namespace Domain\Order\Entity; use Domain\Order\ValueObject\OrderId; use Domain\Order\ValueObject\CustomerId; use Domain\Order\ValueObject\Money; use Domain\Order\Enum\OrderStatus; use Domain\Order\Exception\CannotModifyConfirmedOrderException; use Domain\Order\Exception\CannotConfirmEmptyOrderException; use Domain\Order\Exception\InvalidStateTransitionException; final class Order { private OrderStatus $status; /** @var array<OrderLine> */ private array $lines = []; private DateTimeImmutable $createdAt; private ?DateTimeImmutable $confirmedAt = null; public function __construct( private readonly OrderId $id, private readonly CustomerId $customerId ) { $this->status = OrderStatus::Draft; $this->createdAt = new DateTimeImmutable(); } public function id(): OrderId { return $this->id; } public function customerId(): CustomerId { return $this->customerId; } public function status(): OrderStatus { return $this->status; } public function addLine(Product $product, int $quantity): void { if ($this->status !== OrderStatus::Draft) { throw new CannotModifyConfirmedOrderException($this->id); } $this->lines[] = new OrderLine( product: $product, quantity: $quantity, unitPrice: $product->price() ); } public function removeLine(int $index): void { if ($this->status !== OrderStatus::Draft) { throw new CannotModifyConfirmedOrderException($this->id); } if (!isset($this->lines[$index])) { return; } unset($this->lines[$index]); $this->lines = array_values($this->lines); } public function confirm(): void { if ($this->status !== OrderStatus::Draft) { throw new InvalidStateTransitionException( $this->status, OrderStatus::Confirmed ); } if (empty($this->lines)) { throw new CannotConfirmEmptyOrderException($this->id); } $this->status = OrderStatus::Confirmed; $this->confirmedAt = new DateTimeImmutable(); } public function cancel(): void { if (!$this->status->canBeCancelled()) { throw new InvalidStateTransitionException( $this->status, OrderStatus::Cancelled ); } $this->status = OrderStatus::Cancelled; } public function total(): Money { return array_reduce( $this->lines, fn (Money $carry, OrderLine $line) => $carry->add($line->total()), Money::zero('USD') ); } /** * @return array<OrderLine> */ public function lines(): array { return $this->lines; } public function lineCount(): int { return count($this->lines); } public function isEmpty(): bool { return empty($this->lines); } public function createdAt(): DateTimeImmutable { return $this->createdAt; } public function confirmedAt(): ?DateTimeImmutable { return $this->confirmedAt; } }
User Entity
<?php declare(strict_types=1); namespace Domain\User\Entity; use Domain\User\ValueObject\UserId; use Domain\User\ValueObject\Email; use Domain\User\ValueObject\HashedPassword; use Domain\User\Enum\UserStatus; use Domain\User\Exception\UserAlreadyActivatedException; use Domain\User\Exception\UserDeactivatedException; final class User { private UserStatus $status; private DateTimeImmutable $createdAt; private ?DateTimeImmutable $lastLoginAt = null; public function __construct( private readonly UserId $id, private Email $email, private HashedPassword $password, private string $name ) { if (empty(trim($name))) { throw new InvalidUserNameException(); } $this->status = UserStatus::Pending; $this->createdAt = new DateTimeImmutable(); } public function id(): UserId { return $this->id; } public function email(): Email { return $this->email; } public function name(): string { return $this->name; } public function status(): UserStatus { return $this->status; } public function activate(): void { if ($this->status === UserStatus::Active) { throw new UserAlreadyActivatedException($this->id); } $this->status = UserStatus::Active; } public function deactivate(): void { $this->status = UserStatus::Deactivated; } public function changeEmail(Email $newEmail): void { $this->ensureActive(); $this->email = $newEmail; } public function changePassword(HashedPassword $newPassword): void { $this->ensureActive(); $this->password = $newPassword; } public function changeName(string $newName): void { $this->ensureActive(); if (empty(trim($newName))) { throw new InvalidUserNameException(); } $this->name = $newName; } public function recordLogin(): void { $this->ensureActive(); $this->lastLoginAt = new DateTimeImmutable(); } public function verifyPassword(string $plainPassword, PasswordHasherInterface $hasher): bool { return $hasher->verify($this->password, $plainPassword); } public function isActive(): bool { return $this->status === UserStatus::Active; } private function ensureActive(): void { if ($this->status === UserStatus::Deactivated) { throw new UserDeactivatedException($this->id); } } }
Entity Design Principles
1. Behavior Over Data
// BAD: Anemic entity class Order { public function setStatus(string $status): void { $this->status = $status; } } // GOOD: Rich entity with behavior class Order { public function confirm(): void { if (!$this->canBeConfirmed()) { throw new InvalidStateTransitionException(); } $this->status = OrderStatus::Confirmed; $this->confirmedAt = new DateTimeImmutable(); } private function canBeConfirmed(): bool { return $this->status === OrderStatus::Draft && !empty($this->lines); } }
2. Invariant Protection
// Protect invariants in every method public function addLine(Product $product, int $quantity): void { // Invariant: Can only modify draft orders if ($this->status !== OrderStatus::Draft) { throw new CannotModifyConfirmedOrderException(); } // Invariant: Quantity must be positive if ($quantity <= 0) { throw new InvalidQuantityException(); } $this->lines[] = new OrderLine($product, $quantity); }
3. State Transitions
enum OrderStatus: string { case Draft = 'draft'; case Confirmed = 'confirmed'; case Paid = 'paid'; case Shipped = 'shipped'; case Cancelled = 'cancelled'; public function canTransitionTo(self $target): bool { return match($this) { self::Draft => in_array($target, [self::Confirmed, self::Cancelled]), self::Confirmed => in_array($target, [self::Paid, self::Cancelled]), self::Paid => in_array($target, [self::Shipped, self::Cancelled]), self::Shipped => false, self::Cancelled => false, }; } public function canBeCancelled(): bool { return $this->canTransitionTo(self::Cancelled); } }
Generation Instructions
When asked to create an Entity:
- Identify the identity (what makes it unique)
- Define the lifecycle (statuses/states)
- List invariants (business rules to protect)
- Design behavior methods (what it can do)
- Generate tests for behavior and invariants
Naming Conventions
| Concept | Method Pattern | Exception |
|---|---|---|
| State change | , , | |
| Add relation | , | |
| Update property | , | |
| Query state | , | N/A (boolean return) |
Usage
To generate an Entity, provide:
- Name (e.g., "Order", "User")
- Bounded Context (e.g., "Order", "User")
- Identity type (e.g., "OrderId")
- States/Statuses
- Key behaviors needed
- Invariants to protect