# 2623 PHP Modern Patterns Advanced Guide 2025 12 01 3b0ae60d

> PHP Modern Patterns & Advanced Guide (2025)

- Skill: `tools-only/2623-php-modern-patterns-advanced-guide-2025-12-01-3b0ae60d` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tools-only/2623-php-modern-patterns-advanced-guide-2025-12-01-3b0ae60d`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tools-only/2623-php-modern-patterns-advanced-guide-2025-12-01-3b0ae60d/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tools-only (https://skillmd.com/u/tools-only)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/tools-only/2623-php-modern-patterns-advanced-guide-2025-12-01-3b0ae60d

---

# PHP Modern Patterns & Advanced Guide (2025)

**Research Date:** December 1, 2025
**Focus:** PHP 8.3+, Laravel 11+, Modern Architecture Patterns
**Difficulty Level:** Moderate to Advanced

## Executive Summary

This research compiles modern PHP patterns, best practices, and code examples for PHP 8.3+ and Laravel 11+. It covers typed properties, enums, attributes, repository patterns, service layers, dependency injection, advanced Eloquent queries, and comprehensive testing strategies using Pest PHP.

**Key Findings:**
- PHP 8.3 introduces readonly class cloning, enhanced enums, and standalone types
- PHP 8.4 released November 2024, PHP 8.5 expected November 2025
- Laravel 11+ emphasizes Service-Action architecture for complex workflows
- Repository pattern with interface abstraction remains best practice for data access
- Pest PHP has become the leading testing framework with 30% less code than PHPUnit
- Type safety through union types, intersection types, and readonly properties is now standard

---

## Table of Contents

1. [Modern PHP 8.3+ Features](#1-modern-php-83-features)
2. [Laravel Advanced Patterns](#2-laravel-advanced-patterns)
3. [Dependency Injection & Service Container](#3-dependency-injection--service-container)
4. [Database & ORM Patterns](#4-database--orm-patterns)
5. [Testing & Quality Assurance](#5-testing--quality-assurance)
6. [WordPress Modern Patterns](#6-wordpress-modern-patterns)

---

## 1. Modern PHP 8.3+ Features

### 1.1 Typed Properties and Parameters

**PHP Version:** 8.0+
**Use Case:** Ensure type safety at compile time, prevent runtime errors
**Benefits:** IDE autocomplete, early error detection, self-documenting code

```php
<?php

declare(strict_types=1);

namespace App\Models;

use DateTimeImmutable;

class User
{
    // Typed properties (PHP 8.0+)
    private int $id;
    private string $email;
    private ?string $phoneNumber = null; // Nullable type
    private DateTimeImmutable $createdAt;
    private array $roles = []; // Array type

    // Promoted constructor properties (PHP 8.0+)
    public function __construct(
        private string $firstName,
        private string $lastName,
        private int $age,
    ) {
        $this->createdAt = new DateTimeImmutable();
    }

    // Return type declarations
    public function getFullName(): string
    {
        return "{$this->firstName} {$this->lastName}";
    }

    // Void return type
    public function setEmail(string $email): void
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException('Invalid email format');
        }

        $this->email = $email;
    }
}
```

**Type Safety Considerations:**
- Always use `declare(strict_types=1)` at the top of files
- Use nullable types (`?Type`) instead of type|null for clarity
- Avoid mixed types when possible
- Use union types for multiple allowed types

**Testing Approach:**
```php
test('user requires valid email format', function () {
    $user = new User('John', 'Doe', 30);

    expect(fn() => $user->setEmail('invalid-email'))
        ->toThrow(InvalidArgumentException::class);
});
```

---

### 1.2 Union and Intersection Types

**PHP Version:** 8.0+ (Union), 8.1+ (Intersection)
**Use Case:** Handle multiple types safely, enforce multiple constraints
**Benefits:** Type flexibility with safety, complex type requirements

```php
<?php

declare(strict_types=1);

namespace App\Services;

use App\Contracts\Loggable;
use App\Contracts\Cacheable;

class DataProcessor
{
    /**
     * Union types - accepts string OR int
     * PHP 8.0+
     */
    public function processId(string|int $id): string
    {
        return is_string($id) ? $id : (string) $id;
    }

    /**
     * Union with null - multiple nullable types
     */
    public function findUser(string|int|null $identifier): ?User
    {
        if ($identifier === null) {
            return null;
        }

        return is_int($identifier)
            ? User::find($identifier)
            : User::where('email', $identifier)->first();
    }

