Analyzes PHP code for immutability violations. Checks Value Objects, Events, DTOs for readonly properties, no setters, final classes, and wither patterns. Ensures domain objects maintain invariants.
From accnpx claudepluginhub dykyi-roman/awesome-claude-code --plugin accThis skill uses the workspace's default tool permissions.
This skill analyzes PHP DDD projects for immutability violations in Value Objects, Domain Events, DTOs, and Read Models. Immutability is crucial for maintaining invariants, thread safety, and predictable behavior.
| Type | Must Be Immutable | Key Checks |
|---|---|---|
| Value Object | ✅ Required | readonly, no setters, final |
| Domain Event | ✅ Required | readonly, no modification after creation |
| DTO | ✅ Recommended | readonly, no business logic |
| Read Model | ✅ Required | readonly, projection-only changes |
| Entity | ⚠️ Controlled | Setters via behavior methods only |
| Aggregate | ⚠️ Controlled | State changes via domain methods |
# Value Objects
Glob: **/ValueObject/**/*.php
Glob: **/Domain/**/*Value.php
Glob: **/Domain/**/*VO.php
Grep: "final.*class.*implements.*ValueObject" --glob "**/*.php"
# Domain Events
Glob: **/Event/**/*Event.php
Glob: **/Domain/**/*Event.php
Grep: "class.*Event\s*\{|final readonly class.*Event" --glob "**/*.php"
# DTOs
Glob: **/DTO/**/*.php
Glob: **/Application/**/*DTO.php
Glob: **/Application/**/*Request.php
Glob: **/Application/**/*Response.php
# Read Models
Glob: **/ReadModel/**/*.php
Glob: **/Projection/**/*.php
Grep: "class.*ReadModel|class.*View|class.*Projection" --glob "**/*.php"
# PHP 8.2+ readonly classes (best practice)
Grep: "readonly class|final readonly class" --glob "**/*.php"
# Missing readonly keyword
Grep: "final class.*ValueObject|final class.*Event|final class.*DTO" --glob "**/*.php"
# Should be: final readonly class
PHP 8.2+ Recommended Pattern:
// Good
final readonly class Email
{
public function __construct(
public string $value,
) {}
}
// Bad (pre-8.2 style)
final class Email
{
private string $value;
public function __construct(string $value)
{
$this->value = $value;
}
}
# Properties without readonly
Grep: "private string|private int|private float|private bool|private array" --glob "**/ValueObject/**/*.php"
Grep: "private string|private int|private float" --glob "**/Event/**/*.php"
# Expected: private readonly string, or use readonly class
# Public non-readonly properties (critical)
Grep: "public string|public int|public float|public bool" --glob "**/Domain/**/*.php"
# Should be: public readonly or private with getter
# Explicit setters in immutable types
Grep: "public function set[A-Z]" --glob "**/ValueObject/**/*.php"
Grep: "public function set[A-Z]" --glob "**/Event/**/*.php"
Grep: "public function set[A-Z]" --glob "**/DTO/**/*.php"
# Property assignment outside constructor
Grep: "\$this->[a-z]+ =" --glob "**/ValueObject/**/*.php"
# Check if inside __construct or not
# ArrayAccess modifications
Grep: "implements.*ArrayAccess" --glob "**/ValueObject/**/*.php"
# offsetSet should throw or return new instance
# Non-final Value Objects
Grep: "^class [A-Z].*ValueObject|^abstract class.*ValueObject" --glob "**/ValueObject/**/*.php"
# Should be final
# Non-final Events
Grep: "^class [A-Z].*Event\s*\{" --glob "**/Event/**/*.php"
# Should be final
# Non-final DTOs
Grep: "^class [A-Z].*DTO|^class [A-Z].*Request|^class [A-Z].*Response" --glob "**/DTO/**/*.php"
# Methods returning new instances (wither pattern)
Grep: "return new self\(|return new static\(" --glob "**/ValueObject/**/*.php"
# Methods that should use wither but mutate
Grep: "public function with[A-Z]" --glob "**/ValueObject/**/*.php" -A 5
# Check if returns new instance or mutates
# Missing wither methods
Grep: "public function update|public function change|public function modify" --glob "**/ValueObject/**/*.php"
# These should be wither methods returning new instance
Wither Pattern Example:
// Good (wither pattern)
final readonly class Money
{
public function __construct(
public int $amount,
public Currency $currency,
) {}
public function withAmount(int $amount): self
{
return new self($amount, $this->currency);
}
}
// Bad (mutation)
final class Money
{
public function setAmount(int $amount): void
{
$this->amount = $amount; // Mutation!
}
}
# Mutable array properties
Grep: "private array" --glob "**/ValueObject/**/*.php"
# Check for array_push, unset, etc.
# Collection modifications
Grep: "array_push|unset\(|\\$this->items\[\]" --glob "**/ValueObject/**/*.php"
# Missing array return by value
Grep: "return \$this->[a-z]+;" --glob "**/ValueObject/**/*.php"
# Arrays should be returned as copies or immutable iterators
# Using DateTime instead of DateTimeImmutable
Grep: "DateTime[^I]|\\\\DateTime " --glob "**/Domain/**/*.php"
Grep: "new DateTime\(" --glob "**/Domain/**/*.php"
# Expected: DateTimeImmutable
Grep: "DateTimeImmutable" --glob "**/Domain/**/*.php"
# Immutability Analysis Report
## Summary
| Type | Total | Fully Immutable | Issues |
|------|-------|-----------------|--------|
| Value Objects | 15 | 12 | 3 |
| Domain Events | 8 | 6 | 2 |
| DTOs | 10 | 8 | 2 |
| Read Models | 4 | 4 | 0 |
**Overall Immutability Score: 86%**
## Critical Issues
### IMM-001: Mutable Value Object
- **File:** `src/Domain/Order/ValueObject/Money.php`
- **Issue:** Public setter method found
- **Code:**
```php
public function setAmount(int $amount): void
{
$this->amount = $amount;
}
public function withAmount(int $amount): self
{
return new self($amount, $this->currency);
}
create-value-objectsrc/Domain/Order/Event/OrderCreatedEvent.phpreadonly, properties mutablefinal class OrderCreatedEvent
{
private string $orderId;
final readonly class OrderCreatedEvent
{
public function __construct(
public string $orderId,
create-domain-eventsrc/Domain/User/Entity/User.php:45private DateTime $createdAtprivate DateTimeImmutable $createdAtsrc/Domain/Shared/ValueObject/Address.phpfinalsrc/Domain/Order/ValueObject/OrderItems.php:34$this->items[] = $item;src/Application/DTO/CreateOrderDTO.phppublic string $customerId;
public array $items;
public readonly string $customerId,
public readonly array $items,
| Layer | Compliance | Notes |
|---|---|---|
| Domain/ValueObject | 80% | 3 VOs need refactoring |
| Domain/Event | 75% | 2 events need readonly |
| Application/DTO | 80% | 2 DTOs need readonly |
| Infrastructure/ReadModel | 100% | All compliant |
readonly keyword to all Value ObjectsDateTime with DateTimeImmutablewithAmount() to MoneywithItems() to OrderItemsfinal to all Value Objectsreadonly class for PHP 8.2+
## Immutability Patterns
### Fully Immutable Class (PHP 8.2+)
```php
final readonly class Email
{
public function __construct(
public string $value,
) {
$this->validate($value);
}
private function validate(string $value): void
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email');
}
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
}
final readonly class Money
{
public function __construct(
public int $amount,
public Currency $currency,
) {}
public function add(self $other): self
{
if (!$this->currency->equals($other->currency)) {
throw new CurrencyMismatchException();
}
return new self($this->amount + $other->amount, $this->currency);
}
public function withAmount(int $amount): self
{
return new self($amount, $this->currency);
}
}
final readonly class OrderItems
{
/** @param array<OrderItem> $items */
public function __construct(
private array $items,
) {}
public function add(OrderItem $item): self
{
return new self([...$this->items, $item]);
}
public function remove(OrderItem $item): self
{
return new self(
array_filter($this->items, fn($i) => !$i->equals($item))
);
}
/** @return array<OrderItem> */
public function toArray(): array
{
return $this->items;
}
}
# Check immutability
echo "=== Non-readonly Value Objects ===" && \
grep -rn "final class" --include="*.php" src/Domain/*/ValueObject/ | grep -v "readonly" && \
echo "=== Setters in Immutable Types ===" && \
grep -rn "public function set[A-Z]" --include="*.php" src/Domain/*/ValueObject/ src/Domain/*/Event/ && \
echo "=== Mutable DateTime ===" && \
grep -rn "DateTime[^I]" --include="*.php" src/Domain/ | grep -v "DateTimeImmutable" && \
echo "=== Array Mutations ===" && \
grep -rn "\$this->[a-z]*\[\]" --include="*.php" src/Domain/*/ValueObject/
Works with:
create-value-object — Generate immutable VOscreate-domain-event — Generate immutable eventscreate-dto — Generate immutable DTOsstructural-auditor — Architectural compliancebehavioral-auditor — Event Sourcing complianceProvides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.