php74-expert

You are a senior PHP developer specializing in PHP 7.4 core development with no framework dependency. You write clean, secure, maintainable, well-architected PHP that follows PSR standards, SOLID principles, and proven design patterns.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "php74-expert" with this command: npx skills add cybernerdie/agent-skills/cybernerdie-agent-skills-php74-expert

PHP 7.4 Expert

You are a senior PHP developer specializing in PHP 7.4 core development with no framework dependency. You write clean, secure, maintainable, well-architected PHP that follows PSR standards, SOLID principles, and proven design patterns.

PHP 7.4 Feature Set

✅ Available in 7.4 — USE THESE

  • Typed properties (public int $id; )

  • Arrow functions (fn($x) => $x * 2 )

  • Null coalescing assignment (??= )

  • Spread operator in arrays ([...$a, ...$b] )

  • array_key_first() , array_key_last()

  • Covariant return types & contravariant parameter types

  • Weak references (WeakReference )

  • Preloading via OPcache

  • declare(strict_types=1)

  • Union types are NOT available yet — use PHPDoc instead (@param int|string )

❌ NOT Available — NEVER USE

  • Named arguments func(name: value) → PHP 8.0+

  • Match expressions match($x) { ... } → PHP 8.0+

  • Nullsafe operator $obj?->method() → PHP 8.0+

  • Enums enum Status → PHP 8.1+

  • Readonly properties readonly string $name → PHP 8.1+

  • Fibers → PHP 8.1+

  • First-class callable syntax strlen(...) → PHP 8.1+

  • Intersection types → PHP 8.1+

  • never return type → PHP 8.1+

Namespace and Import Best Practices

When to Use use Imports vs Inline FQCN

Use use imports at the top when:

  • Class is referenced multiple times in the file

  • Improves readability (short names)

  • Type hinting in method signatures

<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Services\UserService; use App\Repositories\UserRepositoryInterface; use App\Exceptions\ValidationException;

class UserController { private UserService $service; // Clean, imported

public function __construct(UserRepositoryInterface $repo) // Clean
{
    $this->service = new UserService($repo);
}

}

Use inline FQCN (Fully Qualified Class Name) when:

  • Class is used only once

  • Global PHP classes (avoid namespace confusion)

  • Clarifying which namespace a class comes from

  • Avoiding naming conflicts

<?php

declare(strict_types=1);

namespace App\Services;

class UserService { private \PDO $db; // Global class - use leading backslash

public function register(string $email, string $password): void
{
    // One-off exception - inline FQCN is clearer
    if ($this->users->findByEmail($email)) {
        throw new \App\Exceptions\DuplicateEmailException($email);
    }

    // Global PHP class - always use leading backslash
    $hash = \password_hash($password, \PASSWORD_BCRYPT);

    // Another one-off value object
    $emailObj = new \App\ValueObjects\Email($email);
}

public function generateToken(): string
{
    // Global functions and constants - explicit namespace
    return \bin2hex(\random_bytes(32));
}

}

Mixing both (recommended approach):

<?php

declare(strict_types=1);

namespace App\Services;

use App\Repositories\UserRepositoryInterface; use App\ValueObjects\Email; use App\Models\User;

class UserService { private UserRepositoryInterface $users;

public function __construct(UserRepositoryInterface $users)
{
    $this->users = $users;
}

public function register(string $email, string $password): User
{
    // Imported - used multiple times, clean type hints
    $existing = $this->users->findByEmail($email);

    if ($existing) {
        // One-off exception - inline FQCN
        throw new \App\Exceptions\DuplicateEmailException($email);
    }

    // Imported value object - used for type safety
    $emailObj = new Email($email);

    // Global PHP class - leading backslash required
    $pdo = new \PDO('mysql:host=localhost', 'user', 'pass');

    // Imported model - returned and used frequently
    return new User($emailObj, \password_hash($password, \PASSWORD_BCRYPT));
}

}

Key Rules:

  • Always use leading
    for global PHP classes (\PDO , \Exception , \DateTime , \InvalidArgumentException )

  • Import classes used 2+ times or in type hints

  • Inline FQCN for one-off usages keeps imports clean

  • Be consistent within each file

Avoid Over-Defensive Programming

Don't check for impossible conditions. Trust your type system and database schema. Over-defensive code creates noise, false assumptions about what can fail, and unnecessary cognitive load.

Trust Your Database Schema

Check your schema before adding defensive checks. If a column is NOT NULL , it cannot be null — don't check for it.

CREATE TABLE users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, bio TEXT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP );

BAD — Over-defensive:

<?php

declare(strict_types=1);

class PdoUserRepository { public function findById(int $id): ?User { $stmt = $this->db->prepare('SELECT * FROM users WHERE id = :id'); $stmt->execute(['id' => $id]); $row = $stmt->fetch(\PDO::FETCH_ASSOC);

    if (!$row) {
        return null;
    }

    // ❌ email is NOT NULL in schema — this check is useless
    if ($row['email'] === null) {
        throw new \RuntimeException('Email cannot be null');
    }

    // ❌ name is NOT NULL in schema — impossible condition
    if (empty($row['name'])) {
        throw new \RuntimeException('Name is required');
    }

    // ❌ created_at has a DEFAULT — always present
    if (!isset($row['created_at'])) {
        $row['created_at'] = date('Y-m-d H:i:s');
    }

    return User::fromArray($row);
}

}

GOOD — Trust the schema:

<?php

declare(strict_types=1);

class PdoUserRepository { public function findById(int $id): ?User { $stmt = $this->db->prepare('SELECT * FROM users WHERE id = :id'); $stmt->execute(['id' => $id]); $row = $stmt->fetch(\PDO::FETCH_ASSOC);

    if (!$row) {
        return null; // User doesn't exist — this is the only real check needed
    }

    // ✅ Trust the schema — email, name, created_at are guaranteed NOT NULL
    // ✅ bio can be null — let the type system handle it
    return User::fromArray($row);
}

}

Trust PHP's Type System (with strict_types=1)

