Back to skills
SkillHub ClubShip Full StackFull Stack

php-pro

Use when building PHP applications with modern PHP 8.3+ features, Laravel, or Symfony frameworks. Invoke for strict typing, PHPStan level 9, async patterns with Swoole, PSR standards.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
7,006
Hot score
99
Updated
March 20, 2026
Overall rating
C5.6
Composite score
5.6
Best-practice grade
B75.9

Install command

npx @skill-hub/cli install jeffallan-claude-skills-php-pro

Repository

Jeffallan/claude-skills

Skill path: skills/php-pro

Use when building PHP applications with modern PHP 8.3+ features, Laravel, or Symfony frameworks. Invoke for strict typing, PHPStan level 9, async patterns with Swoole, PSR standards.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: Jeffallan.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install php-pro into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/Jeffallan/claude-skills before adding php-pro to shared team environments
  • Use php-pro for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: php-pro
description: Use when building PHP applications with modern PHP 8.3+ features, Laravel, or Symfony frameworks. Invoke for strict typing, PHPStan level 9, async patterns with Swoole, PSR standards.
triggers:
  - PHP
  - Laravel
  - Symfony
  - Composer
  - PHPStan
  - PSR
  - PHP API
  - Eloquent
  - Doctrine
role: specialist
scope: implementation
output-format: code
---

# PHP Pro

Senior PHP developer with deep expertise in PHP 8.3+, Laravel, Symfony, and modern PHP patterns with strict typing and enterprise architecture.

## Role Definition

You are a senior PHP developer with 10+ years of experience building enterprise applications. You specialize in PHP 8.3+ with strict typing, Laravel/Symfony frameworks, async patterns (Swoole, ReactPHP), and PSR standards. You build scalable, maintainable applications with PHPStan level 9 compliance and 80%+ test coverage.

## When to Use This Skill

- Building Laravel or Symfony applications
- Implementing strict type systems with PHPStan
- Creating async PHP applications with Swoole/ReactPHP
- Designing clean architecture with DDD patterns
- Optimizing performance (OpCache, JIT, queries)
- Writing comprehensive PHPUnit tests

## Core Workflow

1. **Analyze architecture** - Review framework, PHP version, dependencies, patterns
2. **Design models** - Create typed domain models, value objects, DTOs
3. **Implement** - Write strict-typed code with PSR compliance, DI, repositories
4. **Secure** - Add validation, authentication, XSS/SQL injection protection
5. **Test & optimize** - PHPUnit tests, PHPStan level 9, performance tuning

## Reference Guide

Load detailed guidance based on context:

| Topic | Reference | Load When |
|-------|-----------|-----------|
| Modern PHP | `references/modern-php-features.md` | Readonly, enums, attributes, fibers, types |
| Laravel | `references/laravel-patterns.md` | Services, repositories, resources, jobs |
| Symfony | `references/symfony-patterns.md` | DI, events, commands, voters |
| Async PHP | `references/async-patterns.md` | Swoole, ReactPHP, fibers, streams |
| Testing | `references/testing-quality.md` | PHPUnit, PHPStan, Pest, mocking |

## Constraints

### MUST DO
- Declare strict types (`declare(strict_types=1)`)
- Use type hints for all properties, parameters, returns
- Follow PSR-12 coding standard
- Run PHPStan level 9 before delivery
- Use readonly properties where applicable
- Write PHPDoc blocks for complex logic
- Validate all user input with typed requests
- Use dependency injection over global state

### MUST NOT DO
- Skip type declarations (no mixed types)
- Use deprecated features or Pydantic V1 patterns
- Store passwords in plain text (use bcrypt/argon2)
- Write SQL queries vulnerable to injection
- Mix business logic with controllers
- Hardcode configuration (use .env)
- Deploy without running tests and static analysis
- Use var_dump in production code

## Output Templates

When implementing PHP features, provide:
1. Domain models (entities, value objects)
2. Service/repository classes
3. Controller/API endpoints
4. Test files (PHPUnit)
5. Brief explanation of architecture decisions

## Knowledge Reference

PHP 8.3+, Laravel 11, Symfony 7, Composer, PHPStan, Psalm, PHPUnit, Pest, Eloquent ORM, Doctrine, PSR standards, Swoole, ReactPHP, Redis, MySQL/PostgreSQL, REST/GraphQL APIs

## Related Skills

- **Fullstack Guardian** - Full-stack feature implementation
- **FastAPI Expert** - Alternative Python framework patterns
- **MySQL Expert** - Database optimization


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/modern-php-features.md

```markdown
# Modern PHP 8.3+ Features

> Reference for: PHP Pro
> Load when: Using PHP 8.3 features, strict typing, enums, readonly, attributes

## Strict Types & Type Declarations

```php
<?php

declare(strict_types=1);

namespace App\Domain\User;

final readonly class User
{
    public function __construct(
        public int $id,
        public string $email,
        public UserStatus $status,
        public \DateTimeImmutable $createdAt,
    ) {}
}

function calculateTotal(int $price, float $taxRate): float
{
    return $price * (1 + $taxRate);
}

// Union types
function processId(int|string $id): string
{
    return is_int($id) ? (string)$id : $id;
}

// Intersection types
interface Timestamped {}
interface Authenticatable {}

function handleUser(Timestamped&Authenticatable $user): void {}
```

## Enums with Methods

```php
<?php

declare(strict_types=1);

enum UserStatus: string
{
    case ACTIVE = 'active';
    case SUSPENDED = 'suspended';
    case DELETED = 'deleted';

    public function label(): string
    {
        return match($this) {
            self::ACTIVE => 'Active User',
            self::SUSPENDED => 'Suspended',
            self::DELETED => 'Deleted User',
        };
    }

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

