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.
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.
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.
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
- For macOS you can install MAMP from www.mamp.info/en/mac and use a similar process (create a project inside the
htdocsfolder). If you develop on both macOS and Windows you can also install MAMP for Windows (www.mamp.info/en/windows/). - For any platform you can use XAMPP (www.apachefriends.org) with Apache and MariaDB.
- For Windows you can also try Laragon (laragon.org).
- On Linux you can install native packages; for example
sudo apt install phporsudo dnf install phpthen enable FPM and your web server. If you only need a quick test you can use the built-in server that ships with PHP (covered later).
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>
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
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
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.
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.
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
*/
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.";
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.
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"];
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.
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.
=== 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";
__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.
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";
}
== 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;
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.
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>";
}
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);
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
=== 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);
}
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.
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
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)
=== 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
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;
$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.
| Operators | Associativity |
clone, new | Left |
** | Right |
!, ~, ++, --, +, - (unary) | Right |
*, /, % | Left |
+, -, . | Left |
<<, >> | Left |
<, <=, >, >= | Left |
==, !=, ===, !==, <=> | Left |
& | Left |
^ | Left |
| | Left |
&& | Left |
|| | Left |
?? | Right |
? : (ternary) | Left |
=, +=, -=, *=, /=, .=, %=, &=, |=, ^=, <<=, >>= | Right |
and | Left |
xor | Left |
or | Left |
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";
}
?>
$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";
}
?>
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"
?>
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;
}
?>
| Loop | Best use | Checks |
for | Counting with an index | Before each iteration |
while | Repeat until condition changes | Before each iteration |
do while | Run body at least once | After each iteration |
foreach | Collections and generators | Implicit over items |
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";
?>
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
| Aspect | match | switch |
| Kind | Expression that returns a value | Statement that executes blocks |
| Comparison | Strict === | Often loose; watch types |
| Exhaustiveness | Requires full coverage via default or arms | default optional |
| Fall through | Not allowed | Allowed; requires care |
| Best for | Mapping input to result | Running side effects |
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;
}
?>
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);
?>
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);
?>
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);
?>
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
?>
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;
}
?>
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;
?>
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";
?>
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);
?>
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");
?>
"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);
?>
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");
?>
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"
?>
+) 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);
?>
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
?>
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 . " ";
}
?>
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 . " ";
}
?>
Choosing between SPL and arrays
| Structure | Best for | Notes |
SplStack | LIFO operations | push / pop |
SplQueue | FIFO operations | enqueue / dequeue |
SplHeap | Priority ordering | min or max heaps |
SplFixedArray | Fixed-size numeric arrays | Lower 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";
?>
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
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.
| Visibility | Accessible From |
public | Anywhere |
protected | Class and subclasses |
private | Declaring 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
) {}
}
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;
}
}
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 Hook | Triggered 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 |
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
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));
}
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
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"
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" ]
}
}
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.
| Constraint | Meaning | Examples |
^1.4 | Allow non breaking updates within 1.x | >=1.4.0 <2.0.0 |
~1.4 | Allow 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.0 | Explicit range | Two sided bound |
dev-main | Track a branch | Unstable 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"
}
}
^ 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" }
]
}
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.
| Level | Description |
E_ERROR | Fatal error that stops execution |
E_WARNING | Non fatal issue that continues execution |
E_NOTICE | Informational message about questionable code |
E_DEPRECATED | Use of a feature that will be removed |
E_PARSE | Compile 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");
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();
}
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);
}
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");
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"]);
}
});
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
));
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;
}
}
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);
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);
}
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);
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";
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.
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.
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
'@…' 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";
}
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);
}
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);
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
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 |
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
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.
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);
}
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) ?? '';
$_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);
}
}
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');
}
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 |
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),
];
$_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.
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';
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']);
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']]);
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');
}
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>
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;
}
<?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;
}
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';
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);
$_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();
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();
READ COMMITTED. Use stricter levels such as REPEATABLE READ or SERIALIZABLE when phantom reads or write skew would cause incorrect results.
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.';
}
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();
}
}
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
}
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.
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');
}
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
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";
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'];
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'");
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.
| Header | Purpose | Example |
| Strict-Transport-Security | Forces HTTPS for a period | max-age=31536000; includeSubDomains |
| Referrer-Policy | Limits referrer data | no-referrer-when-downgrade |
| Permissions-Policy | Controls powerful features | geolocation=() |
| X-Content-Type-Options | Disables MIME sniffing | nosniff |
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.
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
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/...
--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.
| Double | Purpose | Typical use |
| Stub | Return fixed values | Service returns predictable data |
| Mock | Verify interactions | Ensure a notifier is called once |
| Spy | Record calls for later | Capture parameters without strict setup |
| Fake | In-memory alternative | In-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; }
}
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);
}
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);
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.
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.
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();
}
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.
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"');
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.
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
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;
}
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.
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
}
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',
};
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,
) {}
}
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.
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');
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.
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.
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.
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']);
}
/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.
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.
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.
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.
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.
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
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
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
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 |
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');
}
}
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.
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.
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";
}
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";
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
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');
}
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"]
}
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";
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.
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";
}
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());
}
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.
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;
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.
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.
© 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