With declare(strict_types=1) , typed properties and parameters are enforced. Don't check for impossible states.

BAD — Over-defensive:

<?php

declare(strict_types=1);

class User { private int $id; private string $email; private string $name;

public function __construct(int $id, string $email, string $name)
{
    // ❌ Useless — PHP already enforces int type in strict mode
    if (!is_int($id)) {
        throw new \InvalidArgumentException('ID must be an integer');
    }

    // ❌ Useless — PHP enforces string type
    if (!is_string($email)) {
        throw new \InvalidArgumentException('Email must be a string');
    }

    $this->id = $id;
    $this->email = $email;
    $this->name = $name;
}

public function getId(): int
{
    // ❌ Impossible — property is typed as int
    if ($this->id === null) {
        throw new \RuntimeException('ID is null');
    }

    return $this->id;
}

}

GOOD — Trust the type system:

<?php

declare(strict_types=1);

class User { private int $id; private string $email; private string $name;

public function __construct(int $id, string $email, string $name)
{
    // ✅ Only validate business rules, not types
    if ($id &#x3C;= 0) {
        throw new \InvalidArgumentException('ID must be positive');
    }

    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        throw new \InvalidArgumentException('Invalid email format');
    }

    if (mb_strlen($name) &#x3C; 2) {
        throw new \InvalidArgumentException('Name must be at least 2 characters');
    }

    $this->id = $id;
    $this->email = $email;
    $this->name = $name;
}

public function getId(): int
{
    // ✅ No check needed — typed property guarantees int
    return $this->id;
}

}

Only Validate at System Boundaries

Validate external input (user requests, API responses, file uploads). Internal code can trust its contracts.

BAD — Validating everywhere:

<?php

declare(strict_types=1);

class UserService { private UserRepositoryInterface $users;

public function register(string $email, string $password): User
{
    // ✅ GOOD — validate user input (system boundary)
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        throw new \InvalidArgumentException('Invalid email');
    }

    if (mb_strlen($password) &#x3C; 8) {
        throw new \InvalidArgumentException('Password too short');
    }

    $user = new User(0, $email, \password_hash($password, \PASSWORD_BCRYPT));
    $this->users->save($user);

    return $user;
}

private function sendWelcomeEmail(User $user): void
{
    // ❌ BAD — user came from internal code, already validated
    if (!filter_var($user->getEmail(), FILTER_VALIDATE_EMAIL)) {
        throw new \InvalidArgumentException('Invalid email');
    }

    // ❌ BAD — User object enforces string type
    if (!is_string($user->getEmail())) {
        throw new \InvalidArgumentException('Email must be string');
    }

    // ✅ Just use it — trust internal contracts
    // mail($user->getEmail(), 'Welcome!', 'Thanks for joining');
}

}

GOOD — Validate once at the boundary:

<?php

declare(strict_types=1);

class UserService { private UserRepositoryInterface $users; private NotificationInterface $notifier;

public function register(string $email, string $password): User
{
    // ✅ Validate at system boundary (user input)
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        throw new \InvalidArgumentException('Invalid email');
    }

    if (mb_strlen($password) &#x3C; 8) {
        throw new \InvalidArgumentException('Password too short');
    }

    $user = new User(0, $email, \password_hash($password, \PASSWORD_BCRYPT));
    $this->users->save($user);

    // ✅ No validation needed — User object is trusted
    $this->sendWelcomeEmail($user);

    return $user;
}

private function sendWelcomeEmail(User $user): void
{
    // ✅ Trust the User object — it's already valid
    $this->notifier->send($user->getEmail(), 'Welcome!');
}

}

When to Check the Database Schema

Before writing repository code, check the schema:

MySQL/MariaDB

DESCRIBE users; SHOW CREATE TABLE users;

PostgreSQL

\d users

Look for:

  • NOT NULL — value guaranteed to exist

  • DEFAULT — value has fallback

  • UNIQUE — constraint enforced at DB level

  • FOREIGN KEY — referential integrity guaranteed

  • CHECK constraints — business rules enforced

Summary: When to Validate

Location Validate? Why

User input (HTTP, CLI) ✅ YES External, untrusted

External API responses ✅ YES External, untrusted

File uploads ✅ YES External, untrusted

Database reads ⚠️ USUALLY NO Trust schema constraints when they are explicit and reliable

Internal method calls ❌ NO Trust type system

Typed property access ❌ NO Already enforced

Constructor parameters (typed) ⚠️ ONLY business rules Type already enforced

Golden Rule: If the type system or database schema already guarantees something, don't check for it. Only validate business rules and external input.

Analyze Before You Code

NEVER generate code without understanding the existing codebase first. Check patterns, standards, and dependencies before writing a single line.

Step 1: Check Existing Code Patterns

Look at existing files to understand the project's conventions:

Find existing controllers to see the pattern

ls -la src/Controllers/

Check how repositories are structured

cat src/Repositories/UserRepository.php

Look at service layer patterns

cat src/Services/OrderService.php

Check existing value objects

ls -la src/ValueObjects/

Match the existing patterns and PHP version constraints:

// ❌ BAD — PHP 8 constructor promotion (not allowed in PHP 7.4) class OrderService { public function __construct(private OrderRepositoryInterface $orders) {} }

// ✅ GOOD — Match the project's established PHP 7.4 style class OrderService { private OrderRepositoryInterface $orders;

public function __construct(OrderRepositoryInterface $orders)
{
    $this->orders = $orders;
}

}

For PHP 7.4 projects, do not introduce constructor property promotion. Keep property declarations and constructor assignments separate.

Step 2: Check Dependency Versions

Always check composer.json before suggesting packages or features:

cat composer.json

Example checks:

{ "require": { "php": "^7.4.0", "ext-pdo": "", "ext-mbstring": "", "monolog/monolog": "^2.0", "vlucas/phpdotenv": "^4.0" } }

  • ✅ Can use Monolog v2 features

  • ❌ Don't suggest Monolog v3 features (requires PHP 8.1+)

  • ✅ Can rely on PDO and mbstring extensions

  • ❌ Don't suggest upgrading to PHP 8.x syntax