    public static function fromString(string $value): self
    {
        return self::from(strtolower($value));
    }
}

enum HttpStatus: int
{
    case OK = 200;
    case CREATED = 201;
    case BAD_REQUEST = 400;
    case UNAUTHORIZED = 401;
    case NOT_FOUND = 404;
    case SERVER_ERROR = 500;

    public function isSuccess(): bool
    {
        return $this->value >= 200 && $this->value < 300;
    }
}
```

## Readonly Properties & Classes

```php
<?php

declare(strict_types=1);

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

    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);
    }
}

// Individual readonly properties
class Configuration
{
    public function __construct(
        public readonly string $apiKey,
        public readonly string $apiSecret,
        private string $cache = '',
    ) {}
}
```

## Attributes (Metadata)

```php
<?php

declare(strict_types=1);

#[\Attribute(\Attribute::TARGET_CLASS)]
final readonly class Route
{
    public function __construct(
        public string $path,
        public string $method = 'GET',
        public array $middleware = [],
    ) {}
}

#[\Attribute(\Attribute::TARGET_PROPERTY)]
final readonly class Validate
{
    public function __construct(
        public ?string $rule = null,
        public ?int $min = null,
        public ?int $max = null,
    ) {}
}

// Using attributes
#[Route('/api/users', method: 'POST', middleware: ['auth'])]
final class CreateUserController
{
    public function __invoke(CreateUserRequest $request): JsonResponse
    {
        // ...
    }
}

class UserDto
{
    #[Validate(rule: 'email')]
    public string $email;

    #[Validate(min: 8, max: 100)]
    public string $password;
}
```

## First-Class Callables

```php
<?php

declare(strict_types=1);

class UserService
{
    public function findById(int $id): ?User {}
    public function create(array $data): User {}
}

$service = new UserService();

// PHP 8.1+ first-class callable syntax
$finder = $service->findById(...);
$user = $finder(42);

// Array operations
$numbers = [1, 2, 3, 4, 5];
$doubled = array_map(fn($n) => $n * 2, $numbers);

// Named arguments with callable
$result = array_filter(
    array: $numbers,
    callback: fn($n) => $n % 2 === 0,
);
```

## Match Expressions

```php
<?php

declare(strict_types=1);

function getStatusColor(UserStatus $status): string
{
    return match ($status) {
        UserStatus::ACTIVE => 'green',
        UserStatus::SUSPENDED => 'yellow',
        UserStatus::DELETED => 'red',
    };
}

function calculateShipping(int $weight, string $zone): float
{
    return match (true) {
        $weight < 1000 => 5.00,
        $weight < 5000 && $zone === 'local' => 10.00,
        $weight < 5000 => 15.00,
        default => 25.00,
    };
}

// Match with multiple conditions
function getHttpMessage(int $code): string
{
    return match ($code) {
        200, 201, 204 => 'Success',
        400, 422 => 'Client Error',
        401, 403 => 'Unauthorized',
        500, 502, 503 => 'Server Error',
        default => 'Unknown',
    };
}
```

## Fibers (PHP 8.1+)

```php
<?php

declare(strict_types=1);

// Basic fiber example
$fiber = new \Fiber(function (): void {
    $value = \Fiber::suspend('fiber started');
    echo "Received: {$value}\n";
    \Fiber::suspend('second suspend');
    echo "Fiber completed\n";
});

$result1 = $fiber->start();
echo "First result: {$result1}\n";

$result2 = $fiber->resume('data from main');
echo "Second result: {$result2}\n";

$fiber->resume('final data');

// Async-style with fibers
function async(callable $callback): \Fiber
{
    return new \Fiber($callback);
}

function await(\Fiber $fiber): mixed
{
    if (!$fiber->isStarted()) {
        return $fiber->start();
    }
    return $fiber->resume();
}
```

## Never Type

```php
<?php

declare(strict_types=1);

function redirect(string $url): never
{
    header("Location: {$url}");
    exit;
}

function abort(int $code, string $message): never
{
    http_response_code($code);
    echo json_encode(['error' => $message]);
    exit;
}

class NotFoundException extends \Exception
{
    public static function throw(string $resource): never
    {
        throw new self("Resource not found: {$resource}");
    }
}
```

## Quick Reference

| Feature | PHP Version | Usage |
|---------|-------------|-------|
| Readonly properties | 8.1+ | `public readonly string $name` |
| Readonly classes | 8.2+ | `readonly class User {}` |
| Enums | 8.1+ | `enum Status: string {}` |
| First-class callables | 8.1+ | `$fn = $obj->method(...)` |
| Never type | 8.1+ | `function exit(): never` |
| Fibers | 8.1+ | `new \Fiber(fn() => ...)` |
| Pure intersection types | 8.1+ | `A&B $param` |
| DNF types | 8.2+ | `(A&B)|C $param` |
| Constants in traits | 8.2+ | `trait T { const X = 1; }` |

```

### references/laravel-patterns.md

```markdown
# Laravel Patterns

> Reference for: PHP Pro
> Load when: Laravel development, repositories, services, resources, jobs

## Service Layer Pattern

```php
<?php

declare(strict_types=1);

namespace App\Services;

use App\DTOs\CreateUserData;
use App\Models\User;
use App\Repositories\UserRepositoryInterface;
use Illuminate\Support\Facades\Hash;

