Chapter 1: Introduction to PHP

PHP is a popular general-purpose scripting language that runs primarily on the server side (you can also use it from the command line). It powers many websites and APIs. PHP integrates well with Apache and Nginx through SAPIs such as mod_php and FPM. You can install it on Windows, macOS, and Linux, and you can start quickly with a bundled stack that includes a web server and a database.

💡 PHP began as a small templating tool and grew into a full language. Modern PHP includes strong typing features, attributes, enums, and a rich standard library.

In this chapter you will install a local environment, create your first script, run PHP from the command line, learn about tags and files, and tune a few core configuration settings.

What PHP Is and Where It Runs

PHP executes on the server to build responses that are sent to browsers or other clients. You can embed PHP into HTML or keep it separate and send JSON. You can execute PHP scripts via a web server (Apache or Nginx using FPM), or directly using the php command in a terminal. The same code typically runs on Windows, macOS, and Linux with only minor environment differences.

Understanding the request lifecycle in simple steps

A browser sends a request to your server; the web server forwards that request to PHP; your PHP script reads input, performs work, and returns output; the web server sends the response to the client. For APIs you usually return JSON. For classic sites you return HTML that may include embedded PHP blocks.

Common PHP stack choices for local work

For Windows you can use WampServer (bundled stack). For macOS you can use MAMP. For any platform you can use XAMPP. For minimal setups you can install PHP alone and use the built-in server during development.

⚠️ Production servers often run PHP-FPM behind Nginx or Apache. This pattern scales better than the basic module configuration for busy sites.

Installing PHP on Your System

This section focuses on a fast Windows setup with WampServer. You will also see brief notes about other choices so you can pick the best path for your platform.

Installing WampServer on Windows for a complete stack

WampServer bundles Apache, MySQL, and PHP so you can run examples immediately. Download the correct installer for your CPU from sourceforge.net/projects/wampserver, run it, then start WampServer. When the tray icon turns green you can open http://localhost/. Create a folder inside the www directory for your projects and place .php files there.

💡 On most systems the default web root is something like C:\wamp64\www. Create a folder such as C:\wamp64\www\playground and put your test files inside it.

Verifying that PHP works after installation

Open a terminal and run php -v to see the version. In your project folder create a file named info.php with <?php phpinfo(); ?>, then visit http://localhost/playground/info.php. You should see details about your PHP build and configuration paths.

Alternative local options in short form

⚠️ Antivirus tools and port conflicts can block localhost. If Apache fails to start, check whether another service uses port 80 or 443 and either stop that service or change the configured ports.

Your First PHP Script

Here is a small script that prints a greeting and shows basic information about your environment. It works by outputing the file contents directly to the browser, but interpreting any PHP commands that are encountered within <?php … ?> sections. Save this as index.php inside your project folder, then open it in the browser through your local server.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello PHP</title>
  </head>
  <body>
    <h1>Hello from PHP</h1>
    <p>The time is: <?php echo date('Y-m-d H:i:s'); ?></p>
    <p>PHP version: <?php echo PHP_VERSION; ?></p>
  </body>
</html>
💡 If you see the PHP code in the browser instead of the output, the file is not being processed by PHP. Make sure you are viewing the file through http://localhost/… rather than opening it directly with a file:// URL.

Placing files in the served project folder

Your PHP files must live inside the server’s document root or a configured virtual host. For WampServer that root is the www folder. Use subfolders to keep projects separate and tidy.

Running PHP from the CLI and Built-in Server

PHP includes a command line interface that is useful for quick experiments and automation. You can also use a lightweight development server that ships with PHP, which is convenient for small demos or local APIs.

Running simple commands with the php CLI

Use php -v to print the version. Use php -i to print configuration. Use php -r "echo 2 + 2;" for quick one-liners. Run a script with php path/to/script.php from the terminal.

Starting the built-in development server

From a project folder that contains an index.php file you can run this command to serve files at http://localhost:8000:

php -S localhost:8000

To serve a specific folder use the -t option. For example:

php -S localhost:8000 -t public

You can also supply a router script for clean URLs that maps requests to a single entry point:

php -S localhost:8000 router.php
⚠️ The built-in server is intended for development. For production use a real web server (Apache or Nginx with FPM).

PHP Tags, Files, and Project Layout

PHP code lives between tags. The standard form is <?php … ?>. When you only need to echo a value, you can use the short echo form <?= … ?>. Use the .php file extension for executable files. Keep a clear folder structure to make navigation easier.

Using standard tags and avoiding legacy short tags

Use <?php and ?> for all code blocks. Avoid legacy short tags like <? … ?> since availability depends on configuration and may not be enabled everywhere. The short echo form <?= is always available and safe to use.

A simple project layout that scales

Start with a small structure, then grow it. Keep public assets in a dedicated folder. Add a src folder for application code and a vendor folder when you introduce Composer. Use an index.php front controller inside public.

my-app/
  public/
    index.php
    styles.css
  src/
    App.php
  tests/
    …
  vendor/
    …
  composer.json
  README.md
💡 Serving only the public folder reduces the risk of exposing private files such as configuration or templates.

php.ini Overview and Configuration Basics

The php.ini file controls how PHP behaves. Common installations include separate configurations for CLI and web SAPI. You can view active settings using php --ini and with phpinfo(). You can override some settings at runtime using ini_set().

Finding and selecting the active php.ini

Run php --ini in a terminal to see the loaded configuration files. In a browser open a page that calls phpinfo() and look for “Loaded Configuration File”. Some stacks provide “development” and “production” templates; copy the one that fits your needs and adjust values carefully.

Tuning common directives with safe defaults

Set a timezone to avoid warnings; for example date.timezone = Europe/London. Use sensible memory limits such as memory_limit = 256M for typical local work. Enable useful error reporting during development with display_errors = On and error_reporting = E_ALL. For uploads adjust upload_max_filesize and post_max_size to values that match your needs.

; Development-friendly snippet
display_errors = On
error_reporting = E_ALL
date.timezone = Europe/London
memory_limit = 256M
upload_max_filesize = 10M
post_max_size = 12M

Overriding settings safely when needed

You can change some options per request using ini_set(). Some SAPIs support per-directory overrides using .user.ini. Apache module users can set a few options inside .htaccess files with specific directives. Only override what you need; keep configuration simple and obvious.

⚠️ Never enable display_errors on a public server. Show friendly messages to users and send errors to logs instead.

Chapter 2: Language Basics

Before working with larger PHP projects you need a solid understanding of the syntax and rules that form the foundation of the language. PHP code is built from statements that end with semicolons, grouped inside blocks, and organized with variables, constants, and types. This chapter explains those basics and prepares you for functions and objects in later chapters.

💡 PHP syntax has remained backward-compatible for decades, so most older code still runs today. The language has added many modern features but keeps its familiar structure.

Syntax, Statements, and Comments

PHP scripts are composed of statements that usually end with a semicolon. Multiple statements form a block when enclosed in braces { }. Each script begins with the <?php tag and may include HTML around or between PHP blocks.

Writing statements and blocks correctly

Each statement must end with a semicolon. A block groups several statements and is often used in loops, conditionals, and functions. Whitespace and indentation make your code easier to read but do not affect execution.

<?php
$greeting = "Hello";
$name = "World";
echo $greeting . ", " . $name . "!";
?>

Adding comments for clarity

Use comments to describe intent and logic. PHP supports single-line comments with // or #, and multi-line comments between /* and */.

// This is a single-line comment
# This is another form
/* 
   This is a multi-line comment
   that can span several lines
*/
⚠️ Keep comments relevant and concise. Outdated comments confuse future readers more than missing ones.

Variables and Constants

Variables in PHP start with a dollar sign and can store any type of data. Constants hold values that do not change during execution. PHP is dynamically typed but you can still use type declarations where helpful.

Creating and using variables

Variable names begin with $ followed by a letter or underscore, and may include letters, numbers, or underscores. PHP variables are case-sensitive and assigned using =.

$language = "PHP";
$version = 8.3;
$isFun = true;

echo "I am learning $language version $version.";
💡 Variables are created the moment they are assigned. Uninitialized variables generate warnings when used.

Defining constants

Constants are defined with define() or the const keyword. They do not use a dollar sign and are global by default.

define("APP_NAME", "My PHP App");
const VERSION = "1.0";

echo APP_NAME;

Understanding variable scope

Variables declared inside functions are local to that function. Global variables can be accessed using the global keyword or the $GLOBALS array. Constants, by contrast, are accessible everywhere.

⚠️ Avoid using global variables for passing data between functions. Prefer parameters and return values for clarity and predictability.

Scalar and Compound Types

PHP supports several scalar types (single values) and compound types (collections or objects). Understanding these helps you predict how expressions and functions behave.

Working with scalar types

Scalars include bool, int, float, and string. PHP converts between them when needed, though this can cause surprises if you are not explicit.

$count = 10;        // int
$price = 19.99;     // float
$title = "Book";    // string
$available = true;  // bool

Exploring compound types

Compound types include array and object. Arrays hold ordered or associative collections of values, while objects group data and behavior in classes. PHP also has callable and iterable pseudo-types.

$colors = ["red", "green", "blue"];
$person = ["name" => "Robin", "role" => "Author"];
💡 PHP 8 introduced union types, allowing variables to accept multiple kinds of values (for example int|float).

Type Declarations and Strict Types

Modern PHP lets you declare expected types for function parameters, return values, and class properties. These declarations make code more predictable and self-documenting.

Using type declarations for safety

Type declarations enforce that only the expected types are passed to functions or methods. You can use built-in types such as int, float, string, array, and bool, or custom class names.

function add(int $a, int $b): int {
  return $a + $b;
}

Enabling strict typing

By default PHP will try to convert values to match declared types. To make type checking strict, include a directive at the top of the file:

<?php
declare(strict_types=1);

With strict types enabled, passing a float to a function expecting an int will cause a TypeError.

⚠️ The strict declaration applies only to that file, not globally. It must appear before any other code.

Operators and Expressions

Operators combine values and variables into expressions. PHP supports arithmetic, assignment, comparison, logical, and string operators, among others.

Performing arithmetic and assignment

$x = 5;
$y = 2;

echo $x + $y;   // 7
echo $x ** $y;  // 25

Compound assignment operators such as += and *= modify variables in place.

Using comparison and logical operators

Equality == compares values after type juggling, while identity === compares both type and value. Logical operators &&, ||, and ! control conditional logic.

💡 Prefer === and !== to avoid unexpected conversions between strings and numbers.

Concatenating and interpolating strings

Use . to concatenate strings. When using double-quoted strings, variables are interpolated automatically.

$name = "Robin";
echo "Hello $name";
echo 'Hello ' . $name;

Include and Require

Large PHP projects use multiple files for organization. The include and require statements insert one file into another at runtime. Both evaluate the imported file in the current scope.

Including external files

Use include when the file is optional. If the file is missing, PHP issues a warning but continues execution.

include "header.php";
echo "<p>Page content</p>";
include "footer.php";

Requiring critical files

Use require for files that are essential. If the file cannot be found, PHP stops execution immediately with a fatal error.

require "config.php";

Avoiding multiple inclusions

Use include_once or require_once to ensure a file loads only once even if referenced multiple times. This prevents redeclaration errors.

require_once "init.php";
⚠️ When using relative paths, remember that PHP resolves them from the current working directory, not necessarily from the file’s own folder. Use __DIR__ to build safe absolute paths.

Chapter 3: Data Types and Structures

Every PHP variable has a type, which determines what kind of data it can hold and how it behaves in expressions. PHP automatically converts between types when necessary, though you can use explicit casting and type declarations to maintain control. Understanding PHP’s built-in types will help you avoid common mistakes and make your code clearer and more predictable.

💡 PHP uses dynamic typing but supports strong type declarations when you want additional safety. This flexibility allows both quick scripting and large, well-structured applications.

Booleans, Integers, and Floats

These are the simplest scalar types in PHP. Booleans represent truth values, integers represent whole numbers, and floats represent numbers with decimal fractions. Each behaves in familiar mathematical ways, though you should be aware of conversion and precision rules.

Using booleans in logical expressions

Booleans are represented by the constants true and false (case-insensitive). Any value can be converted to a boolean; empty strings, zero, null, and empty arrays evaluate as false.

$active = true;
$emptyString = "";
if ($active) {
  echo "Active user";
}
if (!$emptyString) {
  echo "Empty string is false";
}
⚠️ Avoid testing equality directly with == true or == false. Use implicit conditions such as if ($flag) or if (!$flag) instead.

Working with integers and numeric literals

Integers can be written in decimal, hexadecimal (prefix 0x), octal (prefix 0o), or binary (prefix 0b) form. Integer size depends on the platform (usually 64-bit on modern systems).

$a = 42;
$b = 0xFF;
$c = 0b1010;
echo $a + $b + $c;

Using floats and handling precision

Floats represent real numbers but are subject to rounding errors due to binary representation. Use them for measurements and averages, not for exact currency values. For financial calculations, use integers (representing cents) or arbitrary precision libraries like BCMath.

$price = 9.99;
$quantity = 3;
$total = $price * $quantity;
echo $total;  // 29.97 (may vary slightly internally)

Strings and Encodings

Strings hold text data. PHP supports both single-quoted and double-quoted strings, as well as heredoc and nowdoc syntax for multiline text. Each behaves differently regarding variable interpolation and escape sequences.

Using single and double quotes

Single quotes treat most characters literally. Double quotes interpret escape sequences and replace variables with their values.

$name = "Robin";
echo 'Hello $name';  // literal
echo "Hello $name";  // interpolated

Heredoc and nowdoc for multiline text

Heredoc allows embedding of large text blocks with variable interpolation. Nowdoc behaves like single quotes (no interpolation). Both must start and end with an identifier.

$heredoc = <<<HTML
<p>Welcome, $name</p>
HTML;

$nowdoc = <<<'TEXT'
<p>Literal $name</p>
TEXT;
💡 For large embedded HTML or SQL statements, heredoc improves readability and avoids excessive string concatenation.

Handling encodings safely

PHP strings are sequences of bytes, not necessarily Unicode-aware. To handle multibyte characters properly, enable and use the mbstring extension. For example, mb_strlen() counts characters rather than bytes.

⚠️ A plain strlen() call returns the number of bytes, not characters, so a UTF-8 string with accented letters may count higher than its visible length.

Arrays: Indexed and Associative

Arrays in PHP are versatile structures that act as both ordered lists and key-value maps. The same data type supports numeric and associative keys.

Creating indexed arrays

Use square brackets to create arrays. Numeric keys start from zero unless you specify otherwise.

$colors = ["red", "green", "blue"];
echo $colors[1];  // green

Creating associative arrays

Associative arrays use string keys instead of numbers. This form is useful for representing structured data.

$person = [
  "name" => "Robin",
  "role" => "Author",
  "country" => "UK"
];
echo $person["name"];

Iterating through arrays

Use foreach to iterate over values or keys and values together.

foreach ($person as $key => $value) {
  echo "$key: $value<br>";
}
💡 Arrays preserve insertion order. Use array_values() or array_keys() to extract only what you need.

Objects and Resources

Objects represent instances of classes, while resources represent external entities like file handles or database connections. PHP treats both as special types that you can inspect but not directly manipulate as raw values.

Creating and using objects

Objects are created from classes using the new keyword. Properties are accessed with -> notation.

class Book {
  public string $title;
  function __construct($t) {
    $this->title = $t;
  }
}

$book = new Book("This is PHP");
echo $book->title;

Working with resources

Resources represent connections to external systems such as files or databases. You can identify them using get_resource_type(), but you should not rely on internal details.

$handle = fopen("example.txt", "r");
echo get_resource_type($handle);  // stream
fclose($handle);
⚠️ Always close resources when finished. Open file handles and sockets consume system resources that may run out if left dangling.

Null and Type Juggling

null represents the absence of a value. It is a distinct type with a single value: null. PHP automatically converts values between types when used in different contexts, a behavior known as type juggling.

Using and testing for null

You can assign null to any variable and test it with is_null() or a strict comparison.

$value = null;
if (is_null($value)) {
  echo "No value assigned";
}

Understanding type juggling

When performing operations, PHP often converts values automatically. Strings that contain numbers become integers, and booleans convert to numbers or strings as needed. This can simplify quick scripts but also cause subtle bugs.

echo "5" + 10;         // 15
echo "5 apples" + 10;  // 15 with warning
💡 Use === and !== for strict comparisons to prevent unintended conversions during equality checks.

Working with Mixed and Union Types

PHP 8 introduced the mixed and union types to express multiple possible data forms explicitly. These help document flexible functions that can return or accept more than one type of value.

Declaring union types

Use a vertical bar to combine multiple allowed types. This allows functions to safely accept several related types.

function combine(int|float $a, int|float $b): float {
  return $a + $b;
}

Using the mixed type

The mixed type covers all possible values (scalar, array, object, or null). It is used when you cannot constrain input more precisely.

function describe(mixed $value): void {
  var_dump($value);
}
⚠️ While mixed is useful for broad compatibility, prefer explicit union types wherever possible to retain clarity and type safety.

Chapter 4: Operators and Expressions

Operators form the building blocks of expressions. They allow you to combine, compare, and manipulate values. PHP supports a wide variety of operators, many inherited from C-style languages, and a few unique ones such as the spaceship and null coalescing operators. Understanding how operators work and how they interact through precedence rules is key to writing predictable code.

💡 In PHP, almost everything that produces a value is an expression. You can often assign, compare, or combine values directly inside other statements.

Arithmetic and Assignment

Arithmetic operators perform mathematical calculations on numbers. Assignment operators store values in variables and can combine with arithmetic operations for concise updates.

Basic arithmetic operations

PHP supports addition, subtraction, multiplication, division, modulus, and exponentiation.

$a = 10;
$b = 3;

echo $a + $b;   // 13
echo $a - $b;   // 7
echo $a * $b;   // 30
echo $a / $b;   // 3.3333...
echo $a % $b;   // 1
echo $a ** $b;  // 1000
⚠️ Division always produces a float, even when the result appears whole. Use intdiv() for integer division.

Compound assignment operators

Compound operators perform an operation and assignment together. They simplify repetitive updates.

$x = 5;
$x += 2;  // 7
$x *= 3;  // 21
$x -= 1;  // 20

Increment and decrement

You can increase or decrease a numeric variable by one using ++ and --. Prefix form modifies the variable before use; postfix form after use.

$y = 10;
echo ++$y;  // 11
echo $y--;  // prints 11, then becomes 10

Comparison and Spaceship Operator

Comparison operators check relationships between values and return a boolean result. PHP includes standard relational operators as well as a three-way comparison operator known as the spaceship operator.

Standard comparison operators

Use equality == and identity === to test for value or type match. Use inequality != and !== to test for non-matches. Relational operators <, >, <=, and >= work with numbers and strings.

$a = 5;
$b = "5";

var_dump($a == $b);   // true (values are equal)
var_dump($a === $b);  // false (different types)
💡 Prefer === and !== for reliable results that do not rely on automatic type conversion.

Using the spaceship operator <=>

The spaceship operator compares two expressions and returns -1, 0, or 1 depending on whether the left-hand side is less than, equal to, or greater than the right-hand side. It simplifies sorting logic and custom comparisons.

echo 5 <=> 10;   // -1
echo 10 <=> 10;  // 0
echo 15 <=> 10;  // 1

Logical and Bitwise Operations

Logical operators evaluate boolean expressions, while bitwise operators manipulate individual bits in integer values. Both are essential in control flow and low-level programming tasks.

Using logical operators for flow control

Logical operators combine boolean values. && and || short-circuit, meaning they skip evaluating the second operand when the result is already known.

$a = true;
$b = false;

if ($a && !$b) {
  echo "Only a is true";
}

Bitwise manipulation of integers

Bitwise operators act on binary representations of integers. They are useful for flag-based systems and binary masks.

$x = 6;        // 110
$y = 3;        // 011

echo $x & $y;  // 2 (010)
echo $x | $y;  // 7 (111)
echo $x ^ $y;  // 5 (101)
echo ~$x;      // bitwise NOT
⚠️ Bitwise operations convert operands to integers automatically. Floating point or string values will be coerced, sometimes producing unexpected results.

String Operators

Strings can be combined using the concatenation operator or concatenation assignment. PHP does not have arithmetic on strings, so these are the main tools for string assembly.

Concatenating strings with .

Use the dot operator to join strings together. It works with both literals and variables.

$first = "Hello";
$second = "World";
echo $first . " " . $second;

Using concatenation assignment

The concatenation assignment operator .= appends text directly to an existing variable.

$message = "Hello";
$message .= " there";
$message .= "!";
echo $message;  // Hello there!

Null Coalescing and Ternary

PHP includes concise operators for handling optional or conditional values. The null coalescing operator and ternary operator are common in compact control expressions.

Handling defaults with null coalescing ??

Use ?? to return the first operand that is not null. It is ideal for working with optional array keys or request parameters.

$user = $_GET["user"] ?? "Guest";
echo $user;

Writing compact conditionals with the ternary operator

The ternary operator evaluates a condition and selects between two values. Its syntax is condition ? if_true : if_false.

$age = 20;
$status = ($age >= 18) ? "Adult" : "Minor";
echo $status;
💡 PHP also supports a shorthand ternary form $x ?: "default", which returns the left-hand value if it is truthy, otherwise the right-hand value.

Operator Precedence and Associativity

When multiple operators appear in a single expression, precedence and associativity determine evaluation order. Higher precedence operators run before lower ones; associativity decides whether evaluation proceeds left-to-right or right-to-left for operators of equal precedence.

Understanding precedence rules

For example, multiplication has higher precedence than addition, so it executes first unless you override it with parentheses.

echo 2 + 3 * 4;    // 14
echo (2 + 3) * 4;  // 20

Associativity of assignment and concatenation

Most operators are left-associative, meaning they evaluate from left to right. Assignment and concatenation are right-associative, so evaluation happens from right to left.

$a = $b = 5;
echo $a;  // 5

Operator precedence table for quick reference

The following table lists common operators from highest to lowest precedence. Operators on the same line share the same precedence and are evaluated in the same order according to their associativity.

OperatorsAssociativity
clone, newLeft
**Right
!, ~, ++, --, +, - (unary)Right
*, /, %Left
+, -, .Left
<<, >>Left
<, <=, >, >=Left
==, !=, ===, !==, <=>Left
&Left
^Left
|Left
&&Left
||Left
??Right
? : (ternary)Left
=, +=, -=, *=, /=, .=, %=, &=, |=, ^=, <<=, >>=Right
andLeft
xorLeft
orLeft
💡 The and, or, and xor keywords have lower precedence than && and ||. Use them sparingly to avoid unexpected results in compound conditions.

Remember that parentheses are always the clearest and safest way to control evaluation explicitly.

Chapter 5: Flow Control

Programs choose between paths, repeat work, and bail out of work using flow control. PHP provides several constructs for branching and looping, each tuned for a slightly different kind of decision. This chapter introduces the most useful patterns and how to write them clearly.

if, elseif, else

The classic way to branch is to test a condition and execute one of several blocks. Each condition should read like a true or false statement; when a condition is true, the related block runs.

Writing a clear chained if ladder

Use a single chained ladder for mutually exclusive branches. The first condition that evaluates to true selects the block and the rest are skipped.

<?php
$score = 73;

