php-modern-standards

SKILL.md

Required Plugins

Superpowers plugin: MUST be active for all work using this skill. Use throughout the entire build pipeline — design decisions, code generation, debugging, quality checks, and any task where it offers enhanced capabilities. If superpowers provides a better way to accomplish something, prefer it over the default approach.

PHP Modern Standards

Production-grade PHP patterns for maintainable, testable, secure, high-performance applications.

Core Principle: Write type-safe, secure, performant PHP code following PSR standards with modern PHP 8+ features.

See subdirectories: references/security-patterns.md, examples/modern-php-patterns.php, examples/laravel-patterns.php

When to Use

✅ PHP 8+ applications ✅ OOP architecture ✅ Code security ✅ Testable systems ✅ Performance optimization ✅ Laravel conventions

File Structure

<?php

declare(strict_types=1);

namespace App\Domain\User;

use App\Domain\Shared\ValueObject;

final readonly class User
{
    public function __construct(
        private int $id,
        private string $email,
    ) {
    }
}

Rules: Always declare(strict_types=1), one class per file, namespace = directory, import all dependencies.

Cross-Platform File Naming (MANDATORY)

Code runs on Windows (dev), Ubuntu (staging), and Debian (production). Linux is case-sensitive:

  • Directories: Use lowercase for config/utility dirs (src/config/, src/lang/). Use PascalCase for module dirs matching namespace (src/HR/Services/, src/Auth/).
  • Class files: PascalCase matching class name (StaffService.php, EmailService.php).
  • require/include: Must match EXACT case on disk. ../src/Config/database.php will fail on Linux if dir is config/.
  • Paths: Use / (forward slash) in PHP code. Never hardcode C:\... in application logic. Use DIRECTORY_SEPARATOR or / which PHP handles cross-platform.
  • Temp files: Use sys_get_temp_dir(), not hardcoded paths.

Type System

Strict Typing (Required)

declare(strict_types=1); // Always

function calculateTotal(int $quantity, float $price): float { }
function getUser(int $id): ?User { } // Nullable
function log(string $msg): void { } // Void

Modern Types

// Union types (PHP 8.0+)
function process(int|float $value): string|int { }

// Intersection types (PHP 8.1+)
function handle(Countable&Traversable $collection): void { }

// Never type (PHP 8.1+)
function terminate(): never { throw new RuntimeException(); }

// Short nullable (?Type not Type|null)
function getName(): ?string // ✓ CORRECT

Typed Properties (Required)

final class User
{
    private int $id;
    private string $email;
    private ?string $nickname = null;
    private array $roles = [];
}

Constructor Promotion

final readonly class User
{
    public function __construct(
        private int $id,
        private string $email,
        private ?string $nickname = null,
    ) {
    }
}

Readonly (PHP 8.1+)

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

Modern Features

Enums (PHP 8.1+)

enum Status: string
{
    case Pending = 'pending';
    case Active = 'active';

    public function label(): string
    {
        return match ($this) {
            self::Pending => 'Pending',
            self::Active => 'Active',
        };
    }
}

Match (PHP 8.0+)

$status = match ($code) {
    200, 201 => 'success',
    400, 422 => 'error',
    default => 'unknown',
};

Named Arguments

new User(
    id: 1,
    email: 'user@example.com',
    name: 'John',
);

Nullsafe Operator

$country = $user?->getAddress()?->getCountry();

Attributes

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

SOLID Principles

Single Responsibility

final readonly class UserValidator { }
final readonly class UserRepository { }

Open/Closed

interface PaymentGateway { }
final readonly class StripeGateway implements PaymentGateway { }

Dependency Inversion

public function __construct(
    private PaymentGateway $gateway, // Interface, not concrete
) {
}

Control Flow

Happy Path Last

public function process(Order $order): void
{
    if (!$order->isValid()) {
        throw new InvalidOrderException();
    }

    // Happy path
    $this->fulfillment->process($order);
}

Avoid else

if (!$user->isActive()) {
    return null;
}

return $user->process();

Strict Comparison

if ($status === 'active') { } // ✓ CORRECT
if ($count !== 0) { }
if (in_array($role, $roles, true)) { }

Security

See references/security-patterns.md for complete guide.

Input Validation

final readonly class UserValidator
{
    public function validate(array $data): ValidationResult
    {
        $errors = [];

        if (!filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL)) {
            $errors['email'] = 'Invalid email';
        }

        $age = filter_var($data['age'] ?? null, FILTER_VALIDATE_INT, [
            'options' => ['min_range' => 13, 'max_range' => 120],
        ]);
        if ($age === false) {
            $errors['age'] = 'Invalid age';
        }

        return new ValidationResult($errors);
    }
}