final readonly class UserService
{
    public function __construct(
        private UserRepositoryInterface $userRepository,
        private EmailService $emailService,
    ) {}

    public function createUser(CreateUserData $data): User
    {
        $user = $this->userRepository->create([
            'name' => $data->name,
            'email' => $data->email,
            'password' => Hash::make($data->password),
        ]);

        $this->emailService->sendWelcomeEmail($user);

        return $user;
    }

    public function suspendUser(int $userId, string $reason): void
    {
        $user = $this->userRepository->findOrFail($userId);

        $this->userRepository->update($user->id, [
            'status' => UserStatus::SUSPENDED,
            'suspension_reason' => $reason,
            'suspended_at' => now(),
        ]);

        $this->emailService->sendSuspensionNotice($user, $reason);
    }
}
```

## Repository Pattern

```php
<?php

declare(strict_types=1);

namespace App\Repositories;

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

interface UserRepositoryInterface
{
    public function findOrFail(int $id): User;
    public function findByEmail(string $email): ?User;
    public function create(array $data): User;
    public function update(int $id, array $data): User;
    public function delete(int $id): void;
    public function getActive(): Collection;
}

final class UserRepository implements UserRepositoryInterface
{
    public function findOrFail(int $id): User
    {
        return User::findOrFail($id);
    }

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

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

    public function update(int $id, array $data): User
    {
        $user = $this->findOrFail($id);
        $user->update($data);
        return $user->fresh();
    }

    public function delete(int $id): void
    {
        $this->findOrFail($id)->delete();
    }

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

## Form Requests with Enums

```php
<?php

declare(strict_types=1);

namespace App\Http\Requests;

use App\Enums\UserRole;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\Rules\Password;

final class CreateUserRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()?->can('create', User::class) ?? false;
    }

    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', 'unique:users,email'],
            'password' => ['required', Password::min(8)->mixedCase()->numbers()],
            'role' => ['required', new Enum(UserRole::class)],
            'settings' => ['sometimes', 'array'],
            'settings.theme' => ['string', Rule::in(['light', 'dark'])],
        ];
    }

    public function toDto(): CreateUserData
    {
        return new CreateUserData(
            name: $this->validated('name'),
            email: $this->validated('email'),
            password: $this->validated('password'),
            role: UserRole::from($this->validated('role')),
        );
    }
}
```

## API Resources

```php
<?php

declare(strict_types=1);

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

/**
 * @mixin \App\Models\User
 */
final class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'status' => $this->status->value,
            'role' => $this->role->value,
            'created_at' => $this->created_at->toIso8601String(),

            // Conditional relationships
            'posts' => PostResource::collection($this->whenLoaded('posts')),
            'profile' => new ProfileResource($this->whenLoaded('profile')),

            // Conditional attributes
            'is_admin' => $this->when($this->role === UserRole::ADMIN, true),

            // Pivot data
            'team_role' => $this->whenPivotLoaded('team_user', fn() =>
                $this->pivot->role
            ),
        ];
    }
}

final class UserCollection extends ResourceCollection
{
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'meta' => [
                'total' => $this->total(),
                'per_page' => $this->perPage(),
            ],
        ];
    }
}
```

## Controllers with DTOs

```php
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\CreateUserRequest;
use App\Http\Resources\UserResource;
use App\Services\UserService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

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

    public function index(): AnonymousResourceCollection
    {
        $users = User::with('profile')
            ->where('status', UserStatus::ACTIVE)
            ->paginate(20);

        return UserResource::collection($users);
    }

    public function store(CreateUserRequest $request): JsonResponse
    {
        $user = $this->userService->createUser($request->toDto());

        return (new UserResource($user))
            ->response()
            ->setStatusCode(201);
    }

    public function show(User $user): UserResource
    {
        $user->load(['posts', 'profile']);
        return new UserResource($user);
    }

    public function destroy(User $user): JsonResponse
    {
        $this->authorize('delete', $user);

        $this->userService->deleteUser($user->id);

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

## Jobs & Queues

```php
<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Models\User;
use App\Services\EmailService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

final class SendWelcomeEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $timeout = 30;

    public function __construct(
        private readonly int $userId,
    ) {}

    public function handle(EmailService $emailService): void
    {
        $user = User::findOrFail($this->userId);
        $emailService->sendWelcomeEmail($user);
    }

    public function failed(\Throwable $exception): void
    {
        \Log::error('Failed to send welcome email', [
            'user_id' => $this->userId,
            'error' => $exception->getMessage(),
        ]);
    }
}

// Dispatching jobs
SendWelcomeEmail::dispatch($user->id);
SendWelcomeEmail::dispatch($user->id)->delay(now()->addMinutes(5));
SendWelcomeEmail::dispatch($user->id)->onQueue('emails');
```

## Event Listeners

```php
<?php

declare(strict_types=1);

namespace App\Events;

use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

final readonly class UserRegistered
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public User $user,
    ) {}
}

namespace App\Listeners;

use App\Events\UserRegistered;
use App\Jobs\SendWelcomeEmail;
use Illuminate\Contracts\Queue\ShouldQueue;

final class SendWelcomeNotification implements ShouldQueue
{
    public function handle(UserRegistered $event): void
    {
        SendWelcomeEmail::dispatch($event->user->id);
    }
}

// In EventServiceProvider
protected $listen = [
    UserRegistered::class => [
        SendWelcomeNotification::class,
        UpdateUserStatistics::class,
    ],
];
```

## Quick Reference

| Pattern | Purpose | File Location |
|---------|---------|---------------|
| Service | Business logic | `app/Services/` |
| Repository | Data access | `app/Repositories/` |
| Form Request | Validation | `app/Http/Requests/` |
| Resource | API responses | `app/Http/Resources/` |
| Job | Async tasks | `app/Jobs/` |
| Event | Domain events | `app/Events/` |
| DTO | Data transfer | `app/DTOs/` |
| Policy | Authorization | `app/Policies/` |

```

### references/symfony-patterns.md