if ($score >= 90) {
  echo "A";
} elseif ($score >= 80) {
  echo "B";
} elseif ($score >= 70) {
  echo "C";
} else {
  echo "Try again";
}
?>
💡 Prefer explicit comparisons over relying on truthy or falsy values. For example, write $count > 0 instead of just $count when you mean "more than zero".

Using blocks and scoping correctly

Always use braces, even for a single statement; this prevents accidental binding to the wrong if when you add more lines later.

<?php
$isMember = true;

if ($isMember) {
  $discount = 0.15;
  echo "Member discount applied";
}
?>

Combining conditions safely

Group conditions with parentheses to make intent obvious and to control evaluation order.

<?php
$inStock = true;
$vip = false;
$quantity = 3;

if (($inStock && $quantity >= 1) || $vip) {
  echo "Proceed to checkout";
}
?>
⚠️ Avoid assigning inside conditions unless it is deliberate. Writing if ($x = 1) assigns the value and then evaluates as true; prefer === for equality with type safety.

match Expressions

A match expression selects a result based on a tested value and returns that result. It uses strict comparison for its arms and it must be exhaustive; this encourages precise branching.

Returning a value from match

Because match is an expression, it produces a value that you can assign or echo directly.

<?php
$status = 404;

$message = match ($status) {
  200 => "OK",
  301 => "Moved Permanently",
  404 => "Not Found",
  default => "Unknown status",
};

echo $message;
?>

Relying on strict comparison in match

Each arm uses === semantics. A string "1" does not match the integer 1; be explicit about types.

<?php
$val = "1";

$result = match ($val) {
  1 => "int one",
  "1" => "string one",
  default => "something else",
};

echo $result; // prints "string one"
?>
💡 Combine labels with commas to handle multiple exact values in a single arm, like 0, null => … .

Guarding with conditions in match using true

When you need range checks, match against true and let each arm supply a boolean condition.

<?php
$score = 73;

$grade = match (true) {
  $score >= 90 => "A",
  $score >= 80 => "B",
  $score >= 70 => "C",
  default => "Try again",
};

echo $grade;
?>

Loops: for, while, do while, foreach

Loops repeat a block while a condition holds or while there are items to process. Choose the loop that reads closest to the problem you are solving.

Counting with for

Use for for indexed counting where you control initialization, condition, and increment.

<?php
for ($i = 0; $i < 3; $i++) {
  echo $i . PHP_EOL; // 0, 1, 2
}
?>

Looping until a condition changes with while and do while

while checks before the body runs; do while checks after the body, ensuring the body runs at least once.

<?php
$lines = ["a", "b", "c"];
$i = 0;

while ($i < count($lines)) {
  echo $lines[$i] . PHP_EOL;
  $i++;
}

$j = 0;
do {
  echo "Index: $j" . PHP_EOL;
  $j++;
} while ($j < 1);
?>

Iterating collections with foreach

foreach iterates arrays and Traversable objects without manual indexing. Retrieve the value or both key and value.

<?php
$prices = ["apple" => 1.20, "pear" => 1.00];

foreach ($prices as $item => $price) {
  echo "$item: $price" . PHP_EOL;
}
?>
LoopBest useChecks
forCounting with an indexBefore each iteration
whileRepeat until condition changesBefore each iteration
do whileRun body at least onceAfter each iteration
foreachCollections and generatorsImplicit over items
⚠️ Modifying an array while iterating over it can produce surprising results. When you plan to add or remove entries, iterate over a copy using foreach (array_values($arr) as …) or collect changes and apply them afterward.

continue, break, and goto

Inside loops you can skip the rest of the current iteration with continue or exit the loop entirely with break. The optional numeric argument targets outer loops. PHP also supports goto with labels, although structured control usually reads better.

Skipping iterations with continue

Use continue to move directly to the next iteration when a condition is not relevant.

<?php
for ($i = 1; $i <= 5; $i++) {
  if ($i % 2 === 0) {
    continue; // skip even numbers
  }
  echo $i . " ";
}
// 1 3 5
?>

Exiting loops with break and targeting outer loops

Provide a level to break out of nested loops. A level of 2 exits two loops, and so on.

<?php
for ($i = 0; $i < 3; $i++) {
  for ($j = 0; $j < 3; $j++) {
    if ($i === 1 && $j === 1) {
      break 2; // leave both loops
    }
    echo "($i,$j) ";
  }
}
?>

Understanding when goto harms readability

goto jumps to a label written as label:. Prefer structured loops and functions since they show intent. Reserve goto for simple error handling in very small scripts when refactoring would be disproportionate.

<?php
$ok = false;

if (!$ok) {
  goto bail;
}

echo "All good";
bail:
echo "Cleaning up";
?>
💡 Extract code into a function and return early instead of using goto; early returns are easier to follow.

Declaring and Using switch

A switch statement compares a value against several cases and runs matching blocks until a break is encountered. It is a control statement and does not produce a value.

Writing a basic switch with case and break

Place the target expression after switch, write case labels, and end each handled block with break to prevent falling through.

<?php
$ext = "jpg";

switch ($ext) {
  case "jpg":
  case "jpeg":
    echo "Image";
    break;

  case "mp3":
    echo "Audio";
    break;

  default:
    echo "Unknown";
}
?>

Falling through deliberately and documenting it

When you want a fall through, leave out break deliberately and place a comment that explains why.

<?php
$level = "warning";

switch ($level) {
  case "error":
    logError();
    // fall through
  case "warning":
    notifyTeam();
    break;
}
?>
⚠️ switch uses loose comparison for case labels in many situations. If you depend on type distinctions, use match or add strict checks inside case blocks.

Pattern-style Branching with match vs switch

match suits value to value selection that returns a result with strict rules; switch suits statement based control with optional fall through. Choose based on whether you need a value or a block-oriented path.

Choosing an expression when you need a value

Use match when the outcome is a value to compute. The expression is concise, exhaustive, and clear about types.

<?php
function taxBand(int $income): string {
  return match (true) {
    $income < 12570 => "none",
    $income < 50270 => "basic",
    $income < 125140 => "higher",
    default => "additional",
  };
}
?>

Choosing a statement when you need side effects

Use switch when you want to run statements that do work such as logging, updating metrics, or calling different functions.

<?php
function handleCommand(string $cmd): void {
  switch ($cmd) {
    case "start":
      startService();
      break;
    case "stop":
      stopService();
      break;
    default:
      echo "Unknown command";
  }
}
?>

Comparing behavior at a glance

Aspectmatchswitch
KindExpression that returns a valueStatement that executes blocks
ComparisonStrict ===Often loose; watch types
ExhaustivenessRequires full coverage via default or armsdefault optional
Fall throughNot allowedAllowed; requires care
Best forMapping input to resultRunning side effects
💡 When you start with a switch that only sets a variable, consider rewriting it as a match expression for clarity.

Chapter 6: Functions

Functions package work behind a name so you can reuse logic, return results, and keep code small and clear. PHP supports traditional functions, closures, and concise arrow functions. You can add types, defaults, attributes, and more for precise behavior.

Defining functions and return values

Define a function with the function keyword, then write parameters in parentheses and the body in braces. Return a value with return; omit it when you only need side effects.

Writing a basic function with a return value

Declare the function once, then call it wherever you need the result.

<?php
function areaOfCircle(float $r): float {
  $pi = 3.14159;
  return $pi * $r * $r;
}

echo areaOfCircle(2.5);
?>

Adding parameter and return types for clarity

Use type declarations on parameters and a return type after the signature. Union types allow several accepted types.

<?php
function toIntOrNull(int|string $v): ?int {
  if (is_int($v)) {
    return $v;
  }
  if (ctype_digit($v)) {
    return (int)$v;
  }
  return null;
}
?>
💡 Use declare(strict_types=1); at the top of files where you want strict parameter type checks; strict typing improves predictability.

Returning early to keep branching simple

Return as soon as you have the answer; this keeps nesting shallow and intent obvious.

<?php
function firstPositive(array $nums): ?int {
  foreach ($nums as $n) {
    if ($n > 0) {
      return $n;
    }
  }
  return null;
}
?>

Parameters, defaults, and named arguments

Parameters accept input. Defaults provide fallback values. Named arguments improve readability by pairing names with values at call sites.

Providing sensible defaults in the signature

Place defaulted parameters after required ones. A default must match the declared type or be null when the type allows it.

<?php
function greet(string $name, string $salutation = "Hello"): string {
  return "$salutation, $name";
}

echo greet("Kai");             // Hello, Kai
echo greet("Kai", "Welcome");  // Welcome, Kai
?>

Calling with named arguments to improve clarity

Use name: value pairs at the call site. Order is flexible when you supply names.

<?php
function rectangle(int $width, int $height, bool $border = false): array {
  return compact("width", "height", "border");
}

$config = rectangle(height: 200, width: 300, border: true);
?>
⚠️ Do not mix positional arguments after named ones; once you start naming, all following arguments must be named as well.

Accepting null with nullable types

Prefix the type with ? to allow null. Use a default of null when absence is meaningful.

<?php
function findUser(?int $id = null): array {
  if ($id === null) {
    return ["mode" => "guest"];
  }
  return ["id" => $id, "mode" => "member"];
}
?>

Variadics and unpacking

Variadics collect many values into one parameter. Unpacking spreads an array or Traversable into individual arguments where a list is required.

Gathering many inputs with a variadic ... parameter

Place ... before the parameter name to capture remaining arguments as an array.

<?php
function sumAll(int ...$nums): int {
  $total = 0;
  foreach ($nums as $n) {
    $total += $n;
  }
  return $total;
}

echo sumAll(2, 4, 6);
?>

Unpacking arrays into parameters with ...

Use ... at the call site to unpack an array into separate arguments.

<?php
function joinWith(string $sep, string ...$parts): string {
  return implode($sep, $parts);
}

$words = ["a", "b", "c"];
echo joinWith("-", ...$words);
?>
💡 Named arguments and unpacking can be combined. Supply named options, then unpack a list for the rest; keep the call readable.

Unpacking arrays by key for named-like behavior

When a function expects an associative array, the spread operator can merge arrays to build the final input.

<?php
$base = ["host" => "localhost"];
$overrides = ["port" => 5432];

$config = [...$base, ...$overrides, "user" => "app"];
?>

Closures and arrow functions

Anonymous functions capture surrounding variables and can be passed around like values. Arrow functions offer compact syntax for single expressions.

Creating a Closure and capturing with use

Use use to import variables from the surrounding scope by value; add & to import by reference.

<?php
$factor = 10;

$scale = function (int $n) use ($factor): int {
  return $n * $factor;
};

echo $scale(3);  // 30
?>

Writing concise fn arrow functions

Arrow functions automatically capture variables by value and evaluate a single expression. The expression result becomes the return value.

<?php
$nums = [1, 2, 3, 4];

$doubled = array_map(fn($n) => $n * 2, $nums);
?>
⚠️ Arrow functions cannot contain statements; when you need multiple statements or complex logic, use a full function closure.

Returning closures from functions for customization

Generate specialized behavior by returning a closure configured with parameters.

<?php
function makePrefixer(string $prefix): callable {
  return fn(string $s): string => $prefix . $s;
}

$warn = makePrefixer("[WARN] ");
echo $warn("Low memory");
?>

Scope, static variables, and recursion

Scope controls where names are visible. Static variables persist their value across calls. Recursion lets a function call itself to solve problems in smaller steps.

Understanding local scope and the global symbol table

Variables inside a function are local. To access a global, prefer passing it in; if you must, read it via the global keyword or $GLOBALS array.

<?php
$rate = 0.2;

function vat(float $net): float {
  global $rate;  // prefer dependency injection instead
  return $net * (1 + $rate);
}
?>

Persisting data between calls with static

A static local variable keeps its value across calls. Use this for simple memoization or counters.

<?php
function nextId(): int {
  static $id = 0;
  $id++;
  return $id;
}

echo nextId();  // 1
echo nextId();  // 2
?>
💡 For complex shared state, prefer a small object with private properties; this keeps state explicit and testable.

Applying recursion with a safe base case

Every recursive function needs a base case to stop calling itself. Verify argument sizes and use iteration when recursion depth might be large.

<?php
function factorial(int $n): int {
  if ($n <= 1) {
    return 1;
  }
  return $n * factorial($n - 1);
}
?>

Attributes on functions

Attributes attach structured metadata to declarations. Frameworks and tools read attributes to modify behavior at runtime or during reflection.

Declaring a #[Attribute] above a function

Place attributes in square brackets with a leading hash. Supply arguments like regular constructor calls.

<?php
#[Deprecated("Call newProcess() instead")]
function oldProcess(): void {
  // …
}
?>

Reading attributes with reflection

Use the Reflection API to inspect attributes and their arguments at runtime.

<?php
$func = new ReflectionFunction('oldProcess');

foreach ($func->getAttributes() as $attr) {
  $name = $attr->getName();
  $args = $attr->getArguments();
  echo "$name: " . json_encode($args) . PHP_EOL;
}
?>
⚠️ Attributes do not enforce behavior by themselves; a library, framework, or your code must read them and act on their presence.

Defining a custom attribute class for precise semantics

Mark your own attribute class with #[Attribute] so PHP recognizes it as an attribute target.

<?php
#[Attribute(Attribute::TARGET_FUNCTION)]
class Route {
  public function __construct(
    public string $method,
    public string $path
  ) {}
}

#[Route(method: "GET", path: "/status")]
function status(): array {
  return ["ok" => true];
}
?>

Chapter 7: Strings and Text Processing

Strings store and manipulate text. PHP offers several literal syntaxes, hundreds of string functions, full Unicode support through mbstring, and regular expression tools for advanced matching. Handling text correctly means choosing the right quoting, encoding, and cleaning strategy for each situation.

Single, Double, heredoc, and nowdoc

PHP provides several syntaxes for string literals. The main differences are how variables are interpreted and how quotes or line breaks are handled.

Using single quotes for literal text

Single-quoted strings treat most characters literally. Escape only the single quote itself and backslashes.

<?php
$name = 'Alice';
echo 'Hello $name';  // prints Hello $name
echo 'It\'s PHP time';
?>

Using double quotes for interpolation

Double-quoted strings allow variable and escape sequence expansion. They are convenient for building messages with embedded values.

<?php
$name = "Alice";
echo "Hello $name";  // prints Hello Alice
echo "Line one\nLine two";
?>

Creating multi-line strings with heredoc

A heredoc starts with <<<LABEL and ends with LABEL; on its own line. It behaves like a double-quoted string.

<?php
$text = <<<HTML
Hello $name,
This is a heredoc example.
HTML;

echo $text;
?>

Creating raw multi-line strings with nowdoc

nowdoc syntax uses single quotes around the label and does not expand variables or escapes.

<?php
$snippet = <<<'CODE'
function hello() {
  echo "Hi there";
}
CODE;

echo $snippet;
?>
💡 Use heredoc for generated text such as HTML or SQL, and nowdoc for raw templates or code examples that should remain untouched.

Interpolation and Escape Sequences

Interpolation replaces variable names in double-quoted or heredoc strings with their values. Escape sequences represent special characters like newlines or tabs.

Embedding variables safely

Simple variables interpolate directly. When concatenating arrays or complex expressions, use braces to make boundaries clear.

<?php
$user = "Maya";
echo "Welcome $user";   // OK
echo "Hello {$user}!";  // clearer
?>

Understanding escape sequences

Common sequences include \n (newline), \t (tab), and \\ (backslash). Only recognized in double-quoted or heredoc strings.

<?php
echo "Path: C:\\Program Files\\PHP";
echo "\nTabbed\tText";
?>
⚠️ Variables inside single-quoted or nowdoc strings are not expanded; escaping other sequences has no effect there.

Common String Functions

PHP includes many built-in functions for measuring, searching, and manipulating strings. Most are case-sensitive; some have case-insensitive variants ending in i.

Measuring and slicing strings

When working with text, you often need to know its length or extract a part of it. Functions like strlen() and substr() make this easy, giving you direct access to character counts and specific portions of strings without manually looping through them.

<?php
$str = "Hello World";

echo strlen($str);        // 11
echo substr($str, 6, 5);  // World
?>

Changing case and trimming

Normalizing text to consistent case and removing unwanted whitespace are common preparation steps before comparison or output. PHP’s case-conversion and trimming functions ensure predictable formatting and cleaner presentation.

<?php
echo strtoupper("php");  // PHP
echo strtolower("PHP");  // php
echo trim("  space  ");  // space
?>

Searching and replacing text

Locating or replacing text within larger strings is a frequent task in text processing. PHP provides simple search functions and direct replacements that are fast and safe for plain text operations without needing regular expressions.

<?php
$msg = "red, green, blue";

echo strpos($msg, "green");  // 5
echo str_replace("green", "yellow", $msg);
?>
💡 Use str_contains(), str_starts_with(), and str_ends_with() (PHP 8+) for clean, readable conditionals.

Splitting and joining

Splitting breaks a string into pieces using a delimiter, while joining (or imploding) assembles an array back into a single string. These operations are useful when processing comma-separated data or reconstructing messages from parts.

<?php
$items = explode(",", "a,b,c");
echo implode("-", $items);  // a-b-c
?>

Formatting and padding

Formatting inserts values into templates and adjusts output for display. Padding functions ensure consistent width for aligned text or numeric fields, which is especially helpful when building tables or reports.

<?php
printf("Pi is about %.2f", 3.14159);
echo str_pad("42", 5, "0", STR_PAD_LEFT);  // 00042
?>

Multibyte Strings with mbstring

PHP strings are sequences of bytes, not characters. Multibyte functions ensure correct handling of UTF-8 and other encodings for languages beyond ASCII.

Measuring multibyte strings

Use mb_strlen() instead of strlen() when working with UTF-8 text. Set the internal encoding first for consistency.

<?php
mb_internal_encoding("UTF-8");
echo mb_strlen("café");  // 4, not 5
?>

Extracting and converting substrings

Multibyte strings require special handling to avoid splitting characters mid-sequence. Functions like mb_substr() and mb_convert_case() work safely on UTF-8 data, allowing accurate slicing and case transformation for all languages.

<?php
$str = "Здравствуйте";
echo mb_substr($str, 0, 5); // first few characters
echo mb_convert_case($str, MB_CASE_TITLE, "UTF-8");
?>
⚠️ Always specify "UTF-8" when calling mb_* functions if your environment defaults may differ.

Regular Expressions with PCRE

Regular expressions find or replace complex text patterns. PHP’s PCRE functions use the Perl-Compatible Regular Expression engine, providing a powerful pattern syntax.

Matching patterns with preg_match()

preg_match() tests for a match and optionally captures groups.

<?php
$email = "user@example.com";
if (preg_match("/^[\w\.-]+@[\w\.-]+\.\w+$/", $email)) {
  echo "Valid email";
}
?>

Finding all matches with preg_match_all()

When you need to locate every occurrence of a pattern, preg_match_all() collects them into an array for inspection or further processing. This is useful for tasks like extracting all URLs, numbers, or tags from text.

<?php
$text = "one two three two";
preg_match_all("/two/", $text, $matches);
print_r($matches);
?>

Replacing with preg_replace()

preg_replace() performs regular expression substitutions, allowing dynamic rearrangement of captured groups or conversion of matched patterns. It is the flexible counterpart to simple str_replace() for structured text.

<?php
$sentence = "2025-11-04";
echo preg_replace("/(\d{4})-(\d{2})-(\d{2})/", "$3/$2/$1", $sentence);
?>
💡 Use the u modifier (for Unicode) when processing UTF-8 text, for example /pattern/u.

Testing replacements and captures

Always test patterns on sample data; unexpected greediness or anchoring can cause subtle bugs.

<?php
preg_match("/(a+)(b+)/", "aaabb", $parts);
print_r($parts); // shows groups a and b separately
?>

Filtering and Sanitization

Filtering ensures data is valid; sanitization removes or escapes unsafe characters. PHP’s filter_var() and related functions offer built-in filters for common cases like email and URL validation.

Validating input with filter_var()

Validation confirms that data meets specific rules before use. filter_var() checks input against built-in filters, helping prevent errors or misuse by rejecting malformed values early in your code.

<?php
$email = "user@example.com";
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
  echo "Valid email";
}
?>

Sanitizing text to remove unwanted characters

Sanitization cleans input by stripping or escaping unsafe characters, reducing risks of injection or display errors. PHP’s filters offer simple ways to ensure incoming data stays within safe, predictable bounds.

<?php
$name = "<b>Bob</b>";
echo filter_var($name, FILTER_SANITIZE_STRING);
?>
⚠️ FILTER_SANITIZE_STRING is deprecated in newer PHP versions. Use htmlspecialchars() or stricter escaping based on your context (HTML, URL, SQL).

Escaping for HTML output

Escaping converts special characters into harmless HTML entities, preventing browsers from interpreting them as code. Always escape user-provided data before embedding it in HTML to avoid cross-site scripting vulnerabilities.

<?php
$userInput = "<script>alert('xss')</script>";
echo htmlspecialchars($userInput, ENT_QUOTES | ENT_SUBSTITUTE, "UTF-8");
?>
💡 Always encode data for its target context (HTML, JavaScript, SQL, etc.); validation alone does not make output safe.

Chapter 8: Arrays and Collections

Arrays are PHP’s fundamental data structure. They can act as lists, dictionaries, sets, stacks, or queues. Higher-level constructs such as iterators, generators, and SPL data structures build on this foundation to handle data efficiently and clearly.

Creating and Manipulating Arrays

Arrays can be indexed (numeric keys) or associative (string keys). You can create them with short syntax [] or the older array() form.

Defining indexed and associative arrays

Indexed arrays store values in numeric order, while associative arrays map keys to values. Choosing the right type keeps your data model simple and helps clarify whether position or meaning matters more for your elements.

<?php
$colors = ["red", "green", "blue"];
$user = ["name" => "Alice", "age" => 30];
?>

Adding, updating, and removing elements

Arrays are flexible containers that can grow or shrink as needed. Appending, assigning, or unsetting elements lets you adjust data dynamically in response to user input or program state.

<?php
$colors[] = "yellow";              // append
$user["email"] = "a@example.com";  // add new key
unset($user["age"]);               // remove
?>

Combining arrays and spreading

Use the union operator or spread syntax to merge arrays into one.

<?php
$a = ["a" => 1, "b" => 2];
$b = ["b" => 3, "c" => 4];
$merged = $a + $b;         // keeps first value of "b"
$spread = [...$a, ...$b];  // overwrites "b"
?>
💡 The union operator (+) preserves keys from the left array, while the spread syntax overwrites keys with later values.

Iterating over arrays

Use foreach to traverse arrays easily.

<?php
foreach ($user as $key => $value) {
  echo "$key = $value" . PHP_EOL;
}
?>

array_map, array_filter, array_reduce

These functional-style helpers transform, filter, and combine arrays without manual loops. They are expressive and concise when used with arrow functions or closures.

Transforming values with array_map()

array_map() applies a callback to every element of an array and returns a new array of transformed values. This encourages declarative programming by expressing what should happen to each value rather than how to loop through it.

<?php
$nums = [1, 2, 3];
$squares = array_map(fn($n) => $n ** 2, $nums);
print_r($squares);
?>

Filtering elements with array_filter()

array_filter() removes elements that do not meet a condition, returning only those that do. It is a concise way to clean data sets or apply logical tests without manual iteration.

<?php
$values = [1, 2, 3, 4, 5];
$even = array_filter($values, fn($n) => $n % 2 === 0);
print_r($even);
?>
⚠️ By default, array_filter() preserves keys. Use array_values() if you need consecutive numeric indices afterward.

