agentleFS
Sign inSign up

agentkit / php-best-practices

rcdelfin/agentkit/skills/software-development/php-best-practices/AGENTS.md

Version: 2.1.0 Focus: PHP 8.0 - 8.5, PSR Standards, Modern PHP Features Rules: 51 (9 type + 16 modern + 6 PSR + 5 SOLID + 5 error + 5 perf + 5 security) License: MIT Always check the project's PHP version before giving advice. Features vary across 8.0 - 8.5. Only suggest features available in the detected version: Comprehensive PHP 8.x best practices covering type system, modern features, PSR standards, SOLID principles, error handling, performance, and security. Each rule…

AGENTS.md2 starsChanged 34 days ago
# PHP Best Practices - Complete Guide

**Version:** 2.1.0
**Focus:** PHP 8.0 - 8.5, PSR Standards, Modern PHP Features
**Rules:** 51 (9 type + 16 modern + 6 PSR + 5 SOLID + 5 error + 5 perf + 5 security)
**License:** MIT

---

## Step 1: Detect PHP Version

**Always check the project's PHP version before giving advice.** Features vary across 8.0 - 8.5.

```bash
# Check composer.json
grep '"php"' composer.json    # e.g. "^8.2"

# Check runtime
php -v                        # e.g. PHP 8.3.12
```

**Only suggest features available in the detected version:**

| Version | Key Features Added |
|---------|-------------------|
| 8.0+ | Union types, match, nullsafe, named args, constructor promotion, attributes |
| 8.1+ | Enums, readonly props, intersection types, first-class callables, never |
| 8.2+ | Readonly classes, DNF types |
| 8.3+ | Typed class constants, `#[\Override]`, `json_validate()` |
| 8.4+ | Property hooks, asymmetric visibility, `#[\Deprecated]`, `new` without parens |
| 8.5+ | Pipe operator `\|>` |

## Overview

Comprehensive PHP 8.x best practices covering type system, modern features, PSR standards, SOLID principles, error handling, performance, and security. Each rule includes bad and good examples with detailed explanations.

## Categories

1. **Type System (CRITICAL)** - Strict types, return types, union types, null handling
2. **Modern PHP Features (CRITICAL)** - Constructor promotion, enums, readonly, match, property hooks, pipe operator
3. **PSR Standards (HIGH)** - PSR-4 autoloading, PSR-12 coding style, naming conventions
4. **SOLID Principles (HIGH)** - SRP, OCP, LSP, ISP, DIP
5. **Error Handling (HIGH)** - Custom exceptions, proper try-catch, error recovery
6. **Performance (MEDIUM)** - Generators, lazy loading, optimization techniques
7. **Security (CRITICAL)** - Input validation, output escaping, password hashing, SQL injection prevention

---

## 1. Type System

### 1.1 Strict Types Declaration

**Impact:** CRITICAL

Always enable strict type checking at the beginning of every PHP file with `declare(strict_types=1)`.

**Why:** Prevents silent type coercion bugs, catches type errors immediately, enables better static analysis.

**Bad:**
```php
<?php
// No strict types
function add(int $a, int $b): int {
    return $a + $b;
}
add("5", "10"); // Returns 15 - strings coerced silently
```

**Good:**
```php
<?php

declare(strict_types=1);

function add(int $a, int $b): int {
    return $a + $b;
}
add(5, 10);     // OK: Returns 15
add("5", "10"); // Error: TypeError
```

---

### 1.2 Return Type Declarations

**Impact:** CRITICAL

Always declare return types for all methods and functions.

**Why:** Self-documenting, enforces contracts, enables IDE autocompletion, catches return type mismatches.

**Bad:**
```php
<?php
class UserRepository {
    public function find($id) {
        return $this->db->query("SELECT * FROM users WHERE id = ?", [$id]);
    }
}
```

**Good:**
```php
<?php

declare(strict_types=1);

class UserRepository {
    public function find(int $id): ?User
    {
        $data = $this->db->query("SELECT * FROM users WHERE id = ?", [$id]);
        return $data ? new User($data) : null;
    }
}
```

---

### 1.3 Parameter Type Declarations

**Impact:** CRITICAL

Always declare parameter types for all function and method parameters.

**Bad:**
```php
<?php
function createOrder($user, $items) {
    // What is $user? What format should $items be?
}
```

**Good:**
```php
<?php

declare(strict_types=1);

function createOrder(User $user, array $items): Order {
    $order = new Order($user);
    foreach ($items as $item) {
        $order->addItem($item);
    }
    return $order;
}
```