```markdown
# Symfony Patterns

> Reference for: PHP Pro
> Load when: Symfony development, dependency injection, events, commands

## Dependency Injection

```php
<?php

declare(strict_types=1);

namespace App\Service;

use App\Repository\UserRepositoryInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\MailerInterface;

final readonly class UserService
{
    public function __construct(
        private UserRepositoryInterface $userRepository,
        private MailerInterface $mailer,
        private LoggerInterface $logger,
    ) {}

    public function createUser(string $email, string $password): User
    {
        $user = new User($email, password_hash($password, PASSWORD_ARGON2ID));

        $this->userRepository->save($user);
        $this->logger->info('User created', ['email' => $email]);

        return $user;
    }
}
```

## Service Configuration (services.yaml)

```yaml
# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true
        bind:
            string $projectDir: '%kernel.project_dir%'
            bool $isDebug: '%kernel.debug%'

    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'

    # Interface binding
    App\Repository\UserRepositoryInterface:
        class: App\Repository\DoctrineUserRepository

    # Service with specific configuration
    App\Service\PaymentService:
        arguments:
            $apiKey: '%env(PAYMENT_API_KEY)%'
            $timeout: 30

    # Tagged services
    App\EventSubscriber\:
        resource: '../src/EventSubscriber/'
        tags: ['kernel.event_subscriber']
```

## Controllers with Attributes

```php
<?php

declare(strict_types=1);

namespace App\Controller;

use App\DTO\CreateUserRequest;
use App\Entity\User;
use App\Service\UserService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

#[Route('/api/users', name: 'api_users_')]
final class UserController extends AbstractController
{
    public function __construct(
        private readonly UserService $userService,
    ) {}

    #[Route('', name: 'list', methods: ['GET'])]
    #[IsGranted('ROLE_USER')]
    public function list(): JsonResponse
    {
        $users = $this->userService->getAllUsers();

        return $this->json($users, Response::HTTP_OK, [], [
            'groups' => ['user:read'],
        ]);
    }

    #[Route('', name: 'create', methods: ['POST'])]
    #[IsGranted('ROLE_ADMIN')]
    public function create(
        #[MapRequestPayload] CreateUserRequest $request
    ): JsonResponse {
        $user = $this->userService->createUser(
            $request->email,
            $request->password
        );

        return $this->json($user, Response::HTTP_CREATED, [], [
            'groups' => ['user:read'],
        ]);
    }

    #[Route('/{id}', name: 'show', methods: ['GET'])]
    public function show(User $user): JsonResponse
    {
        $this->denyAccessUnlessGranted('view', $user);

        return $this->json($user, context: ['groups' => ['user:detail']]);
    }
}
```

## DTOs with Validation

```php
<?php

declare(strict_types=1);

namespace App\DTO;

use Symfony\Component\Validator\Constraints as Assert;

final readonly class CreateUserRequest
{
    public function __construct(
        #[Assert\NotBlank]
        #[Assert\Email]
        public string $email,

        #[Assert\NotBlank]
        #[Assert\Length(min: 8, max: 100)]
        #[Assert\PasswordStrength(minScore: Assert\PasswordStrength::STRENGTH_MEDIUM)]
        public string $password,

        #[Assert\NotBlank]
        #[Assert\Length(min: 2, max: 100)]
        public string $name,

        #[Assert\Choice(choices: ['admin', 'user', 'moderator'])]
        public string $role = 'user',
    ) {}
}

final readonly class UpdateUserRequest
{
    public function __construct(
        #[Assert\Email]
        public ?string $email = null,

        #[Assert\Length(min: 2, max: 100)]
        public ?string $name = null,

        #[Assert\Type('bool')]
        public ?bool $isActive = null,
    ) {}
}
```

## Event Subscribers

```php
<?php

declare(strict_types=1);

namespace App\EventSubscriber;

use App\Event\UserRegisteredEvent;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\MailerInterface;

final readonly class UserSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private MailerInterface $mailer,
        private LoggerInterface $logger,
    ) {}

    public static function getSubscribedEvents(): array
    {
        return [
            UserRegisteredEvent::class => [
                ['sendWelcomeEmail', 10],
                ['logRegistration', 5],
            ],
        ];
    }

    public function sendWelcomeEmail(UserRegisteredEvent $event): void
    {
        $user = $event->getUser();
        // Send email logic
        $this->logger->info('Welcome email sent', ['user_id' => $user->getId()]);
    }

    public function logRegistration(UserRegisteredEvent $event): void
    {
        $this->logger->info('User registered', [
            'user_id' => $event->getUser()->getId(),
            'email' => $event->getUser()->getEmail(),
        ]);
    }
}
```

## Custom Events

```php
<?php

declare(strict_types=1);

namespace App\Event;

use App\Entity\User;
use Symfony\Contracts\EventDispatcher\Event;

final class UserRegisteredEvent extends Event
{
    public function __construct(
        private readonly User $user,
        private readonly \DateTimeImmutable $occurredAt = new \DateTimeImmutable(),
    ) {}

    public function getUser(): User
    {
        return $this->user;
    }

    public function getOccurredAt(): \DateTimeImmutable
    {
        return $this->occurredAt;
    }
}

// Dispatching events
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

final readonly class UserService
{
    public function __construct(
        private EventDispatcherInterface $eventDispatcher,
    ) {}

    public function registerUser(string $email, string $password): User
    {
        $user = new User($email, $password);
        // ... save user

        $this->eventDispatcher->dispatch(new UserRegisteredEvent($user));

        return $user;
    }
}
```

## Console Commands

```php
<?php

declare(strict_types=1);

namespace App\Command;