Combining values with array_reduce()

array_reduce() accumulates all elements of an array into a single result, such as a total or concatenated string. It abstracts the common “reduce” pattern found in functional programming, making aggregation simpler and clearer.

<?php
$nums = [2, 3, 4];
$product = array_reduce($nums, fn($carry, $n) => $carry * $n, 1);
echo $product; // 24
?>
💡 array_reduce() is ideal for totals, concatenations, or any accumulation pattern where you combine values into one result.

Sorting and Slicing

PHP offers several functions to order, slice, and partition arrays. Choose the function that preserves or reindexes keys based on your needs.

Sorting arrays by value

Sorting reorders arrays so elements appear in numeric or alphabetical sequence. sort() discards keys and creates a clean index, useful for lists where order matters more than labels.

<?php
$nums = [4, 2, 8, 6];
sort($nums);
print_r($nums); // [2,4,6,8]
?>

Sorting while preserving keys

When keys carry meaning, use key-preserving sorts such as asort() or arsort(). These maintain associations between keys and values while reordering based on value comparisons.

<?php
$prices = ["apple" => 1.2, "pear" => 1.0, "plum" => 2.0];
asort($prices);
print_r($prices);
?>

Custom sorting with usort()

Provide a comparison function that returns negative, zero, or positive values to control ordering.

<?php
$words = ["pear", "banana", "apple"];
usort($words, fn($a, $b) => strlen($a) <=> strlen($b));
print_r($words);
?>

Extracting parts of arrays

array_slice() returns a portion of an array without changing the original. It is perfect for pagination, previews, or working on subsets of data without duplication or side effects.

<?php
$items = ["a", "b", "c", "d"];
print_r(array_slice($items, 1, 2)); // b, c
?>
💡 Combine array_slice() and array_splice() to take and modify portions of arrays in one step.

Iterators and Generators

Iterators provide a consistent interface to traverse data sources. Generators create lightweight iterators without building full arrays in memory.

Using built-in iterator interfaces

SPL iterators wrap collections in objects that support standard traversal methods. They provide uniform behavior for arrays, directory listings, and custom classes, simplifying integration with loops and higher-order functions.

<?php
$arr = new ArrayIterator(["a", "b", "c"]);
foreach ($arr as $item) {
  echo $item;
}
?>

Creating generators with yield

A generator function yields one value at a time. Each yield pauses the function, preserving local state until the next iteration.

<?php
function countdown(int $from) {
  while ($from > 0) {
    yield $from--;
  }
}

foreach (countdown(3) as $n) {
  echo $n . " ";
}
?>

Combining generators and pipelines

Generators compose neatly, allowing you to process streams lazily.

<?php
function squares(iterable $nums): iterable {
  foreach ($nums as $n) {
    yield $n * $n;
  }
}

foreach (squares(countdown(4)) as $sq) {
  echo $sq . " ";
}
?>
⚠️ Generators are single-pass; once you exhaust a generator, you must recreate it to iterate again.

SPL Data Structures

The Standard PHP Library (SPL) provides object-based collections for stacks, queues, heaps, and more. They are efficient alternatives to plain arrays for specific use cases.

Using SplStack and SplQueue

SplStack and SplQueue implement efficient push/pop and enqueue/dequeue operations. They are ideal for managing ordered data where the access pattern strictly follows last-in-first-out or first-in-first-out rules.

<?php
$stack = new SplStack();
$stack->push("a");
$stack->push("b");
echo $stack->pop(); // b

$queue = new SplQueue();
$queue->enqueue("x");
$queue->enqueue("y");
echo $queue->dequeue(); // x
?>

Working with SplHeap and SplPriorityQueue

Heaps and priority queues maintain items in order of priority instead of insertion. These data structures are useful for scheduling, ranking, and any process that must always access the most important element first.

<?php
$heap = new SplMinHeap();
$heap->insert(5);
$heap->insert(2);
$heap->insert(8);

foreach ($heap as $val) {
  echo $val . " ";
}
?>
💡 SPL structures implement Iterator, Countable, and ArrayAccess, allowing them to work seamlessly in loops and functions that expect arrays.

Choosing between SPL and arrays

StructureBest forNotes
SplStackLIFO operationspush / pop
SplQueueFIFO operationsenqueue / dequeue
SplHeapPriority orderingmin or max heaps
SplFixedArrayFixed-size numeric arraysLower memory overhead

Destructuring with List Syntax

PHP can unpack arrays directly into variables using list syntax. This is useful for tuples or predictable small collections.

Using list() for numeric arrays

The list() construct unpacks arrays into individual variables by position. It reads naturally when dealing with small fixed-length arrays that represent tuples or coordinate pairs.

<?php
$coords = [10, 20];
list($x, $y) = $coords;
echo "X=$x, Y=$y";
?>

Using short array destructuring syntax

Short array destructuring provides a concise alternative to list(). It improves readability and pairs well with modern PHP style, especially when assigning multiple values in a single statement.

<?php
[$r, $g, $b] = [255, 128, 64];
echo "R=$r G=$g B=$b";
?>

Destructuring associative arrays with named keys

From PHP 7.1 onward, you can destructure associative arrays by key name.

<?php
$user = ["id" => 5, "name" => "Kai"];
["id" => $id, "name" => $name] = $user;
echo "$id: $name";
?>
💡 Use destructuring when functions return arrays with known positions or keys; it reduces boilerplate and clarifies intent.

Chapter 9: Object-Oriented PHP

PHP supports a modern object model that lets you structure programs around collaborating objects. This chapter introduces class declarations, properties, and methods; it moves through construction and visibility; then explores inheritance, interfaces, traits, magic, and newer features such as enums and readonly properties.

Classes, Properties, and Methods

A class defines a blueprint for objects. You declare properties to hold state and methods to define behavior. An object is an instance created with new; each object carries its own property values while sharing the same method definitions.

Declaring a basic class and creating objects

A minimal class uses the class keyword and braces. Methods are functions that live inside the class; properties are variables declared inside the class scope. Instantiate with new to get a working object.

class Counter {
  public int $value = 0;

  public function increment(): void {
    $this->value++;
  }
}

$c = new Counter();
$c->increment();
echo $c->value;  // 1
💡 Keep class files small and cohesive; one primary responsibility per class improves readability and testing.

Accessing object state with $this

Inside a method, the pseudo variable $this refers to the current object. Use the object operator -> to access properties and call other methods on the same instance.

class Greeter {
  public string $name = "World";

  public function hello(): string {
    return "Hello, " . $this->name;
  }
}

Typing properties and method signatures

PHP supports property types and parameter or return types. Use scalar types, class names, union types, and nullable types to make contracts explicit and self documenting.

class Repository {
  /** @var array<int, string> */
  private array $items = [];

  public function add(int $id, string $value): void {
    $this->items[$id] = $value;
  }

  public function get(int $id): ?string {
    return $this->items[$id] ?? null;
  }
}

Using constants and static members

Class constants hold values that do not change. Static properties and static methods belong to the class itself. Access constants with ClassName::CONST and static members with ClassName::member.

class MathUtil {
  public const PI = 3.14159;

  public static function circleArea(float $r): float {
    return self::PI * $r * $r;
  }
}

echo MathUtil::circleArea(2.0);

Constructors, Promotion, and Visibility

Constructors prepare objects for use. Property promotion shortens constructor boilerplate. Visibility controls who may read or call members. Together these features help you create clear object lifecycles and safe encapsulation.

Initializing state in __construct()

The __construct() method runs on new. Validate inputs and set defaults here; avoid heavy work that you could defer to a separate method if possible.

class User {
  private string $email;

  public function __construct(string $email) {
    // Basic check for illustration only
    if ($email === "") {
      throw new InvalidArgumentException("Email required");
    }
    $this->email = $email;
  }
}

Using constructor property promotion for brevity

Constructor property promotion lets you declare and assign properties in the parameter list. This reduces duplication while keeping types and visibility in one place.

class Point {
  public function __construct(
    public float $x,
    public float $y
  ) {}
}

$p = new Point(1.2, 3.4);
echo $p->x;

Choosing public, protected, or private

public exposes members to any caller; protected exposes to the class and subclasses; private limits to the declaring class. Prefer the most restrictive visibility that still meets your needs.

VisibilityAccessible From
publicAnywhere
protectedClass and subclasses
privateDeclaring class only

Using promoted readonly parameters for safe construction

Combine promotion with readonly to guarantee a property can only be written during construction. This ensures the object’s state remains stable after it is built.

class Config {
  public function __construct(
    public readonly string $dsn,
    public readonly string $user,
    public readonly string $password
  ) {}
}
⚠️ Validation should still happen even when using promotion. Throwing early avoids partially initialized objects that fail later under load.

Inheritance and Late Static Binding

Inheritance allows a class to extend another and reuse or refine behavior. When working with static references, late static binding helps subclasses get the correct class context during static access.

Extending behavior with extends

Use extends to inherit properties and methods. Override a method by redeclaring it with the same signature; call the parent implementation with parent::method() when you need to build on it.

class Logger {
  public function log(string $msg): void {
    echo "[LOG] " . $msg . PHP_EOL;
  }
}

class FileLogger extends Logger {
  public function log(string $msg): void {
    parent::log($msg);
    // write to file …
  }
}

Calling parent constructors safely

If a subclass defines its own constructor, call parent::__construct(…) when the base class requires initialization. This ensures shared state is prepared correctly.

class Connection {
  public function __construct(protected string $dsn) {}
}

class PdoConnection extends Connection {
  private \PDO $pdo;

  public function __construct(string $dsn) {
    parent::__construct($dsn);
    $this->pdo = new \PDO($dsn);
  }
}

Understanding late static binding with static::

Late static binding resolves a static reference to the called class rather than the class where the method was defined. Use static:: or the static return type to make fluent inheritance behave correctly.

class Base {
  public static function factory(): static {
    return new static();
  }
}

class Child extends Base {}

$made = Child::factory(); // instance of Child

Preventing inheritance with final

Mark a class or a specific method as final to prevent extension or overriding. This locks behavior when variation would be unsafe.

final class IdGenerator {
  public static function make(): string {
    return bin2hex(random_bytes(16));
  }
}

Interfaces, Abstract Classes, and Traits

Interfaces define capabilities without implementation; abstract classes can include shared code and abstract methods; traits let you reuse method implementations across unrelated classes.

Defining capabilities with an interface

An interface declares method signatures. Any class that implements the interface agrees to provide those methods. This enables polymorphism without coupling to a concrete type.

interface Cache {
  public function get(string $key): mixed;
  public function set(string $key, mixed $value, int $ttl = 0): void;
}

Sharing partial implementations via an abstract class

Abstract classes can provide common code while declaring abstract methods that subclasses must implement. Use this pattern when implementations share data layout or helper logic.

abstract class Shape {
  public function area(): float {
    return $this->computeArea();
  }

  abstract protected function computeArea(): float;
}

class Rectangle extends Shape {
  public function __construct(private float $w, private float $h) {}
  protected function computeArea(): float {
    return $this->w * $this->h;
  }
}

Reusing method sets with a trait

Traits inject methods into classes through use. They are ideal for cross cutting helpers that do not fit an inheritance hierarchy.

trait Timestamps {
  protected \DateTimeImmutable $createdAt;

  public function markCreated(): void {
    $this->createdAt = new \DateTimeImmutable();
  }
}

class Post {
  use Timestamps;
}

Resolving trait method conflicts

When two traits provide the same method, use insteadof and as to resolve conflicts or rename. This keeps call sites simple while avoiding ambiguity.

trait A { public function run() { echo "A"; } }
trait B { public function run() { echo "B"; } }

class Task {
  use A, B {
    A::run insteadof B;
    B::run as runB;
  }
}
💡 Prefer interfaces for external contracts and traits for internal reuse; this keeps dependencies clear and testable.

Magic Methods and Overloading

Magic methods are special hooks that begin with double underscores. PHP calls them automatically to support string casting, serialization, property or method overloading, and object lifecycle events.

Using __toString() for friendly output

Return a concise representation that helps debugging and logging. Keep it safe; never throw exceptions from __toString().

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

  public function __toString(): string {
    return $this->currency . " " . number_format($this->cents / 100, 2);
  }
}

Intercepting property access with __get() and __set()

These hooks run when reading or writing inaccessible properties. Use them to proxy to internal storage or compute values on demand.

class DataBag {
  private array $data = [];

  public function __get(string $name): mixed {
    return $this->data[$name] ?? null;
  }

  public function __set(string $name, mixed $value): void {
    $this->data[$name] = $value;
  }
}

Catching dynamic calls with __call() and __callStatic()

When a method is not found, PHP invokes these handlers. Common uses include forwarding to composed objects or implementing simple query builders; keep behavior predictable.

class Forwarder {
  public function __call(string $name, array $args): mixed {
    // Forward to an inner service …
    return null;
  }

  public static function __callStatic(string $name, array $args): mixed {
    // Handle static calls …
    return null;
  }
}

Managing lifecycle with __construct() and __destruct()

Use __construct() for setup and __destruct() for best effort cleanup such as closing files. Destructors run during shutdown; avoid throwing and avoid long blocking operations.

class TempFile {
  public function __construct(private string $path) {}

  public function __destruct() {
    @unlink($this->path);
  }
}
Magic HookTriggered When
__get(), __set(), __isset(), __unset()Accessing non public or missing properties
__call(), __callStatic()Calling inaccessible or missing methods
__toString()Casting to string
__serialize(), __unserialize()Custom serialization
__debugInfo()Var dumping
⚠️ Overuse of magic can hide control flow. Prefer explicit methods; reserve magic for integration points and thin convenience layers.

Enums and Readonly Properties

Enums model a fixed set of values with type safety. Readonly properties protect state after construction. Both features reduce accidental mutation and make intent clear.

Declaring a pure enum for fixed choices

A pure enum defines named cases without payloads. Use it when you need a stable closed set like statuses or modes.

enum Status {
  case Pending;
  case Active;
  case Suspended;
}

function activate(Status $s): bool {
  return $s === Status::Pending;
}

Using a backed enum for external representation

Backed enums associate each case with a scalar. This is useful for persistence or APIs that work with strings or integers.

enum Role: string {
  case Admin = "admin";
  case Editor = "editor";
  case Viewer = "viewer";
}

$role = Role::from("editor");     // throws if unknown
$maybe = Role::tryFrom("guest");  // null if unknown

Attaching behavior to enum cases

Enums can include methods. Add helpers for display labels or transitions; keep logic small and focused.

enum Traffic: string {
  case Red = "red";
  case Amber = "amber";
  case Green = "green";

  public function canGo(): bool {
    return $this === self::Green;
  }
}

Protecting state with readonly properties

A readonly property can be written during construction and then remains immutable. Use this for identifiers or configuration that should never change after the object is built.

class Order {
  public function __construct(
    public readonly string $id,
    public readonly Status $status
  ) {}
}

// $order->id = "new";  // Error
💡 Combine enums with readonly properties to make illegal states unrepresentable; your code becomes simpler and safer.

Chapter 10: Namespaces, Autoloading, and Composer

Namespaces keep code organized and avoid symbol collisions. Autoloading turns class names into file paths so that you do not write require calls everywhere. Composer ties the ecosystem together with dependency management, scripts, and publishing. This chapter shows how to declare namespaces, configure PSR-4 autoloading, work with composer.json, optimize autoloading, express version constraints, and publish public or private packages.

Declaring and Using Namespaces

A namespace groups related classes, interfaces, traits, functions, and constants. Declare the namespace once at the top of the file; then reference names inside or import external names with use. Namespaces prevent collisions and make intent clear.

Declaring a file level namespace

Place the declaration at the first line of the file before any code output. The name should reflect your project and directory layout.

<?php
namespace App\Service;

class Mailer {
  public function send(string $to, string $msg): void {
    // send …
  }
}

Importing with use and aliasing with as

Import long names to keep code readable. Aliasing helps when two imported names would clash. You can import classes, functions, and constants.

<?php
namespace App\Controller;

use App\Service\Mailer;
use Vendor\Package\Http\Client as HttpClient;
use function Vendor\Str\join_words as joinWords;
use const Vendor\Config\TIMEOUT;

class SignupController {
  public function __construct(private Mailer $mailer, private HttpClient $http) {}

  public function handle(): void {
    $msg = joinWords(["Welcome", "aboard"]);
    $this->mailer->send("user@example.com", $msg);
    $this->http->get("https://example.com", timeout: TIMEOUT);
  }
}

Referring to global names with fully qualified paths

Prefix with a leading backslash to bypass the current namespace. This is useful when calling built in classes or functions from inside your own namespace.

<?php
namespace App\Util;

function uuid(): string {
  return \bin2hex(\random_bytes(16));
}
💡 Mirror your namespaces in directories. This keeps navigation easy in large codebases and matches PSR-4 expectations.

PSR-4 Autoloading

PSR-4 defines how fully qualified class names map to file paths. Composer reads your mappings and generates an autoloader that resolves classes on demand. This removes the need for manual require statements and keeps startup fast.

Understanding the PSR-4 name to path mapping

With PSR-4 the prefix Acme\Utils\ mapped to the base directory src/ means Acme\Utils\Image\Filter is loaded from src/Image/Filter.php. Case must match the class declaration on case sensitive filesystems.

{
  "autoload": {
    "psr-4": {
      "Acme\\Utils\\": "src/"
    }
  }
}

Autoloading multiple roots and testing code

You can map several prefixes and include a separate map for development only code. Use autoload-dev to load test namespaces without polluting production.

{
  "autoload": {
    "psr-4": {
      "App\\": "src/",
      "App\\Domain\\": "domain/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "App\\Tests\\": "tests/"
    }
  }
}

Regenerating the autoloader after changes

When you add classes or update mappings, regenerate the autoloader. The optimized variant builds a class map for faster lookups in production.

composer dump-autoload
composer dump-autoload -o
⚠️ Keep prefixes unique. Overlapping or ambiguous prefixes cause surprising resolutions that are hard to debug at runtime.

Composer Basics and composer.json

Composer manages dependencies and autoloading for PHP projects. The composer.json file declares package metadata, requirements, autoload rules, scripts, and more. Composer writes composer.lock to pin exact versions for reproducible installs.

Creating a new project definition

Initialize in an empty directory or an existing repository. Provide a package name in the form vendor/name, then answer prompts or edit the file later.

composer init
# Fill fields, or generate a minimal file and edit …

Declaring dependencies and development tools

Use require for runtime dependencies and require-dev for tools needed only during development. Composer installs everything into vendor/.

{
  "name": "acme/app",
  "require": {
    "guzzlehttp/guzzle": "^7.9",
    "psr/log": "^3.0"
  },
  "require-dev": {
    "phpunit/phpunit": "^11.0",
    "phpstan/phpstan": "^2.0"
  }
}

Committing composer.lock for repeatable builds

Always commit the lock file for applications so every environment installs the same versions. For libraries, commit it if your team wants deterministic local testing; publishing ignores the lock by design.

git add composer.json composer.lock
git commit -m "Track dependencies"
💡 Run composer install in CI for applications. Use composer update only when you intend to refresh versions and review changes.

Scripts, Autoload Optimization, and Classmaps

Composer can run lifecycle scripts, optimize autoloading for production, and include classmaps for legacy directories. These features smooth local workflows and improve runtime performance.

Defining lifecycle scripts in scripts

Attach commands to Composer events such as post-install-cmd or define custom script names. Scripts run in project context with Composer’s vendor binaries on the path.

{
  "scripts": {
    "test": "phpunit",
    "lint": "php -l src",
    "post-install-cmd": [
      "@test",
      "php bin/warm-cache.php"
    ]
  }
}

Optimizing autoloading for deployment

Use optimization flags to preload a complete classmap and collapse PSR-0 or PSR-4 lookups. This reduces filesystem checks during production startup.

composer install --no-dev --classmap-authoritative
# or
composer dump-autoload -o --apcu

Mixing PSR-4 with classmap and files autoloaders

When dealing with legacy or non namespaced code, include specific directories in the classmap or load helper files that define functions or constants.

{
  "autoload": {
    "psr-4": { "App\\": "src/" },
    "classmap": [ "legacy/", "database/seeds/" ],
    "files": [ "src/helpers.php" ]
  }
}
⚠️ The files autoloader executes files on every request. Keep that list small to avoid unnecessary work during bootstrap.

Semantic Versioning and Constraints

Semantic Versioning uses MAJOR.MINOR.PATCH. Increment major when you break compatibility; increment minor when you add features without breaking; increment patch for bug fixes. Composer constraints express the compatible range that your package accepts.

Expressing version intent with constraint operators

Choose an operator that matches your tolerance for updates. The caret is most common for libraries that follow SemVer strictly; the tilde is narrower; wildcards and inequalities are available when needed.

ConstraintMeaningExamples
^1.4Allow non breaking updates within 1.x>=1.4.0 <2.0.0
~1.4Allow updates up to next minor>=1.4.0 <1.5.0
1.4.*Any patch in 1.4>=1.4.0 <1.5.0
>=2.0 <3.0Explicit rangeTwo sided bound
dev-mainTrack a branchUnstable development

Controlling stability with flags

Use minimum-stability and prefer-stable to admit pre releases selectively. You can also mark a single requirement as @beta or @dev without weakening the whole project.

{
  "minimum-stability": "stable",
  "prefer-stable": true,
  "require": {
    "vendor/feature": "^2.1@beta"
  }
}
💡 For libraries, prefer ^ constraints so downstream users get bug fixes automatically while staying within the same major series.

Publishing and Private Packages

Publishing makes your library available through Packagist so others can install it with Composer. Private packages use custom repositories or authenticated access so you can share code within a team.

Preparing a library for Packagist

Give the package a unique vendor/name, add a description, license, and keywords, then tag releases in Git. Packagist reads tags to determine versions.

{
  "name": "acme/logger",
  "description": "Structured logging utilities",
  "type": "library",
  "license": "MIT",
  "keywords": ["logging","psr-3"],
  "autoload": { "psr-4": { "Acme\\Logger\\": "src/" } },
  "require": { "php": "^8.2", "psr/log": "^3.0" }
}
git tag v1.0.0
git push --tags
# Submit repository URL on Packagist …

Using private repositories for internal code

Add a repositories section that points to a VCS host, a local path, or an artifact directory. Authenticate with tokens stored in Composer’s config rather than in composer.json.

{
  "repositories": [
    { "type": "vcs", "url": "https://github.com/acme/private-lib" },
    { "type": "path", "url": "../shared/*", "options": { "symlink": true } }
  ],
  "require": {
    "acme/private-lib": "^1.2",
    "acme/shared-tool": "^0.4"
  }
}
# Store tokens locally
composer config --global github-oauth.github.com <token>
composer config --global http-basic.git.example.com username password

Mirroring and aggregating with a private index

Tools such as Satis or Private Packagist generate a private repository index that mirrors selected packages. Point Composer at that index for faster installs and controlled provenance.

{
  "repositories": [
    { "type": "composer", "url": "https://packages.acme.internal" }
  ]
}
⚠️ Never commit credentials or tokens to version control. Use Composer’s config, environment variables, or your CI secret store to inject secure values at build time.

Chapter 11: Errors and Exceptions

PHP distinguishes between errors and exceptions. Errors signal runtime problems in code or environment; exceptions represent controllable failures that you can catch and handle. This chapter explores error levels, the Throwable interface, structured exception handling, custom exception types, user error handlers, and practical logging strategies.