    /**
     * Intersection types - must implement BOTH interfaces
     * PHP 8.1+
     */
    public function processEntity(Loggable&Cacheable $entity): void
    {
        // Guaranteed to have both log() and cache() methods
        $entity->log('Processing started');
        $entity->cache();
    }

    /**
     * DNF (Disjunctive Normal Form) types
     * PHP 8.2+
     * Combines union and intersection
     */
    public function handleResource((Loggable&Cacheable)|null $resource): void
    {
        if ($resource !== null) {
            $resource->log('Handling resource');
            $resource->cache();
        }
    }
}
```

**Type Safety Considerations:**
- Union types are checked at runtime, not compile time
- Use type guards (`is_string()`, `is_int()`) to narrow types
- Intersection types are more strict than union types
- Cannot mix union and intersection freely (use DNF in PHP 8.2+)

**Laravel Integration:**
```php
// In routes or controllers
Route::get('/users/{id}', function (string|int $id) {
    return User::findOrFail($id);
});

// Form requests with union types
class UpdateUserRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'id' => ['required', 'string|integer'],
            'status' => ['required', new Enum(UserStatus::class)],
        ];
    }

    public function getId(): string|int
    {
        return $this->input('id');
    }
}
```

**Testing Approach:**
```php
test('processId handles both string and int', function () {
    $processor = new DataProcessor();

    expect($processor->processId(123))->toBe('123')
        ->and($processor->processId('abc'))->toBe('abc');
});

test('intersection type enforces both interfaces', function () {
    $entity = new class implements Loggable, Cacheable {
        public function log(string $message): void { }
        public function cache(): void { }
    };

    $processor = new DataProcessor();
    expect(fn() => $processor->processEntity($entity))->not->toThrow();
});
```

---

### 1.3 Enums and Readonly Classes

**PHP Version:** 8.1+ (Enums), 8.2+ (Readonly Classes)
**Use Case:** Type-safe constants, immutable value objects
**Benefits:** Prevents invalid states, IDE support, exhaustive matching

```php
<?php

declare(strict_types=1);

namespace App\Enums;

/**
 * Basic enum (backed by string)
 * PHP 8.1+
 */
enum UserStatus: string
{
    case ACTIVE = 'active';
    case INACTIVE = 'inactive';
    case SUSPENDED = 'suspended';
    case DELETED = 'deleted';

    /**
     * Enum methods - add behavior
     */
    public function label(): string
    {
        return match($this) {
            self::ACTIVE => 'Active User',
            self::INACTIVE => 'Inactive',
            self::SUSPENDED => 'Suspended',
            self::DELETED => 'Deleted',
        };
    }

    public function color(): string
    {
        return match($this) {
            self::ACTIVE => 'green',
            self::INACTIVE => 'gray',
            self::SUSPENDED => 'orange',
            self::DELETED => 'red',
        };
    }

    public function canLogin(): bool
    {
        return $this === self::ACTIVE;
    }

    /**
     * Static constructor
     */
    public static function fromString(string $status): self
    {
        return self::from($status); // Throws ValueError if invalid
    }

    public static function tryFromString(string $status): ?self
    {
        return self::tryFrom($status); // Returns null if invalid
    }
}

/**
 * Enum with interface implementation
 */
enum OrderStatus: string implements \JsonSerializable
{
    case PENDING = 'pending';
    case PROCESSING = 'processing';
    case SHIPPED = 'shipped';
    case DELIVERED = 'delivered';
    case CANCELLED = 'cancelled';

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

    public function isTerminal(): bool
    {
        return in_array($this, [self::DELIVERED, self::CANCELLED], true);
    }
}

/**
 * Pure enum (not backed)
 */
enum Permission
{
    case VIEW;
    case CREATE;
    case UPDATE;
    case DELETE;
    case ADMIN;
}
```

**Readonly Classes (PHP 8.2+):**

```php
<?php

declare(strict_types=1);

namespace App\ValueObjects;

/**
 * Readonly class - all properties are readonly
 * PHP 8.2+
 */
readonly class Money
{
    public function __construct(
        public int $amount,      // In cents
        public string $currency,
    ) {
        if ($amount < 0) {
            throw new \InvalidArgumentException('Amount cannot be negative');
        }

        if (strlen($currency) !== 3) {
            throw new \InvalidArgumentException('Currency must be 3-letter ISO code');
        }
    }

    public function add(Money $other): self
    {
        if ($this->currency !== $other->currency) {
            throw new \InvalidArgumentException('Cannot add different currencies');
        }

        return new self(
            $this->amount + $other->amount,
            $this->currency
        );
    }

    public function format(): string
    {
        $formatted = number_format($this->amount / 100, 2);
        return "{$this->currency} {$formatted}";
    }
}