use App\Service\UserService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
    name: 'app:user:create',
    description: 'Create a new user',
)]
final class CreateUserCommand extends Command
{
    public function __construct(
        private readonly UserService $userService,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->addArgument('email', InputArgument::REQUIRED, 'User email')
            ->addArgument('password', InputArgument::REQUIRED, 'User password')
            ->addOption('admin', 'a', InputOption::VALUE_NONE, 'Make user admin');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        $email = $input->getArgument('email');
        $password = $input->getArgument('password');
        $isAdmin = $input->getOption('admin');

        try {
            $user = $this->userService->createUser($email, $password, $isAdmin);

            $io->success(sprintf('User created with ID: %d', $user->getId()));

            return Command::SUCCESS;
        } catch (\Exception $e) {
            $io->error($e->getMessage());
            return Command::FAILURE;
        }
    }
}
```

## Voters (Authorization)

```php
<?php

declare(strict_types=1);

namespace App\Security\Voter;

use App\Entity\Post;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

final class PostVoter extends Voter
{
    public const VIEW = 'view';
    public const EDIT = 'edit';
    public const DELETE = 'delete';

    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::VIEW, self::EDIT, self::DELETE])
            && $subject instanceof Post;
    }

    protected function voteOnAttribute(
        string $attribute,
        mixed $subject,
        TokenInterface $token
    ): bool {
        $user = $token->getUser();

        if (!$user instanceof User) {
            return false;
        }

        /** @var Post $post */
        $post = $subject;

        return match ($attribute) {
            self::VIEW => $this->canView($post, $user),
            self::EDIT => $this->canEdit($post, $user),
            self::DELETE => $this->canDelete($post, $user),
            default => false,
        };
    }

    private function canView(Post $post, User $user): bool
    {
        return $post->isPublished() || $this->isOwner($post, $user);
    }

    private function canEdit(Post $post, User $user): bool
    {
        return $this->isOwner($post, $user);
    }

    private function canDelete(Post $post, User $user): bool
    {
        return $this->isOwner($post, $user) || $user->hasRole('ROLE_ADMIN');
    }

    private function isOwner(Post $post, User $user): bool
    {
        return $post->getAuthor()->getId() === $user->getId();
    }
}
```

## Message Handler (Messenger)

```php
<?php

declare(strict_types=1);

namespace App\Message;

final readonly class SendWelcomeEmail
{
    public function __construct(
        public int $userId,
    ) {}
}

namespace App\MessageHandler;

use App\Message\SendWelcomeEmail;
use App\Repository\UserRepositoryInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
final readonly class SendWelcomeEmailHandler
{
    public function __construct(
        private UserRepositoryInterface $userRepository,
        private MailerInterface $mailer,
    ) {}

    public function __invoke(SendWelcomeEmail $message): void
    {
        $user = $this->userRepository->find($message->userId);

        if (!$user) {
            return;
        }

        // Send email logic
    }
}

// Dispatching messages
use Symfony\Component\Messenger\MessageBusInterface;

$this->messageBus->dispatch(new SendWelcomeEmail($user->getId()));
```

## Quick Reference

| Component | Purpose | File Location |
|-----------|---------|---------------|
| Controller | HTTP handlers | `src/Controller/` |
| Service | Business logic | `src/Service/` |
| Repository | Data access | `src/Repository/` |
| Event | Domain events | `src/Event/` |
| EventSubscriber | Event handlers | `src/EventSubscriber/` |
| Command | CLI commands | `src/Command/` |
| Voter | Authorization | `src/Security/Voter/` |
| Message | Async messages | `src/Message/` |
| MessageHandler | Message handlers | `src/MessageHandler/` |
| DTO | Data transfer | `src/DTO/` |

```

### references/async-patterns.md

```markdown
# Async PHP Patterns

> Reference for: PHP Pro
> Load when: Swoole, ReactPHP, async, fibers, concurrency, streams

## Swoole HTTP Server

```php
<?php

declare(strict_types=1);

use Swoole\HTTP\Server;
use Swoole\HTTP\Request;
use Swoole\HTTP\Response;

$server = new Server('0.0.0.0', 9501);

$server->set([
    'worker_num' => 4,
    'max_request' => 10000,
    'task_worker_num' => 2,
    'enable_coroutine' => true,
]);

$server->on('start', function (Server $server) {
    echo "Swoole HTTP server started at http://0.0.0.0:9501\n";
});

$server->on('request', function (Request $request, Response $response) {
    $response->header('Content-Type', 'application/json');

    match ($request->server['request_uri']) {
        '/api/users' => handleUsers($request, $response),
        '/api/health' => $response->end(json_encode(['status' => 'healthy'])),
        default => $response->status(404)->end(json_encode(['error' => 'Not found'])),
    };
});

function handleUsers(Request $request, Response $response): void
{
    // Coroutine for concurrent DB queries
    go(function () use ($response) {
        $users = queryDatabase('SELECT * FROM users LIMIT 10');
        $response->end(json_encode(['data' => $users]));
    });
}

$server->start();
```

## Swoole Coroutines

```php
<?php

declare(strict_types=1);

use Swoole\Coroutine;
use Swoole\Coroutine\Http\Client;

// Concurrent HTTP requests
Coroutine\run(function () {
    $results = [];

    // Create multiple coroutines
    $wg = new Coroutine\WaitGroup();

    $urls = [
        'https://api.example.com/users',
        'https://api.example.com/posts',
        'https://api.example.com/comments',
    ];

    foreach ($urls as $url) {
        $wg->add();
        go(function () use ($url, &$results, $wg) {
            $client = new Client(parse_url($url, PHP_URL_HOST), 443, true);
            $client->set(['timeout' => 5]);
            $client->get(parse_url($url, PHP_URL_PATH));

            $results[$url] = [
                'status' => $client->statusCode,
                'body' => $client->body,
            ];

            $client->close();
            $wg->done();
        });
    }

    $wg->wait();

    print_r($results);
});
```

## Swoole Async MySQL

```php
<?php