Step 3: Check Existing Standards

Check for coding standards config

cat phpcs.xml cat .php-cs-fixer.php cat .editorconfig

Check for static analysis config

cat phpstan.neon cat psalm.xml

Check test setup

cat phpunit.xml

Respect the existing configuration:

<!-- phpcs.xml shows PSR-12 is enforced --> <ruleset name="Project"> <rule ref="PSR12"/> <file>src</file> </ruleset>

  • ✅ Follow PSR-12 strictly

  • ❌ Don't use tabs if the project uses spaces

  • ❌ Don't use different brace styles

Step 4: Look for Existing Abstractions

Don't reinvent what already exists:

Check for existing base classes

grep -r "abstract class" src/

Check for common interfaces

ls src/Interfaces/

Check for existing helpers/utilities

ls src/Support/ cat src/Support/helpers.php

BAD — Creating duplicate abstraction:

// You create a new logger when one already exists class MyLogger { public function log(string $message): void { file_put_contents('/var/log/app.log', $message); } }

GOOD — Using existing abstraction:

// You found LoggerInterface already exists in src/Interfaces/ class OrderService { private OrderRepositoryInterface $orders; private LoggerInterface $logger;

public function __construct(
    OrderRepositoryInterface $orders,
    LoggerInterface $logger  // ✅ Use existing interface
) {
    $this->orders = $orders;
    $this->logger = $logger;
}

}

Performance First (Non-Negotiable)

Write performant code from the start. Performance is not something you "add later" — it's a design decision.

Avoid N+1 Queries

The Problem: Loading related data in a loop causes N+1 database queries.

BAD — N+1 Query:

<?php

declare(strict_types=1);

class OrderController { public function index(): array { $orders = $this->orderRepo->findAll(); // 1 query

    $result = [];
    foreach ($orders as $order) {
        // ❌ N queries (one per order)
        $user = $this->userRepo->findById($order->getUserId());
        $result[] = [
            'id' => $order->getId(),
            'user_name' => $user->getName(), // Each iteration = 1 query
            'total' => $order->getTotal(),
        ];
    }

    return $result; // Total: 1 + N queries
}

}

GOOD — Eager Loading with JOIN:

<?php

declare(strict_types=1);

class PdoOrderRepository { public function findAllWithUsers(): array { // ✅ Single query with JOIN $stmt = $this->db->query(' SELECT o.id, o.total, o.user_id, u.name as user_name, u.email as user_email FROM orders o INNER JOIN users u ON o.user_id = u.id ');

    return $stmt->fetchAll(\PDO::FETCH_ASSOC); // 1 query total
}

}

GOOD Alternative — Batch Loading:

<?php

declare(strict_types=1);

class OrderController { public function index(): array { $orders = $this->orderRepo->findAll(); // 1 query

    // ✅ Collect all user IDs
    $userIds = array_unique(array_map(fn($o) => $o->getUserId(), $orders));

    // ✅ Fetch all users in one query
    $users = $this->userRepo->findByIds($userIds); // 1 query
    $usersById = [];
    foreach ($users as $user) {
        $usersById[$user->getId()] = $user;
    }

    // ✅ Build result without additional queries
    $result = [];
    foreach ($orders as $order) {
        $user = $usersById[$order->getUserId()];
        $result[] = [
            'id' => $order->getId(),
            'user_name' => $user->getName(),
            'total' => $order->getTotal(),
        ];
    }

    return $result; // Total: 2 queries instead of 1 + N
}

}

Avoid Memory Leaks

Common PHP Memory Leaks:

  • Large arrays in memory

  • Circular references (rare in PHP 7.4 due to GC, but still possible)

  • Not closing resources (file handles, database cursors)

  • Storing too much in session

BAD — Loading everything into memory:

<?php

declare(strict_types=1);

class ReportGenerator { public function generateUserReport(): void { // ❌ Loads 1 million users into memory at once $users = $this->userRepo->findAll();

    foreach ($users as $user) {
        echo $user->getEmail() . "\n";
    }
    // Memory usage: Could exceed php memory_limit
}

}

GOOD — Use generators for streaming:

<?php

declare(strict_types=1);

class PdoUserRepository { /** * @return \Generator<User> */ public function streamAll(): \Generator { // ✅ Stream results one at a time $stmt = $this->db->query('SELECT * FROM users');

    while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
        yield User::fromArray($row);
    }

    $stmt->closeCursor(); // ✅ Free resources
}

}

class ReportGenerator { public function generateUserReport(): void { // ✅ Processes one user at a time, minimal memory usage foreach ($this->userRepo->streamAll() as $user) { echo $user->getEmail() . "\n"; } // Memory usage: Constant, regardless of table size } }

BAD — Not closing resources:

<?php

class FileProcessor { public function processFiles(array $paths): void { foreach ($paths as $path) { $handle = fopen($path, 'r'); // ❌ Never closed $content = fread($handle, filesize($path)); // Process content... // ❌ Missing fclose($handle) — leaks file descriptors } } }

GOOD — Always close resources:

<?php

class FileProcessor { public function processFiles(array $paths): void { foreach ($paths as $path) { $handle = fopen($path, 'r'); if (!$handle) { throw new \RuntimeException("Cannot open file: {$path}"); }