---

### 1.4 Property Type Declarations

**Impact:** CRITICAL

Always declare types for class properties (PHP 7.4+).

**Bad:**
```php
<?php
class Product {
    private $id;
    private $name;
    private $price;
}
```

**Good:**
```php
<?php

declare(strict_types=1);

class Product {
    private int $id;
    private string $name;
    private float $price;
    private array $categories = [];
}
```

---

### 1.5 Union Types

**Impact:** HIGH

Use union types when a value can legitimately be one of multiple types (PHP 8.0+).

**Bad:**
```php
<?php
class DataLoader {
    /**
     * @param string|array $source
     */
    public function load($source) {
        // Relies on docblock, no type enforcement
    }
}
```

**Good:**
```php
<?php

declare(strict_types=1);

class DataLoader {
    public function load(string|array $source): array
    {
        if (is_string($source)) {
            return $this->loadFromFile($source);
        }
        return $this->loadFromArray($source);
    }
}
```

---

### 1.6 Nullable Types

**Impact:** CRITICAL

Use nullable types explicitly when null is a valid value.

**Bad:**
```php
<?php
class UserService {
    public function findByEmail(string $email) {
        // Unclear if null is valid return
        return $this->repository->find($email) ?: null;
    }
}
```

**Good:**
```php
<?php

declare(strict_types=1);

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

---

## 2. Modern PHP Features

### 2.1 Constructor Property Promotion

**Impact:** CRITICAL

Use constructor property promotion to reduce boilerplate (PHP 8.0+).

**Bad:**
```php
<?php
class User {
    private string $id;
    private string $name;
    private string $email;
    
    public function __construct(string $id, string $name, string $email) {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
    }
}
```

**Good:**
```php
<?php

declare(strict_types=1);

class User {
    public function __construct(
        private string $id,
        private string $name,
        private string $email,
    ) {}
}
```

---

### 2.2 Type-Safe Enums

**Impact:** CRITICAL

Use enums instead of class constants for finite sets of values (PHP 8.1+).

**Bad:**
```php
<?php
class OrderStatus {
    public const PENDING = 'pending';
    public const SHIPPED = 'shipped';
}

function updateStatus(string $status): void {
    // 'invalid' would be accepted
}
```

**Good:**
```php
<?php

declare(strict_types=1);

enum OrderStatus: string {
    case Pending = 'pending';
    case Shipped = 'shipped';
    case Delivered = 'delivered';
    
    public function label(): string {
        return match($this) {
            self::Pending => 'Awaiting Processing',
            self::Shipped => 'On the Way',
            self::Delivered => 'Delivered',
        };
    }
}

function updateStatus(OrderStatus $status): void {
    // Only valid enum values accepted
}
```

---

### 2.3 Readonly Properties

**Impact:** CRITICAL

Use readonly properties for immutable data (PHP 8.1+).

**Bad:**
```php
<?php
class Invoice {
    private string $invoiceNumber;
    
    // Setter allows modification after creation
    public function setInvoiceNumber(string $number): void {
        $this->invoiceNumber = $number;
    }
}
```

**Good:**
```php
<?php

declare(strict_types=1);

class Invoice {
    public function __construct(
        public readonly string $invoiceNumber,
        public readonly DateTimeImmutable $issuedAt,
        public readonly float $amount,
    ) {}
    
    // No setters - properties are immutable
}
```

---

### 2.4 Match Expression

**Impact:** HIGH

Use match expressions instead of switch for cleaner, type-safe code (PHP 8.0+).

**Bad:**
```php
<?php
function getStatusMessage(int $code): string {
    switch ($code) {
        case 200:
            $message = 'OK';
            break;
        case 404:
            $message = 'Not Found';
            break;
        default:
            $message = 'Unknown';
    }
    return $message;
}
```

**Good:**
```php
<?php

declare(strict_types=1);

function getStatusMessage(int $code): string {
    return match ($code) {
        200 => 'OK',
        404 => 'Not Found',
        default => 'Unknown',
    };
}
```

---

### 2.5 Nullsafe Operator

**Impact:** HIGH

Use the nullsafe operator for cleaner null checking chains (PHP 8.0+).

**Bad:**
```php
<?php
function getCountry(?Order $order): ?string {
    if ($order !== null) {
        $customer = $order->getCustomer();
        if ($customer !== null) {
            $address = $customer->getAddress();
            if ($address !== null) {
                return $address->getCountry();
            }
        }
    }
    return null;
}
```

**Good:**
```php
<?php

