php-guide

SKILL.md

PHP Guide

Applies to: PHP 8.1+, Web Applications, APIs, CLIs, Microservices

Core Principles

  1. Strict Types Always: Every PHP file starts with declare(strict_types=1)
  2. Type Declarations Everywhere: All parameters, return types, and properties must have type declarations
  3. PSR Standards: Follow PSR-12 coding standard, PSR-4 autoloading, PSR-7 HTTP messages
  4. Composition Over Inheritance: Prefer interfaces, traits, and dependency injection over deep class hierarchies
  5. Modern PHP First: Use PHP 8.1+ features (enums, readonly properties, fibers, named arguments, match expressions)

Guardrails

Version & Dependencies

  • Target PHP 8.1+ (enums, readonly properties, fibers, intersection types)
  • Define all dependencies in composer.json with version constraints
  • Run composer validate and composer audit before committing
  • Use composer.lock for applications (commit it), omit for libraries
  • Separate require-dev for development-only dependencies
  • Never use composer update in production (use composer install --no-dev)

Code Style (PSR-12)

  • Run PHP-CS-Fixer or PHP_CodeSniffer before every commit
  • Naming: PascalCase classes/enums, camelCase methods/properties, UPPER_SNAKE constants
  • One class per file, file name matches class name
  • Opening braces on same line for control structures, next line for classes/methods
  • declare(strict_types=1) as first statement after <?php in every file
  • No closing ?> tag in pure PHP files
  • Imports: one use per declaration, grouped (classes, functions, constants), alphabetized
<?php

declare(strict_types=1);

namespace App\Domain\User;

use App\Domain\Exception\ValidationException;
use App\Domain\ValueObject\Email;

final class UserService
{
    public function __construct(
        private readonly UserRepositoryInterface $repository,
        private readonly EventDispatcherInterface $dispatcher,
    ) {}

    public function register(string $name, string $email): User
    {
        $emailVO = Email::fromString($email);

        if ($this->repository->existsByEmail($emailVO)) {
            throw ValidationException::duplicateEmail($email);
        }

        $user = User::create(name: $name, email: $emailVO);
        $this->repository->save($user);
        $this->dispatcher->dispatch(new UserRegistered($user->id));

        return $user;
    }
}

Type Declarations

  • All function parameters MUST have type declarations
  • All methods MUST declare return types (including void)
  • Use union types (string|int) instead of mixed when possible
  • Use intersection types (Countable&Iterator) for combined type constraints
  • Use Type|null for nullable parameters (prefer explicit over ?Type)
  • Use never return type for functions that always throw or exit
  • Avoid mixed -- if truly needed, document why

Error Handling

  • Never use @ error suppression operator
  • Convert PHP errors to exceptions with set_error_handler at bootstrap
  • Create domain-specific exception hierarchies extending a base exception
  • Use specific exception types (not generic \Exception or \RuntimeException)
  • Always include context in exception messages
  • Catch specific exceptions, never bare catch (\Throwable $e) without re-throwing
  • Use previous parameter to chain exceptions
<?php

declare(strict_types=1);

namespace App\Domain\Exception;

abstract class DomainException extends \RuntimeException
{
    public function __construct(
        string $message,
        public readonly string $errorCode = 'UNKNOWN',
        int $code = 0,
        ?\Throwable $previous = null,
    ) {
        parent::__construct($message, $code, $previous);
    }
}

final class NotFoundException extends DomainException
{
    public static function forResource(string $resource, string $id): self
    {
        return new self(
            message: sprintf('%s with ID "%s" not found', $resource, $id),
            errorCode: 'NOT_FOUND',
        );
    }
}

Security

  • All user input validated before processing (filter functions or validation libraries)
  • All SQL queries use prepared statements with bound parameters (PDO or Doctrine DBAL)
  • All output escaped for context: htmlspecialchars() for HTML, parameterized for SQL
  • All file operations validate paths (realpath() + check against allowed directories)
  • Never use eval(), exec(), system(), passthru(), or backtick operator with user input
  • Use password_hash() with PASSWORD_ARGON2ID (or PASSWORD_BCRYPT minimum)
  • Set session.cookie_httponly, session.cookie_secure, session.cookie_samesite
  • Use CSRF tokens for all state-changing requests

Project Structure

myproject/
├── src/                        # Application source (PSR-4: App\)
│   ├── Domain/                 # Business logic, entities, value objects
│   ├── Application/            # Use cases, command/query handlers
│   ├── Infrastructure/         # Framework, database, external services
│   └── Kernel.php
├── tests/
│   ├── Unit/                   # Fast, isolated unit tests
│   ├── Integration/            # Tests with real dependencies
│   └── bootstrap.php
├── config/                     # Configuration files
├── public/                     # Web root (index.php entry point)
├── composer.json
├── composer.lock
├── phpunit.xml
├── phpstan.neon
└── .php-cs-fixer.php
  • PSR-4 autoloading: "App\\": "src/" in composer.json
  • Domain layer has zero framework dependencies
  • Infrastructure implements domain interfaces
  • One class per file, directory structure mirrors namespace
  • Keep public/ as the web root with a single index.php front controller