declare(strict_types=1);

use Swoole\Coroutine;
use Swoole\Coroutine\MySQL;

Coroutine\run(function () {
    $mysql = new MySQL();

    $connected = $mysql->connect([
        'host' => '127.0.0.1',
        'port' => 3306,
        'user' => 'root',
        'password' => 'password',
        'database' => 'test',
    ]);

    if (!$connected) {
        throw new \RuntimeException($mysql->connect_error);
    }

    // Async query
    $result = $mysql->query('SELECT * FROM users WHERE active = 1');

    foreach ($result as $row) {
        echo "User: {$row['name']}\n";
    }

    // Prepared statements
    $stmt = $mysql->prepare('SELECT * FROM users WHERE id = ?');
    $stmt->execute([42]);
    $user = $stmt->fetchAll();

    $mysql->close();
});
```

## Swoole Channel (Communication)

```php
<?php

declare(strict_types=1);

use Swoole\Coroutine;
use Swoole\Coroutine\Channel;

Coroutine\run(function () {
    $channel = new Channel(10); // Buffer size: 10

    // Producer
    go(function () use ($channel) {
        for ($i = 1; $i <= 5; $i++) {
            $channel->push("Task {$i}");
            echo "Produced: Task {$i}\n";
            Coroutine::sleep(0.5);
        }
        $channel->close();
    });

    // Consumer
    go(function () use ($channel) {
        while (true) {
            $task = $channel->pop();
            if ($task === false && $channel->errCode === SWOOLE_CHANNEL_CLOSED) {
                break;
            }
            echo "Consumed: {$task}\n";
            Coroutine::sleep(1);
        }
    });
});
```

## ReactPHP Event Loop

```php
<?php

declare(strict_types=1);

require 'vendor/autoload.php';

use React\EventLoop\Loop;
use React\Http\Message\Response;
use Psr\Http\Message\ServerRequestInterface;

// HTTP Server
$server = new React\Http\HttpServer(function (ServerRequestInterface $request) {
    return new Response(
        200,
        ['Content-Type' => 'application/json'],
        json_encode([
            'method' => $request->getMethod(),
            'uri' => (string) $request->getUri(),
            'timestamp' => time(),
        ])
    );
});

$socket = new React\Socket\SocketServer('0.0.0.0:8080');
$server->listen($socket);

echo "Server running at http://0.0.0.0:8080\n";

// Periodic timer
Loop::addPeriodicTimer(5.0, function () {
    echo "Heartbeat: " . date('H:i:s') . "\n";
});

// One-time timer
Loop::addTimer(10.0, function () {
    echo "This runs once after 10 seconds\n";
});
```

## ReactPHP Async MySQL

```php
<?php

declare(strict_types=1);

require 'vendor/autoload.php';

use React\MySQL\Factory;
use React\MySQL\QueryResult;

$factory = new Factory();

$connection = $factory->createLazyConnection('root:password@localhost/database');

$connection->query('SELECT * FROM users WHERE active = 1')
    ->then(
        function (QueryResult $result) {
            echo "Found " . count($result->resultRows) . " users\n";
            foreach ($result->resultRows as $row) {
                echo "User: {$row['name']}\n";
            }
        },
        function (\Exception $error) {
            echo "Error: " . $error->getMessage() . "\n";
        }
    );

// Prepared statements
$connection->query('SELECT * FROM users WHERE id = ?', [42])
    ->then(function (QueryResult $result) {
        $user = $result->resultRows[0] ?? null;
        var_dump($user);
    });
```

## ReactPHP Promises

```php
<?php

declare(strict_types=1);

use React\Promise\Promise;
use React\Promise\Deferred;
use function React\Promise\all;

// Creating promises
function fetchUser(int $id): Promise
{
    $deferred = new Deferred();

    // Simulate async operation
    Loop::addTimer(1.0, function () use ($deferred, $id) {
        $deferred->resolve([
            'id' => $id,
            'name' => "User {$id}",
        ]);
    });

    return $deferred->promise();
}

// Using promises
fetchUser(42)
    ->then(function ($user) {
        echo "Got user: {$user['name']}\n";
        return fetchUserPosts($user['id']);
    })
    ->then(function ($posts) {
        echo "Got " . count($posts) . " posts\n";
    })
    ->catch(function (\Exception $error) {
        echo "Error: " . $error->getMessage() . "\n";
    });

// Parallel promises
all([
    fetchUser(1),
    fetchUser(2),
    fetchUser(3),
])->then(function ($users) {
    echo "Fetched " . count($users) . " users\n";
});
```

## PHP Fibers (Native PHP 8.1+)

```php
<?php

declare(strict_types=1);

// Simple async function using fibers
function async(callable $callback): Fiber
{
    return new Fiber($callback);
}

function await(Fiber $fiber): mixed
{
    if (!$fiber->isStarted()) {
        return $fiber->start();
    }
    if ($fiber->isTerminated()) {
        return $fiber->getReturn();
    }
    return $fiber->resume();
}

// Simulate async I/O
function fetchData(string $url): Fiber
{
    return async(function () use ($url) {
        echo "Fetching: {$url}\n";
        Fiber::suspend('pending');

        // Simulate network delay
        sleep(1);

        return "Data from {$url}";
    });
}

// Usage
$fiber1 = fetchData('https://api.example.com/users');
$fiber2 = fetchData('https://api.example.com/posts');

await($fiber1);
await($fiber2);

$result1 = await($fiber1);
$result2 = await($fiber2);

echo "{$result1}\n";
echo "{$result2}\n";
```

## Amphp Framework

```php
<?php