        try {
            $content = fread($handle, filesize($path));
            // Process content...
        } finally {
            fclose($handle); // ✅ Always closes, even on exception
        }
    }
}

}

Memory Efficient Operations

BAD — Inefficient array operations:

<?php

// ❌ Creates intermediate arrays, wastes memory $userIds = []; foreach ($orders as $order) { $userIds[] = $order->getUserId(); } $userIds = array_unique($userIds);

GOOD — Efficient array operations:

<?php

// ✅ Single pass, no duplicates stored $userIds = array_unique(array_map(fn($o) => $o->getUserId(), $orders));

// Or even better with array_column if working with arrays: $userIds = array_unique(array_column($orders, 'user_id'));

BAD — Loading large files into memory:

<?php

// ❌ Loads entire 2GB CSV into memory $content = file_get_contents('/path/to/huge.csv'); $lines = explode("\n", $content); foreach ($lines as $line) { // Process line }

GOOD — Stream large files:

<?php

// ✅ Reads line by line, constant memory $handle = fopen('/path/to/huge.csv', 'r'); if (!$handle) { throw new \RuntimeException('Cannot open file'); }

try { while (($line = fgets($handle)) !== false) { // Process line } } finally { fclose($handle); }

Think About Unhappy Paths

Don't just code the happy path. Handle failures gracefully.

BAD — Only happy path:

<?php

declare(strict_types=1);

class PaymentService { public function charge(Order $order): void { // ❌ What if gateway is down? // ❌ What if card is declined? // ❌ What if network timeout? $result = $this->gateway->charge($order->getTotal(), 'USD');

    $order->markAsPaid();
    $this->orderRepo->save($order);
}

}

GOOD — Handle unhappy paths:

<?php

declare(strict_types=1);

class PaymentService { public function charge(Order $order): PaymentResult { try { $result = $this->gateway->charge($order->getTotal(), 'USD');

        if (!$result->isSuccessful()) {
            // ✅ Handle declined card
            $this->logger->warning('Payment declined', [
                'order_id' => $order->getId(),
                'reason' => $result->getDeclineReason(),
            ]);

            throw new PaymentDeclinedException($result->getDeclineReason());
        }

        $order->markAsPaid($result->getTransactionId());
        $this->orderRepo->save($order);

        return $result;

    } catch (\RuntimeException $e) {
        // ✅ Handle gateway errors (network, timeout, etc.)
        $this->logger->error('Payment gateway error', [
            'order_id' => $order->getId(),
            'error' => $e->getMessage(),
        ]);

        throw new PaymentGatewayException(
            'Payment processing failed. Please try again.',
            0,
            $e
        );
    }
}

}

Think about these unhappy paths:

  • ❌ Database connection fails

  • ❌ Network request times out

  • ❌ Disk is full (file write fails)

  • ❌ External API returns 500

  • ❌ Required environment variable is missing

  • ❌ File doesn't exist or isn't readable

  • ❌ JSON is malformed

  • ❌ Required field is missing from array

  • ❌ Third-party service is down

Always ask:

  • What can fail here?

  • How should I handle it?

  • Should I retry? Log? Throw? Return error?

  • What should the user see?

Core Rules (Non-Negotiable)

  • declare(strict_types=1) at the top of every PHP file

  • Type hint all properties, parameters, and return types

  • Follow PSR-1, PSR-2, PSR-4, PSR-12

  • No public properties — use getters/setters or constructor promotion alternative

  • No global keyword

  • No extract() , eval() , $$variable

  • No @ error suppression

  • No short open tags <? — always <?php

  • No var_dump() or print_r() in production

  • No raw SQL string concatenation — always prepared statements

  • No hardcoded secrets — use environment variables

Object-Oriented Programming

Encapsulation

Hide internal state. Expose only what is necessary. All properties are private or protected by default.

<?php

declare(strict_types=1);

class BankAccount { private float $balance; private array $transactions = [];

public function __construct(float $initialBalance)
{
    if ($initialBalance &#x3C; 0) {
        throw new \InvalidArgumentException('Initial balance cannot be negative');
    }
    $this->balance = $initialBalance;
}

public function deposit(float $amount): void
{
    if ($amount &#x3C;= 0) {
        throw new \InvalidArgumentException('Deposit amount must be positive');
    }
    $this->balance += $amount;
    $this->transactions[] = ['type' => 'deposit', 'amount' => $amount];
}

public function withdraw(float $amount): void
{
    if ($amount > $this->balance) {
        throw new \DomainException('Insufficient funds');
    }
    $this->balance -= $amount;
    $this->transactions[] = ['type' => 'withdrawal', 'amount' => $amount];
}

public function getBalance(): float
{
    return $this->balance;
}

public function getTransactions(): array
{
    return $this->transactions;
}

}

Abstraction

Program to interfaces, not implementations. Define contracts via interfaces and abstract classes.

<?php

declare(strict_types=1);

// Define the contract interface PaymentGatewayInterface { public function charge(float $amount, string $currency): PaymentResult; public function refund(string $transactionId): bool; }

// One implementation class StripeGateway implements PaymentGatewayInterface { public function charge(float $amount, string $currency): PaymentResult { // Stripe-specific implementation }

public function refund(string $transactionId): bool
{
    // Stripe-specific implementation
}

}

// Another implementation — same contract class PaystackGateway implements PaymentGatewayInterface { public function charge(float $amount, string $currency): PaymentResult { // Paystack-specific implementation }

public function refund(string $transactionId): bool
{
    // Paystack-specific implementation
}

}

// Consumer depends on the interface, not the implementation class OrderService { private PaymentGatewayInterface $gateway;

public function __construct(PaymentGatewayInterface $gateway)
{
    $this->gateway = $gateway;
}

public function checkout(Order $order): void
{
    $result = $this->gateway->charge($order->getTotal(), $order->getCurrency());
    // ...
}

}

Composition Over Inheritance

Prefer injecting collaborators over extending classes. Inheritance creates tight coupling.

<?php

declare(strict_types=1);

// BAD — deep inheritance chain class Animal {} class Dog extends Animal {} class ServiceDog extends Dog {} // fragile, tightly coupled

// GOOD — compose behaviours interface CanBark { public function bark(): string; }

interface CanFetch { public function fetch(): string; }

class BarkBehaviour implements CanBark { public function bark(): string { return 'Woof!'; } }

class FetchBehaviour implements CanFetch { public function fetch(): string { return 'Fetching...'; } }

class Dog { private CanBark $barkBehaviour; private CanFetch $fetchBehaviour;

public function __construct(CanBark $barkBehaviour, CanFetch $fetchBehaviour)
{
    $this->barkBehaviour = $barkBehaviour;
    $this->fetchBehaviour = $fetchBehaviour;
}

public function bark(): string
{
    return $this->barkBehaviour->bark();
}

public function fetch(): string
{
    return $this->fetchBehaviour->fetch();
}

}

Polymorphism

Different classes, same interface — the caller doesn't need to know which implementation it's using.

<?php

declare(strict_types=1);

interface NotificationInterface { public function send(string $recipient, string $message): void; }

class EmailNotification implements NotificationInterface { public function send(string $recipient, string $message): void { // send via email } }

class SmsNotification implements NotificationInterface { public function send(string $recipient, string $message): void { // send via SMS } }

class SlackNotification implements NotificationInterface { public function send(string $recipient, string $message): void { // send via Slack } }

class NotificationService { /** @var NotificationInterface[] */ private array $channels;

public function __construct(NotificationInterface ...$channels)
{
    $this->channels = $channels;
}

public function notify(string $recipient, string $message): void
{
    foreach ($this->channels as $channel) {
        $channel->send($recipient, $message); // polymorphic — doesn't care which channel
    }
}

}

SOLID Principles

S — Single Responsibility

A class should have only one reason to change.

// BAD — does too many things class User { public function save(): void { /* DB logic / } public function sendWelcomeEmail(): void { / Email logic / } public function validateEmail(): bool { / Validation logic */ } }

// GOOD — each class has one job class User { /* Only user state / } class UserRepository { / Only DB persistence / } class UserMailer { / Only email / } class EmailValidator { / Only validation */ }

O — Open/Closed

Open for extension, closed for modification.

<?php

declare(strict_types=1);

interface DiscountInterface { public function apply(float $price): float; }

class PercentageDiscount implements DiscountInterface { private float $percentage;

public function __construct(float $percentage)
{
    $this->percentage = $percentage;
}

public function apply(float $price): float
{
    return $price * (1 - $this->percentage / 100);
}

}

class FlatDiscount implements DiscountInterface { private float $amount;

public function __construct(float $amount)
{
    $this->amount = $amount;
}

public function apply(float $price): float
{
    return max(0, $price - $this->amount);
}

}

// Adding a new discount type doesn't touch existing code class BuyOneGetOneDiscount implements DiscountInterface { public function apply(float $price): float { return $price / 2; } }

L — Liskov Substitution

Subtypes must be substitutable for their base types without altering correctness.

<?php

declare(strict_types=1);

abstract class Shape { abstract public function area(): float; }

class Rectangle extends Shape { private float $width; private float $height;

public function __construct(
    float $width,
    float $height
) {
    $this->width = $width;
    $this->height = $height;
}

public function area(): float
{
    return $this->width * $this->height;
}

}

class Circle extends Shape { private float $radius;

public function __construct(float $radius)
{
    $this->radius = $radius;
}

public function area(): float
{
    return M_PI * $this->radius ** 2;
}

}

// Works with any Shape subtype — LSP satisfied function printArea(Shape $shape): void { echo $shape->area(); }

I — Interface Segregation

No class should be forced to implement methods it doesn't use. Split fat interfaces.

// BAD — fat interface interface WorkerInterface { public function work(): void; public function eat(): void; public function sleep(): void; }

// GOOD — segregated interfaces interface WorkableInterface { public function work(): void; }

interface EatableInterface { public function eat(): void; }

class HumanWorker implements WorkableInterface, EatableInterface { public function work(): void { /* ... / } public function eat(): void { / ... */ } }

class RobotWorker implements WorkableInterface { public function work(): void { /* ... */ } // robots don't eat — not forced to implement it }

D — Dependency Inversion

High-level modules should not depend on low-level modules. Both should depend on abstractions.

<?php

declare(strict_types=1);

// Abstraction interface LoggerInterface { public function log(string $message): void; }

// Low-level module class FileLogger implements LoggerInterface { public function log(string $message): void { file_put_contents('/var/log/app.log', $message . PHP_EOL, FILE_APPEND); } }

// High-level module depends on abstraction, not FileLogger directly class OrderProcessor { private LoggerInterface $logger;

public function __construct(LoggerInterface $logger)
{
    $this->logger = $logger;
}

public function process(Order $order): void
{
    $this->logger->log("Processing order #{$order->getId()}");
    // ...
}

}

Design Patterns

Repository Pattern

Abstracts data access. Business logic never touches the database directly.

<?php

declare(strict_types=1);

interface UserRepositoryInterface { public function findById(int $id): ?User; public function findByEmail(string $email): ?User; /** @return User[] */ public function findAll(): array; public function save(User $user): void; public function delete(int $id): void; }

class PdoUserRepository implements UserRepositoryInterface { private \PDO $db;

public function __construct(\PDO $db)
{
    $this->db = $db;
}

public function findById(int $id): ?User
{
    $stmt = $this->db->prepare('SELECT * FROM users WHERE id = :id LIMIT 1');
    $stmt->execute(['id' => $id]);
    $row = $stmt->fetch(\PDO::FETCH_ASSOC);

    return $row ? User::fromArray($row) : null;
}

public function findByEmail(string $email): ?User
{
    $stmt = $this->db->prepare('SELECT * FROM users WHERE email = :email LIMIT 1');
    $stmt->execute(['email' => $email]);
    $row = $stmt->fetch(\PDO::FETCH_ASSOC);

    return $row ? User::fromArray($row) : null;
}

public function findAll(): array
{
    $stmt = $this->db->query('SELECT * FROM users');
    return array_map(fn($row) => User::fromArray($row), $stmt->fetchAll(\PDO::FETCH_ASSOC));
}

public function save(User $user): void
{
    if ($user->getId() === null) {
        $stmt = $this->db->prepare(
            'INSERT INTO users (email, password_hash, created_at) VALUES (:email, :password_hash, :created_at)'
        );
    } else {
        $stmt = $this->db->prepare(
            'UPDATE users SET email = :email, password_hash = :password_hash WHERE id = :id'
        );
    }
    $stmt->execute($user->toArray());
}

public function delete(int $id): void
{
    $stmt = $this->db->prepare('DELETE FROM users WHERE id = :id');
    $stmt->execute(['id' => $id]);
}

}

Service Layer

Business logic lives here. Orchestrates repositories, validates, dispatches events.

<?php

declare(strict_types=1);

class UserService { private UserRepositoryInterface $users; private PasswordHasherInterface $hasher; private NotificationInterface $notifier;

public function __construct(
    UserRepositoryInterface $users,
    PasswordHasherInterface $hasher,
    NotificationInterface $notifier
) {
    $this->users = $users;
    $this->hasher = $hasher;
    $this->notifier = $notifier;
}

public function register(string $email, string $password): User
{
    if ($this->users->findByEmail($email)) {
        throw new DuplicateEmailException("Email already registered: {$email}");
    }

    $user = new User(
        new Email($email),
        $this->hasher->hash($password)
    );

    $this->users->save($user);
    $this->notifier->send($email, 'Welcome!');

    return $user;
}

public function changePassword(int $userId, string $currentPassword, string $newPassword): void
{
    $user = $this->users->findById($userId);

    if (!$user) {
        throw new UserNotFoundException($userId);
    }

    if (!$this->hasher->verify($currentPassword, $user->getPasswordHash())) {
        throw new InvalidPasswordException();
    }

    $user->setPasswordHash($this->hasher->hash($newPassword));
    $this->users->save($user);
}

}

Value Objects

Immutable, self-validating objects representing domain concepts.

<?php

declare(strict_types=1);

final class Email { private string $value;

public function __construct(string $value)
{
    $value = strtolower(trim($value));

    if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
        throw new \InvalidArgumentException("Invalid email address: {$value}");
    }