declare(strict_types=1);

function getCountry(?Order $order): ?string {
    return $order?->getCustomer()?->getAddress()?->getCountry();
}
```

---

### 2.6 Arrow Functions

**Impact:** MEDIUM

Use arrow functions for short, single-expression closures (PHP 7.4+).

**Bad:**
```php
<?php
$doubled = array_map(function ($n) {
    return $n * 2;
}, $numbers);

$multiplier = 3;
$multiplied = array_map(function ($n) use ($multiplier) {
    return $n * $multiplier;
}, $numbers);
```

**Good:**
```php
<?php

declare(strict_types=1);

$doubled = array_map(fn($n) => $n * 2, $numbers);

$multiplier = 3;
$multiplied = array_map(fn($n) => $n * $multiplier, $numbers);
```

---

### 2.7 Typed Class Constants (8.3+)

**Impact:** HIGH

Add type declarations to class constants for type safety.

**Bad:**
```php
<?php
class Config {
    public const TIMEOUT = 30;       // int? string? no enforcement
    public const NAME = 'my-app';
}
```

**Good:**
```php
<?php

declare(strict_types=1);

class Config {
    public const int TIMEOUT = 30;
    public const string NAME = 'my-app';
    public const array ALLOWED = ['read', 'write'];
}
```

---

### 2.8 Override Attribute (8.3+)

**Impact:** HIGH

Use `#[\Override]` on methods that override a parent to catch typos and refactoring errors.

**Bad:**
```php
<?php
class UserRepo extends BaseRepo {
    public function findByld(int $id): ?User { /* typo: 'l' not 'I' */ }
}
```

**Good:**
```php
<?php

declare(strict_types=1);

class UserRepo extends BaseRepo {
    #[\Override]
    public function findById(int $id): ?User { /* typo caught when class is loaded */ }
}
```

---

### 2.9 Property Hooks (8.4+)

**Impact:** HIGH

Use property hooks to define get/set logic directly on properties.

**Bad:**
```php
<?php
class User {
    private string $name;
    public function getName(): string { return $this->name; }
    public function setName(string $v): void { $this->name = ucfirst($v); }
}
```

**Good:**
```php
<?php

declare(strict_types=1);

class User {
    public string $name { set => ucfirst(strtolower($value)); }
    public string $fullName { get => $this->firstName . ' ' . $this->lastName; }
}
```

---

### 2.10 Asymmetric Visibility (8.4+)

**Impact:** HIGH

Use `public private(set)` for properties that are publicly readable but privately writable.

**Bad:**
```php
<?php
class Order {
    private string $status;
    public function getStatus(): string { return $this->status; }
}
```

**Good:**
```php
<?php

declare(strict_types=1);

class Order {
    public private(set) string $status = 'pending';

    public function markPaid(): void {
        $this->status = 'paid'; // OK - internal set
    }
}
echo $order->status;      // OK - public read
// $order->status = 'x';  // Error - private set
```

---

### 2.11 Pipe Operator (8.5+)

**Impact:** HIGH

Use the pipe operator for readable left-to-right function chaining.

**Bad:**
```php
<?php
$result = htmlspecialchars(strtolower(trim($input)));
```

**Good:**
```php
<?php

declare(strict_types=1);

$result = $input
    |> trim(...)
    |> strtolower(...)
    |> htmlspecialchars(...);
```

---

## 3. PSR Standards

### 3.1 PSR-4 Autoloading

**Impact:** CRITICAL

Follow PSR-4 autoloading standard for class file organization.

**Structure:**
```
src/
  Domain/
    User/
      User.php          -> App\Domain\User\User
      UserRepository.php
  Application/
    Services/
      UserService.php   -> App\Application\Services\UserService
```

**composer.json:**
```json
{
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}
```

---

### 3.2 PSR-12 Coding Style

**Impact:** HIGH

Follow PSR-12 extended coding style for consistent, readable code.

**Key Rules:**
- Opening braces on their own line for classes, methods, and functions
- One blank line after namespace and use blocks
- Spaces after control structure keywords
- One blank line between methods
- Type declarations with no space before colon

**Good:**
```php
<?php

declare(strict_types=1);

namespace App\Services;

use App\Domain\User\User;
use App\Domain\User\UserRepository;

class UserService
{
    public function __construct(
        private UserRepository $repository,
    ) {}

    public function find(int $id): ?User
    {
        if ($id < 1) {
            return null;
        }

        return $this->repository->find($id);
    }
}
```