/**
 * Readonly class with enum
 */
readonly class Address
{
    public function __construct(
        public string $street,
        public string $city,
        public string $postalCode,
        public Country $country, // Enum type
    ) {}

    public function toArray(): array
    {
        return [
            'street' => $this->street,
            'city' => $this->city,
            'postal_code' => $this->postalCode,
            'country' => $this->country->value,
        ];
    }
}

enum Country: string
{
    case USA = 'US';
    case CANADA = 'CA';
    case UK = 'GB';
    case GERMANY = 'DE';
}
```

**Laravel Integration:**

```php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Enums\UserStatus;

class User extends Model
{
    protected $casts = [
        'status' => UserStatus::class, // Auto-cast to enum
    ];

    public function activate(): void
    {
        $this->status = UserStatus::ACTIVE;
        $this->save();
    }

    public function canPerformAction(): bool
    {
        return $this->status->canLogin();
    }
}

// In migrations
Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('email')->unique();
    $table->enum('status', ['active', 'inactive', 'suspended', 'deleted'])
          ->default('active');
});

// In validation
public function rules(): array
{
    return [
        'status' => ['required', new Enum(UserStatus::class)],
    ];
}
```

**Type Safety Considerations:**
- Use backed enums (string/int) for database storage
- Enums are singletons - use `===` for comparison
- Readonly classes prevent accidental mutation
- Cannot use `clone` with readonly (PHP 8.3 allows in __clone)

**Testing Approach:**

```php
use App\Enums\UserStatus;
use App\ValueObjects\Money;

test('enum provides type safety', function () {
    $status = UserStatus::ACTIVE;

    expect($status->canLogin())->toBeTrue()
        ->and($status->label())->toBe('Active User')
        ->and($status->color())->toBe('green');
});

test('enum handles invalid values', function () {
    expect(fn() => UserStatus::from('invalid'))
        ->toThrow(ValueError::class);

    expect(UserStatus::tryFrom('invalid'))->toBeNull();
});

test('readonly class prevents mutation', function () {
    $money = new Money(1000, 'USD');

    expect(fn() => $money->amount = 2000)
        ->toThrow(Error::class, 'Cannot modify readonly property');
});

test('readonly class immutable operations', function () {
    $money1 = new Money(1000, 'USD');
    $money2 = new Money(500, 'USD');

    $total = $money1->add($money2);

    expect($total->amount)->toBe(1500)
        ->and($money1->amount)->toBe(1000); // Original unchanged
});
```

---

### 1.4 Attributes (Annotations)

**PHP Version:** 8.0+
**Use Case:** Metadata for classes, methods, properties without docblocks
**Benefits:** Native PHP support, reflection API access, framework integration

```php
<?php

declare(strict_types=1);

namespace App\Attributes;

use Attribute;

/**
 * Custom attribute definition
 * PHP 8.0+
 */
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_PROPERTY)]
class Cached
{
    public function __construct(
        public int $ttl = 3600,
        public ?string $key = null,
    ) {}
}

#[Attribute(Attribute::TARGET_CLASS)]
class Entity
{
    public function __construct(
        public string $table,
        public ?string $connection = null,
    ) {}
}

#[Attribute(Attribute::TARGET_PARAMETER)]
class Inject
{
    public function __construct(
        public string $service,
    ) {}
}

/**
 * Route attribute for controllers
 */
#[Attribute(Attribute::TARGET_METHOD)]
class Route
{
    public function __construct(
        public string $path,
        public string $method = 'GET',
        public array $middleware = [],
    ) {}
}
```

**Using Attributes:**

```php
namespace App\Services;

use App\Attributes\Cached;
use App\Attributes\Route;

class UserService
{
    /**
     * Cache method result for 1 hour
     */
    #[Cached(ttl: 3600, key: 'user_list')]
    public function getAllUsers(): array
    {
        return User::all()->toArray();
    }

    /**
     * Cache for 5 minutes
     */
    #[Cached(ttl: 300)]
    public function getUserById(int $id): ?User
    {
        return User::find($id);
    }
}

/**
 * Controller with route attributes
 */
#[Entity(table: 'users', connection: 'mysql')]
class UserController
{
    #[Route('/api/users', method: 'GET', middleware: ['auth'])]
    public function index(): array
    {
        return User::all()->toArray();
    }

    #[Route('/api/users/{id}', method: 'GET')]
    public function show(int $id): ?User
    {
        return User::find($id);
    }