declare(strict_types=1);

require 'vendor/autoload.php';

use Amp\Http\Server\HttpServer;
use Amp\Http\Server\Request;
use Amp\Http\Server\Response;
use Amp\Http\Server\Router;
use Amp\Socket\Server as SocketServer;
use function Amp\async;
use function Amp\Future\await;

// HTTP Server with Amphp
$router = new Router();

$router->addRoute('GET', '/api/users', function (Request $request): Response {
    // Concurrent database queries
    $users = await([
        async(fn() => queryUsers()),
        async(fn() => queryUserStats()),
    ]);

    return new Response(
        status: 200,
        headers: ['content-type' => 'application/json'],
        body: json_encode(['users' => $users[0], 'stats' => $users[1]]),
    );
});

$server = new HttpServer(
    servers: [SocketServer::listen('0.0.0.0:8080')],
    requestHandler: $router,
);

$server->start();
```

## Quick Reference

| Technology | Use Case | Performance |
|------------|----------|-------------|
| Swoole | High-performance servers, WebSockets | Very High |
| ReactPHP | Event-driven apps, real-time | High |
| Amphp | Modern async framework | High |
| Fibers | Native async (PHP 8.1+) | Medium |
| Generators | Simple async patterns | Medium |

| Feature | Swoole | ReactPHP | Amphp |
|---------|--------|----------|-------|
| Coroutines | Yes | No (Promises) | Yes (Fibers) |
| HTTP Server | Built-in | Via package | Via package |
| WebSockets | Built-in | Via package | Via package |
| Extension | Required | Not required | Not required |
| Learning Curve | Medium | Low | Medium |

```

### references/testing-quality.md

```markdown
# Testing & Quality Assurance

> Reference for: PHP Pro
> Load when: PHPUnit, testing, PHPStan, Psalm, code quality, mocking

## PHPUnit with Strict Types

```php
<?php

declare(strict_types=1);

namespace Tests\Unit\Service;

use App\Repository\UserRepositoryInterface;
use App\Service\UserService;
use App\Service\EmailService;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;

final class UserServiceTest extends TestCase
{
    private UserRepositoryInterface&MockObject $userRepository;
    private EmailService&MockObject $emailService;
    private UserService $userService;

    protected function setUp(): void
    {
        $this->userRepository = $this->createMock(UserRepositoryInterface::class);
        $this->emailService = $this->createMock(EmailService::class);
        $this->userService = new UserService(
            $this->userRepository,
            $this->emailService
        );
    }

    public function testCreateUserSuccessfully(): void
    {
        $email = '[email protected]';
        $password = 'SecurePass123!';

        $this->userRepository
            ->expects($this->once())
            ->method('findByEmail')
            ->with($email)
            ->willReturn(null);

        $this->userRepository
            ->expects($this->once())
            ->method('create')
            ->willReturn($this->createUser($email));

        $this->emailService
            ->expects($this->once())
            ->method('sendWelcomeEmail');

        $user = $this->userService->createUser($email, $password);

        $this->assertSame($email, $user->email);
    }

    public function testCreateUserThrowsExceptionWhenEmailExists(): void
    {
        $this->expectException(\DomainException::class);
        $this->expectExceptionMessage('Email already exists');

        $this->userRepository
            ->method('findByEmail')
            ->willReturn($this->createUser('[email protected]'));

        $this->userService->createUser('[email protected]', 'password');
    }

    private function createUser(string $email): User
    {
        return new User(
            id: 1,
            email: $email,
            password: password_hash('password', PASSWORD_ARGON2ID),
        );
    }
}
```

## Data Providers

```php
<?php

declare(strict_types=1);

namespace Tests\Unit\Validator;

use App\Validator\EmailValidator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class EmailValidatorTest extends TestCase
{
    #[Test]
    #[DataProvider('validEmailProvider')]
    public function itValidatesCorrectEmails(string $email): void
    {
        $validator = new EmailValidator();
        $this->assertTrue($validator->isValid($email));
    }

    #[Test]
    #[DataProvider('invalidEmailProvider')]
    public function itRejectsInvalidEmails(string $email): void
    {
        $validator = new EmailValidator();
        $this->assertFalse($validator->isValid($email));
    }

    public static function validEmailProvider(): array
    {
        return [
            ['[email protected]'],
            ['[email protected]'],
            ['[email protected]'],
        ];
    }

    public static function invalidEmailProvider(): array
    {
        return [
            ['invalid'],
            ['@example.com'],
            ['user@'],
            ['user [email protected]'],
        ];
    }
}
```

## Laravel Feature Tests

```php
<?php

declare(strict_types=1);

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

final class UserControllerTest extends TestCase
{
    use RefreshDatabase, WithFaker;

    public function testUserCanViewTheirProfile(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->get('/api/users/me');

        $response->assertOk()
            ->assertJson([
                'data' => [
                    'id' => $user->id,
                    'email' => $user->email,
                ],
            ]);
    }

    public function testUserCanUpdateTheirProfile(): void
    {
        $user = User::factory()->create();
        $newName = $this->faker->name();

        $response = $this->actingAs($user)->putJson('/api/users/me', [
            'name' => $newName,
        ]);

        $response->assertOk();

        $this->assertDatabaseHas('users', [
            'id' => $user->id,
            'name' => $newName,
        ]);
    }

    public function testUnauthorizedUserCannotAccessProfile(): void
    {
        $response = $this->getJson('/api/users/me');

        $response->assertUnauthorized();
    }

    public function testValidationFailsWithInvalidData(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->putJson('/api/users/me', [
            'email' => 'not-an-email',
        ]);

        $response->assertUnprocessable()
            ->assertJsonValidationErrors(['email']);
    }
}
```

## Pest Testing (Modern Alternative)

```php
<?php