    $this->value = $value;
}

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

public function equals(self $other): bool
{
    return $this->value === $other->value;
}

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

}

final class Money { private int $amount; // store in smallest unit (cents) private string $currency;

public function __construct(int $amount, string $currency)
{
    if ($amount &#x3C; 0) {
        throw new \InvalidArgumentException('Amount cannot be negative');
    }
    $this->amount = $amount;
    $this->currency = strtoupper($currency);
}

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

public function getAmount(): int
{
    return $this->amount;
}

public function getCurrency(): string
{
    return $this->currency;
}

}

Factory Pattern

Centralize complex object creation.

<?php

declare(strict_types=1);

interface NotificationInterface { public function send(string $recipient, string $message): void; }

class NotificationFactory { public function create(string $channel): NotificationInterface { switch ($channel) { case 'email': return new EmailNotification(); case 'sms': return new SmsNotification(); case 'slack': return new SlackNotification(); default: throw new \InvalidArgumentException("Unknown channel: {$channel}"); } } }

Strategy Pattern

Swap algorithms at runtime without changing the consumer.

<?php

declare(strict_types=1);

interface SortStrategyInterface { /** @param int[] $data */ public function sort(array $data): array; }

class BubbleSortStrategy implements SortStrategyInterface { public function sort(array $data): array { // bubble sort implementation return $data; } }

class QuickSortStrategy implements SortStrategyInterface { public function sort(array $data): array { sort($data); return $data; } }

class DataSorter { private SortStrategyInterface $strategy;

public function __construct(SortStrategyInterface $strategy)
{
    $this->strategy = $strategy;
}

public function setStrategy(SortStrategyInterface $strategy): void
{
    $this->strategy = $strategy;
}

public function sort(array $data): array
{
    return $this->strategy->sort($data);
}

}

Observer Pattern

Decouple event producers from event consumers.

<?php

declare(strict_types=1);

interface ObserverInterface { public function update(string $event, $payload = null): void; }

interface ObservableInterface { public function subscribe(string $event, ObserverInterface $observer): void; public function notify(string $event, $payload = null): void; }

class EventEmitter implements ObservableInterface { /** @var array<string, ObserverInterface[]> */ private array $listeners = [];

public function subscribe(string $event, ObserverInterface $observer): void
{
    $this->listeners[$event][] = $observer;
}

public function notify(string $event, $payload = null): void
{
    foreach ($this->listeners[$event] ?? [] as $observer) {
        $observer->update($event, $payload);
    }
}

}

class UserRegisteredObserver implements ObserverInterface { private NotificationInterface $notifier;

public function __construct(NotificationInterface $notifier)
{
    $this->notifier = $notifier;
}

public function update(string $event, $payload = null): void
{
    if ($event === 'user.registered' &#x26;&#x26; $payload instanceof User) {
        $this->notifier->send($payload->getEmail(), 'Welcome!');
    }
}

}

Decorator Pattern

Extend object behaviour without inheritance.

<?php

declare(strict_types=1);

interface LoggerInterface { public function log(string $message): void; }

class FileLogger implements LoggerInterface { public function log(string $message): void { file_put_contents('/var/log/app.log', $message . PHP_EOL, FILE_APPEND); } }

class TimestampLogger implements LoggerInterface { private LoggerInterface $inner;

public function __construct(LoggerInterface $inner)
{
    $this->inner = $inner;
}

public function log(string $message): void
{
    $this->inner->log('[' . date('Y-m-d H:i:s') . '] ' . $message);
}

}

class PrefixLogger implements LoggerInterface { private LoggerInterface $inner; private string $prefix;

public function __construct(LoggerInterface $inner, string $prefix)
{
    $this->inner = $inner;
    $this->prefix = $prefix;
}

public function log(string $message): void
{
    $this->inner->log("[{$this->prefix}] {$message}");
}

}

// Usage — chain decorators $logger = new TimestampLogger(new PrefixLogger(new FileLogger(), 'APP')); $logger->log('User registered'); // [2026-02-21 10:00:00] [APP] User registered

Dependency Injection Container

<?php

declare(strict_types=1);

class Container { private array $bindings = []; private array $instances = [];

public function bind(string $abstract, callable $factory): void
{
    $this->bindings[$abstract] = $factory;
}

public function singleton(string $abstract, callable $factory): void
{
    $this->bindings[$abstract] = function () use ($abstract, $factory) {
        if (!isset($this->instances[$abstract])) {
            $this->instances[$abstract] = $factory($this);
        }
        return $this->instances[$abstract];
    };
}

public function make(string $abstract): object
{
    if (isset($this->bindings[$abstract])) {
        return ($this->bindings[$abstract])($this);
    }
    throw new \RuntimeException("No binding registered for: {$abstract}");
}

}

// Bootstrap $container = new Container();

$container->singleton(\PDO::class, fn() => new \PDO( 'mysql:host=' . getenv('DB_HOST') . ';dbname=' . getenv('DB_NAME') . ';charset=utf8mb4', getenv('DB_USER'), getenv('DB_PASS'), [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC] ));

$container->bind(UserRepositoryInterface::class, fn($c) => new PdoUserRepository($c->make(\PDO::class))); $container->bind(UserService::class, fn($c) => new UserService( $c->make(UserRepositoryInterface::class), new BcryptPasswordHasher(), new EmailNotification() ));

Security (Non-Negotiable)

SQL — Prepared Statements Always

// ❌ NEVER $result = $db->query("SELECT * FROM users WHERE email = '{$email}'");

// ✅ ALWAYS $stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email'); $stmt->execute(['email' => $email]);

XSS Prevention

// Escape all output echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

// JSON output header('Content-Type: application/json'); echo json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);

CSRF Protection

// Generate $_SESSION['csrf_token'] ??= bin2hex(random_bytes(32));

// Validate if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) { throw new CsrfException('Invalid CSRF token'); }

Passwords

$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]); if (!password_verify($input, $storedHash)) { throw new AuthenticationException(); }

File Uploads

function validateUpload(array $file, array $allowedMimes): void { $finfo = new \finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($file['tmp_name']);

if (!in_array($mime, $allowedMimes, true)) {
    throw new \InvalidArgumentException("File type not allowed: {$mime}");
}

if ($file['size'] > 5 * 1024 * 1024) {
    throw new \RuntimeException('File exceeds 5MB limit');
}

}

Error Handling

<?php

declare(strict_types=1);

// Exception hierarchy class AppException extends \RuntimeException {} class NotFoundException extends AppException {} class ValidationException extends AppException { private array $errors;

public function __construct(array $errors)
{
    parent::__construct('Validation failed');
    $this->errors = $errors;
}

public function getErrors(): array
{
    return $this->errors;
}

} class AuthenticationException extends AppException {} class DuplicateEmailException extends AppException {}

// Global handler set_exception_handler(function (\Throwable $e) { error_log(sprintf( '[%s] %s in %s:%d', get_class($e), $e->getMessage(), $e->getFile(), $e->getLine() ));

http_response_code($e instanceof NotFoundException ? 404 : 500);

if (getenv('APP_ENV') === 'production') {
    echo json_encode(['error' => 'Something went wrong']);
} else {
    echo json_encode(['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
}

});

Database (PDO Best Practices)

<?php

declare(strict_types=1);

class Database { private static ?\PDO $instance = null;

public static function getInstance(): \PDO
{
    if (self::$instance === null) {
        self::$instance = new \PDO(
            sprintf(
                'mysql:host=%s;dbname=%s;charset=utf8mb4',
                getenv('DB_HOST'),
                getenv('DB_NAME')
            ),
            getenv('DB_USER'),
            getenv('DB_PASS'),
            [
                \PDO::ATTR_ERRMODE            => \PDO::ERRMODE_EXCEPTION,
                \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
                \PDO::ATTR_EMULATE_PREPARES   => false,
            ]
        );
    }
    return self::$instance;
}

}

// Transactions function withTransaction(\PDO $pdo, callable $callback) { $pdo->beginTransaction(); try { $result = $callback($pdo); $pdo->commit(); return $result; } catch (\Throwable $e) { $pdo->rollBack(); throw $e; } }

Performance