    #[Route('/api/users', method: 'POST', middleware: ['auth', 'admin'])]
    public function store(#[Inject('validator')] ValidatorInterface $validator): User
    {
        // Use injected validator
        $data = $validator->validate(request()->all());
        return User::create($data);
    }
}
```

**Reading Attributes via Reflection:**

```php
namespace App\Core;

use ReflectionClass;
use ReflectionMethod;
use App\Attributes\Cached;
use App\Attributes\Route;

class AttributeReader
{
    public function getCacheConfig(object $instance, string $method): ?Cached
    {
        $reflection = new ReflectionMethod($instance, $method);
        $attributes = $reflection->getAttributes(Cached::class);

        if (empty($attributes)) {
            return null;
        }

        return $attributes[0]->newInstance();
    }

    public function getRoutes(string $controllerClass): array
    {
        $reflection = new ReflectionClass($controllerClass);
        $routes = [];

        foreach ($reflection->getMethods() as $method) {
            $attributes = $method->getAttributes(Route::class);

            foreach ($attributes as $attribute) {
                $route = $attribute->newInstance();
                $routes[] = [
                    'path' => $route->path,
                    'method' => $route->method,
                    'handler' => [$controllerClass, $method->getName()],
                    'middleware' => $route->middleware,
                ];
            }
        }

        return $routes;
    }
}

/**
 * Caching interceptor using attributes
 */
class CacheInterceptor
{
    public function __construct(
        private CacheInterface $cache,
        private AttributeReader $reader,
    ) {}

    public function intercept(object $instance, string $method, array $args): mixed
    {
        $cacheConfig = $this->reader->getCacheConfig($instance, $method);

        if ($cacheConfig === null) {
            // No caching - call method directly
            return $instance->$method(...$args);
        }

        $cacheKey = $cacheConfig->key ?? $this->generateKey($instance, $method, $args);

        // Check cache
        if ($this->cache->has($cacheKey)) {
            return $this->cache->get($cacheKey);
        }

        // Execute and cache
        $result = $instance->$method(...$args);
        $this->cache->set($cacheKey, $result, $cacheConfig->ttl);

        return $result;
    }

    private function generateKey(object $instance, string $method, array $args): string
    {
        return sprintf(
            '%s::%s:%s',
            get_class($instance),
            $method,
            md5(serialize($args))
        );
    }
}
```

**Laravel Integration:**

```php
// Laravel uses attributes for routes (Laravel 11+)
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Routing\Attribute\Route;
use Illuminate\Routing\Attribute\Middleware;

#[Middleware('auth')]
class UserController extends Controller
{
    #[Route('GET', '/users')]
    public function index(): array
    {
        return User::all()->toArray();
    }

