acc-check-immutability
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.
Best use case
acc-check-immutability is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
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.
Teams using acc-check-immutability should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/acc-check-immutability/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How acc-check-immutability Compares
| Feature / Agent | acc-check-immutability | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
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.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Immutability Analyzer
## Overview
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.
## Immutability Requirements by Type
| 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 |
## Detection Patterns
### Phase 1: Identify Immutable Candidates
```bash
# 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"
```
### Phase 2: Readonly Class Check
```bash
# 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:**
```php
// 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;
}
}
```
### Phase 3: Readonly Properties Check
```bash
# 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
```
### Phase 4: Setter Detection
```bash
# 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
```
### Phase 5: Final Class Check
```bash
# 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"
```
### Phase 6: Wither Pattern Check
```bash
# 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:**
```php
// 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!
}
}
```
### Phase 7: Collection Immutability
```bash
# 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
```
### Phase 8: DateTimeImmutable Check
```bash
# 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"
```
## Report Format
```markdown
# 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;
}
```
- **Expected:** Use wither pattern
```php
public function withAmount(int $amount): self
{
return new self($amount, $this->currency);
}
```
- **Skills:** `acc-create-value-object`
### IMM-002: Non-readonly Event
- **File:** `src/Domain/Order/Event/OrderCreatedEvent.php`
- **Issue:** Class not marked as `readonly`, properties mutable
- **Code:**
```php
final class OrderCreatedEvent
{
private string $orderId;
```
- **Expected:**
```php
final readonly class OrderCreatedEvent
{
public function __construct(
public string $orderId,
```
- **Skills:** `acc-create-domain-event`
### IMM-003: DateTime Instead of DateTimeImmutable
- **File:** `src/Domain/User/Entity/User.php:45`
- **Issue:** Using mutable DateTime
- **Code:** `private DateTime $createdAt`
- **Expected:** `private DateTimeImmutable $createdAt`
- **Impact:** Date can be accidentally modified
## Warning Issues
### IMM-004: Non-final Value Object
- **File:** `src/Domain/Shared/ValueObject/Address.php`
- **Issue:** Class not marked as `final`
- **Impact:** Subclasses could break immutability contract
### IMM-005: Array Mutation
- **File:** `src/Domain/Order/ValueObject/OrderItems.php:34`
- **Issue:** Array property modified after construction
- **Code:** `$this->items[] = $item;`
- **Refactoring:** Return new collection instance
### IMM-006: Missing Readonly Properties
- **File:** `src/Application/DTO/CreateOrderDTO.php`
- **Issue:** Properties not readonly
- **Code:**
```php
public string $customerId;
public array $items;
```
- **Expected:**
```php
public readonly string $customerId,
public readonly array $items,
```
## Compliance by Layer
| 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 |
## Refactoring Recommendations
### Immediate Actions
1. Add `readonly` keyword to all Value Objects
2. Replace `DateTime` with `DateTimeImmutable`
3. Remove setters from Events and DTOs
### Wither Method Additions
4. Add `withAmount()` to Money
5. Add `withItems()` to OrderItems
### Class Modifiers
6. Add `final` to all Value Objects
7. Consider `readonly 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;
}
}
```
### Wither Pattern for Modifications
```php
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);
}
}
```
### Immutable Collection
```php
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;
}
}
```
## Quick Analysis Commands
```bash
# 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/
```
## Integration
Works with:
- `acc-create-value-object` — Generate immutable VOs
- `acc-create-domain-event` — Generate immutable events
- `acc-create-dto` — Generate immutable DTOs
- `acc-structural-auditor` — Architectural compliance
- `acc-behavioral-auditor` — Event Sourcing compliance
## References
- PHP 8.2 readonly classes RFC
- "Domain-Driven Design" (Eric Evans) — Value Objects chapter
- "Implementing DDD" (Vaughn Vernon) — Immutability patternsRelated Skills
adr-check
Validate code changes against Architecture Decision Records (ADRs)
add-strict-checks
Enable stricter TypeScript and linting checks to catch bugs early, especially useful when iterating with AI assistance.
add-check
Add a new code quality check to CI, justfile, and pre-commit hooks. Use when adding linters, formatters, type checkers, or other code quality tools to the project.
accessibility-design-checklist
Эксперт по accessibility дизайну. Используй для WCAG, a11y чеклистов и inclusive design.
accessibility-design-checker
Ensures designs meet accessibility requirements including WCAG compliance, color contrast, keyboard navigation, screen reader support, and focus management. Reviews designs for accessibility issues and provides recommendations.
accessibility-checklist
When building UI components, forms, or any user-facing interface. Check before every frontend PR.
accessibility-checker
Validate WCAG 2.1 Level AA compliance and accessibility best practices. Use when performing accessibility audits and WCAG certification.
accessibility-check
Run accessibility audit on frontend components for WCAG 2.1 AA compliance
accessibility-basic-check
Run a basic accessibility checklist for UI changes. Use when a junior developer needs quick a11y guidance.
acc-check-leaky-abstractions
Detects leaky abstractions in PHP code. Identifies implementation details exposed in interfaces, concrete returns from abstract methods, framework leakage into domain, and infrastructure concerns in application layer.
acc-check-encapsulation
Analyzes PHP code for encapsulation violations. Detects public mutable state, exposed internals, Tell Don't Ask violations, getter/setter abuse, and information hiding breaches.
acc-check-bounded-contexts
Analyzes bounded context boundaries in DDD projects. Detects cross-context coupling, shared kernel violations, context mapping issues, and ubiquitous language inconsistencies. Generates context map diagrams and boundary recommendations.