Error Levels and Reporting

PHP classifies errors by severity and behavior. Some issues are recoverable; others halt execution. You can configure reporting to suit development or production needs. Controlling error levels ensures useful diagnostics without leaking sensitive details.

Understanding PHP error types

Error levels include fatal errors, warnings, notices, and deprecations. Modern PHP represents all errors as Error exceptions that implement Throwable, but legacy code may still emit traditional messages.

LevelDescription
E_ERRORFatal error that stops execution
E_WARNINGNon fatal issue that continues execution
E_NOTICEInformational message about questionable code
E_DEPRECATEDUse of a feature that will be removed
E_PARSECompile time parse error

Configuring error_reporting() and display settings

Use error_reporting() to control which levels PHP reports, and set display_errors and log_errors in php.ini or at runtime. During development show everything; in production hide output and log instead.

// Show all errors during development
error_reporting(E_ALL);
ini_set("display_errors", "1");

// Hide errors but log in production
error_reporting(E_ALL);
ini_set("display_errors", "0");
ini_set("log_errors", "1");
ini_set("error_log", "/var/log/php_errors.log");
💡 Always test production builds with display disabled. Unexpected error output can break JSON or HTML responses.

Exceptions and Throwable

Exceptions are objects that represent failures during program execution. They implement the Throwable interface, which unifies Error and Exception under a single hierarchy. Throwing and catching exceptions lets you separate error handling from normal logic.

Throwing and catching basic exceptions

Use the throw keyword to signal an error and try...catch to intercept it. PHP unwinds the call stack until it finds a matching catch block or terminates with a fatal error.

function divide(float $a, float $b): float {
  if ($b === 0.0) {
    throw new Exception("Division by zero");
  }
  return $a / $b;
}

try {
  echo divide(4, 0);
} catch (Exception $e) {
  echo "Error: " . $e->getMessage();
}

Understanding the Throwable interface

Both Error and Exception implement Throwable, which provides methods such as getMessage(), getCode(), getFile(), and getTrace(). This means you can catch Throwable to handle both kinds uniformly.

try {
  undefined_function();
} catch (Throwable $t) {
  echo "Caught: " . $t->getMessage();
}
⚠️ Catching Throwable is useful in top level error handlers but can obscure root causes if overused. Catch more specific types where possible.

try, catch, finally

The try block wraps code that might throw; catch handles exceptions; finally executes regardless of outcome. Use finally for cleanup such as releasing resources or closing files.

Using multiple catch blocks

Catch blocks are tested in order. Each may specify a different type. The first that matches handles the exception.

try {
  risky();
} catch (InvalidArgumentException $e) {
  echo "Invalid argument: " . $e->getMessage();
} catch (RuntimeException $e) {
  echo "Runtime problem: " . $e->getMessage();
} finally {
  echo "Done.";
}

Using union catch syntax

PHP allows multiple types in one catch block with a pipe separator. This is convenient for related errors that share handling logic.

try {
  risky();
} catch (LogicException | RuntimeException $e) {
  echo "Caught logic or runtime issue: " . $e->getMessage();
}

Re-throwing and wrapping exceptions

You can re-throw an exception with throw or create a new one that wraps the previous via the previous parameter. This preserves the original trace while adding context.

try {
  risky();
} catch (Exception $e) {
  throw new RuntimeException("Higher level failure", 0, $e);
}
💡 Always preserve the previous exception when wrapping. It saves hours of debugging later.

Custom Exception Types

Defining custom exception classes clarifies intent and allows fine-grained handling. They can extend Exception or one of its descendants, adding context or structured data.

Creating domain specific exceptions

Keep exception names descriptive and hierarchical. Group related exceptions under a shared base type.

namespace App\Exception;

class AppException extends \Exception {}
class ValidationException extends AppException {}
class DatabaseException extends AppException {}

Adding extra data to custom exceptions

You can include additional properties or methods that describe the failure more precisely.

class HttpException extends Exception {
  public function __construct(
    private int $status,
    string $message = "",
    ?Throwable $previous = null
  ) {
    parent::__construct($message, $status, $previous);
  }

  public function getStatus(): int {
    return $this->status;
  }
}
throw new HttpException(404, "Not Found");
⚠️ Keep custom exceptions lightweight. They should describe failure, not perform logic. Complex handling belongs in catch blocks or dedicated handlers.

Error Handlers and set_error_handler()

The error handler converts traditional errors into exceptions or logs them centrally. You can register a custom function with set_error_handler() to intercept PHP errors before default handling.

Registering a basic error handler

The handler receives the level, message, file, and line. Returning true prevents PHP’s internal handler from running. Converting errors to ErrorException simplifies unified exception handling.

set_error_handler(function(
  int $level,
  string $message,
  string $file,
  int $line
) {
  throw new ErrorException($message, 0, $level, $file, $line);
});

Restoring and suppressing handlers

Use restore_error_handler() to revert to the previous handler. The @ operator suppresses errors for a specific expression, though it is best avoided since it hides useful information.

@unlink("/nonexistent/file.txt");  // Suppresses warning
restore_error_handler();

Handling fatal errors with register_shutdown_function()

Shutdown functions can inspect error_get_last() to detect fatal errors that escaped earlier handling.

register_shutdown_function(function() {
  $error = error_get_last();
  if ($error && $error["type"] === E_ERROR) {
    error_log("Fatal: " . $error["message"]);
  }
});
💡 Centralize error handling early in the request lifecycle. Frameworks often do this for you, but standalone scripts benefit just as much.

Logging Strategies

Logging preserves diagnostic information without showing it to users. PHP’s error_log() function writes messages to the configured log, and PSR-3 loggers offer structured approaches suitable for larger systems.

Using error_log() for simple logging

This function writes to the system log or a configured file. Supply a custom message type to send via email or socket if required.

error_log("Cache miss for key=$key");

Integrating a PSR-3 compatible logger

Libraries such as Monolog implement the Psr\Log\LoggerInterface. You can inject a logger and use levels like info, warning, or error consistently across your codebase.

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$logger = new Logger("app");
$logger->pushHandler(new StreamHandler(__DIR__ . "/app.log", Logger::INFO));

$logger->info("User login", ["user" => $id]);
$logger->error("Exception caught", ["exception" => $e]);

Structuring and rotating logs

Adopt a consistent format such as JSON lines for easy ingestion. Use rotation tools or Monolog’s RotatingFileHandler to prevent large files. Include request identifiers or correlation IDs for tracing distributed operations.

$logger->pushHandler(new \Monolog\Handler\RotatingFileHandler(
  __DIR__ . "/logs/app.log",
  7
));
⚠️ Logs can contain sensitive data. Sanitize user input and avoid dumping full payloads in production environments.

Chapter 12: Files, Streams, and IO

PHP provides comprehensive tools for file and stream operations. You can read and write data, traverse directories, handle uploads, and work with wrappers for protocols like php:// and http://. This chapter explores the file system API, stream abstraction, structured data formats, temporary file handling, serialization, and key security practices for input and storage.

Working with Files and Directories

PHP offers high level functions for file reading, writing, and directory traversal. Always verify file existence and handle permissions carefully. Prefer built in helpers like file_get_contents() and file_put_contents() for simplicity when you do not need fine control.

Reading and writing entire files

Use file_get_contents() to read and file_put_contents() to write complete files in one call. These are efficient for small and medium sized data.

$text = file_get_contents("notes.txt");
file_put_contents("backup.txt", $text);

Processing files line by line

For large files, open a handle and read incrementally with fopen(), fgets(), and fclose(). Always check for false return values when opening.

$handle = fopen("log.txt", "r");
if ($handle) {
  while (($line = fgets($handle)) !== false) {
    echo $line;
  }
  fclose($handle);
}

Listing and managing directories

Use scandir() or directory handles to enumerate files. Combine with is_dir() and is_file() for filtering.

$entries = scandir("/var/www");
foreach ($entries as $entry) {
  if (is_dir($entry)) {
    echo "[DIR] $entry" . PHP_EOL;
  } else {
    echo "     $entry" . PHP_EOL;
  }
}
💡 Use SplFileInfo or DirectoryIterator for object oriented directory iteration with metadata access.

Streams and Wrappers

Streams abstract file, network, and memory IO behind a unified interface. Wrappers extend the concept to protocols such as HTTP, FTP, ZIP, and data URIs. This consistency means you can use the same functions on many sources and destinations.

Opening and writing to stream resources

Use fopen() with wrappers to access different transports. For example, php://memory provides a temporary in-memory buffer.

$stream = fopen("php://memory", "r+");
fwrite($stream, "Example data");
rewind($stream);
echo stream_get_contents($stream);
fclose($stream);

Working with remote and compressed data

When URL wrappers are enabled, the same functions work for remote resources and compressed files.

$html = file_get_contents("https://example.com");
$gz = gzopen("archive.gz", "rb");
while (!gzeof($gz)) {
  echo gzread($gz, 4096);
}
gzclose($gz);

Creating custom filters

Stream filters let you transform data as it is read or written. PHP includes built in filters for compression, string operations, and conversions. You can register your own for specialized processing.

$fp = fopen("data.txt", "r");
stream_filter_append($fp, "string.toupper");
echo stream_get_contents($fp);
fclose($fp);
⚠️ Remote and wrapper access is governed by allow_url_fopen. Disable it in production if you do not require network IO to prevent arbitrary fetches.

Reading and Writing CSV and JSON

Structured text formats are common for configuration and data exchange. PHP includes native functions for CSV and JSON processing that handle encoding and escaping safely.

Parsing and generating CSV files

Use fgetcsv() to parse and fputcsv() to write rows. Both respect quoted fields and delimiters.

$fp = fopen("people.csv", "r");
while (($row = fgetcsv($fp)) !== false) {
  [$name, $email] = $row;
  echo "$name <$email>" . PHP_EOL;
}
fclose($fp);
$fp = fopen("out.csv", "w");
fputcsv($fp, ["Name", "Email"]);
fputcsv($fp, ["Ada Lovelace", "ada@example.com"]);
fclose($fp);

Encoding and decoding JSON

PHP’s json_encode() and json_decode() convert between arrays or objects and JSON text. Always check json_last_error() after decoding to detect malformed input.

$data = ["title" => "Example", "active" => true];
$json = json_encode($data, JSON_PRETTY_PRINT);
file_put_contents("config.json", $json);

$decoded = json_decode(file_get_contents("config.json"), true);
if (json_last_error() === JSON_ERROR_NONE) {
  print_r($decoded);
}
💡 Use JSON_THROW_ON_ERROR flag in modern PHP to automatically raise exceptions for invalid data.

Uploads and tmp Handling

When users upload files via HTTP forms, PHP writes them to temporary storage. You can access details in the $_FILES array and move them to a permanent location after validation.

Processing uploaded files safely

Check size, MIME type, and upload errors before moving files. Always use move_uploaded_file() to prevent directory traversal or overwriting unintended files.

if ($_FILES["photo"]["error"] === UPLOAD_ERR_OK) {
  $tmp = $_FILES["photo"]["tmp_name"];
  $dest = __DIR__ . "/uploads/" . basename($_FILES["photo"]["name"]);
  move_uploaded_file($tmp, $dest);
}

Creating and cleaning temporary files

Use tmpfile() or tempnam() for scratch storage. tmpfile() deletes automatically on close; tempnam() returns a persistent path you can manage manually.

$temp = tmpfile();
fwrite($temp, "Session data");
rewind($temp);
echo fread($temp, 1024);
fclose($temp);
⚠️ Validate and sanitize upload filenames. Reject unexpected extensions, and never execute uploaded content directly.

Serialization and var_export

Serialization turns data structures into strings for storage or transmission. PHP provides serialize() and unserialize(), and safer alternatives like json_encode() or var_export() for code generation.

Serializing and unserializing PHP data

serialize() encodes mixed structures with type information. Use only for trusted data; it can execute code during unserialization if objects define magic methods.

$a = ["x" => 1, "y" => [2, 3]];
$s = serialize($a);
file_put_contents("data.ser", $s);
$restored = unserialize(file_get_contents("data.ser"));

Exporting data as PHP code

var_export() returns valid PHP code that recreates a value. Combine with eval() or include for configuration patterns.

$config = ["debug" => true, "host" => "localhost"];
$code = "<?php\nreturn " . var_export($config, true) . ";\n";
file_put_contents("config.php", $code);

$loaded = include "config.php";
💡 Prefer var_export() for configuration snapshots or caching known structures. Avoid unserializing untrusted input.

Permissions and Security Considerations

File and stream operations can expose sensitive data or enable injection if handled carelessly. Apply strict permissions, validate paths, and restrict writable locations to controlled directories.

Setting and checking file permissions

Use chmod(), fileperms(), and related functions to inspect and modify access rights. In shared environments, prefer read only where possible.

$file = "config.ini";
chmod($file, 0640);
printf("Permissions: %o", fileperms($file) & 0777);

Avoiding path traversal and injection

Never concatenate user input directly into paths. Use basename() or whitelist directories to ensure safe access. When including files dynamically, restrict to known roots.

$safe = basename($_GET["file"]);
include __DIR__ . "/pages/" . $safe;

Restricting wrapper and stream access

Disable or limit wrappers that you do not use. For critical systems, set open_basedir to restrict file operations to specific paths. Review stream filters to prevent injection through php://filter abuse.

⚠️ Never process unvalidated user file paths or serialized input. Proper sandboxing and principle of least privilege are essential for secure IO.

Chapter 13: Dates, Times, and Internationalization

Working with dates and localized text is a common requirement for web applications. PHP provides two complementary sets of tools: the core DateTime API for precise arithmetic and formatting, and the intl extension for locale-aware presentation. This chapter introduces reliable strategies for calculating with dates, formatting for users in different regions, and comparing human text correctly.

⚠️ Always store timestamps in UTC; convert to the user’s timezone only when displaying or accepting input.

DateTime, timezones, and DateInterval

The core DateTime family models points in time with timezone awareness, while DateInterval represents measured differences. You can also iterate recurring spans using DatePeriod for schedules and reports.

Constructing a DateTime with timezone awareness

Create a DateTime from a string or timestamp; pass a DateTimeZone when the source is not UTC. The object stores absolute time internally and tracks the chosen zone for presentation.

<?php
// Now in UTC
$utcNow = new DateTime('now', new DateTimeZone('UTC'));

// Specific local time
$la = new DateTime('2030-07-14 09:30', new DateTimeZone('America/Los_Angeles'));

// From UNIX timestamp
$fromTs = (new DateTime('@1730700000'))->setTimezone(new DateTimeZone('Europe/London'));

// Changing display timezone
$la->setTimezone(new DateTimeZone('Europe/Paris'));
echo $la->format('Y-m-d H:i T');  // 2030-07-14 18:30 CEST
💡 Strings like '@…' are interpreted as UNIX timestamps. Always set a display timezone afterward for human-readable output.

Measuring time with DateInterval and doing arithmetic

DateInterval stores durations such as 3 days or 1 month. Add or subtract intervals to move a date forward or backward; months and years account for variable lengths correctly.

<?php
$start = new DateTime('2030-01-31', new DateTimeZone('UTC'));

// P1M means a period of one month
$oneMonth = new DateInterval('P1M');
$next = (clone $start)->add($oneMonth);                // 2030-02-28 (leap rules apply)
$prev = (clone $start)->sub(new DateInterval('P2D'));  // 2030-01-29

echo $next->format('Y-m-d');
echo "\n";
echo $prev->format('Y-m-d');

Iterating recurring spans with DatePeriod

Use DatePeriod to generate occurrences between a start and an end given a stepping interval. This is ideal for calendars, invoices, and reports.

<?php
$start = new DateTime('2030-10-01', new DateTimeZone('UTC'));
$end   = new DateTime('2030-10-31 23:59:59', new DateTimeZone('UTC'));
$step  = new DateInterval('P1W');  // weekly

$period = new DatePeriod($start, $step, $end);
foreach ($period as $dt) {
  echo $dt->format('Y-m-d'), "\n";
}
⚠️ Daylight saving transitions can cause repeated or skipped times in local zones. Use UTC for storage and arithmetic; convert for display.

Formatting and parsing

Formatting turns a DateTime into a string; parsing turns a string into a DateTime. For machine-stable formats use DateTime::format and DateTime::createFromFormat. For locale-aware formats use IntlDateFormatter from the intl extension.

Using DateTime::format with tokens

Token strings like 'Y-m-d H:i:s' control output. Use ISO-8601 or RFC-3339 for APIs and logs to keep values unambiguous.

<?php
$dt = new DateTime('2030-11-04 14:00', new DateTimeZone('Europe/London'));
echo $dt->format(DateTime::RFC3339);  // 2030-11-04T14:00:00+00:00
echo "\n";
echo $dt->format('l, d F Y H:i T');   // Tuesday, 04 November 2030 14:00 GMT

Parsing with DateTime::createFromFormat

When you know the exact input pattern, create dates reliably by matching the tokens. Always check for parsing errors and set the timezone explicitly.

<?php
$tz = new DateTimeZone('America/New_York');
$input = '11/04/2030 09:15';
$dt = DateTime::createFromFormat('m/d/Y H:i', $input, $tz);

if ($dt === false) {
  foreach (DateTime::getLastErrors()['errors'] as $e) {
    error_log($e);
  }
} else {
  echo $dt->setTimezone(new DateTimeZone('UTC'))->format(DateTime::RFC3339);
}
💡 Prefer numeric, unambiguous input formats for end users such as YYYY-MM-DD). If you must accept regional formats, require an explicit locale or show an example near the input field.

Localizing output with IntlDateFormatter

IntlDateFormatter uses ICU patterns and locale rules for month names, weekday names, and numbering systems. Choose date and time styles or provide a custom skeleton.

<?php
$dt = new DateTime('2030-11-04 14:00', new DateTimeZone('Europe/London'));

$fmtEn = new IntlDateFormatter(
  'en_GB',
  IntlDateFormatter::FULL,
  IntlDateFormatter::SHORT,
  'Europe/London'
);

$fmtFr = new IntlDateFormatter(
  'fr_FR',
  IntlDateFormatter::FULL,
  IntlDateFormatter::SHORT,
  'Europe/Paris'
);

echo $fmtEn->format($dt);  // Tuesday, 4 November 2030 at 14:00
echo "\n";
echo $fmtFr->format($dt);  // mardi 4 novembre 2030 à 15:00

Working with DateTimeImmutable

DateTimeImmutable performs the same operations as DateTime) but returns a new object for every change. This removes accidental mutation, which makes code easier to reason about in complex flows.

Avoiding accidental mutation by cloning on change

With DateTimeImmutable, calls to add, sub, or setTimezone return a new instance; the original stays the same.

<?php
$base = new DateTimeImmutable('2030-03-01 10:00', new DateTimeZone('UTC'));
$a = $base->add(new DateInterval('P1D'));
$b = $base->setTimezone(new DateTimeZone('Asia/Tokyo'));

echo $base->format('c');  // 2030-03-01T10:00:00+00:00
echo "\n";
echo $a->format('c');     // 2030-03-02T10:00:00+00:00
echo "\n";
echo $b->format('c');     // 2030-03-01T19:00:00+09:00

Switching between mutable and immutable where needed

Convert to mutable with DateTime::createFromImmutable for APIs that require DateTime, or build immutables from mutable instances when you need safety.

<?php
$mutable = new DateTime('now', new DateTimeZone('UTC'));
$immutable = DateTimeImmutable::createFromMutable($mutable);

// Back to mutable
$back = DateTime::createFromImmutable($immutable);
⚠️ Choose one style for a given subsystem to keep behavior consistent. Mixing freely can hide subtle bugs in shared helpers.

Localization with intl

The intl extension relies on ICU to present language- and region-appropriate text. Localization is more than translation; it includes date formats, numbering systems, plural rules, and collation.

Selecting an appropriate locale tag

Locales follow the pattern language_REGION@keywordsLocale::acceptFromHttp to negotiate from Accept-Language.

<?php
$header = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
$locale = Locale::acceptFromHttp($header) ?: 'en_GB';
Locale::setDefault($locale);

Formatting dates with IntlDateFormatter skeletons

Skeletons describe desired fields without fixing literal text. ICU maps them to idiomatic patterns per locale.

<?php
$dt = new DateTime('2030-11-04 14:00', new DateTimeZone('Europe/London'));
$fmt = IntlDateFormatter::create(
  'de_DE',
  IntlDateFormatter::NONE,
  IntlDateFormatter::NONE,
  'Europe/Berlin',
  IntlDateFormatter::GREGORIAN,
  'yyyyMMMEd HH:mm'  // skeleton
);
echo $fmt->format($dt);  // Di., 4. Nov. 2030 15:00
💡 Keep translated strings outside code files and load per locale. PHP’s MessageFormatter supports plural rules and placeholders cleanly.

Handling pluralization with MessageFormatter

Plural forms depend on the language. MessageFormatter applies the correct grammar for counts automatically when configured with a locale.

<?php
$fmt = new MessageFormatter('en_GB',
  '{count, plural, =0{No items} one{# item} other{# items}} in cart'
);
echo $fmt->format(['count' => 1]);

Number and currency formatting

Use NumberFormatter to format quantities, percentages, and currencies according to locale rules, including grouping separators and currency symbols.

Presenting quantities with NumberFormatter

Create a formatter with a style such as DECIMAL or PERCENT; apply minimum and maximum fraction digits as needed for your domain.

<?php
$nf = new NumberFormatter('fr_FR', NumberFormatter::DECIMAL);
$nf->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, 2);
echo $nf->format(12345.6);  // 12 345,60

Displaying money with CURRENCY style

Format amounts with the correct symbol, spacing, and rounding for the user’s locale. Always store amounts in minor units or decimal strings to avoid floating point drift.

<?php
$money = new NumberFormatter('en_GB', NumberFormatter::CURRENCY);
echo $money->formatCurrency(1999.5, 'GBP');    // £1,999.50

$moneyDe = new NumberFormatter('de_DE', NumberFormatter::CURRENCY);
echo $moneyDe->formatCurrency(1999.5, 'EUR');  // 1.999,50 €
Locale Amount Output
en_GB 1999.5 GBP £1,999.50
de_DE 1999.5 EUR 1.999,50 €
hi_IN 1999.5 INR ₹1,999.50
⚠️ Do not format for storage; serialize plain numbers and ISO currency codes. Formatting is a presentation concern only.

Calendars and collation

Some users follow non-Gregorian calendars, and different languages sort words using rules that are not simple binary comparisons. The intl extension provides calendar types and collation strategies to handle these cases correctly.

Working with non-Gregorian calendars via IntlCalendar

IntlCalendar exposes calendars such as Gregorian, Islamic, Hebrew, and Japanese. You can construct a calendar for a locale and timezone, then read or set fields.

<?php
// Japanese calendar example
$cal = IntlCalendar::createInstance('Asia/Tokyo', 'ja_JP@calendar=japanese');
$cal->set(2030, 10, 4, 14, 0, 0);  // month is 0-based: 10 = November
echo $cal->get(IntlCalendar::FIELD_ERA), "\n";
echo $cal->get(IntlCalendar::FIELD_YEAR);

Formatting calendar values with locale rules

IntlDateFormatter can target alternative calendars by specifying the @calendar=… keyword in the locale tag.