    #[Route('POST', '/users')]
    #[Middleware('admin')]
    public function store(Request $request): User
    {
        return User::create($request->validated());
    }
}
```

**Type Safety Considerations:**
- Attributes are validated at instantiation, not declaration
- Use attribute targets to restrict usage (CLASS, METHOD, PROPERTY, etc.)
- Attributes can be repeatable with `Attribute::IS_REPEATABLE`
- Reflection adds runtime overhead - cache attribute reads

**Testing Approach:**

```php
use App\Attributes\Cached;
use App\Services\UserService;

test('attribute is correctly defined on method', function () {
    $reflection = new ReflectionMethod(UserService::class, 'getAllUsers');
    $attributes = $reflection->getAttributes(Cached::class);

    expect($attributes)->toHaveCount(1);

    $cached = $attributes[0]->newInstance();
    expect($cached->ttl)->toBe(3600)
        ->and($cached->key)->toBe('user_list');
});

test('cache interceptor uses attribute configuration', function () {
    $cache = Mockery::mock(CacheInterface::class);
    $reader = new AttributeReader();
    $interceptor = new CacheInterceptor($cache, $reader);

    $service = new UserService();

    $cache->shouldReceive('has')->with('user_list')->andReturn(false);
    $cache->shouldReceive('set')->with('user_list', Mockery::any(), 3600);

    $interceptor->intercept($service, 'getAllUsers', []);
});
```

---

### 1.5 Named Arguments

**PHP Version:** 8.0+
**Use Case:** Improve readability, skip optional parameters
**Benefits:** Self-documenting code, flexible parameter order

```php
<?php

declare(strict_types=1);

namespace App\Services;

class EmailService
{
    /**
     * Send email with many optional parameters
     */
    public function send(
        string $to,
        string $subject,
        string $body,
        ?string $from = null,
        ?string $replyTo = null,
        array $cc = [],
        array $bcc = [],
        array $attachments = [],
        bool $html = true,
        int $priority = 3,
    ): bool {
        // Implementation
        return true;
    }
}

// Traditional way - positional arguments
$email = new EmailService();
$email->send(
    'user@example.com',
    'Welcome',
    'Welcome to our platform',
    null, // from
    null, // replyTo
    [], // cc
    [], // bcc
    [], // attachments
    true, // html
    1 // priority - hard to understand
);

// Named arguments - much clearer
$email->send(
    to: 'user@example.com',
    subject: 'Welcome',
    body: 'Welcome to our platform',
    priority: 1, // Can skip optional params
    html: true,
);

// Can reorder named arguments
$email->send(
    body: 'Welcome to our platform',
    subject: 'Welcome',
    to: 'user@example.com',
);
```

**Named Arguments with Enums:**

```php
enum EmailPriority: int
{
    case LOW = 5;
    case NORMAL = 3;
    case HIGH = 1;
    case URGENT = 0;
}

class NotificationService
{
    public function notify(
        string $recipient,
        string $message,
        EmailPriority $priority = EmailPriority::NORMAL,
        bool $immediate = false,
    ): void {
        // Implementation
    }
}

// Very readable
$service = new NotificationService();
$service->notify(
    recipient: 'admin@example.com',
    message: 'System alert',
    priority: EmailPriority::URGENT,
    immediate: true,
);
```

**Laravel Integration:**

```php
// Validation rules with named arguments
validator(
    data: $request->all(),
    rules: [
        'email' => ['required', 'email', 'unique:users'],
        'password' => ['required', 'min:8'],
    ],
    messages: [
        'email.required' => 'Email is required',
        'password.min' => 'Password must be at least 8 characters',
    ],
);

// Query builder with named arguments
User::where(
    column: 'status',
    operator: '=',
    value: 'active',
)->get();

// More readable without operator
User::where(
    column: 'status',
    value: 'active',
)->get();

// Cache with named arguments
Cache::remember(
    key: "user_{$id}",
    ttl: 3600,
    callback: fn() => User::find($id),
);
```

**Type Safety Considerations:**
- Named arguments are checked at compile time
- Cannot mix required named args with positional args
- Great for reducing parameter object patterns
- Use with constructor property promotion for DTOs

**Testing Approach:**

```php
test('named arguments improve test readability', function () {
    $service = new EmailService();

    $result = $service->send(
        to: 'test@example.com',
        subject: 'Test',
        body: 'Test body',
        html: false,
        priority: 1,
    );

    expect($result)->toBeTrue();
});

test('can skip optional parameters with named arguments', function () {
    $service = new NotificationService();

    // Only provide required params
    expect(fn() => $service->notify(
        recipient: 'user@example.com',
        message: 'Hello',
    ))->not->toThrow();
});
```

---

## 2. Laravel Advanced Patterns

### 2.1 Service Layer Architecture

**Laravel Version:** 11+
**Use Case:** Separate business logic from controllers
**Benefits:** Reusability, testability, maintainability

**Architecture Flow:**
`Controller → Service → Action → Repository → Model`

```php
<?php

declare(strict_types=1);

namespace App\Services;

use App\Actions\CreateUserAction;
use App\Actions\UpdateUserAction;
use App\Actions\DeleteUserAction;
use App\Repositories\UserRepositoryInterface;
use App\DTOs\CreateUserData;
use App\DTOs\UpdateUserData;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;

/**
 * Service layer handles business logic
 * Orchestrates actions and repositories
 */
class UserService
{
    public function __construct(
        private UserRepositoryInterface $userRepository,
        private CreateUserAction $createUserAction,
        private UpdateUserAction $updateUserAction,
        private DeleteUserAction $deleteUserAction,
    ) {}

    /**
     * Register new user with email verification
     */
    public function registerUser(CreateUserData $data): User
    {
        return DB::transaction(function () use ($data) {
            // Business logic: Hash password
            $data->password = Hash::make($data->password);

            // Action: Create user
            $user = $this->createUserAction->execute($data);

            // Business logic: Send welcome email
            Mail::to($user->email)->send(new WelcomeEmail($user));

            // Business logic: Assign default role
            $user->assignRole('user');

            return $user;
        });
    }

    /**
     * Update user profile with validation
     */
    public function updateUserProfile(int $userId, UpdateUserData $data): User
    {
        $user = $this->userRepository->findOrFail($userId);

        // Business logic: Check email uniqueness
        if ($data->email && $data->email !== $user->email) {
            if ($this->userRepository->existsByEmail($data->email)) {
                throw new \DomainException('Email already in use');
            }
        }

        // Action: Update user
        return $this->updateUserAction->execute($user, $data);
    }