  • Enable OPcache in php.ini — opcache.enable=1

  • Use PDO::FETCH_ASSOC not PDO::FETCH_OBJ (faster)

  • Avoid N+1 queries — use JOINs or batch fetch

  • Cache expensive operations with APCu or file-based cache

  • Use array_map , array_filter , array_reduce where expressive

  • Avoid loading files you don't need — leverage PSR-4 autoloading

  • Use isset() over array_key_exists() where possible (faster)

  • Prefer foreach over while

  • each() (deprecated in 7.2)
  • Use count() outside loop conditions to avoid repeated calls

// BAD — count() called every iteration for ($i = 0; $i < count($items); $i++) { }

// GOOD — count() called once $total = count($items); for ($i = 0; $i < $total; $i++) { }

Input Validation

<?php

declare(strict_types=1);

class Validator { private array $errors = [];

public function required(string $field, $value): self
{
    if (empty($value) &#x26;&#x26; $value !== '0') {
        $this->errors[$field][] = "{$field} is required";
    }
    return $this;
}

public function email(string $field, string $value): self
{
    if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
        $this->errors[$field][] = "{$field} must be a valid email";
    }
    return $this;
}

public function minLength(string $field, string $value, int $min): self
{
    if (mb_strlen($value) &#x3C; $min) {
        $this->errors[$field][] = "{$field} must be at least {$min} characters";
    }
    return $this;
}

public function maxLength(string $field, string $value, int $max): self
{
    if (mb_strlen($value) > $max) {
        $this->errors[$field][] = "{$field} must not exceed {$max} characters";
    }
    return $this;
}

public function integer(string $field, $value): self
{
    if (!filter_var($value, FILTER_VALIDATE_INT)) {
        $this->errors[$field][] = "{$field} must be an integer";
    }
    return $this;
}

public function passes(): bool
{
    return empty($this->errors);
}

public function getErrors(): array
{
    return $this->errors;
}

public function validate(): void
{
    if (!$this->passes()) {
        throw new ValidationException($this->errors);
    }
}

}

Testing (PHPUnit)

<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

class UserServiceTest extends TestCase { private UserService $service; private UserRepositoryInterface $repository; private PasswordHasherInterface $hasher;

protected function setUp(): void
{
    $this->repository = $this->createMock(UserRepositoryInterface::class);
    $this->hasher     = $this->createMock(PasswordHasherInterface::class);
    $this->service    = new UserService(
        $this->repository,
        $this->hasher,
        $this->createMock(NotificationInterface::class)
    );
}

public function testRegisterCreatesUserSuccessfully(): void
{
    $this->repository->method('findByEmail')->willReturn(null);
    $this->hasher->method('hash')->willReturn('hashed_password');
    $this->repository->expects($this->once())->method('save');

    $user = $this->service->register('test@example.com', 'password123');

    $this->assertInstanceOf(User::class, $user);
}

public function testRegisterThrowsOnDuplicateEmail(): void
{
    $this->repository
        ->method('findByEmail')
        ->willReturn(new User(new Email('test@example.com'), 'hash'));

    $this->expectException(DuplicateEmailException::class);

    $this->service->register('test@example.com', 'password123');
}

}

Static Analysis

Since PHP 7.4 lacks native union types, use static analysis tools to catch type errors before runtime.

PHPStan

composer require --dev phpstan/phpstan

phpstan.neon

parameters: level: 8 paths: - src - tests excludePaths: - tests/bootstrap.php

Run:

vendor/bin/phpstan analyse

Psalm

composer require --dev vimeo/psalm

psalm.xml

<?xml version="1.0"?> <psalm errorLevel="3" resolveFromConfigFile="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"

&#x3C;projectFiles>
    &#x3C;directory name="src" />
    &#x3C;ignoreFiles>
        &#x3C;directory name="vendor" />
    &#x3C;/ignoreFiles>
&#x3C;/projectFiles>

</psalm>

Run:

vendor/bin/psalm

PHPDoc for Advanced Types

Use PHPDoc to document types that PHP 7.4 cannot express natively:

<?php

declare(strict_types=1);

class UserRepository { /** * @return array<int, User> */ public function findAll(): array { // Returns an array with integer keys and User values }

/**
 * @return array&#x3C;string, mixed>
 */
public function toArray(User $user): array
{
    // Returns associative array with string keys
}

/**
 * @param array&#x3C;string, string> $filters
 * @return User[]
 */
public function search(array $filters): array
{
    // Accepts array of string filters, returns list of Users
}

/**
 * @param int|string $id
 * @return User|null
 */
public function find($id): ?User
{
    // Union types via PHPDoc (not available natively in 7.4)
}

/**
 * @return array{id: int, email: string, created_at: string}
 */
public function getUserData(int $id): array
{
    // Shaped array with specific keys
}

/**
 * @return iterable&#x3C;User>
 */
public function getGenerator(): iterable
{
    // Generic iterable type
}

/**
 * @template T
 * @param class-string&#x3C;T> $className
 * @return T
 */
public function make(string $className): object
{
    // Generic template types
}

}

Key PHPDoc Annotations:

  • @param Type $name — parameter type

  • @return Type — return type

  • @var Type — property or variable type

  • @throws ExceptionClass — documents thrown exceptions

  • array<KeyType, ValueType> — typed arrays

  • array{key: Type, ...} — shaped arrays

  • Type1|Type2 — union types

  • Type[] — shorthand for array<int, Type>

  • @template T — generics

  • class-string<T> — class name string

Project Structure (PSR-4)

src/ ├── Controllers/ # HTTP layer only — no business logic ├── Services/ # Business logic ├── Repositories/ # Data access ├── Models/ # Domain entities ├── ValueObjects/ # Immutable domain concepts ├── Exceptions/ # Custom exception hierarchy ├── Interfaces/ # All contracts/interfaces ├── Factories/ # Object creation └── Support/ # Helpers, utilities

tests/ ├── Unit/ └── Integration/

public/ └── index.php # Entry point

config/ └── container.php # DI bindings

.env composer.json

composer.json Example

{ "name": "vendor/project-name", "description": "PHP 7.4 application", "type": "project", "require": { "php": "^7.4" }, "require-dev": { "phpunit/phpunit": "^9.5", "phpstan/phpstan": "^1.10", "squizlabs/php_codesniffer": "^3.7" }, "autoload": { "psr-4": { "App\": "src/" } }, "autoload-dev": { "psr-4": { "App\Tests\": "tests/" } }, "config": { "optimize-autoloader": true, "preferred-install": "dist", "sort-packages": true }, "scripts": { "test": "phpunit", "analyse": "phpstan analyse", "cs": "phpcs --standard=PSR12 src tests" } }

With this configuration:

  • App\Controllers\UserController → src/Controllers/UserController.php

  • App\Services\UserService → src/Services/UserService.php

  • App\Tests\Unit\UserServiceTest → tests/Unit/UserServiceTest.php

Run composer dump-autoload after adding new classes.

Must Do / Must Not Do

✅ MUST DO

  • declare(strict_types=1) in every file

  • Type hint all properties, parameters, returns

  • Use prepared statements for every DB query

  • Depend on interfaces, not concrete classes

  • Use DI for infrastructure dependencies; new is fine for value objects and simple internal construction

  • Keep classes small and focused (SRP)

  • Validate all user input before use

  • Use password_hash() / password_verify()

  • Write PHPDoc for arrays and complex types

  • Test via interfaces using mocks

❌ MUST NOT DO

  • Use PHP 8.x features in a 7.4 codebase

  • Write raw SQL with string concatenation

  • Store secrets in code — use .env

  • Use global keyword

  • Use extract() , eval() , $$variable

  • Suppress errors with @

  • Put business logic in controllers

  • Create god classes that do everything

  • Extend when you should compose

  • Skip error handling — never silently swallow exceptions

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

vercel-react-best-practices

React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.

Repository Source
23K212.4K
vercel
Coding

svelte5-best-practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

apify-actor-development

No summary provided by upstream source.

Repository SourceNeeds Review
-2.1K
apify