<?php
$dt = new DateTime('2030-11-04 14:00', new DateTimeZone('Asia/Tokyo'));
$fmt = IntlDateFormatter::create(
  'ar_EG@calendar=islamic',
  IntlDateFormatter::FULL,
  IntlDateFormatter::SHORT,
  'Africa/Cairo'
);
echo $fmt->format($dt);

Sorting human text with Collator

String comparison should respect language rules for accents and case. Collator implements ICU collation so sorted lists look natural to native speakers.

<?php
$words = ['ábaco', 'azúcar', 'árbol', 'avión'];
$coll = new Collator('es_ES'); // Spanish collation
$coll->asort($words);
print_r($words);

Tuning collation strength for searching

Adjust strength to control sensitivity to case and accents. Weaker strengths are useful for find-as-you-type; stronger strengths are better for canonical ordering.

<?php
$coll = new Collator('de_DE');
$coll->setStrength(Collator::PRIMARY);  // accent-insensitive, case-insensitive
var_dump($coll->compare('ä', 'a'));     // 0 at PRIMARY strength
💡 Normalize text to NFC before storing or comparing. ICU functions handle many differences, but normalized data simplifies downstream logic.

Chapter 14: HTTP Essentials

HTTP underpins every PHP web application. Understanding the request lifecycle, the server interface, and the correct use of headers, cookies, sessions, and JSON lets you build predictable applications that behave well behind proxies and browsers.

⚠️ Treat input from clients as untrusted. Validate and sanitize before using values, and escape output for the correct context.

Request and Response Lifecycle

A PHP script usually runs behind a web server and PHP SAPI (Apache, FPM, CLI server). The server parses the HTTP request, sets environment variables and superglobals, runs your script, and returns the bytes you echo along with headers you emit.

Tracing the flow from client to server

A browser opens a TCP connection, sends a request line, headers, and possibly a body. PHP exposes these details via $_SERVER and input streams. Your script reads data, produces content, sets headers, and the SAPI flushes a response to the client.

<?php
// Request line and headers as seen by PHP
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$uri    = $_SERVER['REQUEST_URI'] ?? '/';
$agent  = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';

header('Content-Type: text/plain; charset=UTF-8');
echo "Method: $method\nURI: $uri\nUser-Agent: $agent\n";

Reading the request body safely

For JSON and other raw payloads, read php://input. For form posts, rely on $_POST and $_FILES. Avoid reading php://input multiple times since it is a stream.

<?php
if (($_SERVER['CONTENT_TYPE'] ?? '') === 'application/json') {
  $raw = file_get_contents('php://input');
  $data = json_decode($raw, true);
}
💡 Use a front controller (one entry script) that routes all requests through a single file. Handle bootstrapping and shared middleware in one place.

Forms and Superglobals

HTML forms submit key value pairs through query strings or request bodies. PHP provides $_GET, $_POST, and $_REQUEST for these values, and filter_input offers explicit validation filters.

Submitting and handling a simple form

Use the correct method and enctype attributes. Read values from $_POST or $_GET, then validate and escape output for HTML.

<form method="post" action="/subscribe.php">
  <label>Email: <input type="email" name="email" required></label>
  <button>Subscribe</button>
</form>
<?php
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email) {
  // save and respond
  echo htmlspecialchars($email, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
} else {
  http_response_code(422);
  echo 'Invalid email address';
}

Using filter_input with validation and defaults

filter_input reads from the request and applies a filter in one step. Provide options to set ranges and default values.

<?php
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT,
  ['options' => ['min_range' => 1, 'default' => 1]]
);
$q = filter_input(INPUT_GET, 'q', FILTER_UNSAFE_RAW) ?? '';
⚠️ Avoid $_REQUEST since it merges sources in a configurable order. Read from the specific bag that matches your form method.

Cookies and Sessions

Cookies allow small per browser key value storage. Sessions associate a server side record with a client by sending a session identifier cookie. Use secure flags and regenerate identifiers after privilege changes.

Setting and reading cookies correctly

Set cookies before output starts. Use secure, httponly, and an appropriate samesite value to reduce risk.

<?php
setcookie(
  'prefs',
  json_encode(['theme' => 'dark']),
  [
    'expires'  => time() + 60 * 60 * 24 * 30,
    'path'     => '/',
    'secure'   => true,
    'httponly' => true,
    'samesite' => 'Lax',
  ]
);

$prefs = [];
if (isset($_COOKIE['prefs'])) {
  $prefs = json_decode($_COOKIE['prefs'], true) ?: [];
}

Starting sessions and managing identifiers

Call session_start before reading or writing $_SESSION. Regenerate the identifier after login to prevent fixation, and set strict cookie flags in session_set_cookie_params.

<?php
session_set_cookie_params([
  'path' => '/',
  'secure' => true,
  'httponly' => true,
  'samesite' => 'Lax',
]);

session_start();

if (!isset($_SESSION['csrf'])) {
  $_SESSION['csrf'] = bin2hex(random_bytes(32));
}

function rotate_session_id(): void {
  if (session_status() === PHP_SESSION_ACTIVE) {
    session_regenerate_id(true);
  }
}
💡 Store only small identifiers in sessions. Load user profiles and permissions from your database per request to avoid stale copies.

File Uploads and Validation

Uploading files uses multipart/form-data. PHP collects metadata in $_FILES. Always verify upload success, check size, validate content type with finfo, and move the file to controlled storage.

Building an upload form with constraints

Specify enctype="multipart/form-data" and provide input constraints with accept and max limits on the server.

<form method="post" enctype="multipart/form-data">
  <label>Avatar: <input type="file" name="avatar" accept="image/*"></label>
  <button>Upload</button>
</form>

Validating and moving uploaded files

Use UPLOAD_ERR_OK to verify status, filesize and finfo to inspect content, then call move_uploaded_file.

<?php
if (!isset($_FILES['avatar']) || $_FILES['avatar']['error'] !== UPLOAD_ERR_OK) {
  http_response_code(400);
  exit('Upload failed');
}

$tmp = $_FILES['avatar']['tmp_name'];
$size = filesize($tmp);

if ($size < 1 || $size > 2 * 1024 * 1024) {
  http_response_code(413);
  exit('File too large');
}

$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($tmp);

$allowed = ['image/png' => 'png', 'image/jpeg' => 'jpg', 'image/webp' => 'webp'];
if (!isset($allowed[$mime])) {
  http_response_code(415);
  exit('Unsupported media type');
}

$ext = $allowed[$mime];
$dest = __DIR__ . '/uploads/' . bin2hex(random_bytes(16)) . ".$ext";

if (!move_uploaded_file($tmp, $dest)) {
  http_response_code(500);
  exit('Could not store file');
}
⚠️ Never trust the client provided filename. Generate your own safe name and store the original as metadata if needed.

Headers, Redirects, and Status Codes

HTTP headers describe metadata such as content type, caching, authentication, and redirection. Choose status codes that reflect the outcome accurately, and send headers before the body.

Setting response headers explicitly

Send Content-Type with a charset when returning text. For caching, control validators and max age. For authentication, include the correct scheme and realm text.

<?php
header('Content-Type: text/html; charset=UTF-8');
header('Cache-Control: no-store');
header('X-Content-Type-Options: nosniff');

// Example bearer challenge with placeholder
header('WWW-Authenticate: Bearer realm="api", error="invalid_token", error_description="…"', true, 401);

Issuing redirects safely with Location

Use a 303 for redirect after POST, since it instructs the client to fetch the new location with GET. Validate any user supplied return URL against an allow list.

<?php
$next = '/account';
http_response_code(303);
header('Location: ' . $next);
exit;

Choosing accurate status codes

Use a small, consistent subset for clarity. The following list covers common scenarios in web applications.

Code Name Typical use
200 OK Successful request with body
201 Created New resource created
204 No Content Successful request without body
303 See Other Redirect after POST to a GET URL
400 Bad Request Malformed input or missing parameters
401 Unauthorized Authentication required, send challenge
403 Forbidden Authenticated but not allowed
404 Not Found Resource does not exist
409 Conflict State conflict such as duplicate
422 Unprocessable Content Validation failed for a well formed request
500 Internal Server Error Unexpected failure on the server
💡 Set headers first, then output the body. If output buffering is on, flush with flush()) only after headers are established.

Working with JSON

JSON is the most common wire format for APIs. Set correct content types, encode with the right options, and handle decoding errors explicitly.

Sending JSON responses with safe encoding

Use JSON_UNESCAPED_UNICODE for readability and JSON_THROW_ON_ERROR to catch encoding issues. Always send application/json with a charset.

<?php
header('Content-Type: application/json; charset=UTF-8');

$payload = [
  'ok' => true,
  'items' => [['id' => 1, 'name' => 'Café']],
];

try {
  echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
  http_response_code(500);
  echo json_encode(['ok' => false, 'error' => 'Encoding failed']);
}

Receiving JSON requests from clients

Check the content type, read the raw body, decode with exceptions, and validate required fields. Return 400 when payloads are malformed.

<?php
if (stripos($_SERVER['CONTENT_TYPE'] ?? '', 'application/json') !== 0) {
  http_response_code(415);
  exit('Unsupported media type');
}

try {
  $data = json_decode(file_get_contents('php://input'), true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
  http_response_code(400);
  exit('Invalid JSON');
}

if (!isset($data['title']) || !is_string($data['title'])) {
  http_response_code(422);
  exit('Missing or invalid title');
}

Avoiding common pitfalls with numbers and dates

Large integers can exceed 64 bit limits in JavaScript clients, and naive date strings cause timezone confusion. Use strings for identifiers, and prefer RFC 3339 for timestamps.

<?php
$order = [
  'id' => (string) 20251104123456789,  // serialized as string to preserve precision
  'created_at' => (new DateTime('now', new DateTimeZone('UTC')))->format(DateTime::RFC3339),
];
⚠️ Never trust $_SERVER['HTTP_X_FORWARDED_FOR'] directly. When behind a proxy, configure trusted proxies and read client IPs through your server or framework settings.

Chapter 15: Web Programming Basics

Modern PHP can serve as both a lightweight scripting layer and the foundation of full web applications. Even without frameworks, you can build modular structures using routing, templates, simple controllers, and consistent state management. This chapter covers the essential web architecture concepts that lead toward more scalable patterns.

⚠️ Frameworks automate much of this, but understanding how the parts fit together helps you debug and extend them intelligently.

Routing Strategies

Routing connects URLs to code. In plain PHP, you can dispatch requests by inspecting $_SERVER['REQUEST_URI'] and calling handlers for each path. For larger applications, a routing table or pattern matcher keeps code organized.

Using a front controller with path routing

With a single entry point such as index.php, interpret the requested path and include the appropriate handler. This keeps routing centralized.

<?php
// public/index.php
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

switch ($path) {
  case '/':
    require '../src/home.php';
    break;
  case '/about':
    require '../src/about.php';
    break;
  case '/contact':
    require '../src/contact.php';
    break;
  default:
    http_response_code(404);
    echo 'Page not found';
}

Using pattern based dispatching

For dynamic URLs, match with regular expressions and extract parameters. This allows you to map routes like /posts/42 to a handler automatically.

<?php
$routes = [
  '#^/posts/(?P<id>\d+)$#' => 'show_post',
  '#^/users/(?P<name>[a-z]+)$#i' => 'show_user',
];

$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
foreach ($routes as $pattern => $handler) {
  if (preg_match($pattern, $path, $m)) {
    require "../src/$handler.php";
    exit;
  }
}

http_response_code(404);
echo 'Not found';
💡 Keep routing definitions near your controllers or in a small routes.php file for clarity. Frameworks later replace this with routers and middlewares.

Templating Approaches

Templates separate presentation from logic. You can use plain PHP as a templating language, or bring in engines such as Twig or Plates for stricter separation. Each template receives data variables and produces HTML.

Embedding PHP directly within HTML

The simplest template is an HTML file with PHP echo blocks for data output. Escape variables to prevent injection.

<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title><?= htmlspecialchars($title) ?></title></head>
<body>
  <h1><?= htmlspecialchars($heading) ?></h1>
  <p><?= htmlspecialchars($content) ?></p>
</body>
</html>

Rendering partials and layout wrappers

To reuse headers and footers, define layout templates and include content blocks. Capture output with ob_start and ob_get_clean.

<?php
// render.php
function render(string $view, array $data = []): string {
  extract($data, EXTR_SKIP);
  ob_start();
  require __DIR__ . "/views/$view.php";
  return ob_get_clean();
}

// usage
echo render('page', ['title' => 'Home', 'heading' => 'Welcome', 'content' => 'Hello world']);
⚠️ Never pass unfiltered user input directly into templates. Escape for HTML, attributes, or URLs as appropriate.

Simple MVC in Plain PHP

The Model View Controller pattern organizes code into three roles: Models manage data, Views present it, and Controllers coordinate between them. You can implement this manually with small files and clear conventions.

Defining models as lightweight data classes

Models can simply be classes that query or represent data. This example uses PDO to fetch a record safely.

<?php
// src/models/Post.php
class Post {
  public static function find(PDO $db, int $id): ?array {
    $stmt = $db->prepare('SELECT * FROM posts WHERE id = ?');
    $stmt->execute([$id]);
    return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
  }
}

Connecting controller actions to models and views

A controller loads a model, decides what to show, and calls a template renderer. Keep business logic minimal inside controllers.

<?php
// src/controllers/show_post.php
require_once __DIR__ . '/../models/Post.php';
require_once __DIR__ . '/../render.php';

$db = new PDO('sqlite:' . __DIR__ . '/../data/blog.db');
$id = (int) ($_GET['id'] ?? 0);
$post = Post::find($db, $id);

if (!$post) {
  http_response_code(404);
  echo 'Not found';
  exit;
}

echo render('post', ['title' => $post['title'], 'content' => $post['body']]);
💡 Even small MVC setups benefit from consistent naming like models/, views/, and controllers/ directories.

State and CSRF Protection

Maintaining user state safely is critical. Always regenerate session identifiers after login and include anti CSRF tokens in forms that change data.

Embedding a CSRF token in forms

Generate a token per session, include it as a hidden field, and verify it on submission. Use random bytes for cryptographic safety.

<?php
session_start();
if (empty($_SESSION['csrf'])) {
  $_SESSION['csrf'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['csrf'];
?>

<form method="post">
  <input type="hidden" name="csrf" value="<?= htmlspecialchars($token) ?>">
  <button>Submit</button>
</form>

Verifying and rotating CSRF tokens

Before processing any modifying request, compare the provided token with the stored value. On logout, destroy the session completely.

<?php
session_start();
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
  http_response_code(403);
  exit('CSRF validation failed');
}
⚠️ Protect GET routes by design: do not use them for changes. Only POST, PUT, PATCH, and DELETE should alter data.

Pagination and Sorting

When showing large data sets, limit results per page and let users navigate through pages. Combine this with ordering for a user friendly list view.

Limiting results and calculating offsets

Use LIMIT and OFFSET in SQL with parameters. Derive current page and page size safely from query input.

<?php
$page = max(1, filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, ['options' => ['default' => 1]]));
$per  = 10;
$offset = ($page - 1) * $per;

$stmt = $db->prepare('SELECT * FROM posts ORDER BY created DESC LIMIT :limit OFFSET :offset');
$stmt->bindValue(':limit', $per, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$posts = $stmt->fetchAll(PDO::FETCH_ASSOC);

Creating navigation links

Build next and previous links dynamically. Use htmlspecialchars for URLs and mark the active page clearly.

<nav>
  <a href="?page=<?= $page - 1 ?>">Previous</a>
  <span>Page <?= $page ?></span>
  <a href="?page=<?= $page + 1 ?>">Next</a>
</nav>
💡 For stable ordering, include a unique column such as id in your ORDER BY clause. This avoids duplicate or missing rows between pages.

Building a Small CRUD App

CRUD stands for Create, Read, Update, and Delete—the four basic operations for persistent data. Using the building blocks from earlier sections, you can assemble a simple but functional mini application.

Structuring directories and dependencies

Keep files grouped logically and include a bootstrap that sets up configuration and database access.

app/
  public/
    index.php
  src/
    models/
    controllers/
    views/
  data/
    blog.db

Implementing create and read actions

Use prepared statements for inserts, and display records with proper escaping.

<?php
// create_post.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  $title = trim($_POST['title'] ?? '');
  $body  = trim($_POST['body'] ?? '');
  $stmt = $db->prepare('INSERT INTO posts (title, body) VALUES (?, ?)');
  $stmt->execute([$title, $body]);
  header('Location: /');
  exit;
}
<?php
// list_posts.php
$stmt = $db->query('SELECT id, title FROM posts ORDER BY id DESC');
$posts = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<ul>
  <?php foreach ($posts as $p): ?>
    <li><a href="/post.php?id=<?= $p['id'] ?>"><?= htmlspecialchars($p['title']) ?></a></li>
  <?php endforeach ?>
</ul>

Updating and deleting safely

Include CSRF protection and use parameterized statements for updates and deletes. Confirm destructive actions before executing them.

<?php
// update_post.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  $id = (int)$_POST['id'];
  $title = trim($_POST['title']);
  $body = trim($_POST['body']);
  $stmt = $db->prepare('UPDATE posts SET title = ?, body = ? WHERE id = ?');
  $stmt->execute([$title, $body, $id]);
  header('Location: /post.php?id=' . $id);
  exit;
}
💡 Even this minimal CRUD structure mirrors the architecture of many frameworks. Once these fundamentals are clear, adopting tools such as Laravel or Slim feels natural.
<?php
// delete_post.php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['id'])) {
  $id = (int) $_POST['id'];
  $stmt = $db->prepare('DELETE FROM posts WHERE id = ?');
  $stmt->execute([$id]);
  header('Location: /');
  exit;
}
⚠️ Always restrict delete operations to authenticated users. Validate ownership or roles before allowing destructive requests.

Chapter 16: Database Access with PDO

PHP Data Objects (PDO) provides a consistent interface for working with many databases using the same set of classes and methods. You write one set of calls that target MySQL, PostgreSQL, SQLite, SQL Server, and more, then switch by altering the Data Source Name (DSN) and a few options. This chapter explains connecting, preparing queries, handling transactions, fetching rows, dealing with errors, and organizing a small repository layer.

Connecting and DSNs

A PDO connection requires a DSN string, a username, a password, and often an array of options. The DSN identifies the driver and connection parameters. You can store credentials securely outside your document root, or inject them via environment variables.

Understanding DSN formats for common drivers

Each driver has its own DSN shape. The following examples show typical forms. Replace host, port, file paths, and database names with values that match your environment.

// MySQL or MariaDB
$dsn = 'mysql:host=localhost;port=3306;dbname=example;charset=utf8mb4';

// PostgreSQL
$dsn = 'pgsql:host=localhost;port=5432;dbname=example';

// SQLite (file)
$dsn = 'sqlite:/path/to/database.sqlite';

// SQL Server (sqlsrv)
$dsn = 'sqlsrv:Server=localhost,1433;Database=example';
⚠️ Make sure you have the correct PDO driver installed and enabled for your target database (for example, pdo_mysql, pdo_pgsql, pdo_sqlite). If a driver is missing, the constructor will fail, and you will receive a useful exception if you enable exception mode.

Creating a robust connection using options

Options can improve safety and performance. Use exceptions for errors, set a sensible default fetch mode, and request native prepared statements when appropriate.

$options = [
  PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
  PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  PDO::ATTR_EMULATE_PREPARES   => false
];

$pdo = new PDO($dsn, $user, $pass, $options);
💡 Environment variables help keep secrets out of source control. Read $_ENV['DB_USER'], $_ENV['DB_PASS'], and so on, then assemble your DSN at runtime.

Prepared Statements and Binding

Prepared statements separate query structure from data, reducing the risk of SQL injection and improving performance when you execute the same statement many times. You can bind by position or by name, and you can specify parameter types to help the driver cast values correctly.

Preparing queries and executing with placeholders

Placeholders keep user data out of the SQL text. You prepare once, then execute with values that match the placeholders.

// Positional placeholders
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ? AND active = ?');
$stmt->execute([$email, 1]);
$rows = $stmt->fetchAll();

// Named placeholders
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND active = :active');
$stmt->execute([':email' => $email, ':active' => 1]);

Binding values with explicit PDO::PARAM_* types

Explicit types reduce ambiguity when the driver converts PHP values to SQL values. Use bindValue() for immediate values, and bindParam() when you want to bind a variable by reference that may change before execution.

$stmt = $pdo->prepare('INSERT INTO logs (user_id, message, created_at) VALUES (:uid, :msg, :ts)');
$stmt->bindValue(':uid', $userId, PDO::PARAM_INT);
$stmt->bindValue(':msg', $message, PDO::PARAM_STR);
$stmt->bindValue(':ts', $timestamp, PDO::PARAM_STR);
$stmt->execute();
⚠️ When using booleans, some engines store them as integers. Bind with PDO::PARAM_BOOL or convert to 0 or 1 to match your schema consistently.

Executing many rows efficiently

For repetitive inserts or updates, keep the statement prepared and call execute() repeatedly inside a transaction for better throughput.

$pdo->beginTransaction();
$stmt = $pdo->prepare('INSERT INTO visits (user_id, path) VALUES (?, ?)');
foreach ($batch as [$uid, $path]) {
  $stmt->execute([$uid, $path]);
}
$pdo->commit();

Transactions and Isolation

Transactions group multiple statements into a single unit of work. If any step fails, the whole group can be rolled back. Isolation levels control how concurrent transactions interact, which affects consistency and performance.

Starting, committing, and rolling back

Use the trio of beginTransaction(), commit(), and rollBack() to protect critical sequences. Wrap the work in a try and ensure rollback occurs on failure.

try {
  $pdo->beginTransaction();

  $pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')
      ->execute([$amount, $fromId]);

  $pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')
      ->execute([$amount, $toId]);

  $pdo->commit();
} catch (Throwable $e) {
  if ($pdo->inTransaction()) {
    $pdo->rollBack();
  }
  // Re-throw or log the error, then handle it upstream
  throw $e;
}

Selecting an isolation level with SQL

PDO does not set isolation levels directly. You issue an SQL command that your engine understands, then start the transaction. Choose a level that balances correctness with concurrency needs.

// Example: PostgreSQL or SQL Server style
$pdo->exec('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');
$pdo->beginTransaction();
// perform work...
$pdo->commit();
💡 Many workloads do well with READ COMMITTED. Use stricter levels such as REPEATABLE READ or SERIALIZABLE when phantom reads or write skew would cause incorrect results.
⚠️ Isolation syntax varies by engine. MySQL uses SET SESSION TRANSACTION ISOLATION LEVEL …, PostgreSQL uses SET TRANSACTION ISOLATION LEVEL …, SQLite emulates levels with write locks, and actual guarantees differ. Check your database documentation when correctness is critical.

Fetching Modes and Iteration

PDO can return rows in many shapes. Associative arrays are convenient for most PHP code, numeric arrays are compact, and objects can be useful for mapping to simple data carriers or DTOs. Choose a default fetch mode that fits, then override per statement when needed.

Selecting a suitable PDO::FETCH_* mode

The following table summarizes common fetch modes. You can set a default on the connection, then change per statement with setFetchMode() or by passing the mode to fetch().