    /**
     * Deactivate user (soft delete with cleanup)
     */
    public function deactivateUser(int $userId): bool
    {
        return DB::transaction(function () use ($userId) {
            $user = $this->userRepository->findOrFail($userId);

            // Business logic: Clear sessions
            $user->tokens()->delete();

            // Business logic: Anonymize personal data
            $user->update([
                'email' => "deleted_{$user->id}@example.com",
                'phone' => null,
            ]);

            // Action: Delete user
            return $this->deleteUserAction->execute($user);
        });
    }

    /**
     * Get active users with caching
     */
    public function getActiveUsers(): Collection
    {
        return Cache::remember(
            key: 'active_users',
            ttl: 3600,
            callback: fn() => $this->userRepository->getActive(),
        );
    }
}
```

**Action Classes (Single Responsibility):**

```php
namespace App\Actions;

use App\Models\User;
use App\DTOs\CreateUserData;
use App\Repositories\UserRepositoryInterface;

/**
 * Action: Single-purpose operation
 * No business logic - just execution
 */
class CreateUserAction
{
    public function __construct(
        private UserRepositoryInterface $userRepository,
    ) {}

    public function execute(CreateUserData $data): User
    {
        return $this->userRepository->create([
            'name' => $data->name,
            'email' => $data->email,
            'password' => $data->password,
        ]);
    }
}

class UpdateUserAction
{
    public function __construct(
        private UserRepositoryInterface $userRepository,
    ) {}

    public function execute(User $user, UpdateUserData $data): User
    {
        return $this->userRepository->update($user, $data->toArray());
    }
}

class DeleteUserAction
{
    public function __construct(
        private UserRepositoryInterface $userRepository,
    ) {}

    public function execute(User $user): bool
    {
        return $this->userRepository->delete($user);
    }
}
```

**DTOs (Data Transfer Objects):**

```php
namespace App\DTOs;

use Illuminate\Http\Request;

/**
 * Readonly DTO with constructor property promotion
 * PHP 8.2+
 */
readonly class CreateUserData
{
    public function __construct(
        public string $name,
        public string $email,
        public string $password,
    ) {}

    public static function fromRequest(Request $request): self
    {
        return new self(
            name: $request->input('name'),
            email: $request->input('email'),
            password: $request->input('password'),
        );
    }
}

readonly class UpdateUserData
{
    public function __construct(
        public ?string $name = null,
        public ?string $email = null,
        public ?string $phone = null,
    ) {}

    public static function fromRequest(Request $request): self
    {
        return new self(
            name: $request->input('name'),
            email: $request->input('email'),
            phone: $request->input('phone'),
        );
    }

    public function toArray(): array
    {
        return array_filter([
            'name' => $this->name,
            'email' => $this->email,
            'phone' => $this->phone,
        ], fn($value) => $value !== null);
    }
}
```

**Controller (Thin Layer):**

```php
namespace App\Http\Controllers;

use App\Http\Requests\RegisterUserRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Services\UserService;
use Illuminate\Http\JsonResponse;

class UserController extends Controller
{
    public function __construct(
        private UserService $userService,
    ) {}

    /**
     * Controller only handles HTTP concerns
     */
    public function store(RegisterUserRequest $request): JsonResponse
    {
        $data = CreateUserData::fromRequest($request);
        $user = $this->userService->registerUser($data);

        return response()->json($user, 201);
    }

    public function update(UpdateUserRequest $request, int $id): JsonResponse
    {
        $data = UpdateUserData::fromRequest($request);
        $user = $this->userService->updateUserProfile($id, $data);

        return response()->json($user);
    }