---

### 3.3 Class Naming Conventions

**Impact:** HIGH

Use PascalCase with descriptive, intention-revealing names.

**Patterns:**
- Entities: `User`, `Order`, `Product`
- Services: `UserService`, `OrderService`
- Repositories: `UserRepository`, `OrderRepository`
- Controllers: `UserController`, `OrderController`
- Commands: `CreateUserCommand`, `ProcessPaymentCommand`
- Events: `UserCreated`, `OrderShipped` (past tense)
- Exceptions: `UserNotFoundException`, `InvalidPaymentMethodException`
- Interfaces: `Cacheable`, `UserRepository`, `PaymentGateway`

---

## 4. SOLID Principles

### 4.1 Single Responsibility Principle

**Impact:** CRITICAL

A class should have only one reason to change.

**Bad:**
```php
<?php
class User {
    // Multiple responsibilities
    public function save(): void { /* DB */ }
    public function sendEmail(): void { /* Email */ }
    public function toJson(): string { /* Serialization */ }
}
```

**Good:**
```php
<?php

declare(strict_types=1);

class User {
    // Just user data
}

class UserRepository {
    public function save(User $user): void { /* DB */ }
}

class UserMailer {
    public function sendWelcome(User $user): void { /* Email */ }
}

class UserSerializer {
    public function toJson(User $user): string { /* Serialization */ }
}
```

---

### 4.2 Open/Closed Principle

**Impact:** HIGH

Classes should be open for extension but closed for modification.

**Bad:**
```php
<?php
class PaymentProcessor {
    public function process(string $type, float $amount): void {
        if ($type === 'credit_card') { /* ... */ }
        if ($type === 'paypal') { /* ... */ }
        // Adding new type requires modifying this class
    }
}
```

**Good:**
```php
<?php

declare(strict_types=1);

interface PaymentMethod {
    public function process(Money $amount): PaymentResult;
}

class CreditCardPayment implements PaymentMethod {
    public function process(Money $amount): PaymentResult { /* ... */ }
}

class PayPalPayment implements PaymentMethod {
    public function process(Money $amount): PaymentResult { /* ... */ }
}

// Add new payment methods without modifying existing code
class CryptoPayment implements PaymentMethod {
    public function process(Money $amount): PaymentResult { /* ... */ }
}
```

---

### 4.3 Dependency Inversion Principle

**Impact:** CRITICAL

Depend on abstractions, not concretions.

**Bad:**
```php
<?php
class OrderService {
    private MySqlDatabase $db;
    
    public function __construct() {
        $this->db = new MySqlDatabase();
    }
}
```

**Good:**
```php
<?php

declare(strict_types=1);

interface OrderRepository {
    public function save(Order $order): void;
    public function find(OrderId $id): ?Order;
}

class OrderService {
    public function __construct(
        private OrderRepository $repository,
        private PaymentGateway $payment,
        private Logger $logger,
    ) {}
}

class DoctrineOrderRepository implements OrderRepository {
    // Implementation
}
```

---

## 5. Error Handling

### 5.1 Custom Exceptions

**Impact:** HIGH

Create specific exception classes instead of using generic `\Exception`.

**Bad:**
```php
<?php
throw new \Exception('Email already exists'); // Caller can't distinguish error types
```

**Good:**
```php
<?php

declare(strict_types=1);

class DuplicateEmailException extends \RuntimeException
{
    public function __construct(private readonly string $email)
    {
        parent::__construct("Email already registered: {$email}");
    }
}

// Caller handles specifically
try {
    $service->register($data);
} catch (DuplicateEmailException $e) {
    return response()->json(['error' => 'Email taken'], 409);
} catch (ValidationException $e) {
    return response()->json(['errors' => $e->getErrors()], 422);
}
```

---

### 5.2 Catch Specific Exceptions

**Impact:** HIGH

Never catch generic `\Exception` or `\Throwable` except at top-level error boundaries.

**Bad:**
```php
<?php
try {
    $user = $repo->find($id);
    $mailer->send($user);
} catch (\Exception $e) {
    return null; // Swallows ALL errors - hides bugs
}
```

**Good:**
```php
<?php

declare(strict_types=1);

class NotificationService
{
    public function notifyUser(int $id): void
    {
        try {
            $user = $this->repo->find($id);
            $this->mailer->send($user);
        } catch (UserNotFoundException $e) {
            return;
        } catch (MailerException $e) {
            $this->logger->error('Email failed', ['error' => $e->getMessage()]);
            // Continue - email is non-critical
        }
    }
}
```