SQL Injection Prevention

// ✓ CORRECT
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);

// ✗ WRONG
$query = "SELECT * FROM users WHERE email = '$email'"; // VULNERABLE!

XSS Protection

echo htmlspecialchars($userInput, ENT_QUOTES | ENT_HTML5, 'UTF-8');
echo json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP);

Password Handling

// Hash (Argon2id)
$hash = password_hash($plainPassword, PASSWORD_ARGON2ID, [
    'memory_cost' => 65536,
    'time_cost' => 4,
    'threads' => 3,
]);

// Verify
if (password_verify($plainPassword, $hash)) {
    if (password_needs_rehash($hash, PASSWORD_ARGON2ID)) {
        $newHash = password_hash($plainPassword, PASSWORD_ARGON2ID);
    }
}

CSRF Protection

final readonly class CsrfProtection
{
    public function generateToken(): string
    {
        $token = bin2hex(random_bytes(32));
        $_SESSION['csrf_token'] = $token;
        $_SESSION['csrf_token_time'] = time();
        return $token;
    }

    public function validateToken(string $token): bool
    {
        if (!isset($_SESSION['csrf_token'])) {
            return false;
        }

        if (time() - ($_SESSION['csrf_token_time'] ?? 0) > 7200) {
            return false;
        }

        return hash_equals($_SESSION['csrf_token'], $token);
    }
}

Performance

Generators

function readLargeFile(string $path): \Generator
{
    $handle = fopen($path, 'r');
    while (($line = fgets($handle)) !== false) {
        yield trim($line);
    }
    fclose($handle);
}

foreach (readLargeFile('large.csv') as $line) {
    processLine($line);
}

SPL Data Structures

$queue = new \SplQueue();
$queue->enqueue('task');
$task = $queue->dequeue();

$pq = new \SplPriorityQueue();
$pq->insert('low', 1);
$pq->insert('high', 10);

Laravel Conventions

Routes

// URLs: kebab-case, Names: camelCase, Params: camelCase
Route::get('/open-source', [OpenSourceController::class, 'index'])
    ->name('openSource');

Controllers

// Plural for resources
final class PostsController extends Controller
{
    public function index(): Response { }
    public function show(Post $post): Response { }
    public function store(StorePostRequest $request): Response { }
}

// Singular for single resources
final class ProfileController extends Controller
{
    public function show(): Response { }
}

Models

final class User extends Model
{
    protected $fillable = ['name', 'email'];
    protected $hidden = ['password'];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'is_active' => 'boolean',
        ];
    }

    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}

Class Design

Use final by Default

final readonly class User { } // Default
abstract class BaseController { } // Only when needed

Class Structure Order

final class Example
{
    // 1. Constants
    private const MAX = 3;

    // 2. Properties (public → protected → private)
    public readonly int $id;
    private string $name;

    // 3. Constructor
    public function __construct(int $id) { }

    // 4. Public methods
    public function getName(): string { }

    // 5. Private methods
    private function helper(): void { }
}

Traits (Sparingly)

// One trait per line
final class Article
{
    use Timestampable;
    use Publishable;
}

Anti-Patterns (Avoid)

// ✗ No types
function process($data) { }

// ✗ Loose comparison
if ($value == 1) { }

// ✗ Switch for values
switch ($status) { }

// ✗ Globals
$GLOBALS['config'] = [];

// ✗ Redundant docblocks
/** @param string $name */
public function setName(string $name): void { }

PSR Standards

  • PSR-1: Basic coding
  • PSR-12: Style guide (follow this)
  • PSR-4: Autoloading
  • PSR-7: HTTP messages
  • PSR-11: Container
  • PSR-15: Request handlers

Tooling

  • PHPStan: Static analysis (level 8+)
  • PHP CS Fixer: PSR-12 formatting
  • PHPUnit/Pest: Testing

Checklist

declare(strict_types=1) ✅ Full type hints ✅ Readonly for immutable ✅ Final by default ✅ Match over switch ✅ Enums for fixed values ✅ Early returns ✅ Strict comparison (===) ✅ Input validation ✅ Prepared statements ✅ Output escaping ✅ Argon2id passwords ✅ Generators for large data ✅ PSR-12 compliant

References:

Weekly Installs
1
GitHub Stars
3
First Seen
Feb 28, 2026
Installed on
amp1
cline1
opencode1
cursor1
continue1
kimi-cli1