    public function destroy(int $id): JsonResponse
    {
        $this->userService->deactivateUser($id);

        return response()->json(null, 204);
    }
}
```

**Type Safety Considerations:**
- Use readonly DTOs to prevent mutation
- Type-hint service dependencies in controllers
- Return specific types from service methods
- Use exceptions for business rule violations

**Testing Approach:**

```php
use App\Services\UserService;
use App\DTOs\CreateUserData;
use App\Repositories\UserRepositoryInterface;

test('service layer orchestrates user registration', function () {
    $repository = Mockery::mock(UserRepositoryInterface::class);
    $createAction = Mockery::mock(CreateUserAction::class);

    $service = new UserService(
        userRepository: $repository,
        createUserAction: $createAction,
        updateUserAction: Mockery::mock(UpdateUserAction::class),
        deleteUserAction: Mockery::mock(DeleteUserAction::class),
    );

    $data = new CreateUserData(
        name: 'John Doe',
        email: 'john@example.com',
        password: 'password123',
    );

    $createAction->shouldReceive('execute')
        ->once()
        ->with(Mockery::type(CreateUserData::class))
        ->andReturn(new User(['id' => 1, 'email' => 'john@example.com']));

    Mail::fake();

    $user = $service->registerUser($data);

    expect($user->email)->toBe('john@example.com');
    Mail::assertSent(WelcomeEmail::class);
});
```

---

### 2.2 Repository Pattern Implementation

**Laravel Version:** Any
**Use Case:** Abstract data access layer, swap implementations
**Benefits:** Testability, database independence, clear contracts

```php
<?php

declare(strict_types=1);

namespace App\Repositories;

use App\Models\User;
use Illuminate\Database\Eloquent\Collection;

/**
 * Repository interface (contract)
 * Defines data access operations
 */
interface UserRepositoryInterface
{
    public function find(int $id): ?User;
    public function findOrFail(int $id): User;
    public function getAll(): Collection;
    public function getActive(): Collection;
    public function create(array $data): User;
    public function update(User $user, array $data): User;
    public function delete(User $user): bool;
    public function existsByEmail(string $email): bool;
    public function findByEmail(string $email): ?User;
}

/**
 * Eloquent implementation
 */
class EloquentUserRepository implements UserRepositoryInterface
{
    public function find(int $id): ?User
    {
        return User::find($id);
    }

    public function findOrFail(int $id): User
    {
        return User::findOrFail($id);
    }

    public function getAll(): Collection
    {
        return User::all();
    }

    public function getActive(): Collection
    {
        return User::where('status', 'active')
            ->orderBy('created_at', 'desc')
            ->get();
    }

    public function create(array $data): User
    {
        return User::create($data);
    }

    public function update(User $user, array $data): User
    {
        $user->update($data);
        return $user->fresh();
    }

    public function delete(User $user): bool
    {
        return $user->delete();
    }

    public function existsByEmail(string $email): bool
    {
        return User::where('email', $email)->exists();
    }

    public function findByEmail(string $email): ?User
    {
        return User::where('email', $email)->first();
    }
}

/**
 * Cache decorator (adds caching layer)
 */
class CachedUserRepository implements UserRepositoryInterface
{
    public function __construct(
        private UserRepositoryInterface $repository,
    ) {}

    public function find(int $id): ?User
    {
        return Cache::remember(
            key: "user_{$id}",
            ttl: 3600,
            callback: fn() => $this->repository->find($id),
        );
    }

    public function findOrFail(int $id): User
    {
        return $this->find($id) ?? throw new ModelNotFoundException();
    }

    public function getActive(): Collection
    {
        return Cache::remember(
            key: 'active_users',
            ttl: 3600,
            callback: fn() => $this->repository->getActive(),
        );
    }

    public function create(array $data): User
    {
        $user = $this->repository->create($data);
        Cache::forget("user_{$user->id}");
        Cache::forget('active_users');
        return $user;
    }

    public function update(User $user, array $data): User
    {
        $updated = $this->repository->update($user, $data);
        Cache::forget("user_{$user->id}");
        Cache::forget('active_users');
        return $updated;
    }

    public function delete(User $user): bool
    {
        $result = $this->repository->delete($user);
        Cache::forget("user_{$user->id}");
        Cache::forget('active_users');
        return $result;
    }

    // Delegate other methods...
    public function getAll(): Collection
    {
        return $this->repository->getAll();
    }

    public function existsByEmail(string $email): bool
    {
        return $this->repository->existsByEmail($email);
    }

    public function findByEmail(string $email): ?User
    {
        return $this->repository->findByEmail($email);
    }
}
```

**Service Provider Registration:**

```php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Repositories\UserRepositoryInterface;
use App\Repositories\EloquentUserRepository;
use App\Repositories\CachedUserRepository;

class RepositoryServiceProvider extends ServiceProvider
{
    /**
     * Register repository bindings
     */
    public function register(): void
    {
        // Bind interface to implementation
        $this->app->bind(
            UserRepositoryInterface::class,
            EloquentUserRepository::class
        );

        // Or use singleton
        $this->app->singleton(
            UserRepositoryInterface::class,
            EloquentUserRepository::class
        );

        // With caching decorator
        $this->app->singleton(UserRepositoryInterface::class, function ($app) {
            $eloquentRepo = new EloquentUserRepository();
            return new CachedUserRepository($eloquentRepo);
        });
    }
}
```

**Advanced Repository with Specifications:**

```php
namespace App\Repositories\Specifications;

use Illuminate\Database\Eloquent\Builder;

/**
 * Specification pattern for complex queries
 */
interface Specification
{
    public function apply(Builder $query): Builder;
}

class ActiveUsersSpecification implements Specification
{
    public function apply(Builder $query): Builder
    {
        return $query->where('status', 'active');
    }
}

class CreatedAfterSpecification implements Specification
{
    public function __construct(
        private \DateTimeInterface $date,
    ) {}

