Modern PHP 8.x Masterclass
A definitive guide to PHP programming from core interpreter fundamentals to enterprise engineering and high-performance deployment. Every topic is presented through the lens of Modern PHP, with idiomatic PHP 8+ code, legacy contrast, and explicit anti-pattern corrections.
topics
1 The Modern PHP Ecosystem
Conceptual Explanation
PHP is no longer a simple CGI script runner. In production, it runs primarily as PHP-FPM (FastCGI Process Manager) behind a reverse proxy such as nginx or Apache. The CLI SAPI handles cron jobs, queues, migrations, and local development. PHP 8.x also ships a built-in development server (php -S localhost:8000) which is useful only for local prototyping — never expose it to untrusted networks.
The Zend Engine compiles PHP source into opcodes, executes them in a request lifecycle, and destroys the request-bound state at the end of every HTTP request. This shared-nothing architecture is PHP's superpower for horizontal scaling and isolation, but it also means the developer must explicitly persist state via sessions, cookies, databases, or caches.
Modern php.ini configuration is security-critical: disable dangerous functions, set sane upload limits, enforce expose_php = Off, configure OPcache in production, and keep error display disabled on public-facing servers.
Idiomatic Code Example
; Hide PHP version from response headers
expose_php = Off
; Do not show errors to users in production
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log
; Sane resource limits
memory_limit = 256M
upload_max_filesize = 10M
post_max_size = 12M
max_execution_time = 30
; OPcache — mandatory in production
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 10000
opcache.validate_timestamps = 0 ; Set 1 only in development
Common Anti-Patterns & Mistakes
- Using the built-in server in production: It is single-threaded, has no process manager, and is trivial to crash. Use PHP-FPM behind nginx/Apache.
- Leaving
display_errors = Onin production: Stack traces leak paths and internals. Log server-side, return generic messages. - Editing code directly on shared hosting without version control: Modern PHP projects live in Git, use Composer, and deploy through CI/CD pipelines.
2 Variables, Scope, & Dynamic Typing
Conceptual Explanation
PHP variables are declared with a leading $ sigil and are dynamically typed by default. The Zend Engine stores variables as zvals — containers that hold both a value and a type tag. This allows type juggling at runtime, but it also creates entire classes of bugs when implicit conversions surprise the developer.
Scope is predominantly function-local. Variables created inside a function are destroyed when the function returns unless explicitly captured via global or closure use. Global scope is available but should be treated as a code smell; modern code prefers dependency injection or explicit function arguments.
Variable variables ($$name) and variable functions remain supported, but they are rarely justified in production because they break static analysis, autocompletion, and security review.
Idiomatic Code Example
<?php
declare(strict_types=1);
$greeting = 'Hello'; // Script/global scope
function createMessage(string $name): string
{
// Local scope: $prefix is not visible outside
$prefix = 'Welcome,';
return "{$prefix} {$name}!";
}
// Prefer passing data explicitly instead of global
echo createMessage('Azhar'); // Welcome, Azhar!
// Variable variables exist, but avoid them in production
$field = 'username';
$username = 'admin';
// echo $$field; // Works, but impossible to statically analyze
Common Anti-Patterns & Mistakes
- Relying on
globalinside functions: It couples code to hidden state. Pass dependencies as parameters or inject them. - Assuming dynamic typing means "no types": PHP 8 has first-class scalar types, union types, and return types. Use them.
- Variable variables in business logic: They make refactoring dangerous and bypass static analysis. Use arrays or objects instead.
3 Data Types & Strict Types
Conceptual Explanation
PHP provides scalar types (int, float, string, bool), compound types (array, object, callable), and special types (null, resource). PHP 8.0 added union types, mixed, and static return types; PHP 8.1 added never.
By default, PHP coerces types when possible. A string "42" passed to an int parameter becomes 42 without error. Coercion is convenient but dangerous. Modern PHP places declare(strict_types=1); at the top of every file to enforce strict type checking. In strict mode, a wrong type throws a TypeError at the call boundary.
The Zend Engine performs type checks at the call boundary and, in strict mode, rejects any value that does not exactly match the declared type. This catches bugs earlier and eliminates whole categories of injection-style type confusion.
Idiomatic Code Example
<?php
declare(strict_types=1);
function calculateTotal(int $quantity, float $unitPrice): float
{
return $quantity * $unitPrice;
}
// Strict mode rejects strings that look like numbers.
// calculateTotal("5", "10.5"); // TypeError
$total = calculateTotal(5, 10.5); // 52.5
// Union types are allowed when a value legitimately varies
function describeValue(int|string|null $value): string
{
return match (true) {
is_int($value) => "integer: {$value}",
is_string($value) => "string: {$value}",
default => 'no value',
};
}
Common Anti-Patterns & Mistakes
- Omitting
declare(strict_types=1): Coercion hides bugs. Every file should opt into strict types. - Using
mixedeverywhere: It is useful for generic boundaries, but it should not replace specific type hints. - Trusting input types because a form field is numeric: Validate and type-hint at the boundary. Never trust user data.
4 Output & String Manipulation
Conceptual Explanation
PHP offers multiple output mechanisms. echo is a language construct that accepts comma-separated arguments and has no return value. print is also a construct but behaves like a function and returns 1; it is rarely used in modern code. printf() provides formatted output using type specifiers and is useful for CLI tooling.
String interpolation allows embedding variables directly inside double-quoted strings or heredocs. Complex expressions require the {$object->property} syntax. Heredoc (<<<LABEL) behaves like double quotes but spans multiple lines. Nowdoc (<<<'LABEL') behaves like single quotes and does not interpolate — ideal for SQL snippets, templates, or raw payloads.
Modern output is almost never raw echo in a web context. It is rendered through templates, JSON encoders, or view components, and is always escaped to prevent XSS.
Idiomatic Code Example
<?php
declare(strict_types=1);
$name = 'Azhar';
$score = 95;
// Interpolation
echo "Welcome, {$name}!\n";
// printf formatting
printf("Score: %d%% (%s)\n", $score, $name);
// Nowdoc: no interpolation, perfect for raw SQL
$sql = <<<'SQL'
SELECT id, email
FROM users
WHERE active = 1
SQL;
// Heredoc: interpolation allowed
$html = <<<HTML
<p>Hello, {$name}</p>
HTML;
Common Anti-Patterns & Mistakes
- Echoing user input unescaped: Always escape with
htmlspecialchars($value, ENT_QUOTES, 'UTF-8')before HTML output. - Concatenating massive strings with
.in loops: PHP strings are immutable; repeated concatenation reallocates memory. Build arrays andimplode()them. - Using heredoc for SQL with variables: That is SQL injection. Use nowdoc with prepared statements instead.
5 Operators & Comparisons
Conceptual Explanation
PHP operators should be chosen deliberately. The identity operator === compares both value and type, while the equality operator == performs type juggling. Legacy PHP code is full of == bugs such as "0e12345" == "0e67890" evaluating to true due to numeric string coercion.
The spaceship operator <=>, introduced in PHP 7, returns -1, 0, or 1 for less-than, equal-to, and greater-than comparisons. It is the canonical choice for sorting callbacks and comparison logic. The null coalescing operator ?? returns the left operand if it exists and is not null; otherwise it returns the right operand. PHP 7.4 added the null coalescing assignment ??=.
Idiomatic Code Example
<?php
declare(strict_types=1);
$a = 10;
$b = '10';
// Always prefer identity comparison
var_dump($a == $b); // true (type juggling)
var_dump($a === $b); // false (type-safe)
// Spaceship operator
$points = [34, 12, 89, 5];
usort($points, static fn (int $x, int $y): int => $x <=> $y);
// Null coalescing
$config = [];
$timeout = $config['timeout'] ?? 30;
$config['cache'] ??= true; // assign only if missing/null
Common Anti-Patterns & Mistakes
- Using
==for security checks: Use===everywhere, especially when comparing tokens, hashes, or identifiers. - Confusing
isset()with??: They behave similarly for null checks, but??is cleaner and chainable. - Using
<=>in boolean contexts: It returns-1,0, or1. All three are truthy except 0, which is a footgun if treated as boolean.
6 Control Flow
Conceptual Explanation
Control flow in PHP follows familiar C-style syntax. if/else chains remain the workhorse for conditional logic. The switch statement performs loose comparisons by default and falls through cases unless break is used — a historically common source of bugs.
PHP 8 introduced the match expression. Unlike switch, match is an expression (it returns a value), uses strict identity comparison (===), does not fall through, and requires every branch to be exhaustive. It is the modern replacement for many switch use cases.
Idiomatic Code Example
<?php
declare(strict_types=1);
$statusCode = 404;
// Modern match expression
$message = match ($statusCode) {
200 => 'OK',
301, 302 => 'Redirect',
401 => 'Unauthorized',
403 => 'Forbidden',
404 => 'Not Found',
default => 'Unknown Status',
};
// Match can branch on arbitrary conditions
$score = 85;
$grade = match (true) {
$score >= 90 => 'A',
$score >= 80 => 'B',
$score >= 70 => 'C',
default => 'F',
};
Common Anti-Patterns & Mistakes
- Missing
breakin switch: Accidental fall-through causes logic errors. Usematchor explicitly comment intentional fall-through. - Using
switchwith mixed types: Loose comparison leads to surprising results. Prefermatchfor type safety. - Deeply nested if/else ladders: Flatten with early returns, guard clauses, or
match.
7 Loop Constructs
Conceptual Explanation
PHP provides for, while, do-while, and foreach. The foreach loop is the canonical tool for iterating arrays and objects implementing Traversable. By default it iterates over the array without modifying it; use foreach ($items as &$item) to mutate the original array in place.
Modern PHP favors functional-style transformations (array_map, array_filter) or generators for large datasets instead of manual loops. The Zend Engine optimizes foreach with copy-on-write iterators, but generators still avoid materializing entire collections in memory.
Idiomatic Code Example
<?php
declare(strict_types=1);
$users = [
['id' => 1, 'name' => 'Alice'],
['id' => 2, 'name' => 'Bob'],
];
// Foreach with key and value
foreach ($users as $index => $user) {
echo "{$index}: {$user['name']}\n";
}
// Prefer array functions for transformations
$names = array_map(
static fn (array $user): string => $user['name'],
$users
);
// For numeric ranges
for ($i = 0; $i < 10; $i++) {
echo $i . PHP_EOL;
}
Common Anti-Patterns & Mistakes
- Modifying an array while iterating it by value: Use reference iteration or build a new array.
- Using
for ($i = 0; $i < count($arr); $i++):count()is evaluated every iteration. Cache it or useforeach. - Leaving reference variables alive after foreach: Unset
$itemafter a by-reference loop to avoid accidental mutations later.
8 Array Basics
Conceptual Explanation
PHP arrays are ordered hash maps. An indexed array uses integer keys starting at 0, while an associative array uses string keys. Under the hood, the Zend Engine stores arrays as packed hash tables with complex collision handling and copy-on-write semantics.
The modern short array syntax [] has fully replaced the legacy array() keyword. PHP 7.4 introduced the spread operator ... for arrays, allowing arrays to be unpacked inline with integer keys. String-keyed spreads arrived in PHP 8.1.
Idiomatic Code Example
<?php
declare(strict_types=1);
// Indexed array
$tags = ['php', 'alpine', 'tailwind'];
// Associative array
$config = [
'host' => 'localhost',
'port' => 3306,
'ssl' => true,
];
// Array unpacking with spread operator
$defaults = ['timeout' => 30, 'retries' => 3];
$overrides = ['retries' => 5];
$merged = [...$defaults, ...$overrides]; // overrides win for string keys
// Destructuring
['host' => $host, 'port' => $port] = $config;
Common Anti-Patterns & Mistakes
- Using
array()syntax: Use[]. It is shorter, faster to read, and the modern standard. - Treating arrays as untyped bags: For structured data, prefer objects, DTOs, or enums over nested associative arrays.
- Assuming
+merges arrays: It preserves keys from the left operand and ignores duplicates. Use spread orarray_merge()depending on desired behavior.
9 Functions
Conceptual Explanation
Functions are the primary unit of reusable logic in PHP. Modern functions declare scalar, object, nullable, and union types for every parameter and return value. PHP 8.0 introduced named arguments, which allow callers to pass parameters by name rather than position, improving readability and enabling selective skipping of optional parameters.
Variadic functions capture an arbitrary number of arguments using ...$values. Combined with typed parameters and named arguments, variadics make APIs flexible without sacrificing type safety. Default parameter values must be constant expressions and should be placed after required parameters.
Idiomatic Code Example
<?php
declare(strict_types=1);
function buildUrl(
string $path,
array $query = [],
bool $secure = true
): string {
$scheme = $secure ? 'https' : 'http';
$queryString = $query === [] ? '' : '?' . http_build_query($query);
return "{$scheme}://example.com{$path}{$queryString}";
}
// Named arguments improve clarity
$url = buildUrl('/search', secure: false, query: ['q' => 'php']);
// Variadic typed function
function sum(int ...$numbers): int
{
return array_sum($numbers);
}
echo sum(1, 2, 3, 4); // 10
Common Anti-Patterns & Mistakes
- Functions with no return type: Always declare return types; use
voidfor procedures. - Default parameters before required ones: PHP 8 still requires optional parameters after required ones in many cases.
- Returning mixed/ambiguous values: A function should return one predictable type. Use null or throw exceptions for errors.
10 Anonymous Functions & Closures
Conceptual Explanation
Closures in PHP are objects of the internal Closure class. They can capture variables from the surrounding scope using the use keyword. Captures are by value by default; prefixing with & captures by reference.
PHP 7.4 introduced arrow functions (fn ($x) => $x * 2). Arrow functions capture variables from the parent scope automatically by value. They are ideal for short, single-expression callbacks but cannot contain statements or multi-line bodies.
Idiomatic Code Example
<?php
declare(strict_types=1);
$multiplier = 3;
// Arrow function captures $multiplier automatically
$scale = fn (int $value): int => $value * $multiplier;
// Closure with explicit use, by reference
$counter = 0;
$increment = function () use (&$counter): int {
return ++$counter;
};
$numbers = [1, 2, 3, 4];
$scaled = array_map($scale, $numbers); // [3, 6, 9, 12]
Common Anti-Patterns & Mistakes
- Using closures for complex logic: Arrow functions are for single expressions. Use named functions or classes for complex work.
- Forgetting
usein traditional closures: Outer variables are not automatically available inside regular closures. - Capturing by reference unnecessarily: It can cause subtle state leaks. Capture by value unless mutation is intentional.
11 Superglobals & Request Handling
Conceptual Explanation
Superglobals ($_GET, $_POST, $_SERVER, $_FILES, $_COOKIE, $_SESSION, $_ENV, $_REQUEST, $GLOBALS) are associative arrays populated by the SAPI before user code runs. They are mutable and globally accessible, which makes them convenient and dangerous.
Sanitization removes or neutralizes unwanted characters. Validation checks whether input meets business rules. They are not interchangeable. Sanitizing an email does not validate it; validating a string does not make it safe for HTML output. Modern PHP uses dedicated libraries (e.g., Symfony Validator, laminas-filter) or strict typed request objects instead of raw superglobals scattered through code.
Idiomatic Code Example
<?php
declare(strict_types=1);
// Never trust raw input. Filter, validate, then cast.
$rawEmail = $_POST['email'] ?? '';
$email = filter_var($rawEmail, FILTER_VALIDATE_EMAIL);
if ($email === false) {
http_response_code(422);
echo 'Invalid email address.';
exit;
}
// Escape before output
$safeEmail = htmlspecialchars($email, ENT_QUOTES, 'UTF-8');
echo "<p>Registered: {$safeEmail}</p>";
Common Anti-Patterns & Mistakes
- Directly echoing
$_GET/$_POSTvalues: This is the root cause of XSS. Escape or parameterize output. - Confusing sanitization with validation: Use validation to reject bad input, not to silently fix it.
- Using
$_REQUEST: It merges GET, POST, and COOKIE in unpredictable order. Be explicit about the source.
12 State Persistence
Conceptual Explanation
PHP's shared-nothing model means no request can see another request's memory. State is persisted via sessions (server-side, identified by a session cookie) and cookies (client-side name-value pairs). $_SESSION is populated when session_start() is called and serialized to a handler at the end of the request.
Modern session configuration must be secure: use httponly, secure, and SameSite flags; regenerate IDs on privilege escalation; store sessions in Redis or a database instead of the filesystem at scale; and never store sensitive raw data client-side in cookies.
Idiomatic Code Example
<?php
declare(strict_types=1);
// Secure session cookie settings before starting
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', '1');
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
session_start();
// After verifying credentials, rotate the session ID to prevent fixation
// authenticateUser() is your credential-checking routine (returns int|null)
$userId = authenticateUser($_POST['email'] ?? '', $_POST['password'] ?? '');
if ($userId !== null) {
session_regenerate_id(true);
$_SESSION['authenticated'] = true;
$_SESSION['user_id'] = $userId;
}
Common Anti-Patterns & Mistakes
- Sessions without
httponly/secure/SameSite: These flags prevent XSS cookie theft and CSRF in transit. - Not regenerating session IDs after login: Session fixation attacks hijack pre-assigned IDs.
- Storing secrets or large objects in session: Sessions should contain identifiers and small, serializable state, not raw passwords or huge arrays.
13 Classes & Objects
Conceptual Explanation
A class is a blueprint; an object is a runtime instance. PHP classes support typed properties, methods, constants, and visibility modifiers. Visibility — public, protected, and private — enforces encapsulation. Modern PHP rarely exposes public properties directly; instead it uses getters, setters, or readonly properties to maintain invariants.
The Zend Engine stores objects as reference-counted structures. Assigning an object variable does not copy the object; it copies a handle to the same object. This is different from arrays, which are copy-on-write.
Idiomatic Code Example
<?php
declare(strict_types=1);
final class User
{
public function __construct(
private string $name,
private string $email,
) {}
public function name(): string
{
return $this->name;
}
public function email(): string
{
return $this->email;
}
public function updateEmail(string $email): void
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email.');
}
$this->email = $email;
}
}
Common Anti-Patterns & Mistakes
- Public mutable properties: Break encapsulation. Use private properties with controlled methods.
- Untyped properties: PHP 7.4+ supports typed properties. Use them.
- Massive "god classes": A class should have one reason to change. Split responsibilities.
14 Modern Object Construction
Conceptual Explanation
PHP 8.0 introduced Constructor Property Promotion, allowing constructor parameters to be declared as class properties inline. This removes boilerplate and makes value objects trivial to write. PHP 8.1 added readonly properties, which can be written exactly once — typically during construction — and then become immutable.
Together, promotion and readonly make PHP's value objects (DTOs, entities, command objects) concise, type-safe, and immutable. Readonly properties can only be initialized from the declaring scope and cannot be reassigned, including via reflection after initialization.
Idiomatic Code Example
<?php
declare(strict_types=1);
final class Money
{
public function __construct(
public readonly float $amount,
public readonly string $currency,
) {}
}
$price = new Money(amount: 99.99, currency: 'USD');
// $price->amount = 100.00; // Fatal Error: readonly property
Common Anti-Patterns & Mistakes
- Manual constructor assignments: Use promotion to reduce noise.
- Mutable DTOs: If data should not change after creation, mark properties
readonly. - Initializing readonly properties outside the constructor: They must be assigned exactly once, before construction completes.
15 Inheritance & Polymorphism
Conceptual Explanation
Inheritance in PHP uses the extends keyword. A child class inherits public and protected members from the parent. Polymorphism allows code to operate on objects of different classes through a common interface or parent type. Method overriding lets a child class replace parent behavior, but the override must be compatible with the parent's signature (covariance/contravariance rules apply).
The final keyword prevents a class from being extended or a method from being overridden. Modern PHP uses final aggressively on classes that are not designed for inheritance, because inheritance is a fragile extension mechanism compared to composition.
Idiomatic Code Example
<?php
declare(strict_types=1);
abstract class Notification
{
abstract public function send(string $message): void;
}
final class EmailNotification extends Notification
{
public function send(string $message): void
{
echo "Sending email: {$message}\n";
}
}
final class SmsNotification extends Notification
{
public function send(string $message): void
{
echo "Sending SMS: {$message}\n";
}
}
function notifyUser(Notification $notification, string $message): void
{
$notification->send($message); // polymorphic dispatch
}
Common Anti-Patterns & Mistakes
- Deep inheritance hierarchies: Prefer composition. Deep trees are hard to test and refactor.
- Non-final concrete classes without intent: If a class is not explicitly designed for extension, mark it
final. - Breaking Liskov Substitution: Overrides should not strengthen preconditions or weaken postconditions.
16 Abstraction & Interfaces
Conceptual Explanation
Abstract classes provide a partial implementation and cannot be instantiated. They are useful when related classes share common state or concrete behavior. Interfaces define a pure contract — a set of methods that implementing classes must provide. A class can implement multiple interfaces but extend only one class.
Modern PHP uses interfaces to decouple code from concrete implementations. Type-hinting against interfaces allows dependency injection, mocking in tests, and swapping implementations without changing callers. PHP 8.0+ added static return types and constructor signature enforcement through interfaces; interface inheritance has been supported since PHP 5.
Idiomatic Code Example
<?php
declare(strict_types=1);
interface Cache
{
public function get(string $key): ?string;
public function set(string $key, string $value, int $ttl): void;
}
final class InMemoryCache implements Cache
{
/** @var array<string, array{value: string, expiresAt: int}> */
private array $storage = [];
public function get(string $key): ?string
{
$entry = $this->storage[$key] ?? null;
if ($entry === null || $entry['expiresAt'] < time()) {
return null;
}
return $entry['value'];
}
public function set(string $key, string $value, int $ttl): void
{
$this->storage[$key] = ['value' => $value, 'expiresAt' => time() + $ttl];
}
}
Common Anti-Patterns & Mistakes
- Type-hinting concrete classes everywhere: Depend on interfaces, not implementations.
- Fat interfaces: An interface should be small and focused. Split bloated contracts.
- Using abstract classes for pure contracts: If there is no shared implementation, use an interface.
17 Traits & Code Reuse
Conceptual Explanation
Traits provide horizontal code reuse. Unlike inheritance, which is vertical, a trait is a group of methods that can be inserted into multiple unrelated classes. Traits are compiled into the using class at runtime; they do not create a new type and cannot be instantiated or type-hinted.
Traits are useful for cross-cutting concerns such as logging, event dispatching, or timestampable behavior. However, they can introduce naming conflicts when two traits provide the same method. PHP resolves this with insteadof and as operators.
Idiomatic Code Example
<?php
declare(strict_types=1);
trait Timestampable
{
private readonly DateTimeImmutable $createdAt;
public function __construct()
{
$this->createdAt = new DateTimeImmutable();
}
public function createdAt(): DateTimeImmutable
{
return $this->createdAt;
}
}
final class Article
{
use Timestampable;
}
final class Comment
{
use Timestampable;
}
Common Anti-Patterns & Mistakes
- Traits as a substitute for composition: If a class needs behavior, inject a collaborator instead of copy-pasting methods.
- Traits with hidden dependencies: A trait that expects certain properties or methods on the using class is fragile and hard to trace.
- Ignoring trait conflicts: Always resolve method collisions explicitly with
insteadof.
18 Defensive Programming
Conceptual Explanation
PHP errors have evolved from silent failures and legacy error levels to a robust exception model. Modern PHP code throws exceptions for exceptional conditions and catches them at appropriate boundaries. try/catch/finally blocks isolate failure handling, and finally guarantees cleanup regardless of success or failure.
Custom exceptions extend \Exception or \RuntimeException and communicate domain-specific failures. They should be named after the problem they represent, not the layer that throws them. PHP 8.0 made the throw statement an expression, allowing constructs like $value ?? throw new InvalidArgumentException().
Idiomatic Code Example
<?php
declare(strict_types=1);
final class PaymentFailedException extends RuntimeException
{
public static function forInsufficientFunds(): self
{
return new self('Payment declined: insufficient funds.');
}
}
function auditLog(string $event): void
{
// Production: write to immutable audit store
error_log("[AUDIT] {$event}");
}
function processPayment(float $amount, float $balance): void
{
if ($amount > $balance) {
throw PaymentFailedException::forInsufficientFunds();
}
}
try {
processPayment(100.00, 50.00);
} catch (PaymentFailedException $e) {
error_log($e->getMessage());
echo 'Unable to complete payment.';
} finally {
// Always release resources or record audit trail
auditLog('payment_attempt');
}
Common Anti-Patterns & Mistakes
- Catching
\Exceptioneverywhere: Catch specific exceptions. Catching the root class hides bugs. - Swallowing exceptions silently: Empty catch blocks destroy debugging information. Log, rethrow, or translate.
- Using exceptions for normal control flow: Exceptions are for exceptional cases, not expected branches.
19 Namespaces & Autoloading
Conceptual Explanation
Namespaces prevent naming collisions and organize code by vendor or domain. They map directly to the filesystem via PSR-4 autoloading, the de facto standard managed by Composer. A namespace such as App\Domain\User corresponds to a file path like src/Domain/User.php.
The Zend Engine resolves class names lazily through the autoloader when a class is first referenced. This avoids loading unused classes and keeps bootstrap memory low. Modern code uses use statements for imports, grouped alphabetically, and never relies on global class names unless necessary.
Idiomatic Code Example
<?php
declare(strict_types=1);
namespace App\Domain\User;
use App\Shared\Email;
use DateTimeImmutable;
final class User
{
public function __construct(
private readonly string $id,
private readonly Email $email,
private readonly DateTimeImmutable $createdAt,
) {}
}
Common Anti-Patterns & Mistakes
- Single global namespace: All classes should live in a namespace.
- Manual
require/includeeverywhere: Use Composer's PSR-4 autoloader. - Namespaces that do not match directories: PSR-4 requires strict correspondence. Mismatches break autoloading.
20 Composer Dependency Management
Conceptual Explanation
Composer is PHP's dependency manager and autoloader generator. composer.json declares project metadata, dependencies (require), development-only dependencies (require-dev), autoloading rules, and scripts. composer.lock pins exact versions and must be committed for applications to ensure reproducible builds.
Modern Composer usage includes PSR-4 autoloading, autoload optimization with composer dump-autoload --optimize or --classmap-authoritative in production, and strict version constraints. Libraries use semantic versioning constraints; applications pin versions via the lock file.
Idiomatic Code Example
{
"name": "acme/php-tutorial",
"type": "project",
"require": {
"php": ">=8.1",
"guzzlehttp/guzzle": "^7.8",
"monolog/monolog": "^3.5"
},
"require-dev": {
"phpunit/phpunit": "^10.5",
"phpstan/phpstan": "^1.10"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"config": {
"sort-packages": true
}
}
Common Anti-Patterns & Mistakes
- Committing
vendor/: It bloats the repository. Commitcomposer.lockinstead. - Loose version constraints in production: Pin dependencies and audit them for CVEs.
- Running
composer installas root: Creates security risks. Use dedicated deploy users.
21 Professional Testing
Conceptual Explanation
PHPUnit is the standard testing framework for PHP. Unit tests isolate a single class or function, verify expected behavior through assertions, and provide fast feedback during refactoring. Modern PHPUnit uses attributes (#[Test], #[DataProvider]) instead of legacy @test annotations.
Mocking replaces real collaborators with test doubles to verify interactions. PHPUnit's built-in mocking or specialized libraries like Mockery let you assert that a dependency was called with expected arguments. Mock only boundaries (databases, HTTP clients, mailers), not pure value objects.
Idiomatic Code Example
<?php
declare(strict_types=1);
namespace App\Tests;
use App\Calculator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class CalculatorTest extends TestCase
{
#[Test]
#[DataProvider('additionProvider')]
public function it_adds_numbers(int $a, int $b, int $expected): void
{
$calculator = new Calculator();
$result = $calculator->add($a, $b);
self::assertSame($expected, $result);
}
/** @return array<string, array{int, int, int}> */
public static function additionProvider(): array
{
return [
'positive' => [2, 3, 5],
'negative' => [-2, -3, -5],
'zero' => [0, 0, 0],
];
}
}
Common Anti-Patterns & Mistakes
- Tests that hit real databases/networks: Use repositories and mocks to keep units isolated.
- Assertions without messages: Descriptive failure messages save debugging time.
- Testing implementation details: Test behavior and outcomes, not private methods.
22 Secure Database Access
Conceptual Explanation
PDO (PHP Data Objects) is the modern database abstraction layer. It provides a consistent API for multiple drivers and supports prepared statements, which separate SQL logic from data. Prepared statements eliminate SQL injection when used correctly because bound values are never interpolated into the query string.
Modern database code uses PDO with explicit error mode ERRMODE_EXCEPTION, fetch modes mapped to classes or associative arrays, and transactions for multi-step operations. Raw queries are wrapped in repository classes, and credentials are injected via environment variables, never hardcoded.
Idiomatic Code Example
<?php
declare(strict_types=1);
// getenv() is more reliable than $_ENV across php.ini configurations
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', (string) getenv('DB_HOST'), (string) getenv('DB_NAME'));
$pdo = new PDO($dsn, (string) getenv('DB_USER'), (string) getenv('DB_PASS'), [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // use real prepared statements
]);
$email = $_POST['email'] ?? '';
$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = :email');
$stmt->execute([':email' => $email]);
$user = $stmt->fetch();
Common Anti-Patterns & Mistakes
- String concatenation into SQL: Never do
"WHERE id = $id". Always bind. - Emulated prepares left on: Set
ATTR_EMULATE_PREPARES => falsefor real DB-side parameterization. - Credentials in source control: Use environment variables or secret managers.
23 Advanced Data Structuring
Conceptual Explanation
Modern PHP provides strong tools for structured data. JSON encoding/decoding uses json_encode() and json_decode() with flags such as JSON_THROW_ON_ERROR for exception-based error handling. Enums, introduced in PHP 8.1, allow defining a closed set of named values, optionally backed by scalar values (int or string).
For date and time, modern PHP uses DateTimeImmutable instead of mutable DateTime. Immutable dates prevent accidental mutations when passed between functions or stored in aggregates.
Idiomatic Code Example
<?php
declare(strict_types=1);
enum Status: string
{
case Draft = 'draft';
case Published = 'published';
case Archived = 'archived';
}
$payload = [
'id' => 1,
'status' => Status::Published->value,
'createdAt' => (new DateTimeImmutable())->format(DateTimeInterface::ATOM),
];
$json = json_encode($payload, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
$decoded = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
$status = Status::from($decoded['status']);
Common Anti-Patterns & Mistakes
- Using magic strings instead of enums: Enums make invalid states unrepresentable.
- Ignoring
json_last_error(): UseJSON_THROW_ON_ERRORto handle failures via exceptions. - Mutating DateTime objects shared across code: Prefer
DateTimeImmutable.
24 HTTP & REST Clients
Conceptual Explanation
PHP's cURL extension is powerful but verbose and error-prone. Modern PHP applications typically use higher-level HTTP clients. Guzzle is the most popular and implements PSR-7 (HTTP messages), PSR-17 (HTTP factories), and PSR-18 (HTTP client) standards. PSR-18 abstracts the client so that code can be tested with mock HTTP clients and swapped without changes.
Modern HTTP code sets explicit timeouts, handles exceptions, validates status codes, and never trusts remote data. JSON payloads are decoded with error checking, and credentials are passed via headers or environment, never logged.
Idiomatic Code Example
<?php
declare(strict_types=1);
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
$client = new Client([
'base_uri' => 'https://api.example.com/',
'timeout' => 5.0,
'headers' => [
'Accept' => 'application/json',
'Authorization' => 'Bearer ' . (string) getenv('API_TOKEN'),
],
]);
try {
$response = $client->get('users/1');
$user = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
} catch (GuzzleException $e) {
error_log('HTTP request failed: ' . $e->getMessage());
throw new RuntimeException('Unable to fetch user data.');
}
Common Anti-Patterns & Mistakes
- Raw cURL without error handling: Use a robust client and always catch exceptions.
- No request timeouts: Missing timeouts allow requests to hang indefinitely.
- Trusting remote JSON blindly: Validate the decoded structure before using it.
25 Reflection API & Attributes
Conceptual Explanation
The Reflection API allows PHP code to inspect classes, methods, properties, and parameters at runtime. Reflection is used by frameworks, serializers, dependency injection containers, and test runners. However, it is slower than direct code access and should be cached in production.
Attributes (PHP 8.0+) replace the legacy docblock annotation pattern. They are native metadata tags declared with #[Attribute] and attached to classes, methods, properties, or parameters. Attributes are parsed by the Zend Engine and accessible via reflection without string parsing of comments.
Idiomatic Code Example
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD)]
final class Route
{
public function __construct(public readonly string $path) {}
}
final class UserController
{
#[Route('/users')]
public function index(): array
{
return [];
}
}
$reflector = new ReflectionClass(UserController::class);
foreach ($reflector->getMethods() as $method) {
$attributes = $method->getAttributes(Route::class);
foreach ($attributes as $attribute) {
$route = $attribute->newInstance();
echo $route->path;
}
}
Common Anti-Patterns & Mistakes
- Parsing docblocks manually: Use native attributes instead of regex on comments.
- Heavy reflection in hot paths: Reflect once, cache metadata, reuse it.
- Attributes with side effects: Attributes should hold metadata. Let the application code act on it.
26 Advanced OOP Mechanics
Conceptual Explanation
Magic methods intercept language-level operations. __get and __set handle inaccessible property access. __call intercepts undefined method calls. __clone controls object copying. These are powerful but should be used sparingly because they break static analysis, IDE autocompletion, and explicit contracts.
Magic constants such as __DIR__, __FILE__, __CLASS__, and __METHOD__ provide context at compile time. Use ::class for class name resolution instead of hardcoded strings.
Idiomatic Code Example
<?php
declare(strict_types=1);
final class Config
{
/** @var array<string, mixed> */
private array $values = [];
public function __get(string $name): mixed
{
return $this->values[$name] ?? throw new OutOfBoundsException("Unknown config: {$name}");
}
public function __set(string $name, mixed $value): void
{
$this->values[$name] = $value;
}
}
$config = new Config();
$config->timeout = 30;
echo $config->timeout;
Common Anti-Patterns & Mistakes
- Magic methods instead of explicit APIs: Prefer typed methods and properties when possible.
- Returning null from
__getsilently: It masks bugs. Throw exceptions for invalid access. - Forgetting to implement
__clonefor deep copies: Default clone is shallow; nested objects remain shared.
27 Enterprise Design Patterns
Conceptual Explanation
Dependency Injection (DI) is the practice of supplying a class's collaborators from the outside rather than letting the class create them. It enables testability, loose coupling, and composition over inheritance. A DI container automates object graph construction and lifecycle management.
The Singleton pattern ensures only one instance exists, but it is often abused as a global state container. Modern PHP prefers container-managed services over hand-rolled singletons. The Strategy pattern defines a family of interchangeable algorithms behind a common interface, making behavior selectable at runtime.
Idiomatic Code Example
<?php
declare(strict_types=1);
// Amounts are integer cents to avoid floating-point precision loss.
interface PaymentProcessor
{
public function pay(int $amountInCents): void;
}
final class StripeProcessor implements PaymentProcessor
{
public function pay(int $amountInCents): void
{
echo "Charging $" . number_format($amountInCents / 100, 2) . " via Stripe\n";
}
}
final class Checkout
{
public function __construct(private PaymentProcessor $processor) {}
public function completeOrder(int $amountInCents): void
{
$this->processor->pay($amountInCents);
}
}
// Inject the strategy
$checkout = new Checkout(new StripeProcessor());
$checkout->completeOrder(4999); // $49.99
Common Anti-Patterns & Mistakes
- Service locators and static facades: They hide dependencies. Use constructor injection.
- DI containers referenced inside domain code: The container belongs at the composition root, not in business logic.
- Singletons holding mutable state: A singleton is not a global variable. Keep it stateless or use the container.
28 Streams & File System
Conceptual Explanation
Streams in PHP are a unified way to read from and write to files, network sockets, compressed archives, and more. The stream wrapper abstraction lets you use familiar file functions on diverse sources. For large files, loading everything into memory is wasteful and can exhaust memory_limit.
Generators (functions using yield) let you iterate over large datasets one item at a time. They preserve memory by suspending execution between values. Combined with streams, generators are the canonical PHP pattern for processing massive CSVs, logs, or result sets without loading them entirely into RAM.
Idiomatic Code Example
<?php
declare(strict_types=1);
/** @return Generator<int, string> */
function readLargeFile(string $path): Generator
{
$handle = fopen($path, 'rb');
if ($handle === false) {
throw new RuntimeException("Unable to open {$path}");
}
try {
while (($line = fgets($handle)) !== false) {
yield trim($line);
}
} finally {
fclose($handle);
}
}
foreach (readLargeFile('/var/log/app.log') as $line) {
if (str_contains($line, 'ERROR')) {
echo $line . PHP_EOL;
}
}
Common Anti-Patterns & Mistakes
file_get_contents()on multi-gigabyte files: It loads the entire file into memory. Use streams and generators.- Forgetting to close handles: Always close streams, preferably with
try/finally. - Building giant arrays from queries: Iterate with generators or pagination.
29 Memory Management & OPcache
Conceptual Explanation
The Zend Engine compiles PHP scripts into bytecode (opcodes), executes them on a virtual machine, and manages memory through reference counting with a cycle-collecting garbage collector. Objects and arrays are reference-counted; when the count drops to zero, memory is reclaimed. Circular references are detected by the garbage collector.
OPcache stores compiled bytecode in shared memory so that PHP does not recompile files on every request. It is the single most important performance extension for production PHP. PHP 8.0+ also includes a JIT (Just-In-Time) compiler that translates hot bytecode into native machine code; it can improve CPU-bound workloads, but typical I/O-bound web requests often see modest gains.
Idiomatic Code Example
; OPcache production tuning
opcache.enable=1
opcache.enable_cli=0 ; usually disabled for CLI
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0 ; disable in production; reload on deploy
; JIT for PHP 8+
opcache.jit=tracing
opcache.jit_buffer_size=128M
Common Anti-Patterns & Mistakes
- Running production without OPcache: Every request recompiles source. This is a massive waste.
- Holding references to large objects unnecessarily: Unset or narrow variable scope to allow garbage collection.
- Enabling
validate_timestampsin production: It adds filesystem checks. Restart PHP-FPM on deploy instead.
30 Security Hardening
Conceptual Explanation
PHP security is a stack-wide concern. Password hashing must use password_hash() with PASSWORD_DEFAULT, which automatically upgrades to the current best algorithm (currently bcrypt). XSS prevention requires escaping all output based on context: HTML, JavaScript, CSS, URL, or HTML attribute.
CSRF protection validates that state-changing requests originated from your application by requiring a token tied to the user's session. Secure headers such as Content-Security-Policy, X-Frame-Options, and Strict-Transport-Security reduce the impact of injection and clickjacking.
Idiomatic Code Example
<?php
declare(strict_types=1);
// Hash a password
$hash = password_hash($plainPassword, PASSWORD_DEFAULT);
if ($hash === false) {
throw new RuntimeException('Password hashing failed.');
}
// Verify on login
if (password_verify($plainPassword, $hash)) {
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$newHash = password_hash($plainPassword, PASSWORD_DEFAULT);
if ($newHash === false) {
throw new RuntimeException('Password rehashing failed.');
}
$hash = $newHash;
// store updated hash
}
}
// Escape output
$safe = htmlspecialchars($userInput, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo "<p>{$safe}</p>";
// Secure headers. In production, serve inline scripts/styles with nonces/hashes instead of 'unsafe-inline'.
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
header('X-Frame-Options: DENY');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
Common Anti-Patterns & Mistakes
- Storing passwords with md5/sha1: These are broken. Use
password_hash. - Escaping output once and reusing it everywhere: Escape at the render boundary, in the correct context.
- No CSRF tokens on state-changing forms: Every POST/PUT/DELETE must validate a session-bound token.
31 Asynchronous & Event-Driven PHP
Conceptual Explanation
Traditional PHP is synchronous and request-bound, but the ecosystem now supports asynchronous and event-driven programming. PHP 8.1 introduced fibers, lightweight cooperative threads that allow code to suspend and resume execution. Fibers are low-level primitives; most applications use them through libraries like Amphp v3 or ReactPHP.
Swoole and FrankenPHP take a different approach by providing a long-running application server that keeps PHP bootstrapped across requests. This dramatically reduces bootstrap overhead and enables persistent connections, but it requires careful state management because the shared-nothing assumption no longer holds.
Idiomatic Code Example
<?php
declare(strict_types=1);
// Fibers allow suspending and resuming execution
$fiber = new Fiber(function (): int {
echo "Fiber started\n";
Fiber::suspend('paused');
echo "Fiber resumed\n";
return 42;
});
$value = $fiber->start();
echo "Got from fiber: {$value}\n";
$result = $fiber->resume();
echo "Fiber returned: {$result}\n";
Common Anti-Patterns & Mistakes
- Treating async PHP like Node.js: PHP fibers are cooperative; blocking I/O still blocks unless the runtime handles it.
- Storing request state in globals under Swoole: Long-running servers share memory. Use request-scoped containers.
- Adding async complexity where it is not needed: Most CRUD applications do not need fibers; traditional PHP-FPM scales well.
32 Production Deployment & Benchmarking
Conceptual Explanation
Modern PHP deployment is stateless and automated. Application code is built into a container or package, dependencies are installed from a pinned lock file, and configuration is injected via environment variables. PHP-FPM tuning involves setting the correct pool manager type (static, dynamic, or ondemand), pm.max_children, and request timeouts to match available memory and traffic patterns.
Benchmarking should be done under realistic load with tools like wrk, k6, or ab. Profiling tools such as Xdebug (development), Tideways, Blackfire, or perf/pprof reveal actual bottlenecks. Metrics via StatsD or Prometheus track latency, throughput, and error rates in production.
Idiomatic Code Example
[www]
user = www-data
group = www-data
listen = /run/php/php8.2-fpm.sock
; Dynamic pool scales with traffic
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 1000 ; recycle workers to control memory leaks
; Slow log for profiling
request_slowlog_timeout = 2s
slowlog = /var/log/php/slow.log
Common Anti-Patterns & Mistakes
- Stateful servers with local file uploads or sessions: Store sessions in Redis and files in object storage.
- Guessing at FPM pool sizes: Calculate
max_childrenfrom available RAM and average request memory. - Profiling only in development: Production traces under real load reveal different bottlenecks than synthetic tests.