Mode Description
PDO::FETCH_ASSOC Returns an associative array keyed by column names, convenient and readable.
PDO::FETCH_NUM Returns a numeric array keyed by column position, compact but less explicit.
PDO::FETCH_BOTH Returns both associative and numeric keys, useful when you need either form.
PDO::FETCH_OBJ Returns an anonymous object with properties named after columns.
PDO::FETCH_CLASS Maps rows into instances of a class, optionally calling the constructor.
PDO::FETCH_COLUMN Returns a single column from the next row, handy for one-column queries.

Iterating rows efficiently

Use a forward loop that calls fetch() repeatedly to keep memory usage low. For large exports, stream rows rather than loading everything at once.

$stmt = $pdo->query('SELECT id, email FROM users ORDER BY id');
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
  // process $row['id'], $row['email']
}

Fetching into classes with PDO::FETCH_CLASS

Mapping directly to a simple class can reduce array indexing noise in your code. For complex domains, prefer an explicit mapper or a repository that constructs entities carefully.

class User {
  public int $id;
  public string $email;
}

$stmt = $pdo->query('SELECT id, email FROM users');
$stmt->setFetchMode(PDO::FETCH_CLASS, User::class);
foreach ($stmt as $user) {
  // $user is a User instance
}

Error Modes and Exceptions

PDO supports several error modes. Exception mode pairs well with structured error handling and keeps your control flow clear. Silent and warning modes can hide problems or spread checks across your codebase, which increases the chance of missed failures.

Choosing an error mode with PDO::ATTR_ERRMODE

Set the error mode on the connection, then rely on exceptions to signal failure. The summary table helps you pick a strategy that matches your application style.

Error Mode Behavior
PDO::ERRMODE_EXCEPTION Throws PDOException on errors, recommended for most apps.
PDO::ERRMODE_WARNING Emits a PHP warning, then continues. You must inspect return values.
PDO::ERRMODE_SILENT Sets errors on the statement object only. You have to check codes manually.

Handling PDOException safely

Catch exceptions at boundaries where you can recover or translate them into user-friendly messages. Log the details, avoid exposing SQL text to end users, and ensure transactions are closed properly.

try {
  $stmt = $pdo->prepare('SELECT * FROM orders WHERE id = :id');
  $stmt->execute([':id' => $orderId]);
  $order = $stmt->fetch();
  if (!$order) {
    // handle not found
  }
} catch (PDOException $e) {
  // log $e->getMessage() securely
  http_response_code(500);
  echo 'Database error occurred.';
}
💡 Normalize database errors into a small set of application error codes. Your web layer can map those codes to HTTP status responses and concise messages for users.

Lightweight Repository Pattern

A small repository module centralizes SQL and reduces duplication. The repository receives a PDO instance, exposes intent-focused methods, and returns arrays or simple objects. Keep it light, keep it testable, and avoid hiding SQL so deeply that performance tuning becomes difficult.

Defining a minimal repository API

The class below wraps a user table with a few operations. It uses prepared statements, typed parameters, and concise return shapes. Expand the pattern gradually rather than inventing a complex abstraction early.

final class UserRepository
{
  public function __construct(private PDO $pdo) {}

  public function findById(int $id): ?array
  {
    $stmt = $this->pdo->prepare('SELECT id, email, active FROM users WHERE id = :id');
    $stmt->execute([':id' => $id]);
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    return $row ?: null;
  }

  public function findActiveByEmail(string $email): ?array
  {
    $stmt = $this->pdo->prepare(
      'SELECT id, email, active FROM users WHERE email = :email AND active = 1'
    );
    $stmt->execute([':email' => $email]);
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    return $row ?: null;
  }

  public function create(string $email): int
  {
    $stmt = $this->pdo->prepare('INSERT INTO users (email, active) VALUES (:email, 1)');
    $stmt->bindValue(':email', $email, PDO::PARAM_STR);
    $stmt->execute();
    return (int)$this->pdo->lastInsertId();
  }

  public function deactivate(int $id): void
  {
    $stmt = $this->pdo->prepare('UPDATE users SET active = 0 WHERE id = :id');
    $stmt->bindValue(':id', $id, PDO::PARAM_INT);
    $stmt->execute();
  }
}
⚠️ Keep SQL visible and close to the repository boundaries. If you hide it behind many layers, troubleshooting slow queries becomes harder, and adding an index later becomes guesswork.

Integrating the repository in application code

Construct the repository once per request or per container scope, then call the intent methods. When a unit of work involves many changes, bracket the operations in a transaction for atomic updates.

$repo = new UserRepository($pdo);

try {
  $pdo->beginTransaction();

  $id = $repo->create('new.user@example.com');
  $user = $repo->findById($id);

  if ($user && !$user['active']) {
    // handle inactive user …
  }

  $pdo->commit();
} catch (Throwable $e) {
  if ($pdo->inTransaction()) {
    $pdo->rollBack();
  }
  // translate and log the error here
}
💡 Add a thin interface for repositories only when you need multiple implementations (for example, a fake or in-memory version for tests). Avoid adding abstractions until a concrete need appears.

Chapter 17: Security Fundamentals

Security in PHP is about careful data handling, conservative defaults, and consistent defense in depth. You will validate and sanitize inputs, you will escape outputs, you will parameterize queries, and you will protect state with secure sessions and headers. This chapter walks through practical techniques you can apply in every project, with compact examples that you can adapt and reuse.

💡 Start from a deny by default mindset, then explicitly allow only what you need. Fewer features and simpler code often produce fewer vulnerabilities.

Validating and Sanitizing Input

Every external value is untrusted, including values from $_GET, $_POST, $_COOKIE, $_FILES, php://input, and JSON bodies. Validation checks whether a value conforms to your rules. Sanitizing transforms the value into a safe representation. Perform validation first, then sanitize for the destination where the value will be used.

Using filter_var() and filter_input() for strict validation

The filter_* functions provide fast, readable validators for common data shapes. You can require an email address, a url, an integer range, or a boolean flag. Craft specific rules and reject everything else.

// Integer id in range
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT, [
  'options' => ['min_range' => 1, 'max_range' => 1000000]
]);
if ($id === false) {
  http_response_code(400);
  exit('Invalid id');
}

// Email with dns check
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
if ($email === false) {
  exit('Invalid email');
}

Normalizing and trimming user input safely

After validation you may normalize whitespace, normalize Unicode, or convert locale specific formats. Keep normalization simple. Trimming and collapsing repeated spaces are often enough for names or free text.

$name = trim((string)($_POST['name'] ?? ''));
$name = preg_replace('/\s{2,}/u', ' ', $name);

Allowlisting with regular expressions

For custom formats prefer allowlists. Write a pattern that strictly defines what is accepted. Reject partial matches and ambiguous forms. Anchor your pattern at the start and end.

// Usernames: letters, digits, underscore, length 3 to 24
$username = $_POST['username'] ?? '';
if (!preg_match('/^[A-Za-z0-9_]{3,24}$/', $username)) {
  exit('Invalid username');
}
⚠️ Do not rely on client side checks. Browsers can skip or alter them. Always repeat validation on the server.

Sanitizing free text with strip_tags() and htmlspecialchars()

When storing or echoing free text you can strip tags to remove markup, then escape on output. Store the raw text if you need full fidelity, then escape for each context later. If you only permit a small set of tags pass an allowlist to strip_tags().

$raw = (string)($_POST['comment'] ?? '');
$clean = strip_tags($raw, '<b><i><code>');  // allow a few tags
// Store $clean, then escape when rendering in HTML

Escaping Output and XSS

Cross site scripting occurs when untrusted input appears in HTML without correct escaping. Escape at the final moment for the specific sink where the value lands, such as HTML text, attribute values, JavaScript strings, CSS strings, or URLs. The correct escaping function depends on the context.

Escaping for HTML text with htmlspecialchars()

For plain HTML text nodes convert special characters. Use ENT_QUOTES and UTF-8.

