php-patterns

PHP 8.4+ patterns: readonly classes/properties, enums, named arguments, match expressions, value objects, repository pattern, service layer with command/handler, Laravel controller/FormRequest, Symfony service wiring, Doctrine QueryBuilder, Fiber async. Use when writing or reviewing PHP code.

8 stars

Best use case

php-patterns is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

PHP 8.4+ patterns: readonly classes/properties, enums, named arguments, match expressions, value objects, repository pattern, service layer with command/handler, Laravel controller/FormRequest, Symfony service wiring, Doctrine QueryBuilder, Fiber async. Use when writing or reviewing PHP code.

Teams using php-patterns 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

$curl -o ~/.claude/skills/php-patterns/SKILL.md --create-dirs "https://raw.githubusercontent.com/marvinrichter/clarc/main/skills/php-patterns/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/php-patterns/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How php-patterns Compares

Feature / Agentphp-patternsStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

PHP 8.4+ patterns: readonly classes/properties, enums, named arguments, match expressions, value objects, repository pattern, service layer with command/handler, Laravel controller/FormRequest, Symfony service wiring, Doctrine QueryBuilder, Fiber async. Use when writing or reviewing PHP code.

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

# PHP Patterns

## When to Activate

- Writing PHP 8.4+ source files (`.php`)
- Designing service/repository layers
- Reviewing PHP code for idiomatic patterns
- Working with Laravel or Symfony frameworks
- Replacing stringly-typed status comparisons with backed enums and `match` expressions for exhaustive, type-safe branching
- Modeling domain value objects as `readonly` classes to enforce immutability and input validation at construction time
- Structuring command/handler pairs to keep controllers thin and business logic independently testable

---

## PHP 8.4+ Language Features

### Readonly Classes

```php
<?php

declare(strict_types=1);

// Entire class is readonly — all properties implicitly readonly
final readonly class Money
{
    public function __construct(
        public int $amount,
        public string $currency,
    ) {}

    public function add(Money $other): self
    {
        if ($this->currency !== $other->currency) {
            throw new \InvalidArgumentException('Currency mismatch');
        }
        return new self($this->amount + $other->amount, $this->currency);
    }
}
```

### Enums with Methods

```php
<?php

declare(strict_types=1);

enum OrderStatus: string
{
    case Pending    = 'pending';
    case Processing = 'processing';
    case Shipped    = 'shipped';
    case Delivered  = 'delivered';
    case Cancelled  = 'cancelled';

    public function canTransitionTo(self $next): bool
    {
        return match($this) {
            self::Pending    => in_array($next, [self::Processing, self::Cancelled]),
            self::Processing => in_array($next, [self::Shipped, self::Cancelled]),
            self::Shipped    => $next === self::Delivered,
            default          => false,
        };
    }

    public function label(): string
    {
        return match($this) {
            self::Pending    => 'Awaiting processing',
            self::Processing => 'Being processed',
            self::Shipped    => 'On the way',
            self::Delivered  => 'Delivered',
            self::Cancelled  => 'Cancelled',
        };
    }
}

// Usage
$status = OrderStatus::from($row['status']);
$status->canTransitionTo(OrderStatus::Shipped); // bool
```

### Named Arguments

```php
// WRONG — positional, order-dependent
$result = array_slice($items, 0, 5, true);

// CORRECT — named, self-documenting
$result = array_slice(array: $items, offset: 0, length: 5, preserve_keys: true);
```

### Match Expression (exhaustive)

```php
$discount = match(true) {
    $order->total >= 1000 => 0.15,
    $order->total >= 500  => 0.10,
    $order->total >= 100  => 0.05,
    default               => 0.00,
};
```

---

## Value Objects

```php
<?php

declare(strict_types=1);

final readonly class Email
{
    public string $value;

    public function __construct(string $value)
    {
        $normalized = strtolower(trim($value));
        if (!filter_var($normalized, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException("Invalid email: {$value}");
        }
        $this->value = $normalized;
    }

    public function equals(self $other): bool
    {
        return $this->value === $other->value;
    }

    public function __toString(): string
    {
        return $this->value;
    }
}

final readonly class UserId
{
    public function __construct(public int $value)
    {
        if ($value <= 0) {
            throw new \InvalidArgumentException('UserId must be positive');
        }
    }
}
```

---

## Repository Pattern

```php
<?php

declare(strict_types=1);

interface UserRepository
{
    public function findById(UserId $id): ?User;
    public function findByEmail(Email $email): ?User;
    /** @return User[] */
    public function findAll(int $limit, int $offset): array;
    public function save(User $user): void;
    public function remove(UserId $id): void;
}

// Doctrine implementation
final class DoctrineUserRepository implements UserRepository
{
    public function __construct(private readonly EntityManagerInterface $em) {}

    public function findById(UserId $id): ?User
    {
        return $this->em->find(User::class, $id->value);
    }

    public function findByEmail(Email $email): ?User
    {
        return $this->em->getRepository(User::class)
            ->findOneBy(['email' => $email->value]);
    }

    public function save(User $user): void
    {
        $this->em->persist($user);
        $this->em->flush();
    }
}
```

---

## Command / Handler (CQRS-lite)

```php
<?php

declare(strict_types=1);

// Command — immutable data bag
final readonly class RegisterUserCommand
{
    public function __construct(
        public string $name,
        public string $email,
        public string $password,
    ) {}
}

// Handler — single responsibility
final class RegisterUserHandler
{
    public function __construct(
        private readonly UserRepository $users,
        private readonly PasswordHasher $hasher,
        private readonly EventBus $events,
    ) {}

    public function handle(RegisterUserCommand $cmd): User
    {
        $email = new Email($cmd->email);

        if ($this->users->findByEmail($email) !== null) {
            throw new DuplicateEmailException($email);
        }

        $user = User::register(
            name: $cmd->name,
            email: $email,
            passwordHash: $this->hasher->hash($cmd->password),
        );

        $this->users->save($user);
        $this->events->dispatch(new UserRegistered($user->getId()));

        return $user;
    }
}
```