Key Patterns

Use PHP 8.1+ features idiomatically:

  • Enums: Backed enums with methods for labels, state machines, role-based permissions
  • Readonly classes (PHP 8.2+): Value objects, DTOs, Money pattern -- all properties implicitly readonly
  • Named arguments: Improve readability for constructors and functions with many parameters
  • Match expressions: Prefer over switch -- strict comparison, expression-based, no fallthrough
  • Fibers: Cooperative multitasking foundation for async frameworks

Enums (PHP 8.1+)

<?php

declare(strict_types=1);

enum UserRole: string
{
    case Admin = 'admin';
    case Editor = 'editor';
    case Viewer = 'viewer';

    public function label(): string
    {
        return match ($this) {
            self::Admin => 'Administrator',
            self::Editor => 'Editor',
            self::Viewer => 'Viewer',
        };
    }

    /** @return list<Permission> */
    public function permissions(): array
    {
        return match ($this) {
            self::Admin => Permission::cases(),
            self::Editor => [Permission::Read, Permission::Write],
            self::Viewer => [Permission::Read],
        };
    }
}

Readonly Classes (PHP 8.2+)

<?php

declare(strict_types=1);

readonly class Money
{
    public function __construct(
        public int $amount,
        public string $currency,
    ) {}

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

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

See references/patterns.md for additional patterns: dependency injection, repository pattern, value objects, command/handler, fibers, match expressions, and tooling configurations.

Testing

Standards

  • Test files: *Test.php in tests/ mirroring src/ structure
  • Test methods: test_<unit>_<scenario>_<expected> or #[Test] attribute
  • PHPUnit as primary framework; Pest as alternative
  • Coverage: >80% business logic, >60% overall
  • Use data providers for parameterized tests
  • Mock external dependencies with Mockery or PHPUnit mocks
  • No database or network calls in unit tests
  • Each test method tests one behavior

PHPUnit with Data Providers

<?php

declare(strict_types=1);

namespace Tests\Unit\Domain\ValueObject;

use App\Domain\Exception\ValidationException;
use App\Domain\ValueObject\Email;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class EmailTest extends TestCase
{
    #[Test]
    #[DataProvider('validEmailProvider')]
    public function it_accepts_valid_emails(string $input): void
    {
        $email = Email::fromString($input);

        self::assertSame(strtolower($input), $email->toString());
    }

    /** @return iterable<string, array{string}> */
    public static function validEmailProvider(): iterable
    {
        yield 'simple' => ['user@example.com'];
        yield 'with subdomain' => ['user@mail.example.com'];
        yield 'with plus' => ['user+tag@example.com'];
        yield 'uppercase' => ['User@Example.COM'];
    }

    #[Test]
    #[DataProvider('invalidEmailProvider')]
    public function it_rejects_invalid_emails(string $input): void
    {
        $this->expectException(ValidationException::class);

        Email::fromString($input);
    }

    /** @return iterable<string, array{string}> */
    public static function invalidEmailProvider(): iterable
    {
        yield 'empty string' => [''];
        yield 'no at sign' => ['userexample.com'];
        yield 'no domain' => ['user@'];
        yield 'no local part' => ['@example.com'];
        yield 'spaces' => ['user @example.com'];
    }
}

Tooling

Required Dev Dependencies

  • phpunit/phpunit ^10.0 -- testing
  • phpstan/phpstan ^1.10 -- static analysis (level 8)
  • friendsofphp/php-cs-fixer ^3.0 -- code style
  • mockery/mockery ^1.6 -- mocking
  • vimeo/psalm ^5.0 -- alternative static analysis

PHPStan: Always Level 8

# phpstan.neon
parameters:
    level: 8
    paths:
        - src
    treatPhpDocTypesAsCertain: false
    checkMissingIterableValueType: true

See references/patterns.md for full composer.json, phpstan.neon, and .php-cs-fixer.php configurations.

Essential Commands

composer install              # Install dependencies
composer validate             # Validate composer.json
composer audit                # Check for security vulnerabilities
php vendor/bin/phpunit        # Run all tests
php vendor/bin/phpunit --coverage-text  # With coverage summary
php vendor/bin/phpstan analyse         # Static analysis (level 8)
php vendor/bin/psalm                   # Alternative static analysis
php vendor/bin/php-cs-fixer fix        # Auto-fix code style
php vendor/bin/php-cs-fixer fix --dry-run --diff  # Preview fixes

References

For detailed code examples, see:

  • references/patterns.md -- Enum patterns, readonly classes, dependency injection, repository pattern, type declarations, error handling, modern PHP features, mocking, tooling configurations

External References

Weekly Installs
5
Repository
ar4mirez/samuel
GitHub Stars
3
First Seen
Mar 1, 2026
Installed on
opencode5
gemini-cli5
codebuddy5
github-copilot5
codex5
kimi-cli5