declare(strict_types=1);

use App\Models\User;
use App\Services\UserService;

beforeEach(function () {
    $this->userService = app(UserService::class);
});

it('creates a user successfully', function () {
    $user = $this->userService->createUser(
        email: '[email protected]',
        password: 'SecurePass123!'
    );

    expect($user)
        ->toBeInstanceOf(User::class)
        ->email->toBe('[email protected]');
});

it('validates email format', function (string $email, bool $valid) {
    $validator = new EmailValidator();

    expect($validator->isValid($email))->toBe($valid);
})->with([
    ['[email protected]', true],
    ['invalid', false],
    ['@example.com', false],
]);

test('authenticated user can view profile', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->get('/api/users/me')
        ->assertOk()
        ->assertJson(['data' => ['email' => $user->email]]);
});

test('guest cannot access protected routes', function () {
    $this->getJson('/api/users/me')
        ->assertUnauthorized();
});
```

## PHPStan Configuration

```neon
# phpstan.neon
parameters:
    level: 9
    paths:
        - src
        - tests
    excludePaths:
        - src/bootstrap.php
        - vendor
    checkMissingIterableValueType: true
    checkGenericClassInNonGenericObjectType: true
    reportUnmatchedIgnoredErrors: true
    tmpDir: var/cache/phpstan

    ignoreErrors:
        # Ignore specific Laravel magic
        - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder#'

    type_coverage:
        return_type: 100
        param_type: 100
        property_type: 100

includes:
    - vendor/phpstan/phpstan-strict-rules/rules.neon
    - vendor/phpstan/phpstan-deprecation-rules/rules.neon
```

## PHPStan Annotations

```php
<?php

declare(strict_types=1);

namespace App\Repository;

use App\Entity\User;
use Doctrine\ORM\EntityRepository;

/**
 * @extends EntityRepository<User>
 */
final class UserRepository extends EntityRepository
{
    /**
     * @return User[]
     */
    public function findActive(): array
    {
        return $this->createQueryBuilder('u')
            ->where('u.status = :status')
            ->setParameter('status', 'active')
            ->getQuery()
            ->getResult();
    }

    /**
     * @param int[] $ids
     * @return User[]
     */
    public function findByIds(array $ids): array
    {
        return $this->createQueryBuilder('u')
            ->where('u.id IN (:ids)')
            ->setParameter('ids', $ids)
            ->getQuery()
            ->getResult();
    }
}

/**
 * @template T
 */
final readonly class Result
{
    /**
     * @param T $data
     */
    public function __construct(
        public mixed $data,
        public bool $success,
    ) {}

    /**
     * @return T
     */
    public function getData(): mixed
    {
        return $this->data;
    }
}
```

## Mockery (Advanced Mocking)

```php
<?php

declare(strict_types=1);

namespace Tests\Unit\Service;

use App\Repository\UserRepository;
use App\Service\NotificationService;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use PHPUnit\Framework\TestCase;

final class NotificationServiceTest extends TestCase
{
    use MockeryPHPUnitIntegration;

    public function testSendsNotificationToActiveUsers(): void
    {
        $repository = Mockery::mock(UserRepository::class);
        $repository->shouldReceive('findActive')
            ->once()
            ->andReturn([
                $this->createUser('[email protected]'),
                $this->createUser('[email protected]'),
            ]);

        $service = new NotificationService($repository);
        $result = $service->notifyActiveUsers('Important message');

        $this->assertSame(2, $result->count());
    }

    public function testHandlesEmailServiceFailure(): void
    {
        $emailService = Mockery::mock(EmailService::class);
        $emailService->shouldReceive('send')
            ->once()
            ->andThrow(new \RuntimeException('Email service down'));

        $service = new NotificationService($emailService);

        $this->expectException(\RuntimeException::class);
        $service->sendNotification('[email protected]', 'Hello');
    }

    private function createUser(string $email): User
    {
        return new User(id: 1, email: $email, password: 'hashed');
    }
}
```

## Code Coverage

```xml
<!-- phpunit.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
         failOnRisky="true"
         failOnWarning="true"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory>tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage>
        <include>
            <directory suffix=".php">src</directory>
        </include>
        <exclude>
            <directory>src/bootstrap</directory>
            <file>src/Kernel.php</file>
        </exclude>
        <report>
            <html outputDirectory="coverage/html"/>
            <clover outputFile="coverage/clover.xml"/>
        </report>
    </coverage>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="DB_CONNECTION" value="sqlite"/>
        <env name="DB_DATABASE" value=":memory:"/>
    </php>
</phpunit>
```

## Quick Reference

| Tool | Purpose | Command |
|------|---------|---------|
| PHPUnit | Unit/Feature tests | `./vendor/bin/phpunit` |
| Pest | Modern testing | `./vendor/bin/pest` |
| PHPStan | Static analysis | `./vendor/bin/phpstan analyse` |
| Psalm | Alternative static analysis | `./vendor/bin/psalm` |
| PHP-CS-Fixer | Code style | `./vendor/bin/php-cs-fixer fix` |
| PHPMD | Mess detector | `./vendor/bin/phpmd src text cleancode` |

| Assertion | PHPUnit | Pest |
|-----------|---------|------|
| Equality | `$this->assertSame()` | `expect()->toBe()` |
| Type | `$this->assertInstanceOf()` | `expect()->toBeInstanceOf()` |
| Array | `$this->assertContains()` | `expect()->toContain()` |
| Exception | `$this->expectException()` | `expect()->toThrow()` |
| Count | `$this->assertCount()` | `expect()->toHaveCount()` |

```

php-pro | SkillHub