---

### 5.3 Never Suppress Errors

**Impact:** CRITICAL

Never use the `@` operator. Handle errors explicitly.

**Bad:**
```php
<?php
$data = @file_get_contents($path); // Hides all errors
$conn = @mysqli_connect('localhost', 'user', 'pass');
```

**Good:**
```php
<?php

declare(strict_types=1);

if (!is_readable($path)) {
    throw new FileNotFoundException("Not readable: {$path}");
}
$data = file_get_contents($path);
```

---

## 6. Performance

### 6.1 Generators for Large Datasets

**Impact:** MEDIUM

Use generators to process large datasets without loading everything into memory.

**Bad:**
```php
<?php
function getAllUsers(): array {
    return $stmt->fetchAll(); // 1M users = huge memory spike
}
```

**Good:**
```php
<?php

declare(strict_types=1);

function getAllUsers(PDO $pdo, int $chunk = 1000): \Generator {
    $offset = 0;
    do {
        $stmt = $pdo->prepare('SELECT * FROM users LIMIT :l OFFSET :o');
        $stmt->bindValue(':l', $chunk, PDO::PARAM_INT);
        $stmt->bindValue(':o', $offset, PDO::PARAM_INT);
        $stmt->execute();
        $rows = $stmt->fetchAll();
        foreach ($rows as $row) {
            yield User::fromArray($row);
        }
        $offset += $chunk;
    } while (count($rows) === $chunk);
}
```

---

### 6.2 Native String Functions over Regex

**Impact:** MEDIUM

Use PHP 8.0+ string functions instead of regex for simple checks.

**Bad:**
```php
<?php
if (preg_match('/^https/', $url)) { /* starts with */ }
if (preg_match('/\.pdf$/', $file)) { /* ends with */ }
```

**Good:**
```php
<?php

declare(strict_types=1);

if (str_starts_with($url, 'https')) { /* 2-10x faster */ }
if (str_ends_with($file, '.pdf')) { /* clearer intent */ }
if (str_contains($role, 'admin')) { /* no escaping needed */ }
```

---

## 7. Security

### 7.1 Input Validation

**Impact:** CRITICAL

Always validate and sanitize user input.

**Good:**
```php
<?php

declare(strict_types=1);

function createUser(array $data): User {
    $email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);
    if ($email === false) {
        throw new InvalidArgumentException('Invalid email');
    }
    
    $name = trim($data['name'] ?? '');
    if (strlen($name) < 2 || strlen($name) > 100) {
        throw new InvalidArgumentException('Name must be 2-100 characters');
    }
    
    return new User($email, $name);
}
```

---

### 7.2 Prepared Statements

**Impact:** CRITICAL

Always use prepared statements for SQL queries to prevent SQL injection.

**Bad:**
```php
<?php
$sql = "SELECT * FROM users WHERE email = '{$email}'";
$result = $db->query($sql); // SQL injection vulnerable
```

**Good:**
```php
<?php

declare(strict_types=1);

$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
$result = $stmt->fetch();
```

---

### 7.3 Password Hashing

**Impact:** CRITICAL

Use `password_hash()` and `password_verify()` for password security.

**Bad:**
```php
<?php
$hash = md5($password); // Insecure
$hash = sha1($password); // Insecure
```

**Good:**
```php
<?php

declare(strict_types=1);

// Hashing
$hash = password_hash($password, PASSWORD_ARGON2ID);

// Verification
if (password_verify($inputPassword, $storedHash)) {
    // Password correct
    if (password_needs_rehash($storedHash, PASSWORD_ARGON2ID)) {
        // Rehash if algorithm changed
        $newHash = password_hash($inputPassword, PASSWORD_ARGON2ID);
    }
}
```

---

## References

- [PHP Manual](https://www.php.net/manual/en/)
- [PHP 8.3 Release](https://www.php.net/releases/8.3/en.php)
- [PSR Standards](https://www.php-fig.org/psr/)
- [PHPStan - Static Analysis](https://phpstan.org/)
- [Psalm - Static Analysis](https://psalm.dev/)
- [PHP The Right Way](https://phptherightway.com/)

---

**Last Updated:** March 2026
**Version:** 2.1.0
**License:** MIT

Discussion

Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.

Posts are public.Sign in to post

No one has posted yet. Be the first.