    public function apply(Builder $query): Builder
    {
        return $query->where('created_at', '>=', $this->date);
    }
}

class HasRoleSpecification implements Specification
{
    public function __construct(
        private string $role,
    ) {}

    public function apply(Builder $query): Builder
    {
        return $query->whereHas('roles', fn($q) => $q->where('name', $this->role));
    }
}

/**
 * Enhanced repository with specifications
 */
class SpecificationUserRepository extends EloquentUserRepository
{
    public function findBySpecification(Specification ...$specifications): Collection
    {
        $query = User::query();

        foreach ($specifications as $spec) {
            $query = $spec->apply($query);
        }

        return $query->get();
    }
}

// Usage
$repository = new SpecificationUserRepository();
$users = $repository->findBySpecification(
    new ActiveUsersSpecification(),
    new CreatedAfterSpecification(new DateTime('-30 days')),
    new HasRoleSpecification('admin'),
);
```

**Type Safety Considerations:**
- Always define interface contracts
- Use dependency injection, not facade pattern
- Return specific types (User, Collection) not mixed
- Use readonly properties in specifications

**Testing Approach:**

```php
test('repository implements interface contract', function () {
    $repository = new EloquentUserRepository();

    expect($repository)->toBeInstanceOf(UserRepositoryInterface::class);
});

test('repository can swap implementations', function () {
    // Mock repository for testing
    $mockRepo = Mockery::mock(UserRepositoryInterface::class);
    $mockRepo->shouldReceive('getActive')->andReturn(collect([
        new User(['id' => 1, 'name' => 'Test']),
    ]));

    $this->app->instance(UserRepositoryInterface::class, $mockRepo);

    $service = app(UserService::class);
    $users = $service->getActiveUsers();

    expect($users)->toHaveCount(1);
});

test('specification pattern filters users correctly', function () {
    User::factory()->create(['status' => 'active']);
    User::factory()->create(['status' => 'inactive']);

    $repository = new SpecificationUserRepository();
    $users = $repository->findBySpecification(
        new ActiveUsersSpecification()
    );

    expect($users)->toHaveCount(1)
        ->and($users->first()->status)->toBe('active');
});
```

---

### 2.3 Pipeline Pattern

**Laravel Version:** Any (uses Illuminate\Pipeline)
**Use Case:** Chain operations, middleware-like processing
**Benefits:** Composability, reusability, clean separation

```php
<?php

declare(strict_types=1);

namespace App\Pipelines;

use Illuminate\Support\Facades\Pipeline;
use App\DTOs\CreateUserData;

/**
 * Pipeline example: Input sanitization
 * Each pipe transforms the data
 */
class UserRegistrationPipeline
{
    /**
     * Pipes to process user data
     */
    protected array $pipes = [
        TrimStrings::class,
        NormalizeEmail::class,
        ValidatePassword::class,
        CheckDuplicateEmail::class,
    ];

    public function process(CreateUserData $data): CreateUserData
    {
        return Pipeline::send($data)
            ->through($this->pipes)
            ->thenReturn();
    }
}

/**
 * Individual pipe: Trim strings
 */
class TrimStrings
{
    public function handle(CreateUserData $data, \Closure $next): CreateUserData
    {
        $trimmed = new CreateUserData(
            name: trim($data->name),
            email: trim($data->email),
            password: $data->password, // Don't trim password
        );

        return $next($trimmed);
    }
}

/**
 * Individual pipe: Normalize email
 */
class NormalizeEmail
{
    public function handle(CreateUserData $data, \Closure $next): CreateUserData
    {
        $normalized = new CreateUserData(
            name: $data->name,
            email: strtolower($data->email),
            password: $data->password,
        );

        return $next($normalized);
    }
}

/**
 * Individual pipe: Validate password strength
 */
class ValidatePassword
{
    public function handle(CreateUserData $data, \Closure $next): CreateUserData
    {
        if (strlen($data->password) < 8) {
            throw new \DomainException('Password must be at least 8 characters');
        }

        if (!preg_match('/[A-Z]/', $data->password)) {
            throw new \DomainException('Password must contain uppercase letter');
        }

        if (!preg_match('/[0-9]/', $data->password)) {
            throw new \DomainException('Password must contain number');
        }

        return $next($data);
    }
}

/**
 * Individual pipe: Check duplicate email
 */
class CheckDuplicateEmail
{
    public function __construct(
        pr

…(truncated)