---

## Laravel Patterns

### Thin Controller

```php
<?php

declare(strict_types=1);

class UserController extends Controller
{
    public function store(
        RegisterUserRequest $request,
        RegisterUserHandler $handler,
    ): JsonResponse {
        $user = $handler->handle(new RegisterUserCommand(
            name: $request->string('name'),
            email: $request->string('email'),
            password: $request->string('password'),
        ));

        return response()->json([
            'data' => ['id' => $user->getId(), 'email' => (string) $user->getEmail()],
        ], 201);
    }
}
```

### FormRequest Validation

```php
<?php

declare(strict_types=1);

class RegisterUserRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name'     => ['required', 'string', 'max:255'],
            'email'    => ['required', 'email:rfc,dns', 'unique:users,email'],
            'password' => ['required', 'string', 'min:12', 'confirmed'],
        ];
    }
}
```

### Eloquent with Casting

```php
<?php

class User extends Model
{
    protected $casts = [
        'status'     => OrderStatus::class,     // enum cast
        'created_at' => 'datetime',
        'settings'   => 'array',
    ];
}
```

---

## Symfony Service Wiring

```yaml
# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true

    App\Repository\UserRepository:
        class: App\Infrastructure\Doctrine\DoctrineUserRepository

    App\Handler\RegisterUserHandler: ~
```

```php
// Controller with Symfony
#[Route('/users', methods: ['POST'])]
public function register(Request $request, RegisterUserHandler $handler): JsonResponse
{
    $cmd = new RegisterUserCommand(
        name: $request->request->getString('name'),
        email: $request->request->getString('email'),
        password: $request->request->getString('password'),
    );

    $user = $handler->handle($cmd);

    return $this->json(['id' => $user->getId()], 201);
}
```

---

## Anti-Patterns

| Anti-Pattern | Problem | Better |
|---|---|---|
| Fat controller (>30 lines) | Violates SRP | Extract service/handler |
| Active Record god object | Business logic in model | Domain model + repository |
| `$_GET`/`$_POST` in service | Not testable | Inject validated DTO |
| `array` return types without shape | No IDE support | Use typed objects or `@return T[]` |
| `null` return without `?Type` hint | Silent contract violation | Declare `?ReturnType` |
| String `'active'` comparisons | Fragile | Enum with `Status::from()` |

Related Skills

zero-trust-patterns

8
from marvinrichter/clarc

Zero-Trust security patterns — mTLS between microservices (Istio/SPIFFE), SPIRE workload identity, OPA/Envoy authorization, NetworkPolicy default-deny-all, short-lived credentials, service mesh security, and Kubernetes RBAC hardening.

webrtc-patterns

8
from marvinrichter/clarc

WebRTC patterns — peer connection setup, ICE/STUN/TURN configuration, signaling server design, SFU vs mesh topology, screen sharing, media track management, and reconnect/ICE restart handling.

webhook-patterns

8
from marvinrichter/clarc

Webhook patterns for receiving, verifying (HMAC), and idempotently processing third-party events. Covers Stripe, GitHub, and generic webhook patterns, delivery guarantees, retry handling, and testing.

wasm-patterns

8
from marvinrichter/clarc

WebAssembly patterns: wasm-pack, wasm-bindgen (JS↔Wasm interop), WASI, Component Model, wasm-opt, Rust-to-WASM compilation, JS integration (web workers, streaming instantiation), and production deployment (CDN, Content-Type headers).

ux-micro-patterns

8
from marvinrichter/clarc

UX micro-patterns for every product state: Empty States, Loading States (skeleton screens, spinners, optimistic UI), Error States, Success States, Confirmation Dialogs, Onboarding Flows, and Progressive Disclosure. These patterns apply to every feature — done wrong, they're the biggest source of user confusion.

typescript-patterns

8
from marvinrichter/clarc

TypeScript patterns — type system best practices, strict mode, utility types, generics, discriminated unions, error handling with Result types, and module organization. Core patterns for production TypeScript.

typescript-patterns-advanced

8
from marvinrichter/clarc

Advanced TypeScript — mapped types, template literal types, conditional types, infer, type guards, decorators, async patterns, testing with Vitest/Jest, and performance. Extends typescript-patterns.

typescript-monorepo-patterns

8
from marvinrichter/clarc

TypeScript monorepo patterns with Turborepo + pnpm workspaces. Covers package structure, shared configs, task pipeline caching, build orchestration, and publishing strategy.

terraform-patterns

8
from marvinrichter/clarc

Infrastructure as Code with Terraform — project structure, remote state, modules, workspace strategy, AWS/GCP patterns, CI/CD integration, and security hardening. The standard for managing production infrastructure.

swiftui-patterns

8
from marvinrichter/clarc

SwiftUI architecture patterns, state management with @Observable, view composition, navigation, performance optimization, and modern iOS/macOS UI best practices.

swift-patterns

8
from marvinrichter/clarc

Core Swift patterns — value vs reference types, protocols, generics, optionals, Result, error handling, Codable, and module organization. Foundation for all Swift development.

swift-patterns-advanced

8
from marvinrichter/clarc

Advanced Swift patterns — property wrappers, result builders, Combine basics, opaque & existential types, macro system, advanced generics, and performance optimization. Extends swift-patterns.