$safe = htmlspecialchars($clean, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo '<p>' . $safe . '</p>';

Escaping for attributes and URLs

Attributes need HTML escaping plus careful quoting. When building links, validate the scheme and use rawurlencode() for query values. Prevent javascript or data schemes unless you intend to support them.

$q = rawurlencode((string)($_GET['q'] ?? ''));
$url = '/search?q=' . $q;  // server validated path
echo '<a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '">Search</a>';

Safely embedding data in JavaScript

Do not concatenate untrusted strings into inline scripts. Prefer JSON encoding and then read the value. This prevents breaking out of string or script contexts.

$data = ['user' => $username, 'prefs' => ['theme' => 'light']];
$json = json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
?>
<script>
  const APP_DATA = JSON.parse('<?= $json ?>');
</script>
<?php
💡 Add a Content Security Policy to limit where scripts and resources can load from. Even if a bug slips in, CSP can reduce impact.

SQL Injection and Parameterization

SQL injection happens when untrusted data is mixed directly into SQL strings. The fix is to use prepared statements with bound parameters. Never concatenate user input into a query. Use placeholders and let the driver handle encoding and quoting.

Using PDO prepared statements with ? placeholders

Prepared statements separate the SQL plan from the data. Bind values by position or by name. The driver transmits values safely to the database.

$pdo = new PDO($dsn, $user, $pass, [
  PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
  PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

$stmt = $pdo->prepare('SELECT id, email FROM users WHERE email = ?');
$stmt->execute([$_POST['email'] ?? '']);
$user = $stmt->fetch();

Using named parameters for clarity

Named parameters make longer queries easier to read and maintain. Bind by array or with bindValue().

$stmt = $pdo->prepare('
  INSERT INTO posts (user_id, title, body)
  VALUES (:uid, :title, :body)
');
$stmt->execute([
  ':uid' => $userId,
  ':title' => $title,
  ':body' => $body,
]);

Avoiding dynamic identifiers and building safe allowlists

Placeholders only work for values. They do not work for table or column names. If you must switch tables or sort columns from input, map user choices to a fixed allowlist and interpolate only known safe strings.

$allowedSort = ['created_at', 'title', 'id'];
$sort = in_array($_GET['sort'] ?? '', $allowedSort, true) ? $_GET['sort'] : 'created_at';
$sql = "SELECT id, title FROM posts ORDER BY $sort DESC";
⚠️ Escaping values with addslashes() is not sufficient. Use prepared statements for all untrusted data in queries.

Password Hashing and Sessions

Never store passwords in plain text. Hash with strong algorithms, store the hash, and verify on login. Protect session identifiers and rotate them at key moments. Limit session lifetime and scope with cookie attributes.

Hashing with password_hash() and verifying with password_verify()

password_hash() selects a safe default algorithm and salt strategy. Store the returned string. Later call password_verify() to check a login attempt.

// Creating a user
$hash = password_hash($password, PASSWORD_DEFAULT);

// Authenticating
if (password_verify($passwordAttempt, $hash)) {
  // ok
} else {
  // fail
}

Rehashing when algorithm options change with password_needs_rehash()

When PHP updates defaults or you raise the cost, a legacy hash can be upgraded on the next successful login. Check and rehash transparently.

$options = ['cost' => 12];
if (password_needs_rehash($hash, PASSWORD_DEFAULT, $options)) {
  $hash = password_hash($passwordAttempt, PASSWORD_DEFAULT, $options);
  // save new $hash
}

Starting secure sessions and rotating identifiers

Enable strict mode and use secure cookie attributes. Regenerate the session id after login to prevent fixation. Do not store sensitive secrets in the session store.

session_set_cookie_params([
  'secure' => true,
  'httponly' => true,
  'samesite' => 'Lax',  // or 'Strict' for same site only flows
]);
ini_set('session.use_strict_mode', '1');

session_start();

// After successful login
session_regenerate_id(true);
$_SESSION['uid'] = $user['id'];
💡 Consider shorter session lifetimes for admin areas. Idle timeouts reduce risk from stolen tokens.

CSRF, Clickjacking, and Headers

Cross site request forgery tricks a browser into sending a valid request with the victim’s cookies. Defend with same site cookies, anti CSRF tokens, and method design. Clickjacking overlays a trusted page inside an invisible frame. Defend with framing controls and a Content Security Policy.

Protecting forms with synchronizer tokens

Generate a per session or per form token, store it server side, and include it in a hidden field. On submit verify equality and timing safety.

// Generate once per session
if (empty($_SESSION['csrf'])) {
  $_SESSION['csrf'] = bin2hex(random_bytes(32));
}
?>
<form method="post" action="/transfer">
  <input type="hidden" name="csrf" value="<?= htmlspecialchars($_SESSION['csrf'], ENT_QUOTES, 'UTF-8') ?>">
  <!-- other fields -->
</form>
<?php
// On POST
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
  http_response_code(403);
  exit('Invalid CSRF token');
}

Setting cookie attributes to limit cross site sends

SameSite reduces ambient cross site cookie sends. Pair this with Secure and HttpOnly. For sensitive operations pick SameSite=Strict where your flow permits it.

setcookie('session', $sid, [
  'secure' => true,
  'httponly' => true,
  'samesite' => 'Lax'
]);

Blocking frames and defining a Content Security Policy

Prevent clickjacking with X-Frame-Options or CSP frame-ancestors. A minimal CSP can also restrict scripts and styles to your own origin.

header('X-Frame-Options: DENY'); // legacy protection
header("Content-Security-Policy: default-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'");
⚠️ Test CSP in report only mode first: Content-Security-Policy-Report-Only: …. A strict policy can break pages until you add the correct sources.

Secure Configuration and Secrets

Security depends on runtime configuration as much as code. Hide stack traces from users, log errors privately, keep secrets out of the repository, and restrict file system access. Load configuration from the environment at startup and avoid dynamic changes during requests.

Configuring production error handling

Do not display errors to end users. Send errors to logs. Keep logs outside the web root. Review logs regularly and aggregate them in your observability stack.

ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', '/var/log/app/error.log');
error_reporting(E_ALL);

Managing secrets with environment variables and rotation

Store keys in environment variables or a secret store. Rotate credentials on a schedule. Never commit secrets to version control. Restrict read access with file permissions.

$dbDsn = getenv('APP_DB_DSN');
$dbUser = getenv('APP_DB_USER');
$dbPass = getenv('APP_DB_PASS');

$pdo = new PDO($dbDsn, $dbUser, $dbPass, [
  PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);

Generating cryptographic randomness with random_bytes()

Use the built in CSPRNG for tokens and keys. Convert to printable strings with bin2hex() or base64_encode(). Do not use mt_rand() for security decisions.

$token = bin2hex(random_bytes(32));  // 64 hex characters

Hardening the HTTP surface with minimal, explicit headers

Reduce information leakage and enforce safe browser behavior. Set HSTS after you serve over HTTPS reliably. Remove server banners where possible.

HeaderPurposeExample
Strict-Transport-SecurityForces HTTPS for a periodmax-age=31536000; includeSubDomains
Referrer-PolicyLimits referrer datano-referrer-when-downgrade
Permissions-PolicyControls powerful featuresgeolocation=()
X-Content-Type-OptionsDisables MIME sniffingnosniff
💡 Keep a small bootstrap that sets headers, session params, and error handling in one place. Centralized defaults cut repetition and reduce mistakes.

Separating build artifacts from the web root

Place vendor libraries, configuration files, and writable directories outside the document root. Expose only the public front controller, static assets, and nothing else. Deny direct access to backups and temporary files with server rules.

/var/www/app/
  public/  # only this is web accessible
  src/
  vendor/
  var/
  .env

Strong validation, careful escaping, parameterized queries, protected sessions, and disciplined configuration form a cohesive baseline. Apply these habits consistently and your applications will be much harder to exploit while remaining clear and maintainable.

Chapter 18: Testing, Debugging, and Quality

Quality comes from short feedback loops and repeatable checks. You will use Xdebug for interactive debugging, configure helpful php.ini options, write unit tests with PHPUnit, isolate collaborators with test doubles, catch issues earlier using static analysis, enforce a consistent style with PSR-12, and automate everything in continuous integration.

💡 Keep a fast local feedback loop: run a focused test, fix the line, run again. Speed encourages frequent checks and raises overall quality.

Xdebug and Useful php.ini Settings

Xdebug adds step debugging, stack traces, code coverage, and better error information. Pair it with strict runtime settings to catch problems early. Configure it per environment so development has rich detail while production stays minimal and safe.

Installing and enabling Xdebug

Install the extension and enable it in your php.ini or a dedicated ini file. Verify with php -v or php -m.

; xdebug.ini
zend_extension=xdebug
xdebug.mode=develop,debug,coverage
xdebug.start_with_request=yes
xdebug.discover_client_host=true
xdebug.client_port=9003
; For CLI scripts you can keep it manual:
; xdebug.start_with_request=trigger

Connecting your IDE and debugging requests

Modern IDEs listen on port 9003. Use a browser extension or query string trigger to start a session. For CLI, export variables before running commands.

# CLI one-off debug session
XDEBUG_MODE=debug XDEBUG_CONFIG="client_host=127.0.0.1" php script.php

Strengthening php.ini for development safety

Turn on strict reporting, show errors in development only, and use sane resource limits. In production send errors to logs and hide details.

; Development ini snippets
error_reporting=E_ALL
display_errors=1
display_startup_errors=1
log_errors=1
memory_limit=512M
zend.assertions=1
assert.exception=1
⚠️ Keep a separate production config where display_errors=0 and sensitive details are never shown to end users.

PHPUnit and Test Structure

PHPUnit is the standard unit testing framework for PHP. Organize tests to mirror your src/ tree, keep tests independent, and run them in isolation. Favour small deterministic tests and name them clearly so failures point at intent.

Adding phpunit to the project

Install as a development dependency and create a basic configuration file. Composer scripts make the command short to type.

# composer.json (excerpt)
{
  "require-dev": {
    "phpunit/phpunit": "^11.0"
  },
  "scripts": {
    "test": "phpunit --colors=always"
  }
}
<!-- phpunit.xml -->
<phpunit colors="true" bootstrap="vendor/autoload.php">
  <testsuites>
    <testsuite name="unit">
      <directory>tests/Unit</directory>
    </testsuite>
    <testsuite name="integration">
      <directory>tests/Integration</directory>
    </testsuite>
  </testsuites>
  <coverage cacheDirectory="var/.coverage" processUncoveredFiles="true">
    <include><directory suffix=".php">src</directory></include>
  </coverage>
</phpunit>

Writing a simple unit test

Create tests that express behavior. Arrange the inputs, act by calling the method, assert the outcome. Keep one expectation per logical behavior.

// tests/Unit/PriceCalculatorTest.php
use PHPUnit\Framework\TestCase;
use App\Domain\PriceCalculator;

final class PriceCalculatorTest extends TestCase
{
  public function testCalculatingWithTax(): void
  {
    $calc = new PriceCalculator(0.2);
    $total = $calc->total(100.00);
    $this->assertSame(120.00, $total);
  }
}

Structuring the test directories

Mirror src/ for discoverability and faster navigation. Group helpers in tests/Support and reuse builders or fixtures carefully.

project/
  src/
    Domain/PriceCalculator.php
  tests/
    Unit/PriceCalculatorTest.php
    Integration/...
    Support/Builders/...
💡 Run tests with --testdox to get readable behavior summaries that double as living documentation.

Mocking and Test Doubles

Test doubles replace real collaborators during tests. Use them to isolate the unit and to model error paths. Choose the lightest double that satisfies your scenario so tests stay clear and maintainable.

Choosing among stubs, mocks, spies, and fakes

Each double solves a different need. A stub returns canned data. A mock asserts that calls happen. A spy records calls for later assertions. A fake is a lightweight working implementation that is suitable for tests.

DoublePurposeTypical use
StubReturn fixed valuesService returns predictable data
MockVerify interactionsEnsure a notifier is called once
SpyRecord calls for laterCapture parameters without strict setup
FakeIn-memory alternativeIn-memory repository instead of a database

Creating mocks with PHPUnit

Use createMock() to configure behavior and expectations. Keep interaction assertions minimal and focused on observable effects.

$mailer = $this->createMock(App\Service\Mailer::class);
$mailer->expects($this->once())
       ->method('send')
       ->with($this->equalTo('user@example.com'), $this->stringContains('Welcome'));

$service = new App\Service\SignupService($mailer);
$service->signup('user@example.com');

Using fakes to speed up integration tests

Replace slow external systems with in-memory fakes for repeatable tests. Keep interfaces identical so production code swaps implementations without changes.

final class InMemoryUserRepo implements UserRepo
{
  /** @var array<int,User> */
  private array $items = [];

  public function save(User $user): void { $this->items[$user->id] = $user; }

  public function find(int $id): ?User { return $this->items[$id] ?? null; }
}
⚠️ Do not over-mock. When too many interactions are asserted, refactoring becomes painful and tests stop describing behavior.

Static Analysis with PHPStan and Psalm

Static analyzers reason about code without running it. They catch missing types, dead code, incorrect calls, and impossible paths. Start at a lenient level then raise strictness as the codebase improves.

Adding phpstan with a minimal config

Install the tool, configure paths, and set an initial level. Level 0 is loose while higher levels are stricter.

# composer.json (excerpt)
{
  "require-dev": {
    "phpstan/phpstan": "^1.11"
  },
  "scripts": {
    "stan": "phpstan analyse --no-progress"
  }
}
# phpstan.neon
parameters:
  level: 5
  paths:
    - src
  tmpDir: var/.phpstan

Initializing psalm for deeper checks

Psalm can complement PHPStan or run alone. Initialize it to generate a baseline and then remove entries as you fix issues.

# composer require --dev vimeo/psalm
vendor/bin/psalm --init
vendor/bin/psalm --no-cache

Adding precise types and suppressing safely

Prefer native type hints and return types. If a false positive blocks progress, document the intent and add a narrow suppression, then schedule removal.

/** @param array<string,int> $map */
function total(array $map): int {
  return array_sum($map);
}
💡 Raise analysis levels one notch after each cleanup. Small steady steps keep the team moving without friction.

Code Style and PSR-12

Consistent formatting reduces cognitive load and noise in diffs. Adopt PSR-12 and enforce it with a tool so style debates end quickly and results stay uniform.

Checking style with phpcs

PHP_CodeSniffer verifies files against coding standards. Add a Composer script and fail the build when violations occur.

# composer.json (excerpt)
{
  "require-dev": {
    "squizlabs/php_codesniffer": "^3.10"
  },
  "scripts": {
    "cs": "phpcs --standard=PSR12 src",
    "cs:fix": "phpcbf --standard=PSR12 src"
  }
}

Formatting with PHP-CS-Fixer

For auto-formatting, use friendsofphp/php-cs-fixer. Configure rules close to PSR-12 and let the tool rewrite files.

// .php-cs-fixer.php
<?php

$finder = PhpCsFixer\Finder::create()->in(['src', 'tests']);

return (new PhpCsFixer\Config())
  ->setRiskyAllowed(true)
  ->setRules([
    '@PSR12' => true,
    'array_syntax' => ['syntax' => 'short'],
    'ordered_imports' => true,
  ])
  ->setFinder($finder);
⚠️ Do not run multiple formatters on the same files in the same step. Pick one tool for rewriting and one for checking to avoid conflicts.

CI Basics for PHP Projects

Continuous integration runs your checks on every push. Keep the pipeline simple: install dependencies, cache Composer, run static analysis, run tests, and upload coverage. A short pipeline encourages frequent commits and faster review.

GitHub Actions workflow with matrix builds

Use a small workflow that tests multiple PHP versions, caches dependencies, and runs your quality tools. Keep steps explicit so failures are obvious.

# .github/workflows/ci.yml
name: ci

on:
  push:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php: ['8.2', '8.3', '8.4']
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          coverage: xdebug
          tools: composer:v2
      - name: Cache Composer
        uses: actions/cache@v4
        with:
          path: vendor
          key: composer-${{ hashFiles('**/composer.lock') }}
      - run: composer install --no-interaction --prefer-dist
      - run: composer run stan
      - run: composer run cs
      - run: composer run test
      - name: Upload coverage artifact
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: coverage-${{ matrix.php }}
          path: var/.coverage/**/*

GitLab CI and generic runners

The same pattern works on other platforms. Use official images, cache Composer, and run the same scripts so local and CI behavior match.

# .gitlab-ci.yml
stages: [build, test]

variables:
  COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.composer-cache"

cache:
  paths:
    - vendor/
    - .composer-cache/

test:
  image: php:8.3-cli
  stage: test
  before_script:
    - apt-get update && apt-get install -y git unzip libzip-dev
    - docker-php-ext-install zip
    - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
    - composer install --no-interaction --prefer-dist
  script:
    - composer run stan
    - composer run cs
    - composer run test

Failing fast and keeping pipelines healthy

Order steps so cheap checks fail first. Static analysis and style checks run before tests. Keep a stable cache key based on composer.lock for speed. Treat a failing CI status as a blocker for merges.

💡 Add a short pre-push hook that runs a subset of checks locally. Fast feedback avoids wasting CI minutes and reduces iteration cycles.

With a debugger, strict runtime settings, dependable tests, precise analysis, consistent style, and an automated pipeline, your project gains confidence and velocity. Small issues surface earlier and big defects become rare.

Chapter 19: Performance and Deployment

Performance comes from efficient code, well tuned configuration, and good caching at every level. Deployment builds on that foundation by providing repeatable, isolated environments and clear monitoring. This chapter shows how to configure OPcache, profile bottlenecks, introduce caching layers, prepare production settings, containerize PHP, and observe your system in real time.

💡 Measure before optimizing. Guesswork often wastes effort, but profiling focuses attention where it matters.

OPcache and Realpath Cache

PHP normally parses and compiles every script on each request. OPcache stores compiled bytecode in memory to remove this overhead. Realpath cache remembers file path resolutions so repeated includes avoid filesystem lookups.

Enabling and tuning OPcache

Enable OPcache in php.ini and adjust its limits for your project size. Increase the memory size if you have many scripts and raise interned strings buffer for large frameworks.

; opcache.ini
zend_extension=opcache
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0
opcache.save_comments=1

Disabling validate_timestamps avoids checking file modification times on each request. Reload the service after deployment instead.

Clearing OPcache safely during deployments

When you deploy new code, invalidate OPcache so the next request reloads scripts. You can call opcache_reset() in a maintenance script or reload the PHP-FPM pool.

// clear_opcache.php
if (function_exists('opcache_reset')) {
  opcache_reset();
}
⚠️ Never clear OPcache on every request. That removes the benefit entirely and can even slow performance compared to no cache at all.

Tuning the realpath cache

The realpath cache remembers directory lookups and symbolic links. Set larger limits for big frameworks with deep include trees.

; php.ini
realpath_cache_size=4096K
realpath_cache_ttl=600

Profiling and Bottlenecks

Profiling shows where time and memory are consumed. Collect data under realistic loads and use tools that visualize call graphs and memory allocation. Optimize high impact paths, not random code.

Using Xdebug profiler for local analysis

Xdebug can record function call times into cachegrind files viewable with tools like QCacheGrind or Webgrind. Enable only when profiling, since it adds overhead.

; php.ini
xdebug.mode=profile
xdebug.output_dir=/tmp/profiles

Using xhprof and tideways_xhprof

For lower overhead profiling, use xhprof or tideways_xhprof. They sample call times with minimal impact, making them suitable for staging or short bursts in production.

xhprof_enable();
... // your app logic
$data = xhprof_disable();
file_put_contents('/tmp/profile.json', json_encode($data));

Reading call graphs and focusing optimizations

Call graphs reveal which functions dominate runtime. Optimize those first: reduce I/O, cache repeated computations, and precompute constants. Micro-optimizations like string concatenation order rarely help compared to reducing query counts or external API calls.

💡 Measure again after each change. Only commit optimizations that produce measurable wins under realistic conditions.

Caching Layers

Caching removes redundant work. PHP projects typically layer caches: opcode, data, application, and HTTP. Each layer shortens a different path between request and result.

Using APCu for in-memory data caching

APCu stores simple key-value pairs in memory for one process pool. It suits shared configuration, computed templates, or small results reused often.

if (apcu_exists('config')) {
  $config = apcu_fetch('config');
} else {
  $config = load_config_from_db();
  apcu_store('config', $config, 300);
}

Adding shared caches with Redis or Memcached

Distributed caches work across processes or servers. They reduce load on databases and improve response times for frequent reads.

$redis = new Redis();
$redis->connect('redis', 6379);

$key = 'user:' . $id;
if ($cached = $redis->get($key)) {
  return unserialize($cached);
}

$user = $repo->find($id);
$redis->setex($key, 600, serialize($user));

Leveraging HTTP and reverse proxies

Static and dynamic pages can benefit from edge caching. Set response headers like Cache-Control, ETag, and Last-Modified. Reverse proxies such as Varnish or Cloudflare cache responses and serve them directly when valid.

header('Cache-Control: public, max-age=300');
header('ETag: "abc123"');
⚠️ Never cache personalized content without proper user segmentation. Separate caches per session or authorization token.

Configuration for Production

Production configuration prioritizes stability, predictable performance, and controlled logging. Keep detailed error output off, isolate logs, and disable modules you do not use.

Setting secure and consistent defaults

Disable features not needed in production and restrict file uploads. Enable OPcache, set timeouts, and avoid high memory use per process.

; production.ini
display_errors=0
log_errors=1
error_log=/var/log/php/error.log
expose_php=0
file_uploads=1
upload_max_filesize=10M
post_max_size=12M
max_execution_time=30
max_input_time=60
memory_limit=256M
opcache.enable=1

Separating configuration by environment

Store different .ini sets or use environment variables. The phpinfo() page can confirm which files are active but never leave it public on production servers.

💡 Keep configuration under version control but exclude secrets. Combine base settings with small environment overrides.

Docker and Containerized PHP

Containers isolate dependencies and environment settings, ensuring consistent behavior from laptop to production. Docker allows reproducible builds and simplified scaling when combined with orchestration tools.

Creating a minimal PHP-FPM image

Start with an official base image and install only the extensions your app needs. Use multi-stage builds to keep the final image small.

# Dockerfile
FROM php:8.3-fpm-alpine AS base

RUN docker-php-ext-install pdo_mysql opcache

WORKDIR /var/www/html
COPY . .

FROM base AS prod
ENV APP_ENV=production
CMD ["php-fpm"]

Running with Nginx using Docker Compose

Compose files define multiple services and their networks. Connect php-fpm with Nginx through a shared volume and network.

# docker-compose.yml
version: '3.9'
services:
  app:
    build: .
    volumes:
      - .:/var/www/html
    networks:
      - backend

  web:
    image: nginx:1.27-alpine
    volumes:
      - .:/var/www/html
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    ports:
      - "8080:80"
    networks:
      - backend

networks:
  backend:

Configuring PHP-FPM and health checks

Expose health endpoints and tune process settings. For small sites pm = dynamic works well; for APIs under load consider pm = static with fixed workers.

; www.conf
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.status_path = /status
⚠️ Never build secrets into the image. Pass them as environment variables or use a secrets manager at runtime.

Observability and Error Monitoring

Observability turns unknowns into data. Instrument logs, metrics, and traces so you can detect issues before users report them. Choose lightweight libraries that integrate with your framework and centralize outputs.

Structured logging with Monolog

Structured logs include context, timestamp, and level. Use handlers to send logs to files, syslog, or external services. Format logs as JSON for ingestion by tools such as ELK or Loki.

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$log = new Logger('app');
$log->pushHandler(new StreamHandler(__DIR__ . '/../var/app.log', Logger::INFO));

$log->info('User login', ['user' => $id]);

Adding metrics and health endpoints

Expose application metrics in a simple format like Prometheus text. Track request duration, error counts, and cache hits. Add a lightweight /health route that returns 200 OK when dependencies respond.

header('Content-Type: text/plain');
echo "requests_total 1234\n";
echo "errors_total 12\n";

Using error monitoring services

Tools like Sentry or Bugsnag capture uncaught exceptions and contextual data. Install their SDKs and initialize early in your bootstrap.

Sentry\init(['dsn' => getenv('SENTRY_DSN')]);

try {
  // app code
} catch (Throwable $e) {
  Sentry\captureException($e);
  throw $e;
}
💡 Collect metrics, traces, and logs in one dashboard. Triangulating across them shortens investigation time and clarifies system behavior.

Efficient caching, tuned configuration, reliable containers, and thorough observability yield fast and predictable deployments. Combined with testing and security practices from earlier chapters, these complete the core of a robust PHP production stack.

Chapter 20: Modern PHP Features

PHP continues to evolve into a modern, expressive language with robust typing, metadata, asynchronous patterns, and safer defaults. This chapter highlights major features introduced from PHP 8 onward, including attributes, enums, readonly constructs, fibers, callable improvements, and key deprecations you should prepare for when upgrading.

💡 Modern PHP emphasizes explicitness. Type declarations, attributes, and readonly modifiers all make your intent clearer to both humans and tools.

Attributes and Metadata

Attributes provide native structured metadata for classes, functions, methods, parameters, and properties. They replace docblock annotations and are available at runtime through reflection.

Declaring and applying attributes

Define an attribute as a class marked with #[Attribute]. Apply it with square brackets to targets. Attributes can carry constructor arguments for configuration.

<?php
use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
class Route
{
  public function __construct(
    public string $path,
    public array $methods = ['GET']
  ) {}
}

#[Route('/users', methods: ['GET', 'POST'])]
class UserController
{
  // ...
}

Reading attributes with reflection

Use ReflectionClass, ReflectionMethod, or ReflectionProperty to inspect applied attributes and their arguments.

$ref = new ReflectionClass(UserController::class);
foreach ($ref->getAttributes(Route::class) as $attr) {
  $route = $attr->newInstance();
  echo $route->path; // /users
}
⚠️ Avoid mixing attributes and docblock annotations for the same purpose. Pick one convention and use it consistently.

Enums and Backed Enums

Enums introduce a fixed set of named values. They improve clarity, prevent invalid strings, and simplify comparison logic. A backed enum associates each case with a scalar value, typically a string or int.

Defining pure enums

Pure enums have only symbolic names. Use them for small finite sets like states or modes.

enum Status
{
  case Draft;
  case Published;
  case Archived;
}

Using backed enums with scalar values

Backed enums carry a built-in scalar that can map to storage values such as database fields.

enum Role: string
{
  case User = 'user';
  case Admin = 'admin';
}

$role = Role::from('admin');  // Role::Admin
echo $role->value;            // admin

Enumerating and switching over cases

You can iterate over all enum cases or match specific ones with match expressions.

foreach (Role::cases() as $r) {
  echo $r->name . "\n";
}

$message = match ($role) {
  Role::Admin => 'Welcome admin',
  Role::User => 'Hello user',
};
💡 When storing enums in databases, use backed enums with value accessors to keep schema compatible with plain strings.

Readonly Classes and Properties

Readonly properties prevent modification after initialization. They make objects immutable by contract. A readonly class extends this guarantee to all properties.

Using readonly properties

Mark properties as readonly to restrict assignment after construction. Attempting to reassign triggers an error.

class Point
{
  public function __construct(
    public readonly int $x,
    public readonly int $y,
  ) {}
}

$p = new Point(3, 4);
// $p->x = 5;  // Error: Cannot modify readonly property

Declaring fully readonly classes

A readonly class implicitly applies the modifier to every property. This helps define pure value objects with minimal boilerplate.

readonly class Dimensions
{
  public function __construct(
    public int $width,
    public int $height,
  ) {}
}
⚠️ Readonly objects protect state, not deep immutability. If a property holds a mutable object, that object can still change internally.

Fibers and Cooperative Concurrency

Fibers introduce lightweight execution contexts that pause and resume. They allow non-blocking frameworks to structure asynchronous code without callbacks or promises. Each fiber runs until it yields control back to the scheduler.

Creating and resuming a fiber

Fibers wrap a callable. Use start() to begin execution and resume() to continue after yielding.

$fiber = new Fiber(function (): void {
  echo "Start\n";
  $value = Fiber::suspend('pause');
  echo "Resumed with: $value\n";
});

$result = $fiber->start();  // Start
echo "Fiber returned: $result\n";
$fiber->resume('done');     // Resumed with: done

Scheduling fibers for cooperative tasks

Frameworks such as amphp or ReactPHP use fibers to build asynchronous servers. Fibers enable readable, sequential style while remaining non-blocking underneath.

💡 Fibers do not provide parallelism. They let code yield during I/O so other tasks can progress cooperatively.

First-class Callables and Improvements

PHP 8.1 introduced first-class callable syntax and improved Closure behavior. This simplifies passing methods or functions as variables without using call_user_func().

Creating first-class callables

Use the ... syntax to reference a function or method directly. The result is a Closure you can store or call later.

function greet(string $name): string {
  return "Hello, $name";
}

$fn = greet(...);
echo $fn('World');  // Hello, World

Referencing object and static methods

You can obtain callables for instance or static methods using the same syntax.

class Printer
{
  public function echoLine(string $msg): void { echo $msg . "\n"; }
  public static function shout(string $msg): void { echo strtoupper($msg); }
}

$p = new Printer();
$instanceFn = $p->echoLine(...);
$staticFn = Printer::shout(...);

$instanceFn('hello');
$staticFn('attention');

Using Closure::fromCallable() for dynamic references

Closure::fromCallable() safely converts a callable array or string into a closure when syntax sugar is not possible.

$callable = [new Printer(), 'echoLine'];
$closure = Closure::fromCallable($callable);
$closure('Dynamic call');
⚠️ When passing methods as callables, be careful with scope and late static binding. Always test within the context you intend to execute.

Deprecations and Migration Tips

With modernization, PHP deprecates older behaviors to promote consistency and safety. Staying up to date avoids surprises when upgrading major versions.

Common deprecated patterns

Recent releases have deprecated dynamic property creation, mbstring.func_overload, and implicit float to int conversions. These changes encourage explicit definitions and stricter typing.

class Legacy {
  // public $newProp;  // define explicitly
}

$l = new Legacy();
$l->extra = 123;  // Deprecated: dynamic property creation

Adapting code for stricter types

Gradually add scalar and return types. Where compatibility is uncertain, use union types or mixed. Use declare(strict_types=1) at the top of new files for predictable behavior.

declare(strict_types=1);

function add(int|float $a, int|float $b): float {
  return $a + $b;
}

Planning upgrades and testing on multiple versions

Use tools like rector, phpcompatibility/php-compatibility, or phpstan to check readiness. Run your test suite against new PHP versions early to catch regressions before production.

💡 Read each release’s migration guide carefully. Many deprecations emit notices long before removal, giving you time to adapt incrementally.

Modern PHP favors safety, readability, and predictability. By embracing attributes, enums, readonly design, cooperative concurrency, and first-class callables, you prepare your applications for current and future versions of the language with confidence.

Chapter 21: Building APIs

APIs let your PHP code communicate predictable facts to clients over HTTP. This chapter focuses on building small and reliable web APIs with plain PHP; you will see resource-oriented routes, correct status codes, JSON responses, security considerations, throttling, and lightweight documentation. You will also get a short introduction to GraphQL using community packages.

💡 Start with simple and explicit code paths; only factor to classes or frameworks when duplication or complexity appears.

RESTful Design with Plain PHP

RESTful design treats your API as a collection of resources that are addressed by URLs and manipulated with HTTP methods like GET, POST, PUT, and DELETE. A small front controller can route requests, validate input, call domain code, then emit an HTTP response with a body and headers.

Creating a tiny front controller for routing

A single index.php can parse the path and method; then dispatch to handlers in a simple map. Keep handlers pure where possible and return arrays for easy JSON encoding.

<?php
// public/index.php
declare(strict_types=1);

header('Content-Type: application/json; charset=utf-8');

$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$path   = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);

$routes = [
  'GET' => [
    '#^/v1/articles$#' => 'listArticles',
    '#^/v1/articles/(\d+)$#' => 'getArticle',
  ],
  'POST' => [
    '#^/v1/articles$#' => 'createArticle',
  ],
  'PUT' => [
    '#^/v1/articles/(\d+)$#' => 'updateArticle',
  ],
  'DELETE' => [
    '#^/v1/articles/(\d+)$#' => 'deleteArticle',
  ],
];

function respond(int $status, array $body = [], array $headers = []): void {
  http_response_code($status);
  foreach ($headers as $h => $v) header($h . ': ' . $v, true);
  echo json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
  exit;
}

// Parse JSON safely
function jsonInput(): array {
  $raw = file_get_contents('php://input') ?: '';
  $data = json_decode($raw, true);
  if (json_last_error() !== JSON_ERROR_NONE) {
    respond(400, ['error' => 'Invalid JSON']);
  }
  return is_array($data) ? $data : [];
}

// Handlers
function listArticles(): array {
  return ['data' => [['id' => 1, 'title' => 'Hello'], ['id' => 2, 'title' => 'World']]];
}
function getArticle(int $id): array {
  if ($id !== 1) respond(404, ['error' => 'Not found']);
  return ['data' => ['id' => 1, 'title' => 'Hello']];
}
function createArticle(): array {
  $payload = jsonInput();
  if (!isset($payload['title']) || $payload['title'] === '') {
    respond(422, ['error' => 'Title is required']);
  }
  return ['data' => ['id' => 3, 'title' => $payload['title']]];
}
function updateArticle(int $id): array {
  $payload = jsonInput();
  return ['data' => ['id' => $id, 'title' => $payload['title'] ?? 'Untitled']];
}
function deleteArticle(int $id): array {
  return ['meta' => ['deleted' => $id]];
}

// Dispatch
foreach ($routes[$method] ?? [] as $pattern => $handler) {
  if (preg_match($pattern, $path, $m)) {
    $args = array_map('intval', array_slice($m, 1));
    $result = $handler(...$args);
    $status = match ($method) {
      'POST' => 201,
      'DELETE' => 204,  // body will be ignored by most clients
      default => 200
    };
    respond($status, $method === 'DELETE' ? [] : $result);
  }
}
respond(404, ['error' => 'Route not found']);

Choosing resourceful URLs and status codes carefully

Use plural nouns for collections like /v1/articles; use identifiers for single resources like /v1/articles/123. Success paths return 200 for retrievals, 201 for creations, 204 for deletions, and 202 for accepted background work. Validation failures use 422; authentication uses 401; authorization uses 403; missing resources use 404.

⚠️ Do not tunnel actions through GET since many caches and crawlers may trigger those URLs. Mutations belong to POST, PUT, PATCH, or DELETE.

Content Negotiation and JSON

APIs should honor client preferences expressed by Accept and declare their own format with Content-Type. JSON is the default interchange format for many PHP services; you should set charset to UTF-8 and avoid surprising field name changes.

Reading Accept and writing Content-Type clearly

You can branch responses based on Accept; if the client asks for application/json send JSON. If the client requests a format you cannot produce return 406.

<?php
$accept = $_SERVER['HTTP_ACCEPT'] ?? 'application/json';
if (strpos($accept, 'application/json') === false) {
  http_response_code(406);
  header('Content-Type: application/json; charset=utf-8');
  echo json_encode(['error' => 'Not acceptable']);
  exit;
}
header('Content-Type: application/json; charset=utf-8');

Encoding JSON safely and predictably

Use JSON_UNESCAPED_UNICODE and JSON_UNESCAPED_SLASHES for readable output; add JSON_THROW_ON_ERROR when you want exceptions. Always check json_last_error when decoding input.

<?php
try {
  $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
  echo $json;
} catch (JsonException $e) {
  http_response_code(500);
  echo json_encode(['error' => 'Encoding failed']);
}
💡 Keep field names stable; when you must rename, keep both the old and new names for a deprecation window, then remove the old field in a new version like /v2.

Authentication and Tokens

Authentication identifies the caller; authorization checks what the caller may do. For simple APIs you can use static API keys; for broader integration use bearer tokens like JWT or opaque tokens stored server side. Keep secrets out of URLs since many systems log them.

Parsing Authorization: Bearer … tokens

Expect a header named Authorization with a scheme of Bearer followed by the token. Reject missing or malformed tokens with 401 and a WWW-Authenticate challenge.

<?php
function bearerToken(): string {
  $h = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
  if (!preg_match('/^Bearer\s+([A-Za-z0-9\-\._~\+\/]+=*)$/', $h, $m)) {
    header('WWW-Authenticate: Bearer');
    http_response_code(401);
    echo json_encode(['error' => 'Unauthorized']);
    exit;
  }
  return $m[1];
}

// Example verification with an opaque token store
function verifyToken(string $token): ?array {
  // Look up in your database or cache. Return user claims or null.
  // Example stub:
  if ($token === 'test-token-123') return ['sub' => 42, 'scope' => 'articles:read articles:write'];
  return null;
}

$token = bearerToken();
$claims = verifyToken($token);
if ($claims === null) {
  http_response_code(401);
  echo json_encode(['error' => 'Invalid token']);
  exit;
}

Choosing between API keys, JWT, and opaque tokens thoughtfully

API keys are simple for server to server calls; rotate keys regularly and scope them. JWT carry claims and can be verified without storage; rotate signing keys and keep lifetimes short. Opaque tokens are random strings that map to server side session data; they are easy to revoke instantly since the server controls validity.

⚠️ Never put secrets in GET query strings since proxies and browsers store history. Use headers or in some cases a request body over HTTPS.

Rate Limiting and Throttling

Rate limiting protects your service and your users by bounding how often clients may call endpoints. You can implement a fixed window counter, a sliding window log, or a token bucket. Choose an approach that fits your storage and your accuracy needs.

Implementing a fixed window counter with shared storage

A fixed window counts hits per key in a short interval like one minute. Use Redis or another fast store; fall back to the filesystem for demos only.

<?php
// Simple filesystem limiter for demos
function limitKey(string $identity, int $max, int $windowSeconds): bool {
  $bucket = sys_get_temp_dir() . '/rate_' . sha1($identity);
  $now = time();
  $window = intdiv($now, $windowSeconds);
  $path = $bucket . '_' . $window;
  $count = 0;
  if (file_exists($path)) {
    $count = (int)file_get_contents($path);
  }
  $count++;
  file_put_contents($path, (string)$count, LOCK_EX);
  return $count <= $max;
}

// Use IP or user id as the identity
$identity = ($_SERVER['REMOTE_ADDR'] ?? 'anon') . ':articles';
if (!limitKey($identity, 60, 60)) {
  header('Retry-After: 60');
  http_response_code(429);
  echo json_encode(['error' => 'Too many requests']);
  exit;
}

Designing response headers for clients

Expose remaining quota and reset times with headers like X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset so clients can back off without guessing.

💡 Separate limits by route groups; reads may need a higher budget than writes. Keep an emergency global circuit breaker that returns 503 when downstreams fail.

Documentation and OpenAPI

Good documentation reduces support cost and client bugs. OpenAPI describes your endpoints, parameters, and schemas in JSON or YAML so you can generate reference pages and tests. You can serve a static openapi.yaml next to your API and mount a simple viewer.

Authoring a minimal openapi.yaml for clarity

Start with version, info, servers, and a couple of paths. Add request and response schemas as they stabilize.

# openapi.yaml
openapi: 3.0.3
info:
  title: Articles API
  version: 1.0.0
servers:
  - url: https://api.example.com/v1
paths:
  /articles:
    get:
      summary: List articles
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Article' }
    post:
      summary: Create article
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/NewArticle' }
      responses:
        '201': { description: Created }
  /articles/{id}:
    get:
      summary: Get article
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      responses:
        '200': { description: OK }
components:
  schemas:
    Article:
      type: object
      properties:
        id: { type: integer }
        title: { type: string }
    NewArticle:
      type: object
      required: [title]
      properties:
        title: { type: string }

Serving interactive docs responsibly

You can host a self contained documentation page that fetches openapi.yaml from your server. Protect internal or preview specs behind authentication; do not leak private endpoints in public docs.

⚠️ Keep examples realistic and sanitized. Never embed production secrets or internal hostnames inside shared documentation.

GraphQL Basics with Community Tools

GraphQL exposes a single endpoint that accepts queries describing exactly which fields a client wants. Resolvers fetch data for fields and types. In PHP you can use community packages to parse the query and wire types to resolvers without writing a parser yourself.

Defining a schema and a resolver function

The following example uses a community library to execute a query with a simple resolver. The idea is the same with other libraries; define types and fields, then map each field to a function that returns data.

<?php
// Example with webonyx/graphql-php style APIs (pseudocode for clarity)
require __DIR__ . '/vendor/autoload.php';

use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\GraphQL;
use GraphQL\Schema\Schema;

$articleType = new ObjectType([
  'name' => 'Article',
  'fields' => [
    'id' => Type::nonNull(Type::int()),
    'title' => Type::nonNull(Type::string()),
  ],
]);

$queryType = new ObjectType([
  'name' => 'Query',
  'fields' => [
    'articles' => [
      'type' => Type::listOf($articleType),
      'resolve' => function () {
        return [['id' => 1, 'title' => 'Hello'], ['id' => 2, 'title' => 'World']];
      },
    ],
  ],
]);

$schema = new Schema(['query' => $queryType]);

$raw = file_get_contents('php://input') ?: '{}';
$input = json_decode($raw, true);

try {
  $result = GraphQL::executeQuery($schema, $input['query'] ?? '{ articles { id title } }');
  $output = $result->toArray();
} catch (Throwable $e) {
  http_response_code(400);
  $output = ['errors' => [['message' => 'Query failed']]];
}

header('Content-Type: application/json; charset=utf-8');
echo json_encode($output, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

Choosing GraphQL appropriately for client needs

GraphQL shines when clients need to combine several resources into one round trip or when different screens require different shapes of data. For uniform feeds and simple filters, classic REST with pagination is usually easier to cache and to secure.

💡 Whether you choose REST or GraphQL, write contract tests that hit real endpoints and assert status, headers, and a few key fields. Contract tests protect your clients from accidental breaking changes during refactors.

Chapter 22: Frameworks Overview

PHP has a healthy ecosystem of frameworks that help you deliver projects with predictable structure and batteries included. This chapter gives you a practical overview of Laravel, Symfony, and Slim; it also shows how to choose a framework, how to test inside each ecosystem, and how to migrate between them without rewriting everything at once.

💡 Treat a framework as infrastructure. Keep your core business rules in plain PHP so you can reuse them across console, queue workers, and web entry points.

Laravel: Routing, Eloquent, and Blade

Laravel provides batteries that fit together out of the box. You get routing, an expressive ORM named Eloquent, a templating system named Blade, queues, mail, caching, and standard project structure. The defaults encourage sensible conventions for modern applications.

Registering routes and controllers clearly

Routes map verbs and paths to controller actions. Small projects can use closures; larger projects use dedicated controllers so you can test actions in isolation.

// routes/web.php
use App\Http\Controllers\ArticleController;
use Illuminate\Support\Facades\Route;

Route::get('/articles', [ArticleController::class, 'index']);
Route::get('/articles/{id}', [ArticleController::class, 'show']);
Route::post('/articles', [ArticleController::class, 'store']);
// app/Http/Controllers/ArticleController.php
namespace App\Http\Controllers;

use App\Models\Article;
use Illuminate\Http\Request;

class ArticleController extends Controller
{
  public function index()
  {
    return view('articles.index', ['articles' => Article::latest()->paginate(10)]);
  }

  public function show(int $id)
  {
    $article = Article::findOrFail($id);
    return view('articles.show', compact('article'));
  }

  public function store(Request $request)
  {
    $data = $request->validate(['title' => 'required|string|max:255']);
    $article = Article::create($data);
    return redirect('/articles/' . $article->id);
  }
}

Modeling data with Eloquent conveniently

Eloquent models wrap tables and relationships. You define fillable fields and relationships; then you compose queries fluently. Under the hood it uses a query builder so you can drop to raw queries when needed.

// app/Models/Article.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
  protected $fillable = ['title', 'body'];

  public function author()
  {
    return $this->belongsTo(User::class);
  }
}

Rendering views with Blade templates

Blade templates compile to cached PHP. You use sections, layouts, and components to keep markup tidy while still writing plain HTML.

<!-- resources/views/articles/index.blade.php -->
@extends('layouts.app')

@section('content')
  <h1>Articles</h1>

  @foreach ($articles as $article)
    <article>
      <h2>{{ $article->title }}</h2>
      <p>{{ Str::limit($article->body, 120) }}</p>
    </article>
  @endforeach

  {{ $articles->links() }}
@endsection
⚠️ Keep validation and authorization in controllers or form requests, not in Blade. Templates should only present data that is already safe to render.

Symfony: Components and Flex

Symfony is a collection of well tested components and a full stack framework. You can assemble only what you need, or you can start with the standard skeleton that includes HTTP kernel, routing, dependency injection, console, and Twig for views. Flex streamlines installation and configuration through recipes.

Routing and controllers with attributes

Controllers are simple classes; methods receive the request and return a response. Attributes on methods make routes discoverable and local to the action.

// src/Controller/ArticleController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class ArticleController extends AbstractController
{
  #[Route('/articles', name: 'article_index', methods: ['GET'])]
  public function index(): Response
  {
    return $this->render('article/index.html.twig', ['articles' => []]);
  }
}

Composing with components and Flex recipes

You can install components like symfony/http-client, mailer, or messenger as needed. Flex recipes create configuration files and register services so you can focus on your code rather than wiring.

# composer.json snippet …
composer require symfony/orm-pack
composer require symfony/maker-bundle --dev
💡 Use maker commands for consistent scaffolding. Generators save time and ensure new classes are registered in the container correctly.

Slim and Microframework Patterns

Slim focuses on fast routing and middleware with minimal opinions. You bring your preferred template engine, ORM, and container. This works well for APIs and services that need a small surface area with predictable behavior.

Defining routes and middleware succinctly

Middleware wraps your application like layers of an onion. You can add error handlers, authentication, and JSON emission in small reusable functions.

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;

require __DIR__ . '/vendor/autoload.php';

$app = AppFactory::create();

$app->addBodyParsingMiddleware();

$app->get('/articles', function (Request $req, Response $res) {
  $payload = json_encode(['data' => [['id' => 1, 'title' => 'Hello']]]);
  $res->getBody()->write($payload);
  return $res->withHeader('Content-Type', 'application/json');
});

$app->run();

Wiring a container for services

Bring a PSR-11 container to manage services and configuration. This keeps construction logic in one place and makes testing easier.

use DI\Container;
use Slim\Factory\AppFactory;

$container = new Container();
$container->set('db.dsn', 'sqlite::memory:');
AppFactory::setContainer($container);

$app = AppFactory::create();
// resolve services via $this->get('db.dsn') inside route callables when needed
⚠️ Microframeworks do not restrict structure. Establish your own folders for domains, actions, and templates early so growth does not turn into a tangle.

Selecting a Framework

Choose based on team experience, project lifespan, ecosystem features, and operational needs. Laravel favors rapid delivery with strong defaults. Symfony favors explicit composition and long term maintainability. Slim favors small services where you assemble only what you need.

Comparing strengths at a glance

The following table summarizes common considerations for typical web projects. You can adapt these criteria to your own context.

Criterion Laravel Symfony Slim
Learning curve Gentle with opinionated defaults Moderate with explicit configuration Gentle if you already know PSR stack
Out of the box features Rich batteries included Powerful when you add packs Minimal core; you add pieces
Best fit Full stack apps and dashboards Large codebases and long support horizons APIs and microservices
Customization style Extend conventions Assemble components Pick libraries
💡 Run a one day spike in two candidates. Implement the same slice of functionality and compare the code review notes rather than personal preferences.

Testing Inside Frameworks

Frameworks integrate test runners, HTTP clients, and helpers so you can write fast unit tests and realistic feature tests. Aim for a pyramid with many unit tests and fewer end to end tests that hit a real database or HTTP boundary.

Writing Laravel feature tests with Pest or PHPUnit

Laravel ships with helpers for making requests and asserting responses. You can seed data and use in memory databases for speed where possible.

// tests/Feature/ArticleTest.php
it('lists articles', function () {
  \App\Models\Article::factory()->count(2)->create();
  $resp = $this->get('/articles');
  $resp->assertStatus(200)
       ->assertSee('Articles');
});

Using Symfony test client for HTTP assertions

Symfony provides a kernel test client that boots the container in a test environment. You can request URLs and assert status, headers, and content.

// tests/Controller/ArticleControllerTest.php
namespace App\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class ArticleControllerTest extends WebTestCase
{
  public function testIndex(): void
  {
    $client = static::createClient();
    $client->request('GET', '/articles');
    $this->assertResponseIsSuccessful();
    $this->assertSelectorExists('h1');
  }
}
⚠️ Do not couple tests to framework internals. Assert public behavior such as status codes and HTML text so refactors remain cheap.

Migration Between Frameworks

Migrations usually happen when requirements change or when teams consolidate stacks. Plan a route that reduces risk by keeping business logic independent from controllers and HTTP wiring. Adopt a strangler pattern that proxies some routes to the new system while the old system continues to serve the rest.

Extracting domain code into plain PHP first

Move services and use cases into framework agnostic classes that depend on interfaces. Adapters in each framework wire those interfaces to HTTP, CLI, or persistence. Once the domain is independent, swapping the web layer is smaller and safer.

// Domain service independent of framework
namespace Domain;

interface ArticleRepository {
  public function find(int $id): ?Article;
  public function save(Article $a): void;
}

final class PublishArticle {
  public function __construct(private ArticleRepository $repo) {}
  public function __invoke(array $input): int {
    // validate and persist
    // return new id
    return 1;
  }
}

Proxying routes during a staged cutover

Introduce an edge proxy that forwards selected paths to the new app. Keep cookies, sessions, and authentication unified at the edge so users do not notice the change while you move endpoints gradually.

💡 Keep an exit plan. Document how to roll back a route to the old system in one step if metrics or logs show problems after a switch.

Chapter 23: CLI Scripting and Automation

PHP is not only for web applications. It can also serve as a powerful scripting tool for command line automation, background jobs, and system tasks. With the same syntax and runtime you use for web code, you can create command line utilities that integrate with APIs, process data, or monitor systems.

💡 CLI scripts benefit from the same good practices as web code: clear input validation, predictable exits, and proper logging for visibility.

Reading Arguments and Options

When you run a PHP script from the terminal, command line arguments are made available through the $argv array. You can also use getopt() for more structured parsing of options and flags.

Accessing positional arguments

The first element of $argv is always the script name. The remaining elements are the arguments provided by the user. You can read them directly or shift them to handle sequential inputs.

<?php
// example: php greet.php Robin
if ($argc < 2) {
  fwrite(STDERR, "Usage: php greet.php <name>\n");
  exit(1);
}

$name = $argv[1];
echo "Hello, $name\n";

Parsing options with getopt()

The getopt() function supports short options like -v or long options like --verbose. Each option can require a value, have an optional value, or be a simple flag.

<?php
// php script.php -f data.txt --verbose
$options = getopt('f:v', ['file:', 'verbose']);

if (isset($options['file'])) {
  echo "File: {$options['file']}\n";
}
if (isset($options['verbose']) || isset($options['v'])) {
  echo "Verbose mode enabled\n";
}
⚠️ Always provide a usage message for unknown or invalid options to make scripts friendlier and easier to automate.

Environment Variables and Config

Environment variables are key to portable scripts. They let you keep secrets and configuration out of code. You can read them with getenv() or from the $_ENV array, and tools like vlucas/phpdotenv can help load them from .env files.

Loading variables safely

<?php
// php dotenv example
require __DIR__ . '/vendor/autoload.php';

Dotenv\Dotenv::createImmutable(__DIR__)->load();

$dbHost = getenv('DB_HOST') ?: 'localhost';
$dbUser = $_ENV['DB_USER'] ?? 'root';

echo "Connecting to $dbHost as $dbUser\n";
💡 Keep sensitive values like API keys in environment variables rather than in arguments, since process listings may expose arguments to other users.

Interactive Scripts

Interactive CLI scripts prompt the user for input and provide feedback. You can use readline() for typed input and fgets(STDIN) for basic prompts. Formatting output clearly helps guide the user through a task.

Prompting for input and confirmation

<?php
$name = readline("Enter your name: ");
$confirm = readline("Are you sure (y/n)? ");

if (strtolower($confirm) === 'y') {
  echo "Welcome, $name!\n";
} else {
  echo "Cancelled.\n";
}

Adding colors and formatting

You can use ANSI escape codes to color output in terminals that support it. This helps highlight warnings or success messages.

<?php
echo "\033[32mSuccess!\033[0m\n"; // green
echo "\033[31mError!\033[0m\n";   // red
⚠️ Avoid colored output in cron jobs or when redirecting to files since escape codes can clutter logs.

Task Runners and Scheduling

CLI scripts can act as reusable tasks that you run periodically or chain together. You can integrate them with cron, systemd timers, or queue systems. Laravel’s Artisan and Symfony’s Console component both provide structured task management.

Using Laravel Artisan commands

// app/Console/Commands/CleanTemp.php
namespace App\Console\Commands;

use Illuminate\Console\Command;

class CleanTemp extends Command
{
  protected $signature = 'cleanup:temp';
  protected $description = 'Remove temporary files older than one day';

  public function handle(): int
  {
    $count = count(glob(storage_path('temp/*')));
    $this->info("Deleting $count temp files...");
    return Command::SUCCESS;
  }
}

Scheduling with Laravel’s scheduler

// app/Console/Kernel.php
protected function schedule(Schedule $schedule): void
{
  $schedule->command('cleanup:temp')->dailyAt('01:00');
}
💡 Use cron only to trigger one scheduler entry point per project. Let the framework decide which tasks to run at each tick.

Packaging CLI Tools

You can distribute standalone CLI applications by packaging them as PHAR archives or Composer global packages. This makes installation and updates straightforward for other developers or servers.

Creating a PHAR archive

PHAR (PHP Archive) bundles your code and dependencies into a single executable file. You can create it manually or with box or phar-composer.

<?php
$phar = new Phar('tool.phar');
$phar->buildFromDirectory(__DIR__ . '/src');
$phar->setStub($phar->createDefaultStub('main.php'));

Installing with Composer globally

To share a command via Composer, define a bin entry in your composer.json. When installed globally, Composer will add a symlink in the user’s PATH.

{
  "name": "example/tool",
  "bin": ["bin/tool"]
}
⚠️ Keep global tools self-contained. Avoid reading project-specific config or dependencies unless the user explicitly opts in.

Long-running Workers

Long-running workers handle queues, background processing, and asynchronous tasks. PHP can maintain a process loop that polls for jobs or listens for messages, but you must watch memory leaks and handle signals properly to ensure stability.

Implementing a resilient job loop

<?php
declare(strict_types=1);

while (true) {
  $job = getNextJob(); // fetch from queue …
  if ($job) {
    try {
      handleJob($job);
    } catch (Throwable $e) {
      error_log('Job failed: ' . $e->getMessage());
    }
  }
  sleep(5); // adjust polling delay
}

Using system signals for graceful shutdown

Catch system signals like SIGTERM and SIGINT to stop workers cleanly during deployments or restarts. This prevents half-finished work and file corruption.

<?php
declare(ticks = 1);

$running = true;
pcntl_signal(SIGTERM, fn() => $GLOBALS['running'] = false);
pcntl_signal(SIGINT, fn() => $GLOBALS['running'] = false);

while ($running) {
  doWork();
  sleep(2);
}
echo "Worker stopped gracefully.\n";
💡 Monitor worker memory and restart when usage exceeds safe limits. Combine supervisors like systemd or supervisord with health checks for reliable uptime.

Chapter 24: Interoperability and Networking

Modern PHP applications often need to connect with other services, whether by HTTP, sockets, or message queues. This chapter explores how to build robust client connections, handle structured data like JSON, listen for inbound callbacks, and manage concurrent or queued work safely and efficiently.

💡 Networking code benefits from defensive design. Always validate inputs, set explicit timeouts, and handle transient failures gracefully rather than assuming ideal conditions.

HTTP Clients with curl and Guzzle

PHP provides the curl extension for low-level HTTP operations. For higher-level usage, libraries such as Guzzle build on top of curl and provide clean interfaces for requests, responses, middleware, and retries.

Making simple requests with curl

Using the built-in curl functions, you can send requests and read responses while configuring headers, methods, and timeouts.

<?php
$url = 'https://api.example.com/data';
$ch = curl_init($url);
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_TIMEOUT => 10,
  CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);
$response = curl_exec($ch);

if (curl_errno($ch)) {
  fwrite(STDERR, 'Request failed: ' . curl_error($ch) . "\n");
  exit(1);
}

$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "Status: $code\n";
echo "Body: $response\n";

Using Guzzle for structured HTTP clients

Guzzle simplifies requests, provides promise-based async methods, and integrates with PSR-7 and PSR-18 standards.

<?php
require __DIR__ . '/vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

$client = new Client(['base_uri' => 'https://api.example.com/']);

try {
  $response = $client->get('articles', ['query' => ['limit' => 3]]);
  $data = json_decode($response->getBody()->getContents(), true);
  print_r($data);
} catch (RequestException $e) {
  echo "HTTP error: " . $e->getMessage() . "\n";
}
⚠️ Always set both connection and response timeouts to prevent hanging processes when remote servers are slow or unresponsive.

Consuming and Producing JSON

JSON remains the lingua franca of web APIs. PHP’s json_encode() and json_decode() functions make serialization straightforward, but you must always validate and handle errors carefully.

Parsing incoming JSON safely

<?php
$raw = file_get_contents('php://input') ?: '';
$data = json_decode($raw, true);

if (json_last_error() !== JSON_ERROR_NONE) {
  http_response_code(400);
  echo json_encode(['error' => 'Invalid JSON']);
  exit;
}

echo "Received keys: " . implode(', ', array_keys($data)) . "\n";

Encoding JSON responses clearly

Use JSON_UNESCAPED_UNICODE and JSON_UNESCAPED_SLASHES for readability, and JSON_THROW_ON_ERROR to detect encoding failures via exceptions.

<?php
try {
  $payload = ['status' => 'ok', 'data' => ['id' => 1, 'title' => 'Example']];
  echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
  error_log('JSON encode failed: ' . $e->getMessage());
}
💡 Always define a stable schema for both requests and responses so clients know what fields to expect even as you evolve the API.

Webhooks and Callbacks

Webhooks let other systems notify your PHP application via HTTP. Your endpoint should verify the sender, validate input, and respond quickly to avoid timeouts on the sender side.

Handling webhook payloads predictably

<?php
// webhook.php
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$payload = file_get_contents('php://input') ?: '';

if (!hash_equals(hash_hmac('sha256', $payload, 'secret-key'), $signature)) {
  http_response_code(403);
  echo "Invalid signature";
  exit;
}

$data = json_decode($payload, true);
file_put_contents('webhook.log', date('c') . ' ' . json_encode($data) . "\n", FILE_APPEND);
http_response_code(202);
echo "Accepted";

Designing webhook reliability and retries

Many webhook providers retry failed requests. Keep your handler idempotent so repeated deliveries do not cause duplicates. Respond with 2xx codes only when you have safely processed or queued the event.

⚠️ Never trust webhook data blindly. Validate message integrity, check timestamps to avoid replay attacks, and limit payload sizes.

Sockets and Streams

Sockets let PHP scripts talk directly to TCP or UDP endpoints for custom protocols or lower-level services. Stream wrappers abstract many protocols into file-like interfaces so you can read or write with familiar functions.

Creating and using raw TCP sockets

<?php
$socket = stream_socket_client('tcp://example.com:80', $errno, $errstr, 5);
if (!$socket) {
  echo "Connect failed: $errstr\n";
  exit(1);
}

fwrite($socket, "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n");
while (!feof($socket)) {
  echo fgets($socket);
}
fclose($socket);

Using stream contexts for secure connections

You can control TLS verification and headers via stream contexts for both fopen() and file_get_contents().

<?php
$context = stream_context_create([
  'ssl' => ['verify_peer' => true, 'verify_peer_name' => true],
  'http' => ['header' => "User-Agent: PHPExample\r\n"]
]);

$data = file_get_contents('https://example.com/api', false, $context);
echo $data;
💡 Close streams explicitly and set timeouts to avoid resource leaks. In daemons, monitor descriptors and memory usage regularly.

Asynchronous Patterns with Fibers

Fibers, introduced in PHP 8.1, provide cooperative multitasking within a single thread. They allow you to suspend and resume blocks of code without complex callbacks or generators, enabling lightweight asynchronous execution.

Creating and resuming fibers

<?php
$fiber = new Fiber(function ($value) {
  echo "Fiber started with $value\n";
  $pause = Fiber::suspend('paused');
  echo "Resumed with $pause\n";
  return 'done';
});

$result = $fiber->start('init');
echo "Suspend returned: " . $result . "\n";
echo "Return value: " . $fiber->resume('resume-value') . "\n";

Combining fibers with event loops

Libraries like amphp and reactphp use fibers internally to implement non-blocking I/O. You can schedule multiple fibers to handle concurrent network tasks while the event loop waits on I/O efficiently.

⚠️ Fibers do not make blocking calls faster. You still need non-blocking I/O or event-driven libraries underneath to achieve true concurrency.

Message Queues and Jobs

Message queues decouple producers and consumers, letting you build scalable systems where each component processes tasks at its own pace. PHP can interact with brokers like Redis, RabbitMQ, or AWS SQS through their APIs or client libraries.

Publishing and consuming with Redis lists

<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

// Producer
$redis->rPush('jobs', json_encode(['task' => 'send_email', 'to' => 'user@example.com']));

// Consumer
while (true) {
  $job = $redis->blPop(['jobs'], 5);
  if ($job) {
    [$queue, $payload] = $job;
    $data = json_decode($payload, true);
    echo "Processing job: " . $data['task'] . "\n";
  }
}

Integrating with frameworks and external brokers

Laravel’s queue system and Symfony Messenger both abstract different backends behind common interfaces. You can push jobs, configure retries, and define failure handling consistently regardless of the broker implementation.

💡 Keep job payloads small and self-contained. Store large data in object storage and include only references in the queue message.


© 2025 Robin Nixon. All rights reserved

No content may be re-used, sold, given away, or used for training AI without express permission

Questions? Feedback? Get in touch