Basics
Variables, Types & Constants
PHP variables start with $ and are dynamically typed. PHP 7.4+ supports typed properties. Use define() for runtime constants and const for compile-time constants (faster). PHP 8.1 introduced enums — a typed enumeration system superior to class constants. Always declare types where possible (PHP 7+) for early error detection and better IDE support.
<?php
$name = "Alice"; // string
$age = 30; // integer
$price = 19.99; // float (double)
$active = true; // boolean
$items = [1, 2, 3]; // array
$null = null; // null
// constants
define("MAX_USERS", 100); // runtime constant
const PI = 3.14159; // compile-time constant
echo MAX_USERS; // no $ prefix
// PHP 8.1+ enums
enum Status: string {
case Active = 'active';
case Inactive = 'inactive';
}
$s = Status::Active;Echo, Print & Debugging
echo is a language construct (not a function) — fastest for output. print returns 1 so can be used in expressions. printf/sprintf use C-style format specifiers (%s string, %d int, %f float, %x hex). var_dump() is the primary debugging tool — shows types and values. error_log() writes to the PHP error log or syslog. In production, never expose debug output to users.
<?php
// echo: no return, multiple args (fastest)
echo "Hello", " ", "World";
// print: returns 1, single arg
print "Hello World";
// printf: formatted output
printf("Name: %s, Age: %d, Price: %.2f", "Alice", 30, 19.99);
// debugging output
print_r($array); // human-readable
var_dump($variable); // type + value (detailed)
var_export($array, true); // valid PHP code (for caching)
// sprintf: return formatted string (don't print)
$log = sprintf("[%s] %s", date('H:i:s'), "Started");
error_log($log); // log to error logOperators & Comparisons
Always use === (strict comparison) to avoid type coercion bugs. == converts types before comparing, leading to surprising results (0 == 'abc' was true in PHP 7). The spaceship operator (<=>) returns -1/0/1 — useful for usort. The null coalescing operator (??) is the idiomatic way to provide defaults. The null-safe operator (?->) (PHP 8+) short-circuits method chains on null, replacing verbose isset() checks.
<?php
// arithmetic
$sum = 10 + 3; // 13
$mod = 10 % 3; // 1
$pow = 2 ** 3; // 8 (PHP 5.6+)
// comparison: == vs ===
echo (0 == "abc"); // true (loose, PHP 7-); false (PHP 8+)
echo (0 === "abc"); // false (strict — type + value)
echo ("1" == 1); // true (loose)
echo ("1" === 1); // false (strict)
// spaceship operator (PHP 7+)
echo 1 <=> 2; // -1 (less than)
echo 2 <=> 2; // 0 (equal)
echo 3 <=> 2; // 1 (greater than)
// null coalescing
$name = $input ?? "default"; // if $input is null
$deep = $data['user']['name'] ?? "Anonymous";
// null safe operator (PHP 8+)
$country = $user?->getAddress()?->country; // null if any step is nullSuperglobals & Web
Superglobals are built-in associative arrays available in all scopes. $_GET and $_POST contain user input — ALWAYS sanitize/validate before use. filter_input() is safer than direct access. Never trust $_SERVER values that can be spoofed by clients (like HTTP_USER_AGENT). Always call exit after header('Location:') — PHP continues executing otherwise. Start sessions with session_start() before any output.
<?php
// superglobals available everywhere
$_GET['name']; // query string params
$_POST['email']; // form POST data
$_REQUEST['x']; // GET + POST + COOKIE
$_SERVER['HTTP_HOST']; // server/env info
$_SERVER['REQUEST_METHOD']; // GET, POST, etc.
$_COOKIE['session']; // cookies
$_FILES['upload']; // file uploads
$_SESSION['user_id']; // session data (after session_start())
// get client IP
$ip = $_SERVER['REMOTE_ADDR'];
// check request method
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING);
}
// redirect
header("Location: /dashboard");
exit; // always exit after redirectInclude & Require
include/require execute the specified file. require causes a fatal error if the file is missing (use for critical dependencies); include only warns (use for optional templates). The _once variants track included files to prevent double-inclusion — essential for function/class definitions. Composer's autoloader (require_once 'vendor/autoload.php') eliminates manual include management. Files can return values, making them useful for configuration.
<?php
// include: warning on failure, continues
include 'header.php';
include_once 'config.php'; // only once
// require: fatal error on failure, stops
require 'database.php';
require_once 'vendor/autoload.php'; // Composer autoload
// _once variants prevent re-declaration errors
// use require for critical files (DB config, autoloaders)
// use include for optional files (templates)
// return values from included files
$config = include 'config.php';
// config.php: return ['db' => 'mysql://...', 'debug' => true];Strings
String Functions
PHP has 100+ string functions. strpos() returns false if not found — use === false to check (0 is a valid position). str_replace() can take arrays for search/replace. substr() supports negative offsets (from end). For multibyte strings (UTF-8), use mb_* equivalents (mb_strlen, mb_substr) — strlen counts bytes, not characters. Always set default_charset='UTF-8' and use mb_* functions for non-ASCII text.
<?php
$s = "Hello, World";
// length and case
echo strlen($s); // 12
echo str_word_count($s); // 2
echo strtoupper($s); // HELLO, WORLD
echo strtolower($s); // hello, world
echo ucfirst("hello"); // Hello
echo ucwords("hello world"); // Hello World
// search and replace
echo strpos($s, "World"); // 7 (false if not found)
echo str_replace("o", "0", $s); // Hell0, W0rld
echo substr($s, 0, 5); // Hello
echo substr($s, -5); // World
echo strrev($s); // dlroW ,olleH
// trim
echo trim(" hi "); // "hi"
echo ltrim(" hi"); // "hi"
echo rtrim("hi "); // "hi"String Interpolation & Heredoc
Double-quoted strings interpolate variables; single-quoted strings don't (use single quotes for literal text — slightly faster). Use {$var} for complex expressions (object properties, array access, method calls). Heredoc (<<<ID) is ideal for multi-line strings like SQL or HTML — it interpolates variables. Nowdoc (<<<'ID') is the non-interpreting version, useful for regex patterns with backslashes.
<?php
$name = "Alice";
$age = 30;
// double quotes: variable interpolation
echo "Hello, $name!"; // Hello, Alice!
echo "Hello, {$name}!"; // Hello, Alice! (braces for clarity)
echo "Age: {$age}"; // Age: 30
// single quotes: no interpolation (faster)
echo 'Hello, $name'; // Hello, $name (literal)
// complex expressions need braces
echo "Result: {$obj->method()}";
echo "Item: {$array['key']}";
// heredoc: multi-line, interpolates
$sql = <<<SQL
SELECT * FROM users
WHERE name = '$name'
AND age > $age
SQL;
// nowdoc: multi-line, NO interpolation (like single quotes)
$regex = <<<'REGEX'
\d{3}-\d{4}
REGEX;sprintf & Formatting
sprintf() is essential for building formatted strings safely — unlike string interpolation, it handles type conversions and padding. Use %d for integers (not %s) to ensure numeric formatting. number_format() formats numbers with thousands separators — critical for currency display. Always use sprintf for SQL fragments in prepared statements (though prepared statements are still required for user input).
<?php
// sprintf: format string (returns, doesn't print)
$formatted = sprintf(
"%-10s | %5d | %8.2f",
"Alice", 42, 19.99
);
echo $formatted; // "Alice | 42 | 19.99"
// format specifiers:
// %s string, %d int, %f float, %x hex, %b binary, %c char
// %5d right-padded to width 5
// %-5d left-padded
// %05d zero-padded
// %.2f 2 decimal places
// %8.2f width 8, 2 decimals
// numbered placeholders
echo sprintf("Hi %1$s, bye %1$s", "Alice"); // Hi Alice, bye Alice
// number_format
echo number_format(1234567.891, 2); // 1,234,567.89
echo number_format(1234567.89, 2, ',', '.'); // European formatRegex (PCRE)
PHP uses PCRE (Perl-Compatible Regular Expressions) with /pattern/ delimiters. preg_match returns 1 if matched, 0 if not (use ===, not ==, since 0 is falsy). Always validate user input with regex but don't rely on it alone — use filter_var() for emails, URLs. preg_replace is powerful but can be slow on large strings. Use [^...] to whitelist characters rather than blacklisting. The i flag makes matching case-insensitive.
<?php
// preg_match: test pattern (returns 0 or 1)
if (preg_match('/^[a-z]+$/i', "Hello")) {
echo "alphabetic";
}
// capture groups
preg_match('/(d{4})-(d{2})-(d{2})/', "2024-01-15", $matches);
// $matches[0] = "2024-01-15" (full match)
// $matches[1] = "2024" (group 1)
// $matches[2] = "01"
// $matches[3] = "15"
// preg_match_all: all matches
preg_match_all('/d+/', "a1 b22 c333", $matches);
// $matches[0] = ["1", "22", "333"]
// preg_replace: substitute
$clean = preg_replace('/[^a-z0-9]/i', '', "Hello, World!"); // HelloWorld
// preg_split: split by pattern
$parts = preg_split('/[s,]+/', "one, two,three four");
// ["one", "two", "three", "four"]Multibyte & Encoding
PHP's default string functions are byte-oriented, not character-oriented — they break on multibyte characters (UTF-8, Chinese, emoji). Always use mb_* functions (mb_strlen, mb_substr, mb_strpos, mb_strtoupper) for non-ASCII text. Set mb_internal_encoding('UTF-8') at the start of your application. Use JSON_UNESCAPED_UNICODE to keep Chinese/emoji readable in JSON output. This is a common source of bugs in international applications.
<?php
// UTF-8 strings: use mb_* functions
$text = "Café"; // 4 chars, 5 bytes (é = 2 bytes)
echo strlen($text); // 5 (bytes!)
echo mb_strlen($text); // 4 (characters!)
echo substr($text, 0, 3); // "Caf" (might break UTF-8!)
echo mb_substr($text, 0, 3); // "Caf" (safe)
// case conversion
echo strtoupper("straße"); // "STRAßE" (wrong!)
echo mb_strtoupper("straße"); // "STRAßE" (correct, locale-aware)
// encoding detection
$encoding = mb_detect_encoding($text);
$utf8 = mb_convert_encoding($text, 'UTF-8', 'auto');
// set internal encoding
mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');
// JSON with UTF-8
$json = json_encode($data, JSON_UNESCAPED_UNICODE);Arrays
Indexed & Associative Arrays
PHP arrays are actually ordered hash maps — they work as both lists and dictionaries. Indexed arrays auto-assign numeric keys; associative arrays use string keys. isset() returns false for null values; array_key_exists() returns true even for null. unset() removes an element but doesn't reindex. For a true list (no gaps), use array_values() to reindex after deletion. PHP 8.1+ has a readonly array type.
<?php
// indexed array (numeric keys)
$nums = [10, 20, 30];
$nums[] = 40; // append
echo $nums[0]; // 10
echo count($nums); // 4
// associative array (string keys)
$user = [
"name" => "Alice",
"age" => 30,
"email" => "[email protected]",
];
echo $user["name"]; // Alice
$user["phone"] = "555-1234"; // add key
// mixed keys
$mixed = [0 => "a", "name" => "b", 5 => "c"];
// check key existence
if (isset($user["email"])) { /* ... */ }
if (array_key_exists("name", $user)) { /* ... */ }
// remove element
unset($user["phone"]);Multidimensional & Iteration
Multidimensional arrays are arrays of arrays. foreach is the idiomatic way to iterate — it's faster and more readable than for loops. Use &$value to modify elements in place (always unset the reference after the loop to avoid bugs). array_column() extracts a single column from a 2D array — extremely useful for transforming database result sets. PHP arrays maintain insertion order.
<?php
$users = [
["name" => "Alice", "age" => 30],
["name" => "Bob", "age" => 25],
["name" => "Carol", "age" => 35],
];
// iterate with key + value
foreach ($users as $index => $user) {
echo "$index: {$user['name']} ({$user['age']})\n";
}
// modify by reference
foreach ($users as &$user) {
$user['age'] += 1; // increment each age
}
unset($user); // break reference!
// nested iteration
$matrix = [[1, 2], [3, 4], [5, 6]];
foreach ($matrix as $row) {
foreach ($row as $cell) {
echo $cell . " ";
}
echo "\n";
}
// extract column
$names = array_column($users, 'name'); // ["Alice", "Bob", "Carol"]Array Functions: map, filter, reduce
array_map, array_filter, and array_reduce are the functional programming trio for arrays. Arrow functions (fn() =>) make these concise. array_filter preserves keys — use array_values() to reindex if needed. array_merge reindexes numeric keys but preserves string keys (later values overwrite). array_column, array_chunk, and array_slice are essential for data manipulation. These functions are the backbone of data processing in PHP.
<?php
$nums = [1, 2, 3, 4, 5];
// map: transform each element
$doubled = array_map(fn($n) => $n * 2, $nums); // [2, 4, 6, 8, 10]
// filter: keep elements matching condition
$evens = array_filter($nums, fn($n) => $n % 2 === 0); // [2, 4]
// reduce: accumulate to single value
$sum = array_reduce($nums, fn($carry, $n) => $carry + $n, 0); // 15
// walk: like map but modifies in place (by reference)
array_walk($nums, fn(&$n) => $n *= 2);
// combining arrays
$merged = array_merge([1, 2], [3, 4]); // [1, 2, 3, 4]
$combined = array_combine(['a', 'b'], [1, 2]); // ['a' => 1, 'b' => 2]
$sliced = array_slice($nums, 1, 2); // [2, 3]
$chunked = array_chunk($nums, 2); // [[1,2], [3,4], [5]]Sorting Arrays
PHP sort functions modify the array in place (pass by reference). sort/rsort reindex; asort/arsort preserve keys. usort with a comparison function (using <=>) sorts by custom logic. natsort() does natural sorting (img2 before img10) — essential for filenames. For multidimensional arrays, use usort with a closure that compares the desired field. The spaceship operator (<=>) simplifies comparison functions.
<?php
$nums = [3, 1, 4, 1, 5, 9, 2, 6];
// sort by value (reindexes)
sort($nums); // [1, 1, 2, 3, 4, 5, 6, 9]
rsort($nums); // descending
// sort preserving keys
asort($nums); // ascending, preserve keys
arsort($nums); // descending, preserve keys
// sort by key
ksort($nums); // by key ascending
krsort($nums); // by key descending
// custom sort with callback
$users = [["name" => "Bob", "age" => 25], ["name" => "Alice", "age" => 30]];
usort($users, fn($a, $b) => $a['age'] <=> $b['age']);
// sorted by age ascending
// natural sort (for strings with numbers)
$files = ["img10.jpg", "img2.jpg", "img1.jpg"];
natsort($files); // ["img1.jpg", "img2.jpg", "img10.jpg"]
sort($files); // ["img1.jpg", "img10.jpg", "img2.jpg"] (wrong!)Array Inspection & Manipulation
in_array with strict=true (third param) prevents type coercion bugs. array_search returns the key (use === false to check). array_push/pop implement LIFO (stack); array_shift/unshift implement FIFO (queue) — but shift is O(n). For large queues, use SplQueue or SplDoublyLinkedList. array_unique preserves keys. array_diff/intersect compare values; use array_diff_key/intersect_key for key-based comparison.
<?php
$arr = [1, 2, 3, 4, 5];
// inspection
echo count($arr); // 5
echo in_array(3, $arr); // true
echo in_array("3", $arr, true); // false (strict)
echo array_search(3, $arr); // 2 (key, false if not found)
print_r(array_keys($arr)); // [0, 1, 2, 3, 4]
print_r(array_values($arr)); // [1, 2, 3, 4, 5]
// stack/queue operations
array_push($arr, 6); // push to end
$last = array_pop($arr); // pop from end
$first = array_shift($arr); // remove from front
array_unshift($arr, 0); // add to front
// set operations
$unique = array_unique([1, 2, 2, 3]); // [1, 2, 3]
$diff = array_diff([1, 2, 3], [2, 3, 4]); // [1] (in first, not second)
$intersect = array_intersect([1, 2, 3], [2, 3, 4]); // [2, 3]
// flip and reverse
$flipped = array_flip(['a' => 1, 'b' => 2]); // [1 => 'a', 2 => 'b']
$reversed = array_reverse([1, 2, 3]); // [3, 2, 1]Control Flow
If / Else / Elseif
PHP uses elseif (one word) — not 'else if' with a space (though that also works). The alternative syntax (if: ... endif;) is useful in HTML templates to avoid brace-matching confusion. The ternary operator is right-associative — avoid nesting. The null coalescing assignment operator (??=) sets a value only if it's currently null — perfect for lazy initialization of config defaults.
<?php
$score = 85;
if ($score >= 90) {
$grade = "A";
} elseif ($score >= 80) {
$grade = "B";
} elseif ($score >= 70) {
$grade = "C";
} else {
$grade = "F";
}
// alternative syntax (for templates)
if ($score >= 90):
echo "Excellent";
elseif ($score >= 80):
echo "Good";
else:
echo "Try harder";
endif;
// ternary
$status = $age >= 18 ? "adult" : "minor";
// null coalescing assignment (PHP 7.4+)
$config['timeout'] ??= 30; // set if not setSwitch & Match
switch uses loose comparison (==) and requires break to prevent fall-through — a common bug source. match (PHP 8+) uses strict comparison (===), returns a value directly, and throws an exception if no arm matches (no silent failures). match is the modern replacement for switch when you need a value. Use switch for complex multi-statement cases; use match for simple value selection. Always include a default case.
<?php
// switch: loose comparison (==)
$day = "Mon";
switch ($day) {
case "Mon":
case "Tue":
case "Wed":
echo "Weekday";
break;
case "Sat":
case "Sun":
echo "Weekend";
break;
default:
echo "Unknown";
}
// match (PHP 8+): strict comparison (===), returns value
$status = 404;
$message = match($status) {
200, 201 => "Success",
301, 302 => "Redirect",
404 => "Not Found",
500 => "Server Error",
default => "Unknown",
};
// match throws UnhandledMatchError if no match and no defaultLoops: for, while, foreach, do-while
foreach is the idiomatic loop for arrays — it's faster and safer than for with count(). Use continue to skip iterations and break to exit. PHP doesn't have labeled break/continue (unlike Java/Rust). For associative arrays, foreach ($arr as $key => $value) is the standard pattern. do-while runs at least once — useful for input validation. Avoid modifying the array during foreach (use a separate array for results).
<?php
// for loop
for ($i = 0; $i < 5; $i++) {
echo $i; // 01234
}
// foreach (most common in PHP)
$fruits = ["apple", "banana", "cherry"];
foreach ($fruits as $fruit) {
echo $fruit;
}
// foreach with key
foreach ($fruits as $index => $fruit) {
echo "$index: $fruit";
}
// while
$count = 0;
while ($count < 3) {
echo $count++;
}
// do-while (runs at least once)
do {
$line = readline("> ");
} while ($line !== "quit");
// break and continue
for ($i = 0; $i < 10; $i++) {
if ($i === 3) continue; // skip 3
if ($i === 7) break; // stop at 7
echo $i;
}Control Flow in Templates
PHP's alternative control syntax (if:/elseif:/else:/endif;, foreach:/endforeach;) is designed for HTML templates. <?= $var ?> is shorthand for <?php echo $var; ?> — always use it in templates for readability. Always escape output with htmlspecialchars() to prevent XSS. The separation of PHP logic and HTML presentation is the basis of templating systems like Twig and Blade, which offer cleaner syntax and automatic escaping.
<?php // template file ?>
<?php if ($user->isAdmin()): ?>
<div class="admin-panel">Admin Tools</div>
<?php elseif ($user->isEditor()): ?>
<div class="editor-tools">Edit Tools</div>
<?php else: ?>
<div class="user-view">Read Only</div>
<?php endif; ?>
<?php foreach ($products as $p): ?>
<div class="product">
<?= htmlspecialchars($p['name']) ?>
- $<?= number_format($p['price'], 2) ?>
</div>
<?php endforeach; ?>
<?php // shorthand echo ?>
<h1><?= $title ?></h1>
<?php // ternary in templates ?>
<span class="<?= $active ? 'on' : 'off' ?>"><?= $active ? 'Active' : 'Inactive' ?></span>Exceptions & Error Handling
PHP 7+ uses exceptions for most errors. Always catch specific exception types (not just Exception) to handle different failures appropriately. finally always executes — use for cleanup (closing files, connections). Custom exceptions extend Exception and add domain context. PHP 8+ allows catching multiple exception types with |. Set PDO to exception mode for consistent error handling. Never catch exceptions without logging — silent failures hide bugs.
<?php
// try / catch / finally
try {
$pdo = new PDO("mysql:host=localhost;dbname=test", "user", "pass");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
error_log("DB connection failed: " . $e->getMessage());
die("Service unavailable");
} finally {
// always runs, even after return/throw
echo "Cleanup";
}
// custom exception
class ValidationException extends Exception {
public function __construct(string $field, string $message = "") {
parent::__construct("$field: $message");
}
}
// throw
if (empty($email)) {
throw new ValidationException("email", "is required");
}
// catch multiple types (PHP 8+)
try {
riskyOperation();
} catch (PDOException | RuntimeException $e) {
// catches either type
log_error($e);
}Functions
Defining Functions
PHP 7+ supports typed parameters and return types (int, string, array, ?Type for nullable). PHP 8+ adds named arguments (skip defaults, reorder params), union types (int|string), and mixed type. Variadic params (...$nums) collect extra arguments into an array. The spread operator (...$arr) unpacks an array as arguments. Pass by reference (&) modifies the original — use sparingly as it makes code harder to reason about.
<?php
// basic function with return type
function add(int $a, int $b): int {
return $a + $b;
}
// default parameters
function greet(string $name, string $greeting = "Hello"): string {
return "$greeting, $name!";
}
// named arguments (PHP 8+)
echo greet(name: "Alice", greeting: "Hi");
// variadic functions
function sum(int ...$nums): int {
return array_sum($nums);
}
echo sum(1, 2, 3, 4); // 10
// spread operator
$nums = [1, 2, 3];
echo sum(...$nums); // 6
// pass by reference
function increment(int &$n): void {
$n++;
}
$x = 5;
increment($x);
echo $x; // 6Arrow Functions & Closures
Arrow functions (fn() =>) are concise, single-expression closures that automatically capture outer variables by value. Traditional closures (function() use ($var)) are needed for multi-line bodies or capturing by reference (&$var). Closures are essential for array_map, array_filter, usort, and event handlers. Arrow functions can't have statements (no if, for) — use traditional closures for complex logic. Closures are first-class objects (Closure class).
<?php
// arrow function (PHP 7.4+): single expression, auto-capture
$square = fn($x) => $x * $x;
echo $square(5); // 25
// auto-captures outer variables by value
$multiplier = 3;
$multiply = fn($x) => $x * $multiplier;
echo $multiply(5); // 15
// traditional closure (multi-line, explicit capture)
$factor = 10;
$scale = function ($x) use ($factor) {
return $x * $factor + 1;
};
// capture by reference
$count = 0;
$increment = function () use (&$count) {
$count++;
};
$increment();
echo $count; // 1
// closures as callbacks
$nums = [1, 2, 3, 4];
$evens = array_filter($nums, fn($n) => $n % 2 === 0);
$doubled = array_map(fn($n) => $n * 2, $nums);Variable Scope & Globals
PHP has function-level scope — variables defined outside a function are NOT accessible inside without 'global' or $GLOBALS. Avoid 'global' — it creates hidden dependencies and makes testing hard. Use dependency injection instead. static variables persist across function calls but are scoped to the function — useful for caching/memoization but can cause issues in long-running processes. Closures must explicitly capture variables with 'use'.
<?php
$global = "I'm global";
function testScope(): void {
// echo $global; // ERROR: not in scope
global $global; // import global
echo $global; // OK
// $GLOBALS superglobal (alternative)
echo $GLOBALS['global'];
}
// static variables: persist across calls
function counter(): int {
static $count = 0;
return ++$count;
}
echo counter(); // 1
echo counter(); // 2
echo counter(); // 3
// closures don't see outer scope by default
$outer = "hello";
$closure = function () {
// echo $outer; // ERROR
};
$closure2 = function () use ($outer) {
echo $outer; // OK, captured
};Type Declarations & Strict Types
declare(strict_types=1) must be the first statement — it enforces strict type checking (no coercion) for the entire file. Without it, PHP coerces types (int 5 passed to a string param becomes '5'). Always use strict types in new code. PHP 8+ adds union types, mixed, never (function never returns), and static (returns the class). First-class callable syntax (func(...)) creates closures from any callable — cleaner than function references.
<?php
// declare strict types (FIRST line of file)
declare(strict_types=1);
function divide(float $a, float $b): float {
if ($b === 0.0) {
throw new DivisionByZeroError();
}
return $a / $b;
}
// nullable types (?Type or Type|null)
function findUser(?int $id): ?string {
if ($id === null) return null;
return "User $id";
}
// union types (PHP 8+)
function process(int|string $input): int|string {
return is_int($input) ? $input * 2 : strtoupper($input);
}
// return types: void, never, mixed, static
function log(string $msg): void { /* no return */ }
function redirect(): never { header("Location: /"); exit; }
// first-class callable syntax (PHP 8.1+)
$func = strlen(...); // creates Closure from function
echo $func("hello"); // 5Generators & Yield
Generators (functions with yield) produce values lazily — they don't compute all values upfront, saving memory. This is essential for processing large files or datasets. yield pauses the function, returning a value; the function resumes when the next value is requested. Generators implement Iterator, so they work with foreach. Use generators for: file processing, database row iteration, infinite sequences, and pipelines. They can also yield key=>value pairs and accept values via send().
<?php
// generator: memory-efficient iteration
function readLines(string $file): Generator {
$handle = fopen($file, 'r');
while (($line = fgets($handle)) !== false) {
yield trim($line);
}
fclose($handle);
}
foreach (readLines("large.txt") as $line) {
echo $line; // one line at a time, low memory
}
// infinite generator
function fibonacci(): Generator {
[$a, $b] = [0, 1];
while (true) {
yield $a;
[$a, $b] = [$b, $a + $b];
}
}
// take first 10
$fib = fibonacci();
for ($i = 0; $i < 10; $i++) {
echo $fib->current() . " ";
$fib->next();
}
// yield with key
function pairs(): Generator {
yield 'a' => 1;
yield 'b' => 2;
yield 'c' => 3;
}OOP & Classes
Class, Properties & Constructor
PHP 8 constructor promotion eliminates boilerplate — declare properties as constructor parameters. Property visibility: public (anywhere), protected (class + subclasses), private (class only). readonly (PHP 8.1) prevents modification after initialization. self refers to the current class; static refers to the calling class (for late static binding). Use static:: instead of self:: in inheritance hierarchies for proper polymorphism.
<?php
class Person {
// typed properties (PHP 7.4+)
public string $name;
protected int $age;
private string $email;
public readonly string $id; // PHP 8.1+
// constructor promotion (PHP 8+)
public function __construct(
string $name,
int $age,
string $email = "",
string $id = ""
) {
$this->name = $name;
$this->age = $age;
$this->email = $email;
$this->id = $id;
}
// methods
public function greet(): string {
return "Hi, I'm {$this->name}";
}
// static method
public static function create(string $name): self {
return new self($name, 0);
}
}
$p = new Person("Alice", 30);
echo $p->greet();
echo Person::create("Bob")->name;Inheritance & Abstract Classes
abstract classes can't be instantiated — they define a template for subclasses. Abstract methods must be implemented by concrete subclasses. PHP supports single inheritance only (one extends). Use final to prevent inheritance/overriding when the implementation shouldn't change. protected members are accessible in subclasses — use for internal APIs. Always call parent::__construct() if the parent has a constructor. instanceof checks type: if ($dog instanceof Animal).
<?php
abstract class Animal {
protected string $name;
public function __construct(string $name) {
$this->name = $name;
}
// abstract method: must be implemented by subclasses
abstract public function speak(): string;
// concrete method: inherited as-is
public function describe(): string {
return "{$this->name} says {$this->speak()}";
}
}
class Dog extends Animal {
public function speak(): string {
return "Woof";
}
}
class Cat extends Animal {
public function speak(): string {
return "Meow";
}
}
$dog = new Dog("Rex");
echo $dog->describe(); // Rex says Woof
// final class/method: cannot be extended/overridden
final class Singleton { /* ... */ }Interfaces & Traits
Interfaces define contracts — classes can implement multiple interfaces (unlike single inheritance). All interface methods must be public. Traits provide code reuse without inheritance — they're 'copy-paste' at the language level. Traits can have properties, methods, and even abstract methods. Use traits for cross-cutting concerns (timestamps, logging, soft deletes). Conflict resolution: use TraitA::method insteadof TraitB when traits have same method names.
<?php
// interface: contract (no implementation)
interface Comparable {
public function compareTo(object $other): int;
}
interface JsonSerializable {
public function jsonSerialize(): mixed;
}
// a class can implement multiple interfaces
class Product implements Comparable, JsonSerializable {
public function __construct(public float $price) {}
public function compareTo(object $other): int {
return $this->price <=> $other->price;
}
public function jsonSerialize(): mixed {
return ['price' => $this->price];
}
}
// trait: reusable code (horizontal reuse)
trait Timestampable {
public DateTime $createdAt;
public function setCreatedAt(): void {
$this->createdAt = new DateTime();
}
public function age(): DateInterval {
return $this->createdAt->diff(new DateTime());
}
}
class Article {
use Timestampable; // use trait
}Magic Methods
Magic methods are special methods that intercept object operations. __get/__set implement property overloading (dynamic properties). __toString enables string casting. __invoke makes objects callable. __clone runs when cloning (clone $obj). Use sparingly — they add 'magic' that's hard to trace. __get/__set are useful for data transfer objects or lazy loading. Always document magic behavior clearly. PHP 8.2 deprecates dynamic properties — use __get/__set or #[AllowDynamicProperties].
<?php
class Magic {
private array $data = [];
// called when accessing undefined property
public function __get(string $name): mixed {
return $this->data[$name] ?? null;
}
// called when setting undefined property
public function __set(string $name, mixed $value): void {
$this->data[$name] = $value;
}
// called when isset() or empty() on undefined property
public function __isset(string $name): bool {
return isset($this->data[$name]);
}
// called when object is used as string
public function __toString(): string {
return json_encode($this->data);
}
// called when object is called as function
public function __invoke(string $arg): string {
return "Called with: $arg";
}
// called on clone
public function __clone(): void {
$this->data = []; // reset on clone
}
}
$m = new Magic();
$m->foo = "bar"; // __set
echo $m->foo; // __get -> "bar"
echo $m; // __toString -> {"foo":"bar"}
echo $m("test"); // __invokeNamespaces & Autoloading
Namespaces prevent class name collisions — like packages in Java. The namespace must be the first statement. use imports classes (with optional aliases: use Foo\Bar as B). PSR-4 autoloading maps namespaces to file paths: App\Models\User → src/Models/User.php. Composer's autoloader (require 'vendor/autoload.php') handles this automatically. Always use namespaces in modern PHP. The \\ in strings is an escaped backslash (namespace separator).
<?php
// file: src/Models/User.php
namespace App\Models;
use App\Database\Connection;
use App\Exceptions\UserNotFoundException;
class User {
private Connection $db;
public function __construct(Connection $db) {
$this->db = $db;
}
public function find(int $id): ?self {
// ...
throw new UserNotFoundException("User $id not found");
}
}
// composer.json (PSR-4 autoloading)
// {
// "autoload": {
// "psr-4": { "App\\": "src/" }
// }
// }
// usage
use App\Models\User;
$user = new User($db);Web, Forms & File I/O
Form Handling & Validation
Always validate server-side — client-side validation is for UX, not security. filter_input/filter_var with FILTER_VALIDATE_* return false on invalid input. Trim strings before validation. Use prepared statements for database inserts. CSRF tokens prevent cross-site request forgery — generate per session and verify on POST. bin2hex(random_bytes(32)) generates a cryptographically secure token. Never trust user input — validate, sanitize, and escape.
<?php
// process POST form
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = trim($_POST['name'] ?? '');
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$age = filter_var($_POST['age'] ?? 0, FILTER_VALIDATE_INT);
$errors = [];
if (empty($name)) {
$errors[] = "Name is required";
} elseif (strlen($name) > 100) {
$errors[] = "Name too long";
}
if ($email === false) {
$errors[] = "Valid email required";
}
if ($age === false || $age < 18) {
$errors[] = "Must be 18+";
}
if (empty($errors)) {
// process valid data
// save to database, redirect, etc.
header("Location: /success");
exit;
}
}
// CSRF token
session_start();
$token = bin2hex(random_bytes(32));
$_SESSION['csrf'] = $token;
// in form: <input type="hidden" name="csrf" value="<?= $token ?>">Sessions & Cookies
Sessions store data server-side (identified by a session ID cookie). session_start() must be called before any output (or use ob_start()). Store minimal data in sessions — they consume server memory. For cookies, always set secure (HTTPS only), httponly (prevent XSS access), and samesite (CSRF protection). Destroy sessions properly: unset variables, destroy session, clear cookie. For scalable apps, use a session handler backed by Redis/database instead of files.
<?php
// start session (must be before any output)
session_start();
// set session data
$_SESSION['user_id'] = 42;
$_SESSION['username'] = "Alice";
$_SESSION['login_time'] = time();
// read session data
$userId = $_SESSION['user_id'] ?? null;
// check if logged in
function isLoggedIn(): bool {
return isset($_SESSION['user_id']);
}
// destroy session
session_unset(); // clear variables
session_destroy(); // destroy session
setcookie(session_name(), '', time() - 3600, '/'); // clear cookie
// cookies
setcookie("theme", "dark", [
'expires' => time() + 86400 * 30, // 30 days
'path' => '/',
'secure' => true, // HTTPS only
'httponly' => true, // not accessible via JS
'samesite' => 'Strict', // CSRF protection
]);
// read cookie
$theme = $_COOKIE['theme'] ?? 'light';File I/O
file_get_contents/file_put_contents are convenient for small files. For large files, use fopen/fread/fwrite with streams. fgetcsv/fputcsv handle CSV format (including quoting/escaping). json_decode with true returns associative arrays (objects by default). Always check file_exists and handle errors (permissions, disk full). For file uploads, use move_uploaded_file() for security. Lock files with flock() when writing concurrently.
<?php
// read entire file
$content = file_get_contents("data.txt");
$lines = file("data.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
// write file
file_put_contents("output.txt", "Hello World");
file_put_contents("log.txt", "entry\n", FILE_APPEND); // append
// CSV
$csv = fopen("data.csv", "r");
while (($row = fgetcsv($csv)) !== false) {
print_r($row); // array of column values
}
fclose($csv);
// write CSV
$out = fopen("export.csv", "w");
fputcsv($out, ["Name", "Email", "Age"]);
fputcsv($out, ["Alice", "[email protected]", 30]);
fclose($out);
// JSON
$data = json_decode(file_get_contents("config.json"), true); // assoc array
file_put_contents("config.json", json_encode($data, JSON_PRETTY_PRINT));
// file info
file_exists("data.txt"); // bool
filesize("data.txt"); // bytes
filemtime("data.txt"); // modification timestamp
is_dir("folder"); // boolFile Uploads
File uploads come through $_FILES, not $_POST. Always validate: check error code, verify MIME type with finfo (not $_FILES['type'] which is client-provided and spoofable), enforce size limits, and generate safe filenames (never trust the original name). move_uploaded_file() is a security function — it verifies the file was uploaded via HTTP POST. Store uploads outside the web root or serve through PHP to prevent direct access. Consider scanning uploads for malware.
<?php
// HTML: <form method="POST" enctype="multipart/form-data">
// <input type="file" name="document">
// <input type="submit">
// </form>
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$file = $_FILES['document'];
// $file contains:
// ['name'] => original filename
// ['type'] => MIME type (unreliable!)
// ['tmp_name'] => temporary path
// ['error'] => UPLOAD_ERR_OK (0) on success
// ['size'] => file size in bytes
if ($file['error'] !== UPLOAD_ERR_OK) {
die("Upload error: " . $file['error']);
}
// validate
$allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!in_array($mimeType, $allowedTypes)) {
die("Invalid file type");
}
if ($file['size'] > 5 * 1024 * 1024) { // 5MB
die("File too large");
}
// move to permanent location
$dest = "uploads/" . uniqid() . "_" . $file['name'];
move_uploaded_file($file['tmp_name'], $dest);
echo "Uploaded to: $dest";
}cURL & HTTP Requests
cURL is the standard HTTP client in PHP — it handles HTTPS, redirects, cookies, and authentication. Always set CURLOPT_RETURNTRANSFER to get the response as a string (otherwise it's printed). Set timeouts to avoid hanging. For simple requests, file_get_contents with stream_context works but lacks features. For production, use Guzzle (composer require guzzlehttp/guzzle) or Symfony HTTP Client — they offer better APIs, retry logic, and PSR-18 compliance.
<?php
// GET request
$ch = curl_init("https://api.example.com/users");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
print_r($data);
}
// POST request with JSON
$ch = curl_init("https://api.example.com/users");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['name' => 'Alice']),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $token,
],
]);
$response = curl_exec($ch);
curl_close($ch);
// simpler: file_get_contents with context
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode(['name' => 'Alice']),
],
]);
$result = file_get_contents("https://api.example.com/users", false, $context);Database (PDO)
PDO Connection & Basics
PDO (PHP Data Objects) is the standard database abstraction layer — supports MySQL, PostgreSQL, SQLite, and more. Always set ERRMODE_EXCEPTION for proper error handling and ATTR_EMULATE_PREPARES=false for real prepared statements (better security). FETCH_ASSOC returns associative arrays (use FETCH_OBJ for objects, FETCH_CLASS to map to classes). Always use utf8mb4 charset for full Unicode support (including emoji). Store connection in a singleton or DI container.
<?php
// connect (always use exception mode)
$dsn = "mysql:host=localhost;dbname=test;charset=utf8mb4";
$pdo = new PDO($dsn, "username", "password", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // use real prepared statements
]);
// simple query
$stmt = $pdo->query("SELECT * FROM users LIMIT 5");
$users = $stmt->fetchAll(); // array of associative arrays
// fetch one row
$user = $pdo->query("SELECT * FROM users WHERE id = 1")->fetch();
// fetch column
$count = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
// execute (no params)
$pdo->exec("DELETE FROM logs WHERE created_at < '2023-01-01'");
$deleted = $pdo->rowCount(); // affected rowsPrepared Statements (SQL Injection Prevention)
Prepared statements are MANDATORY for any query with user input — they separate SQL structure from data, making injection impossible. Use ? for positional or :name for named parameters. For IN clauses, you must build the placeholder string dynamically (but values are still parameterized). lastInsertId() returns the last auto-increment value. Never concatenate user input into SQL — even with escaping functions. This is the #1 security rule in PHP.
<?php
// prepared statements: THE way to prevent SQL injection
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ? AND status = ?");
$stmt->execute([$email, 'active']);
$user = $stmt->fetch();
// named parameters (more readable)
$stmt = $pdo->prepare(
"INSERT INTO users (name, email, age) VALUES (:name, :email, :age)"
);
$stmt->execute([
':name' => 'Alice',
':email' => '[email protected]',
':age' => 30,
]);
$id = $pdo->lastInsertId(); // get auto-increment ID
// IN clause with prepared statements
$ids = [1, 2, 3, 4];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);
$users = $stmt->fetchAll();
// NEVER do this (SQL injection!):
// $pdo->query("SELECT * FROM users WHERE name = '$_GET[name]'");Transactions & Error Handling
Transactions ensure atomicity — all operations succeed or all fail. beginTransaction/commit/rollBack wrap the unit of work. Always wrap transactions in try/catch and roll back on any exception. PDO throws PDOException on errors (with ERRMODE_EXCEPTION). Keep transactions short to reduce lock contention. For nested transactions, use savepoints or a transaction manager. Never leave a transaction open — always commit or roll back.
<?php
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]);
// log transaction
$pdo->prepare("INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)")
->execute([$fromId, $toId, $amount]);
$pdo->commit();
echo "Transfer complete";
} catch (PDOException $e) {
$pdo->rollBack(); // undo all changes
error_log("Transfer failed: " . $e->getMessage());
throw new RuntimeException("Transfer failed", 0, $e);
}
// check if in transaction
if ($pdo->inTransaction()) {
$pdo->commit();
}Fetching Data Patterns
Choose the right fetch mode for your use case. FETCH_ASSOC is most common (array with column names). FETCH_CLASS maps rows to objects — great for domain models. FETCH_KEY_PAIR creates id=>value maps (for dropdowns). FETCH_GROUP groups rows by the first column — useful for one-to-many relationships. For large result sets, use fetch() in a loop instead of fetchAll() to save memory. Always close cursors with $stmt->closeCursor() when done.
<?php
$stmt = $pdo->prepare("SELECT id, name, email FROM users WHERE active = 1");
$stmt->execute();
// fetch modes
$row = $stmt->fetch(PDO::FETCH_ASSOC); // ['id' => 1, 'name' => 'Alice']
$row = $stmt->fetch(PDO::FETCH_NUM); // [1, 'Alice', '[email protected]']
$row = $stmt->fetch(PDO::FETCH_BOTH); // both (default)
$obj = $stmt->fetch(PDO::FETCH_OBJ); // stdClass with properties
// fetch all
$all = $stmt->fetchAll(PDO::FETCH_ASSOC);
// fetch into class
class User {
public int $id;
public string $name;
}
$stmt->setFetchMode(PDO::FETCH_CLASS, User::class);
$users = $stmt->fetchAll(); // array of User objects
// fetch key-value pairs
$pairs = $pdo->query("SELECT id, name FROM users")
->fetchAll(PDO::FETCH_KEY_PAIR); // [1 => 'Alice', 2 => 'Bob']
// fetch grouped
$grouped = $pdo->query("SELECT dept, name FROM employees")
->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_ASSOC);
// ['IT' => [['name' => 'Alice']], 'HR' => [['name' => 'Bob']]]Database Best Practices
The repository pattern separates data access from business logic — making code testable (mock the PDO) and maintainable. Use dependency injection to pass the PDO connection. Never create new PDO connections per query — reuse a single connection (or pool). For high-traffic apps, consider a connection pooler (ProxySQL for MySQL, PgBouncer for PostgreSQL). Always profile slow queries with EXPLAIN and add appropriate indexes. Consider an ORM (Doctrine, Eloquent) for complex domains.
<?php
// 1. Connection singleton (or use DI container)
class Database {
private static ?PDO $instance = null;
public static function conn(): PDO {
if (self::$instance === null) {
self::$instance = new PDO(
"mysql:host=localhost;dbname=app;charset=utf8mb4",
"user", "pass",
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
);
}
return self::$instance;
}
}
// 2. Repository pattern
class UserRepository {
public function __construct(private PDO $db) {}
public function findById(int $id): ?array {
$stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
$user = $stmt->fetch();
return $user ?: null;
}
}
// 3. Always close statements (or let them go out of scope)
// 4. Use LIMIT for queries that might return huge results
// 5. Index columns used in WHERE, JOIN, ORDER BY
// 6. Use EXPLAIN to analyze slow queriesDate/Time & Security
Date & Time
PHP's date functions use the server's timezone by default — always set date_default_timezone_set('Asia/Shanghai') or use DateTimeZone explicitly. The DateTime class is object-oriented and handles timezones, intervals, and formatting better than procedural functions. strtotime() parses English date descriptions ('next Monday', '+1 month') — convenient but can be surprising at month boundaries. For date math, use DateTime::diff() and DateInterval. Always store dates in UTC in databases.
<?php
// current time
$now = date('Y-m-d H:i:s'); // 2024-06-15 14:30:00
$timestamp = time(); // Unix timestamp
$dt = new DateTime(); // DateTime object
// formatting
echo date('Y-m-d'); // 2024-06-15
echo date('d/m/Y H:i:s'); // 15/06/2024 14:30:00
echo date('l, F j, Y'); // Saturday, June 15, 2024
// create from string
$dt = new DateTime('2024-01-15');
$dt = DateTime::createFromFormat('d/m/Y', '15/01/2024');
// modify dates
$dt->modify('+1 month');
$dt->modify('-2 days');
$tomorrow = date('Y-m-d', strtotime('tomorrow'));
$nextWeek = date('Y-m-d', strtotime('+1 week'));
// difference
$start = new DateTime('2024-01-01');
$end = new DateTime('2024-12-31');
$diff = $start->diff($end);
echo $diff->days; // 365
// timezone
$dt = new DateTime('now', new DateTimeZone('Asia/Shanghai'));
$dt->setTimezone(new DateTimeZone('UTC'));Password Hashing & Security
password_hash() uses bcrypt (or Argon2 if available) with automatic salt generation — never roll your own hashing. password_verify() safely checks passwords against hashes (constant-time comparison to prevent timing attacks). password_needs_rehash() lets you upgrade hashes when you increase the cost factor. For random tokens (CSRF, API keys, password resets), always use random_bytes() — not rand() or mt_rand() which are predictable. Use hash_hmac for message authentication.
<?php
// hash password (bcrypt by default)
$hash = password_hash("mypassword", PASSWORD_DEFAULT);
// $2y$10$... (includes algorithm, cost, salt)
// verify password
if (password_verify($input, $hash)) {
echo "Valid password";
}
// check if hash needs rehash (algorithm upgrade)
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$newHash = password_hash($input, PASSWORD_DEFAULT);
// update stored hash
}
// NEVER use md5() or sha1() for passwords!
// NEVER store plaintext passwords!
// generate secure random
$token = bin2hex(random_bytes(32)); // 64-char hex string
$bytes = random_bytes(16); // raw bytes
$int = random_int(1, 1000); // cryptographically secure
// hash for integrity (not passwords)
$checksum = hash('sha256', $data);
$hmac = hash_hmac('sha256', $data, $secretKey);Output Escaping & XSS Prevention
XSS is the #1 web vulnerability — always escape output based on context. htmlspecialchars() for HTML (ENT_QUOTES escapes both single and double quotes). urlencode() for URLs. json_encode() with hex flags for JavaScript contexts. Never trust user input — escape on output, not input (you may need the raw data elsewhere). Set Content-Security-Policy headers as defense-in-depth. Consider a template engine (Twig, Blade) that auto-escapes by default.
<?php
// XSS: Cross-Site Scripting — always escape output!
$name = $_GET['name']; // could be: <script>alert('xss')</script>
// HTML context
echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
// converts < > " ' & to HTML entities
// in HTML template
?>
<p>Hello, <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></p>
<input value="<?= htmlspecialchars($value, ENT_QUOTES, 'UTF-8') ?>">
<?php
// URL context
$url = "https://example.com/search?q=" . urlencode($query);
// JavaScript context
$json = json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT);
?>
<script>
var data = <?= $json ?>;
</script>
<?php
// Content Security Policy header
header("Content-Security-Policy: default-src 'self'; script-src 'self'");JSON & API Responses
json_encode/decode are the standard JSON functions. Always set Content-Type: application/json for API responses. Use JSON_UNESCAPED_UNICODE to keep Chinese/emoji readable (otherwise they become \uXXXX). json_decode with true returns associative arrays (more common in PHP). Always check json_last_error() after decoding untrusted JSON. For REST APIs, set appropriate HTTP status codes (200, 201, 400, 404, 500) and use consistent response structure.
<?php
// encode PHP array/object to JSON
$data = ['name' => 'Alice', 'age' => 30, 'hobbies' => ['reading', 'coding']];
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
// {"name": "Alice", "age": 30, "hobbies": ["reading", "coding"]}
// decode JSON to PHP
$obj = json_decode($json); // stdClass object
$arr = json_decode($json, true); // associative array
// handle errors
$data = json_decode($badJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException("JSON error: " . json_last_error_msg());
}
// API response
header('Content-Type: application/json; charset=utf-8');
http_response_code(200);
echo json_encode([
'status' => 'success',
'data' => $users,
'meta' => ['page' => 1, 'total' => 100],
], JSON_UNESCAPED_UNICODE);
// API request handling
$input = json_decode(file_get_contents('php://input'), true);Composer & Dependency Management
Composer is PHP's package manager — essential for modern PHP. require specifies production dependencies; require-dev for development (tests, etc.). PSR-4 autoloading maps namespaces to directories. Always commit composer.json and composer.lock (locks exact versions). Use composer install (from lock) in production, composer update to get latest. Popular packages: Monolog (logging), Guzzle (HTTP), PHPUnit (testing), Symfony components, Laravel framework. Never commit vendor/ directory.
<?php
// composer.json
// {
// "require": {
// "monolog/monolog": "^3.0",
// "guzzlehttp/guzzle": "^7.0"
// },
// "autoload": {
// "psr-4": {"App\\": "src/"}
// }
// }
// install: composer install
// update: composer update
// add: composer require monolog/monolog
// autoload (at the top of your app)
require 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('app');
$log->pushHandler(new StreamHandler('app.log', Logger::WARNING));
$log->warning('User not found', ['user_id' => 42]);
// environment variables (vlucas/phpdotenv)
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$dbHost = $_ENV['DB_HOST'];REST API Development
Handling JSON Requests & Responses
REST APIs exchange JSON. Unlike form submissions (which populate $_POST), JSON requests must be read from php://input and decoded with json_decode. Always set Content-Type: application/json for responses and use http_response_code() for proper HTTP status codes. Validate all input — json_decode doesn't guarantee the expected structure. Use the null coalescing operator (??) for safe access. JSON_PRETTY_PRINT is useful for debugging but omit it in production for smaller payloads.
// Get JSON from request body (not $_POST for JSON!)
$json = file_get_contents('php://input');
$data = json_decode($json, true); // true = associative array
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON']);
exit;
}
// Process the data
$name = $data['name'] ?? '';
$email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);
// Return JSON response
header('Content-Type: application/json');
http_response_code(200); // 200 OK, 201 Created, 400 Bad Request, etc.
echo json_encode([
'success' => true,
'data' => ['id' => 1, 'name' => $name],
'message' => 'User created'
], JSON_PRETTY_PRINT);Routing & HTTP Methods
REST APIs map HTTP methods to CRUD operations: GET (read), POST (create), PUT/PATCH (update), DELETE (delete). Routing matches the method + URL path to a handler. Use preg_match for parameterized routes (e.g., /api/users/42). In production, use a router library (FastRoute, Symfony Routing) or framework (Laravel, Slim) for cleaner routing, middleware, and dependency injection. Always return appropriate HTTP status codes: 200 (OK), 201 (Created), 204 (No Content), 400 (Bad Request), 404 (Not Found), 500 (Server Error).
// Simple REST router based on method + path
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$path = trim($path, '/');
// Route table: method => [pattern => handler]
switch (true) {
case ($method === 'GET' && $path === 'api/users'):
echo json_encode(getAllUsers());
break;
case ($method === 'GET' && preg_match('#^api/users/(\d+)$#', $path, $m)):
echo json_encode(getUser((int)$m[1]));
break;
case ($method === 'POST' && $path === 'api/users'):
$data = json_decode(file_get_contents('php://input'), true);
echo json_encode(createUser($data));
http_response_code(201);
break;
case ($method === 'PUT' && preg_match('#^api/users/(\d+)$#', $path, $m)):
$data = json_decode(file_get_contents('php://input'), true);
echo json_encode(updateUser((int)$m[1], $data));
break;
case ($method === 'DELETE' && preg_match('#^api/users/(\d+)$#', $path, $m)):
deleteUser((int)$m[1]);
http_response_code(204); // No Content
break;
default:
http_response_code(404);
echo json_encode(['error' => 'Not found']);
}API Authentication (JWT)
JWT enables stateless authentication — the server doesn't need to store sessions. The token contains a payload (user ID, expiry) signed with a secret key. The client sends the token in the Authorization header (Bearer token). The server verifies the signature to ensure the token wasn't tampered with. JWT is great for APIs and microservices (no shared session store needed). However, JWTs can't be revoked before expiry — use short expiry times and a refresh token strategy. In production, use the firebase/php-jwt library rather than implementing crypto yourself. Never store sensitive data in the JWT payload — it's only base64-encoded, not encrypted.
// JWT (JSON Web Token) authentication flow
// 1. Login: verify credentials, issue JWT
function login($email, $password) {
$user = findUserByEmail($email);
if ($user && password_verify($password, $user['password_hash'])) {
// Create JWT: header.payload.signature
$header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
$payload = base64url_encode(json_encode([
'user_id' => $user['id'],
'exp' => time() + 3600 // expires in 1 hour
]));
$signature = hash_hmac('sha256', "$header.$payload", SECRET_KEY, true);
$jwt = "$header.$payload." . base64url_encode($signature);
return $jwt;
}
return null;
}
// 2. Verify JWT on protected routes
function verifyJWT($token) {
$parts = explode('.', $token);
if (count($parts) !== 3) return false;
[$header, $payload, $signature] = $parts;
$expected = base64url_encode(
hash_hmac('sha256', "$header.$payload", SECRET_KEY, true)
);
if (!hash_equals($expected, $signature)) return false;
$data = json_decode(base64url_decode($payload), true);
return ($data['exp'] ?? 0) > time() ? $data : false;
}
// Use firebase/php-jwt library in production!Input Validation & Sanitization
Input validation is critical for API security. PHP's filter_var provides built-in validators (FILTER_VALIDATE_EMAIL, FILTER_VALIDATE_INT, FILTER_VALIDATE_URL) with options like min/max range. Always validate on the server side — client-side validation is for UX, not security. Sanitize strings with htmlspecialchars to prevent XSS when outputting HTML. For JSON APIs, return 422 (Unprocessable Entity) for validation errors with descriptive messages. Consider using a validation library (Respect/Validation, Symfony Validator) for complex rules. Never trust user input — validate type, length, format, and business rules.
// Validate and sanitize API input
function validateUserInput(array $data): array {
$errors = [];
// Required fields
if (empty($data['name'])) {
$errors[] = 'Name is required';
}
// Email validation
$email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);
if (!$email) {
$errors[] = 'Valid email is required';
}
// Age: integer between 18 and 120
$age = filter_var($data['age'] ?? null, FILTER_VALIDATE_INT, [
'options' => ['min_range' => 18, 'max_range' => 120]
]);
if ($age === false) {
$errors[] = 'Age must be between 18 and 120';
}
// String sanitization (remove tags, trim)
$name = htmlspecialchars(trim($data['name'] ?? ''), ENT_QUOTES, 'UTF-8');
// URL validation
$website = filter_var($data['website'] ?? '', FILTER_VALIDATE_URL);
return ['errors' => $errors, 'data' => compact('email', 'age', 'name')];
}
$result = validateUserInput($input);
if (!empty($result['errors'])) {
http_response_code(422); // Unprocessable Entity
echo json_encode(['errors' => $result['errors']]);
exit;
}CORS (Cross-Origin Resource Sharing)
CORS controls which domains can access your API from a browser. Browsers send a preflight OPTIONS request for non-simple requests (PUT/DELETE, custom headers). Your server must respond with the appropriate Access-Control-Allow-* headers. For security, specify exact origins rather than '*' (especially with credentials). Access-Control-Allow-Credentials: true is needed if the API uses cookies or Authorization headers. Vary: Origin tells caches that the response varies by origin. Misconfigured CORS can expose your API to any website — always whitelist trusted origins.
// Enable CORS for API requests from browsers
header('Access-Control-Allow-Origin: https://example.com'); // specific origin
// OR: header('Access-Control-Allow-Origin: *'); // any origin (less secure)
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Credentials: true'); // for cookies/auth
header('Access-Control-Max-Age: 86400'); // cache preflight for 24h
// Handle preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
// Dynamic origin (check against whitelist)
$allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins)) {
header('Access-Control-Allow-Origin: ' . $origin);
header('Vary: Origin'); // important for caching
}Security (XSS, CSRF, SQL Injection)
Preventing XSS (Cross-Site Scripting)
XSS occurs when untrusted data is inserted into HTML without escaping, allowing attackers to execute JavaScript in victims' browsers. The fix: always escape output with htmlspecialchars (converts <, >, &, ", ' to HTML entities). Different contexts need different escaping: HTML body (htmlspecialchars), HTML attributes (htmlspecialchars with ENT_QUOTES), JavaScript (json_encode), URLs (urlencode). Content Security Policy (CSP) headers add defense-in-depth by restricting where scripts can load from. Never use eval(), innerHTML, or document.write() with user input. Frameworks like Twig and Blade auto-escape by default.
// XSS: attacker injects malicious JavaScript into your page
// BAD: outputting user input without escaping
echo "<p>" . $_GET['name'] . "</p>";
// If name = <script>alert('xss')</script>, it executes!
// GOOD: escape output with htmlspecialchars
echo "<p>" . htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8') . "</p>";
// & < > " ' are converted to HTML entities
// For HTML attributes:
echo '<input value="' . htmlspecialchars($value, ENT_QUOTES) . '">';
// For JavaScript context (JSON encode):
echo '<script>var name = ' . json_encode($name) . ';</script>';
// For URL parameters:
echo '<a href="?q=' . urlencode($query) . '">Search</a>';
// Content Security Policy (CSP) header — defense in depth
header("Content-Security-Policy: default-src 'self'; script-src 'self'");
// Use prepared statements (prevents SQL injection too)Preventing CSRF (Cross-Site Request Forgery)
CSRF tricks an authenticated user's browser into sending a request to your site (e.g., a money transfer) without their knowledge. The defense: include an unpredictable token in forms that the attacker can't guess. The token is stored in the session and verified on submission. Use hash_equals() for timing-safe comparison (prevents timing attacks). For AJAX/API calls, SameSite=Strict cookies and requiring custom headers (like X-Requested-With) provide protection. GET requests should never modify data (they can be triggered by image tags or links). Frameworks like Laravel and Symfony have built-in CSRF middleware.
// CSRF: attacker tricks a logged-in user into submitting a form
// to your site (using their session cookie)
// Defense: anti-CSRF tokens
session_start();
// Generate a token (once per session)
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// Include token in forms as a hidden field
echo '<form method="POST" action="/transfer">';
echo '<input type="hidden" name="csrf_token" value="' . $_SESSION['csrf_token'] . '">';
echo '<input type="text" name="amount">';
echo '<button type="submit">Transfer</button>';
echo '</form>';
// Verify token on POST/PUT/DELETE requests
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST['csrf_token'] ?? '';
if (!hash_equals($_SESSION['csrf_token'], $token)) {
http_response_code(403);
die('CSRF token validation failed');
}
// Process the form...
}
// For APIs: use SameSite cookies + custom headers
// (browsers block cross-origin requests without explicit CORS permission)Preventing SQL Injection
SQL injection is the #1 web vulnerability — it lets attackers read/modify/delete your entire database. The universal fix: prepared statements (parameterized queries). The query structure and data are sent separately, so user input can never be interpreted as SQL. Never concatenate user input into queries. PDO's prepare/execute handles escaping automatically. Bind parameters with their types (PDO::PARAM_INT, PDO::PARAM_STR). For IN clauses with variable items, generate placeholders dynamically. Set PDO::ATTR_EMULATE_PREPARES to false for real server-side prepared statements (better security).
// SQL injection: attacker manipulates queries via unescaped input
// BAD: string concatenation
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
// If id = "1 OR 1=1", returns ALL users!
// If id = "1; DROP TABLE users; --", deletes the table!
// GOOD: prepared statements with parameter binding
$pdo = new PDO('mysql:host=localhost;dbname=myapp', $user, $pass);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // real prepared statements
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// Multiple parameters:
$stmt = $pdo->prepare('INSERT INTO users (name, email, age) VALUES (:name, :email, :age)');
$stmt->execute([
':name' => $name,
':email' => $email,
':age' => $age
]);
// IN clause with variable arguments:
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);Password Hashing & Authentication
password_hash() uses bcrypt (or Argon2) with a random salt — the industry standard for password storage. The salt is embedded in the hash, so you don't manage it separately. password_verify() safely compares the input against the stored hash (timing-safe). password_needs_rehash() lets you upgrade hashes when you increase cost factors or switch algorithms — it checks if the hash matches current settings and rehashes on next login. Never use MD5, SHA1, or plain text for passwords — they're trivially crackable. Enforce strong password policies but prefer length over complexity (NIST recommends 8+ chars minimum).
// NEVER store plain-text or MD5/SHA1 passwords!
// Use password_hash() (bcrypt/argon2 by default)
// Hash a password (on registration)
$password = $_POST['password'];
$hash = password_hash($password, PASSWORD_DEFAULT);
// PASSWORD_DEFAULT = bcrypt (or argon2id in PHP 7.3+)
// Store $hash in the database
// Verify a password (on login)
if (password_verify($inputPassword, $storedHash)) {
// Password is correct
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
} else {
echo "Invalid credentials";
}
// Rehash if algorithm was upgraded (migration)
if (password_verify($input, $hash) &&
password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$newHash = password_hash($input, PASSWORD_DEFAULT);
// Update database with $newHash
}
// Password requirements validation
if (strlen($password) < 8 ||
!preg_match('/[A-Z]/', $password) ||
!preg_match('/[a-z]/', $password) ||
!preg_match('/[0-9]/', $password)) {
echo "Password must be 8+ chars with upper, lower, and number";
}File Upload Security
File uploads are a major attack vector. Never trust $_FILES['type'] (set by the browser, easily faked) — use finfo to detect the real MIME type. Never use the user-supplied filename (it could contain path traversal like ../../script.php) — generate a random name. Store uploads outside the web root or in a directory with PHP execution disabled. For images, re-encode them (imagecreatefromjpeg + imagejpeg) to strip embedded PHP code hidden in EXIF data. Limit file size to prevent denial-of-service. Validate extension, MIME type, and magic bytes. Consider scanning uploads with an antivirus (ClamAV) for additional security.
// Secure file upload handling
$upload = $_FILES['avatar'];
// 1. Check for upload errors
if ($upload['error'] !== UPLOAD_ERR_OK) {
die('Upload failed: error ' . $upload['error']);
}
// 2. Validate file size
$maxSize = 2 * 1024 * 1024; // 2MB
if ($upload['size'] > $maxSize) {
die('File too large (max 2MB)');
}
// 3. Validate MIME type (don't trust $_FILES['type']!)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($upload['tmp_name']);
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mimeType, $allowedTypes)) {
die('Invalid file type');
}
// 4. Generate a safe filename (never use user-supplied name)
$safeName = bin2hex(random_bytes(16)) . '.jpg';
// 5. Store OUTSIDE the web root (or in a non-executable dir)
$dest = __DIR__ . '/uploads/' . $safeName;
if (!move_uploaded_file($upload['tmp_name'], $dest)) {
die('Failed to save file');
}
// 6. For images: re-encode to strip malicious metadata
$img = imagecreatefromjpeg($dest);
imagejpeg($img, $dest, 90); // re-encode strips embedded PHP/scriptscURL & HTTP Requests
Basic cURL GET & POST
cURL is PHP's most powerful HTTP client, supporting GET, POST, custom methods, headers, cookies, and SSL. Always set CURLOPT_RETURNTRANSFER to get the response as a string (otherwise it's echoed directly). CURLOPT_TIMEOUT prevents hanging on slow servers. For POST with JSON, set Content-Type and Content-Length headers explicitly. Check curl_errno() for connection errors and curl_getinfo(CURLINFO_HTTP_CODE) for the HTTP status. Always close cURL handles with curl_close() to free resources. For simpler code, consider Guzzle (a cURL wrapper with a cleaner API).
// GET request
$ch = curl_init('https://api.example.com/users');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
echo 'Error: ' . curl_error($ch);
}
curl_close($ch);
$data = json_decode($response, true);
// POST request with JSON body
$ch = curl_init('https://api.example.com/users');
$payload = json_encode(['name' => 'Alice', 'email' => '[email protected]']);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $token,
'Content-Length: ' . strlen($payload)
],
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);cURL with Authentication & Cookies
cURL supports multiple authentication methods. CURLOPT_USERPWD sets HTTP Basic Auth. Bearer tokens go in the Authorization header. For cookie-based sessions (like logging into a website), use CURLOPT_COOKIEJAR to save cookies and CURLOPT_COOKIEFILE to send them on subsequent requests — this maintains a session across multiple cURL calls. Use a temp file for cookies and clean it up with unlink(). For API calls, prefer token-based auth (Bearer) over cookies. Always use HTTPS (cURL verifies SSL by default — don't disable CURLOPT_SSL_VERIFYPEER in production).
// Basic Auth
$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_USERPWD, 'username:password');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
// Bearer token (API key)
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $apiKey,
'Accept: application/json'
]);
// Cookie-based session (login then reuse cookies)
$cookieFile = tempnam(sys_get_temp_dir(), 'cookie');
// Step 1: Login (saves cookies)
$ch = curl_init('https://example.com/login');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(['user' => 'alice', 'pass' => 'secret']),
CURLOPT_COOKIEJAR => $cookieFile, // save cookies here
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);
// Step 2: Access protected page (sends saved cookies)
$ch = curl_init('https://example.com/dashboard');
curl_setopt_array($ch, [
CURLOPT_COOKIEFILE => $cookieFile, // send cookies from here
CURLOPT_RETURNTRANSFER => true,
]);
$dashboard = curl_exec($ch);
curl_close($ch);
unlink($cookieFile); // cleanupFile Downloads & Streaming
For large file downloads, use CURLOPT_FILE to write directly to a file handle — this avoids loading the entire response into memory. CURLOPT_FOLLOWLOCATION follows HTTP redirects (301, 302). For streaming (e.g., real-time data), use CURLOPT_WRITEFUNCTION to process chunks as they arrive — useful for APIs that stream data. CURLOPT_PROGRESSFUNCTION monitors download/upload progress. Set a generous CURLOPT_TIMEOUT for large files. For very large uploads, use CURLOPT_INFILE to stream from a file instead of loading into memory. Always close file handles and cURL handles to prevent resource leaks.
// Download a file to disk
$ch = curl_init('https://example.com/large-file.zip');
$fp = fopen('downloaded.zip', 'w');
curl_setopt_array($ch, [
CURLOPT_FILE => $fp, // write directly to file
CURLOPT_FOLLOWLOCATION => true, // follow redirects
CURLOPT_TIMEOUT => 300, // 5 min for large files
]);
curl_exec($ch);
fclose($fp);
curl_close($ch);
// Stream response with callback (progress monitoring)
$ch = curl_init('https://example.com/stream');
curl_setopt_array($ch, [
CURLOPT_WRITEFUNCTION => function($ch, $chunk) {
echo $chunk; // stream to output
ob_flush();
flush();
return strlen($chunk);
},
CURLOPT_PROGRESSFUNCTION => function($ch, $dlTotal, $dlNow, $ulTotal, $ulNow) {
if ($dlTotal > 0) {
printf("
Progress: %.1f%%", ($dlNow / $dlTotal) * 100);
}
return 0;
},
]);
curl_exec($ch);
curl_close($ch);Concurrent Requests (Multi cURL)
curl_multi_exec runs multiple HTTP requests in parallel — dramatically faster than sequential requests when you need data from multiple endpoints. The pattern: create a multi handle, add individual cURL handles, execute the multi handle in a loop (curl_multi_exec + curl_multi_select for efficiency), then collect results. This is useful for aggregating data from multiple APIs, prefetching resources, or batch operations. For more advanced concurrency, consider ReactPHP or Amp (async PHP frameworks). Note that multi-cURL still blocks the PHP process — for true async, use event loops or message queues.
// Fetch multiple URLs in parallel (much faster than sequential)
$urls = [
'https://api.example.com/users',
'https://api.example.com/posts',
'https://api.example.com/comments'
];
$multi = curl_multi_init();
$handles = [];
// Create individual handles
foreach ($urls as $i => $url) {
$handles[$i] = curl_init($url);
curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
curl_multi_add_handle($multi, $handles[$i]);
}
// Execute all requests concurrently
$active = null;
do {
$status = curl_multi_exec($multi, $active);
if ($active) {
curl_multi_select($multi); // wait for activity
}
} while ($active && $status === CURLM_OK);
// Collect results
$responses = [];
foreach ($handles as $i => $ch) {
$responses[$i] = json_decode(curl_multi_getcontent($ch), true);
curl_multi_remove_handle($multi, $ch);
curl_close($ch);
}
curl_multi_close($multi);
// $responses[0], [1], [2] now contain all three API responsesUsing Guzzle (Modern HTTP Client)
Guzzle is the standard HTTP client for modern PHP — much cleaner than raw cURL. It provides a fluent API, PSR-7 compliant request/response objects, middleware (logging, retry), and async requests via Promises. The 'json' option auto-encodes the body and sets Content-Type. getAsync/postAsync return Promises for concurrent requests without multi-cURL complexity. Exception handling is built-in: RequestException catches HTTP errors (4xx, 5xx). Guzzle is used by most frameworks (Laravel's HTTP client wraps Guzzle). Install via Composer: composer require guzzlehttp/guzzle.
// Guzzle is the most popular PHP HTTP client (composer require guzzlehttp/guzzle)
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
$client = new Client([
'base_uri' => 'https://api.example.com',
'timeout' => 30,
'headers' => ['Accept' => 'application/json'],
]);
// GET request
$response = $client->get('/users/42');
$body = json_decode($response->getBody(), true);
echo $response->getStatusCode(); // 200
// POST with JSON
$response = $client->post('/users', [
'json' => ['name' => 'Alice', 'email' => '[email protected]'],
'headers' => ['Authorization' => 'Bearer ' . $token],
]);
// Concurrent requests (Promise-based)
$promises = [
'users' => $client->getAsync('/users'),
'posts' => $client->getAsync('/posts'),
];
$results = GuzzleHttp\Promise\Utils::settle($promises)->wait();
// Error handling with try/catch
try {
$response = $client->get('/nonexistent');
} catch (RequestException $e) {
echo $e->getResponse()->getStatusCode(); // 404
}DateTime Deep Dive
Creating & Manipulating DateTime
DateTime is PHP's robust date/time class. DateTimeImmutable is preferred over DateTime — it returns a new object on modification, preventing accidental mutation bugs (critical when the same date is used in multiple places). createFromFormat parses custom formats. modify() accepts relative expressions like '+1 week' or 'last day of next month'. Always specify timezones explicitly to avoid server-config-dependent behavior. For date math (adding intervals), use DateInterval ('P1D' = 1 day, 'P2W' = 2 weeks, 'PT2H' = 2 hours) with add()/sub().
$now = new DateTime(); // current time
$now->setTimezone(new DateTimeZone('Asia/Shanghai'));
// From string (many formats supported)
$date = new DateTime('2024-06-15 14:30:00');
$date = new DateTime('first day of next month');
$date = new DateTime('next Friday');
// From format (precise parsing)
$date = DateTime::createFromFormat('d/m/Y', '15/06/2024');
// Modify dates with relative expressions
$date->modify('+1 week');
$date->modify('-2 days');
$date->modify('last day of this month');
$date->modify('+1 year 3 months');
// Immutable version (recommended — doesn't modify original)
$dt = new DateTimeImmutable('2024-06-15');
$next = $dt->modify('+1 day'); // $dt unchanged, $next is new object
echo $dt->format('Y-m-d'); // 2024-06-15 (unchanged!)
echo $next->format('Y-m-d'); // 2024-06-16Formatting & Timezones
format() uses pattern letters to customize output — Y (4-digit year), m (2-digit month), d (2-digit day), H (24-hour), i (minutes), s (seconds). For ISO 8601 (used in APIs), use 'Y-m-d\TH:i:sP' or the 'c' shortcut. Timezone conversion: create with the source timezone, then setTimezone to convert. Always store dates in UTC in the database and convert to the user's timezone only for display. PHP's timezone database is comprehensive (includes DST rules). Use DateTimeZone::listIdentifiers() to get all supported zones. The 'T' escape character in format() outputs a literal 'T' (for ISO 8601).
// Format codes (most common):
// Y=2024 y=24 m=06 n=6 d=15 j=15
// H=14 (24h) h=02 (12h) i=30 s=00 A=PM a=pm
// D=Mon l=Monday M=Jun F=June N=1 (Mon=1..Sun=7)
$date = new DateTime('2024-06-15 14:30:00');
echo $date->format('Y-m-d H:i:s'); // 2024-06-15 14:30:00
echo $date->format('l, F j, Y'); // Saturday, June 15, 2024
echo $date->format('Y-m-d\TH:i:sP'); // 2024-06-15T14:30:00+08:00 (ISO 8601)
// Timezone conversion
$utc = new DateTime('2024-06-15 14:00:00', new DateTimeZone('UTC'));
$utc->setTimezone(new DateTimeZone('Asia/Shanghai'));
echo $utc->format('H:i'); // 22:00 (UTC+8)
// List supported timezones
$tzs = DateTimeZone::listIdentifiers();
// ['UTC', 'America/New_York', 'Asia/Shanghai', ...]
// Get timezone offset
$tz = new DateTimeZone('America/Los_Angeles');
$offset = $tz->getOffset(new DateTime()); // seconds from UTCDate Intervals & Differences
DateInterval represents a time duration using ISO 8601 duration format (P1Y2M3DT4H5M6S). add() and sub() apply intervals to dates. diff() returns a DateInterval representing the difference between two dates — the 'days' property gives total days, while 'y', 'm', 'd' give component breakdowns. The 'invert' property indicates direction (1 if the second date is earlier). Be careful with month arithmetic: adding 'P1M' to Jan 31 gives Mar 2 (Feb has 28-29 days), not Feb 31. For business day calculations, iterate and skip weekends/holidays manually or use a library like nesbot/carbon.
// DateInterval: represents a duration
$interval = new DateInterval('P1Y2M3DT4H5M6S');
// P = period, 1Y = 1 year, 2M = 2 months, 3D = 3 days
// T = time separator, 4H = 4 hours, 5M = 5 minutes, 6S = 6 seconds
$date = new DateTime('2024-06-15');
$date->add(new DateInterval('P1M')); // +1 month → 2024-07-15
$date->sub(new DateInterval('P10D')); // -10 days → 2024-07-05
// Difference between two dates
$d1 = new DateTime('2024-01-01');
$d2 = new DateTime('2024-12-31');
$diff = $d1->diff($d2);
echo $diff->days; // 365 (total days)
echo $diff->m; // 0 (months component)
echo $diff->y; // 0 (years component)
echo $diff->format('%a days'); // 365 days
echo $diff->format('%y years, %m months, %d days'); // 0 years, 11 months, 30 days
// Check if inverted (d2 < d1)
echo $diff->invert; // 0 if d2 >= d1, 1 if d2 < d1DatePeriod (Iterating Date Ranges)
DatePeriod iterates over a date range at a specified interval — perfect for generating calendars, reports, or recurring events. The constructor takes (start, interval, end) or (start, interval, recurrences). The end date is exclusive. Common use cases: generating all days in a month for a calendar view, listing pay periods, or creating recurring event schedules. Use iterator_to_array() to materialize the period into an array. For complex recurrence rules (e.g., 'every 2nd Tuesday'), consider a dedicated library like rrule (RFC 5545 recurrence rules).
// DatePeriod iterates over a range of dates
$start = new DateTime('2024-06-01');
$end = new DateTime('2024-06-10');
$interval = new DateInterval('P1D'); // 1 day
$period = new DatePeriod($start, $interval, $end);
foreach ($period as $day) {
echo $day->format('Y-m-d (D)') . "\n";
}
// 2024-06-01 (Sat)
// 2024-06-02 (Sun)
// ... through 2024-06-09 (end is exclusive)
// Generate next 12 months
$start = new DateTime('first day of this month');
$interval = new DateInterval('P1M');
$period = new DatePeriod($start, $interval, 12); // 12 recurrences
foreach ($period as $month) {
echo $month->format('F Y') . "\n";
}
// Every Monday for a year
$start = new DateTime('next Monday');
$end = new DateTime('+1 year');
$period = new DatePeriod($start, new DateInterval('P1W'), $end);
$mondays = iterator_to_array($period);Carbon Library (Enhanced DateTime)
Carbon extends DateTime with a fluent, expressive API — it's the de facto standard in the PHP ecosystem (used by Laravel). diffForHumans() produces '5 days ago', '3 hours from now' — perfect for UI timestamps. The fluent API chains methods (addYear()->subMonth()->endOfMonth()). Comparison methods (isWeekend, isPast, isToday) simplify common checks. Localization supports 50+ languages for human-readable output. Carbon 3 (2024+) is immutable by default. Install via Composer: composer require nesbot/carbon. If you're using Laravel, Carbon is already included.
// Carbon: the most popular DateTime library (composer require nesbot/carbon)
use Carbon\Carbon;
$now = Carbon::now('Asia/Shanghai');
$tomorrow = Carbon::tomorrow();
$lastWeek = Carbon::now()->subWeek();
// Human-readable differences
echo Carbon::now()->diffForHumans(Carbon::now()->subDays(5));
// "5 days ago"
echo Carbon::now()->addHours(3)->diffForHumans();
// "3 hours from now"
// Fluent API
$date = Carbon::create(2024, 6, 15, 14, 30, 0)
->addYear()
->subMonth()
->endOfMonth()
->setTimezone('UTC');
// Comparison methods
if ($date->isWeekend()) { echo "Weekend!"; }
if ($date->isPast()) { echo "Past"; }
if ($date->isFuture()) { echo "Future"; }
if ($date->isToday()) { echo "Today"; }
if ($date->isLeapYear()) { echo "Leap year"; }
// Localization
Carbon::setLocale('zh');
echo Carbon::now()->subDay()->diffForHumans(); // "1天前"Namespaces & Autoloading
Namespace Basics
Namespaces organize code into hierarchical packages, preventing class name collisions between libraries. The namespace declaration must be the first statement (after declare()). The 'use' statement imports classes from other namespaces — place use statements at the top of the file. Aliasing (as) resolves conflicts when two classes have the same name. The leading backslash (\DateTime) refers to the global namespace. PHP namespaces use backslashes (\) as separators, mapping to directory structure in PSR-4 autoloading. Group use statements (use App\Models\{User, Post}) reduce boilerplate.
<?php
// Namespaces prevent name collisions (like packages in Java)
namespace App\Services;
class UserService {
public function find($id) { /* ... */ }
}
// Using namespaced classes
use App\Services\UserService;
use App\Models\User;
$service = new UserService();
$user = new User();
// Aliasing (resolve conflicts or shorten names)
use App\Services\UserService as USvc;
use App\Models\User as UserModel;
// Global namespace (backslash prefix)
$now = new \DateTime(); // \ means root/global namespace
$array = new \ArrayObject();
// Multiple use statements grouped
use App\Models\{User, Post, Comment};
use App\Services\{UserService, PostService};PSR-4 Autoloading Standard
PSR-4 is the standard autoloading specification — it maps namespaces to directory paths, so you never need manual require/include statements. The rule: App\Services\UserService maps to src/Services/UserService.php (App\ → src/). Configure the mapping in composer.json's autoload section. After adding new classes, run 'composer dump-autoload' to regenerate the class map. The vendor/autoload.php file (generated by Composer) handles the loading — include it once in your entry point (index.php). PSR-4 enforces that class names match file names (UserService → UserService.php).
// PSR-4: namespace structure maps to file paths
// App\Services\UserService → src/Services/UserService.php
// composer.json PSR-4 configuration:
// {
// "autoload": {
// "psr-4": {
// "App\\": "src/"
// }
// }
// }
// File: src/Services/UserService.php
namespace App\Services;
class UserService {
public function getUser($id) {
return "User $id";
}
}
// File: src/Models/User.php
namespace App\Models;
class User {
public $name;
public function __construct($name) {
$this->name = $name;
}
}
// After adding classes, regenerate autoloader:
// $ composer dump-autoload
// The autoloader is included once in your entry point:
require __DIR__ . '/vendor/autoload.php';
$service = new App\Services\UserService();
$user = new App\Models\User('Alice');Autoloading Without Composer (spl_autoload)
spl_autoload_register registers a function that's called when a class isn't yet loaded — it receives the fully-qualified class name and should require the corresponding file. You can register multiple autoloaders (they're called in order). This is what Composer uses internally. For production, always use Composer's PSR-4 autoloader — it's optimized, handles edge cases, and generates class maps for faster lookups. Use spl_autoload_register directly only for tiny projects or when Composer isn't available. The 'true' parameter in class_exists() triggers autoloading if the class isn't loaded.
<?php
// Simple autoloader for small projects (no Composer needed)
spl_autoload_register(function ($className) {
// Convert namespace separators to directory separators
$file = __DIR__ . '/src/' . str_replace('\\', '/', $className) . '.php';
// App\Services\UserService → src/App/Services/UserService.php
// Adjust prefix mapping as needed:
$file = str_replace('App/', '', $file); // remove 'App' prefix
if (file_exists($file)) {
require $file;
}
});
// Now classes are loaded automatically on first use
$service = new App\Services\UserService();
// Multiple autoloaders (called in order until one loads the class)
spl_autoload_register(function ($class) {
$path = __DIR__ . '/lib/' . $class . '.php';
if (file_exists($path)) require $path;
});
// Check if a class is autoloadable
if (class_exists('App\Helper', true)) { // true = attempt autoload
$helper = new App\Helper();
}Namespace Constants & Functions
Namespaces can contain constants and functions, not just classes. Import them with 'use const' and 'use function' (PHP 5.6+). This is useful for configuration constants and utility functions. Unqualified function/constant calls have a fallback behavior: PHP first looks in the current namespace, then falls back to the global namespace. This is why you can call strlen() without a backslash — but for performance and clarity, prefix global functions with \ in namespaced code. Group imports (use App\Config\{const DB_HOST, function connect}) reduce verbosity.
<?php
namespace App\Config;
// Constants in namespaces
const DB_HOST = 'localhost';
const DB_PORT = 3306;
// Functions in namespaces
function connect() {
return 'Connected to ' . DB_HOST;
}
// Using namespaced constants and functions
use const App\Config\DB_HOST;
use function App\Config\connect;
echo DB_HOST; // localhost
echo connect(); // Connected to localhost
// Or with fully-qualified names:
echo \App\Config\DB_HOST;
echo \App\Config\connect();
// Namespace-level use for multiple imports
use App\Config\{const DB_HOST, const DB_PORT, function connect};
// Fallback: unqualified function calls fall back to global
// if not found in current namespace
namespace App;
$len = strlen('hello'); // calls global strlen() (fallback)Anonymous Classes & Autoloading
Anonymous classes (PHP 7+) let you create simple, one-off objects without defining a named class — useful for interfaces, mock objects, and callbacks. They can implement interfaces, extend classes, have constructors, and use traits. The class is generated at runtime with an auto-generated name (class@anonymous). Anonymous classes are loaded immediately (no autoloading needed). Use them for: simple strategy patterns, test doubles/mocks, event listeners, and DTOs. For reusable classes, always define named classes with proper PSR-4 autoloading. Anonymous classes are especially handy in tests for creating lightweight stubs.
<?php
namespace App\Factory;
// Anonymous class (PHP 7+): create a one-off class inline
interface Logger {
public function log(string $msg): void;
}
class App {
private $logger;
public function setLogger(Logger $logger) {
$this->logger = $logger;
}
}
$app = new App();
$app->setLogger(new class implements Logger {
public function log(string $msg): void {
echo "[LOG] $msg\n";
}
});
// Anonymous class with constructor
$comparator = new class($ascending = true) {
private $asc;
public function __construct(bool $asc) { $this->asc = $asc; }
public function compare($a, $b): int {
return $this->asc ? $a <=> $b : $b <=> $a;
}
};
// Get the auto-generated class name
echo get_class($comparator); // class@anonymous...OOP Deep Dive (Traits, Interfaces, Abstract)
Abstract Classes & Methods
Abstract classes provide a base with shared implementation that subclasses extend. They can't be instantiated directly. Abstract methods define a contract (signature only) that concrete subclasses must implement — this is 'template method pattern'. Unlike interfaces, abstract classes can have properties, constructors, and concrete methods. Use abstract classes when subclasses share significant implementation (the 'is-a' relationship). Use interfaces when you just need a contract that any class can implement (the 'can-do' relationship). A class can extend only one abstract class but implement multiple interfaces.
<?php
// Abstract class: can't be instantiated, may have abstract methods
abstract class Animal {
protected $name;
public function __construct(string $name) {
$this->name = $name;
}
// Abstract method: must be implemented by subclasses
abstract public function makeSound(): string;
// Concrete method: shared implementation
public function describe(): string {
return $this->name . " says " . $this->makeSound();
}
}
class Dog extends Animal {
public function makeSound(): string {
return "Woof!";
}
}
class Cat extends Animal {
public function makeSound(): string {
return "Meow!";
}
}
$dog = new Dog("Rex");
echo $dog->describe(); // Rex says Woof!
// new Animal("test"); // Error: cannot instantiate abstract classInterfaces & Multiple Implementation
Interfaces define a contract — method signatures without implementation. A class can implement multiple interfaces (unlike single inheritance for classes). Interfaces enable polymorphism: any class implementing Comparable can be sorted, regardless of its concrete type. Use interfaces to define capabilities (Comparable, Serializable, Iterable) that cut across class hierarchies. Type hinting with interfaces (function sort(Comparable $a)) is more flexible than concrete classes. Interface inheritance (interface A extends B, C) combines contracts. Modern PHP also supports interface constants and static methods in interfaces.
<?php
// Interface: pure contract (no implementation)
interface Comparable {
public function compareTo($other): int;
}
interface Serializable {
public function serialize(): string;
public function unserialize(string $data): void;
}
// A class can implement MULTIPLE interfaces
class Product implements Comparable, Serializable {
private $price;
public function __construct(float $price) {
$this->price = $price;
}
public function compareTo($other): int {
return $this->price <=> $other->price; // spaceship operator
}
public function serialize(): string {
return serialize($this->price);
}
public function unserialize(string $data): void {
$this->price = unserialize($data);
}
}
// Type hinting with interfaces
function sortItems(array $items): array {
usort($items, fn($a, $b) => $a->compareTo($b));
return $items;
}
// Interface inheritance
interface Repository extends Comparable, Serializable {
public function find(int $id): ?object;
}Traits (Code Reuse Without Inheritance)
Traits provide horizontal code reuse — methods that can be 'pasted' into any class without inheritance. This solves the diamond problem (PHP has single inheritance). Common trait uses: logging, singleton pattern, soft deletes, timestamps. A class can use multiple traits. When traits have conflicting methods, use 'insteadof' to choose one and 'as' to alias the other. Traits can have abstract methods (forcing the using class to implement them) and static methods/properties. Be careful not to overuse traits — they can make code harder to trace. Prefer composition (injecting dependencies) over traits for complex behavior.
<?php
// Trait: reusable method groups (PHP's answer to multiple inheritance)
trait Logger {
protected function log(string $msg, string $level = 'INFO'): void {
echo "[$level] " . date('Y-m-d H:i:s') . " $msg\n";
// In real code: write to file/database
}
}
trait Singleton {
private static $instance;
public static function getInstance(): self {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
}
class UserService {
use Logger, Singleton;
public function findUser($id) {
$this->log("Finding user $id");
return "User $id";
}
}
$svc = UserService::getInstance();
$svc->findUser(42); // [INFO] 2024-06-15 14:30:00 Finding user 42
// Conflict resolution when traits have same method
trait A { public function hello() { return 'A'; } }
trait B { public function hello() { return 'B'; } }
class C {
use A, B {
B::hello insteadof A; // use B's hello
A::hello as helloFromA; // alias A's as helloFromA
}
}Late Static Binding (static:: vs self::)
Late Static Binding (LSB) is the difference between self:: (compile-time, always refers to the defining class) and static:: (runtime, refers to the calling class). This matters in inheritance: if Base has a method using self::$table, it always sees Base's $table even when called on Child. Using static::$table makes it see Child's $table. LSB is essential for factory patterns (new static() creates instances of the called class), ActiveRecord (each model has its own table), and the singleton pattern. The 'static' return type (PHP 8+) declares that the method returns an instance of the called class.
<?php
class Base {
protected static $table = 'base';
public static function getTable(): string {
// self:: refers to the class where the method is DEFINED
return self::$table; // always 'base'
}
public static function getTableStatic(): string {
// static:: refers to the class that was CALLED (runtime)
return static::$table; // late static binding
}
public static function create(): static {
// 'static' return type + new static() = factory pattern
return new static();
}
}
class Child extends Base {
protected static $table = 'child';
}
echo Child::getTable(); // 'base' (self:: = Base)
echo Child::getTableStatic(); // 'child' (static:: = Child)
echo get_class(Child::create()); // 'Child' (new static = Child)
// self:: is resolved at compile time (the defining class)
// static:: is resolved at runtime (the calling class)
// This is "Late Static Binding" — essential for factory patternsMagic Methods
Magic methods are special methods that intercept object operations. __get/__set create dynamic properties (useful for data transfer objects, ORMs). __toString enables echo $object. __invoke makes an object callable like a function. __isset/__unset support isset()/unset() on dynamic properties. __debugInfo customizes var_dump output. Other magic methods: __construct, __destruct, __clone (for deep cloning), __call/__callStatic (for undefined methods, enables fluent APIs and mixins), __serialize/__unserialize (replaces __sleep/__wakeup in PHP 7.4+). Use magic methods sparingly — they add 'magic' behavior that can be hard to debug. Document them clearly.
<?php
class MagicBox {
private $data = [];
// Called when accessing undefined properties
public function __get($name) {
return $this->data[$name] ?? null;
}
// Called when setting undefined properties
public function __set($name, $value) {
$this->data[$name] = $value;
}
// Called when isset() or empty() on undefined property
public function __isset($name): bool {
return isset($this->data[$name]);
}
// Called when unset() on undefined property
public function __unset($name): void {
unset($this->data[$name]);
}
// Called when object is used as string
public function __toString(): string {
return json_encode($this->data);
}
// Called when object is called as function
public function __invoke($arg) {
return "Called with $arg";
}
// Called for var_dump/debugging
public function __debugInfo(): array {
return ['keys' => array_keys($this->data)];
}
}
$box = new MagicBox();
$box->name = "Alice"; // __set
echo $box->name; // __get → Alice
echo $box; // __toString → {"name":"Alice"}
echo $box("test"); // __invoke → Called with testComposer Package Management
composer.json Basics
composer.json is the manifest for PHP projects. require lists production dependencies with version constraints (^ allows minor updates, ~ allows patch). autoload defines PSR-4 namespace-to-directory mapping. require-dev holds development-only dependencies. Run composer install to set up the project.
{
"name": "myorg/myapp",
"type": "project",
"require": {
"php": ">=8.1",
"monolog/monolog": "^3.0",
"symfony/console": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": { "MyApp\\": "src/" }
}
}Installing & Updating
composer install reads composer.lock for exact versions (reproducible builds). composer require adds a package and resolves dependencies. composer update fetches newer versions within constraints. Use --no-dev for production. --optimize-autoloader converts PSR-4 to classmap for faster autoloading in production.
# Install all dependencies from composer.lock
composer install
# Add a package (modifies composer.json)
composer require monolog/monolog
composer require --dev phpunit/phpunit
# Update all packages to latest allowed versions
composer update
# Update a single package
composer update monolog/monolog
# Production install (no dev, optimized autoloader)
composer install --no-dev --optimize-autoloader
# Show installed packages
composer showVersion Constraints
Caret (^) is the most common constraint: allows changes that do not modify the leftmost non-zero digit. Tilde (~) locks to patch level. For 0.x versions, ^0.3 allows 0.3.x but not 0.4. Always use constraints to get security patches while avoiding breaking changes. Pin exact versions in composer.lock for reproducibility.
"require": {
// Caret: >=1.2.0, <2.0.0 (allows minor+patch)
"vendor/pkg": "^1.2",
// Tilde: >=1.2.0, <1.3.0 (patch only)
"vendor/pkg2": "~1.2",
// Exact version
"vendor/pkg3": "1.2.3",
// Range
"vendor/pkg4": ">=1.0 <2.0",
// Wildcard
"vendor/pkg5": "1.2.*",
// Stability flags
"vendor/pkg6": "dev-main",
"vendor/pkg7": "2.0@beta"
}PSR-4 Autoloading
PSR-4 autoloading maps namespace prefixes to directories: MyApp\Services\UserService resolves to src/Services/User.php. Run composer dump-autoload after adding new classes. For production, use --optimize to generate a classmap (one array lookup instead of filesystem checks). Classmap autoloading scans directories and is fastest for fixed codebases.
// PSR-4: "MyApp\\": "src/" means
// MyApp\User -> src/User.php
// MyApp\Services\Auth -> src/Services/Auth.php
namespace MyApp\Services;
class UserService
{
public function find(int $id): ?User
{
// ...
}
}
// After running composer dump-autoload:
// require 'vendor/autoload.php';
// $service = new \MyApp\Services\UserService();
// Classmap (faster for production)
// "autoload": { "classmap": ["src/", "lib/"] }Scripts & Hooks
Composer scripts define project-specific commands. Run with composer <name>. Built-in events (post-install-cmd, post-update-cmd, pre-autoload-dump) fire automatically. Scripts can reference other scripts with @name. Use scripts to standardize development workflows across team members.
{
"scripts": {
"test": "phpunit",
"lint": "phpcs --standard=PSR12 src/",
"fix": "phpcbf --standard=PSR12 src/",
"post-install-cmd": [
"MyApp\\Setup::postInstall",
"php artisan migrate"
],
"post-update-cmd": "@post-install-cmd"
}
}
// Run: composer test, composer lint, etc.Advanced cURL
Multi-Request (Parallel)
curl_multi_exec runs multiple requests in parallel, dramatically reducing total time for batch API calls. curl_multi_select blocks until there is activity, avoiding busy-waiting. Always close handles and the multi handle to free resources. This is the foundation of high-performance HTTP scraping and API aggregation.
<?php
$urls = [
'https://api.example.com/users',
'https://api.example.com/posts',
'https://api.example.com/comments',
];
$multi = curl_multi_init();
$handles = [];
foreach ($urls as $i => $url) {
$handles[$i] = curl_init($url);
curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
curl_setopt($handles[$i], CURLOPT_TIMEOUT, 10);
curl_multi_add_handle($multi, $handles[$i]);
}
do {
$status = curl_multi_exec($multi, $active);
if ($active) curl_multi_select($multi);
} while ($active && $status === CURLM_OK);
foreach ($handles as $i => $ch) {
$responses[$i] = curl_multi_getcontent($ch);
curl_multi_remove_handle($multi, $ch);
}
curl_multi_close($multi);Streaming Responses
CURLOPT_WRITEFUNCTION provides a callback for each chunk of the response, enabling streaming processing of large files without loading them entirely into memory. Return the chunk length to signal consumption. This is essential for downloading large files, processing streaming APIs, or parsing CSV/JSON incrementally.
<?php
$ch = curl_init('https://example.com/large-file.csv');
$file = fopen('download.csv', 'w');
curl_setopt($ch, CURLOPT_FILE, $file); // Write to file
// OR use callback for streaming processing:
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $chunk) use ($file) {
fwrite($file, $chunk);
// Or parse incrementally:
// $lines = explode("\n", $chunk);
return strlen($chunk); // Must return bytes consumed
});
curl_exec($ch);
fclose($file);
curl_close($ch);Authentication & Cookies
Set custom headers with CURLOPT_HTTPHEADER for authentication (Bearer tokens, API keys). COOKIEJAR/COOKIEFILE persist cookies between requests for session-based auth. CURLOPT_USERPWD sets HTTP Basic Auth. For POST, set CURLOPT_POSTFIELDS with JSON and the Content-Type header. Always set Accept to control response format.
<?php
$ch = curl_init('https://api.example.com/data');
// Bearer token
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token,
'Content-Type: application/json',
]);
// Cookie jar (persist across requests)
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt'); // Save
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt'); // Load
// Basic auth
curl_setopt($ch, CURLOPT_USERPWD, 'username:password');
// POST with JSON body
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['key' => 'value']));
$response = curl_exec($ch);Error Handling & Retries
Always check curl_exec return value (false on failure) and curl_error for the message. curl_getinfo provides HTTP status code, timing, and redirect info. Implement exponential backoff for retries to handle rate limits and transient failures. Distinguish between network errors (curl error) and HTTP errors (status code).
<?php
function fetchWithRetry(string $url, int $max = 3): ?string
{
for ($attempt = 1; $attempt <= $max; $attempt++) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$error = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response !== false && $code >= 200 && $code < 300) {
return $response;
}
if ($attempt < $max) {
sleep(pow(2, $attempt)); // Exponential backoff
}
}
throw new RuntimeException("Failed: {$error}");
}cURL Options Reference
CURLOPT_FOLLOWLOCATION follows HTTP redirects (3xx). Always keep SSL_VERIFYPEER true in production to prevent MITM attacks; download cacert.pem from curl.haxx.se. CURLOPT_ENCODING enables compression. Use CURLOPT_VERBOSE with STDERR for debugging connection issues. Set reasonable timeouts to avoid hanging.
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://example.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return not echo
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // Follow redirects
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
// SSL (keep VERIFYPEER true in production!)
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_CAINFO, '/path/to/cacert.pem');
// Performance
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_ENCODING, 'gzip');
// Debug
curl_setopt($ch, CURLOPT_VERBOSE, true);
curl_setopt($ch, CURLOPT_STDERR, fopen('curl.log', 'w'));Image Processing (GD)
Creating & Loading Images
imagecreatetruecolor creates a true color image (millions of colors). imagecolorallocate registers a color and returns an identifier. imagecreatefromjpeg/png/webp loads existing files. Always check the return value (false on failure). Use imagesx/imagesy to get dimensions. Free memory with imagedestroy when done.
<?php
// Create a blank image
$img = imagecreatetruecolor(400, 300);
// Allocate colors
$white = imagecolorallocate($img, 255, 255, 255);
$red = imagecolorallocate($img, 255, 0, 0);
// Fill background
imagefill($img, 0, 0, $white);
// Load existing images
$photo = imagecreatefromjpeg('photo.jpg');
$png = imagecreatefrompng('logo.png');
$webp = imagecreatefromwebp('image.webp');
// Get dimensions
$width = imagesx($photo);
$height = imagesy($photo);Drawing Shapes & Text
GD provides drawing primitives: rectangles, ellipses, lines, polygons, and arcs. Filled variants (imagefilled*) draw solid shapes. imagettftext renders TrueType fonts with angle and size control. Always send a Content-Type header before outputting image data. Call imagedestroy to free memory.
<?php
$img = imagecreatetruecolor(400, 300);
$white = imagecolorallocate($img, 255, 255, 255);
$red = imagecolorallocate($img, 255, 0, 0);
$blue = imagecolorallocate($img, 0, 0, 255);
imagefill($img, 0, 0, $white);
// Shapes
imagerectangle($img, 50, 50, 150, 120, $red);
imagefilledrectangle($img, 200, 50, 300, 120, $blue);
imageellipse($img, 100, 200, 80, 80, $red);
imageline($img, 0, 0, 400, 300, $red);
// Text with TrueType font
imagettftext($img, 20, 0, 50, 270, $red, 'arial.ttf', 'Hello GD!');
header('Content-Type: image/png');
imagepng($img);
imagedestroy($img);Resizing & Cropping
imagecopyresampled produces higher quality results than imagecopyresized (uses interpolation). Maintain aspect ratio by calculating dimensions from the original. For thumbnails, center-crop to a square for consistent layout. Always destroy source images after copying to prevent memory leaks in batch processing.
<?php
function resizeImage($src, $maxW, $maxH) {
list($w, $h) = getimagesize($src);
$ratio = min($maxW / $w, $maxH / $h);
$newW = (int)($w * $ratio);
$newH = (int)($h * $ratio);
$srcImg = imagecreatefromjpeg($src);
$dstImg = imagecreatetruecolor($newW, $newH);
// High-quality resampling
imagecopyresampled($dstImg, $srcImg, 0, 0, 0, 0,
$newW, $newH, $w, $h);
imagedestroy($srcImg);
return $dstImg;
}
// Center crop to square
function cropSquare($src, $size) {
list($w, $h) = getimagesize($src);
$min = min($w, $h);
$x = (int)(($w - $min) / 2);
$y = (int)(($h - $min) / 2);
$dst = imagecreatetruecolor($size, $size);
$img = imagecreatefromjpeg($src);
imagecopyresampled($dst, $img, 0, 0, $x, $y, $size, $size, $min, $min);
return $dst;
}Filters & Effects
imagefilter applies built-in effects: grayscale, brightness (range -255 to 255), contrast (negative increases), blur, edge detection, negate, and colorize (RGB + alpha). Pixelate creates a mosaic effect. These are fast but basic; for advanced effects, use ImageMagick (Imagick extension) which supports convolution matrices and custom filters.
<?php
$img = imagecreatefromjpeg('photo.jpg');
// Built-in filters
imagefilter($img, IMG_FILTER_GRAYSCALE); // B&W
imagefilter($img, IMG_FILTER_BRIGHTNESS, 30); // Brighten
imagefilter($img, IMG_FILTER_CONTRAST, -20); // More contrast
imagefilter($img, IMG_FILTER_GAUSSIAN_BLUR); // Blur
imagefilter($img, IMG_FILTER_EDGEDETECT); // Edge detection
imagefilter($img, IMG_FILTER_NEGATE); // Invert colors
// Colorize (tint)
imagefilter($img, IMG_FILTER_COLORIZE, 0, 0, 100, 0); // Blue tint
// Pixelate
imagefilter($img, IMG_FILTER_PIXELATE, 10, true);
imagepng($img, 'filtered.png');
imagedestroy($img);Watermarks & Compositing
imagecopymerge overlays one image onto another with adjustable opacity (0-100). PNG watermarks with alpha channels blend naturally. For text watermarks, use imagecolorallocatealpha for semi-transparent text. imagejpeg quality ranges from 0 (worst) to 100 (best); 75-90 is a good balance for web. Always destroy both images.
<?php
$photo = imagecreatefromjpeg('photo.jpg');
$watermark = imagecreatefrompng('logo.png');
$pw = imagesx($photo); $ph = imagesy($photo);
$ww = imagesx($watermark); $wh = imagesy($watermark);
// Position: bottom-right with 20px padding
$destX = $pw - $ww - 20;
$destY = $ph - $wh - 20;
// Merge with 50% opacity
imagecopymerge($photo, $watermark, $destX, $destY, 0, 0, $ww, $wh, 50);
// Text watermark
$color = imagecolorallocatealpha($photo, 255, 255, 255, 60);
imagettftext($photo, 30, 0, 20, $ph - 20, $color, 'arial.ttf', '© 2025');
imagejpeg($photo, 'watermarked.jpg', 90); // 90% quality
imagedestroy($photo);
imagedestroy($watermark);REST API Deep
Routing & Request Handling
REST APIs map HTTP methods to CRUD operations: GET (read), POST (create), PUT/PATCH (update), DELETE (delete). Parse the URL path for resource identification. Read request body from php://input for POST/PUT. Always return appropriate HTTP status codes (200, 201, 400, 404, 500) and JSON responses with Content-Type header.
<?php
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$segments = explode('/', trim($path, '/'));
switch (true) {
case $method === 'GET' && $segments[0] === 'users':
if (isset($segments[1])) getUser((int)$segments[1]);
else listUsers();
break;
case $method === 'POST' && $segments[0] === 'users':
$data = json_decode(file_get_contents('php://input'), true);
createUser($data);
break;
case $method === 'PUT' && $segments[0] === 'users' && isset($segments[1]):
$data = json_decode(file_get_contents('php://input'), true);
updateUser((int)$segments[1], $data);
break;
default:
http_response_code(404);
echo json_encode(['error' => 'Not Found']);
}Response & Status Codes
Always set Content-Type: application/json for API responses. Use correct status codes: 200 (OK), 201 (Created), 204 (No Content), 400 (Bad Request), 401 (Unauthorized), 403 (Forbidden), 404 (Not Found), 422 (Unprocessable Entity), 429 (Too Many Requests), 500 (Server Error). Include error details for debugging but never expose stack traces in production.
<?php
function jsonResponse($data, int $status = 200): void
{
http_response_code($status);
header('Content-Type: application/json');
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
exit;
}
// Success responses
jsonResponse(['data' => $users], 200); // OK
jsonResponse(['data' => $user], 201); // Created
// Error responses
jsonResponse(['error' => 'Validation failed'], 400);
jsonResponse(['error' => 'Unauthorized'], 401);
jsonResponse(['error' => 'Forbidden'], 403);
jsonResponse(['error' => 'Not Found'], 404);
jsonResponse(['error' => 'Server error'], 500);Pagination & Filtering
Implement pagination with LIMIT/OFFSET and return metadata (total, current page, total pages). Validate and sanitize sort columns against a whitelist to prevent SQL injection. Cap per_page to prevent excessive queries. Use LIKE for search with wildcards. Return pagination metadata in a meta object separate from data.
<?php
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = min(100, max(1, (int)($_GET['per_page'] ?? 20)));
$offset = ($page - 1) * $perPage;
// Whitelist sort columns (prevent SQL injection)
$sort = in_array($_GET['sort'] ?? '', ['name', 'email'])
? $_GET['sort'] : 'id';
$order = strtolower($_GET['order'] ?? 'asc') === 'desc' ? 'DESC' : 'ASC';
$sql = "SELECT * FROM users ORDER BY $sort $order LIMIT ? OFFSET ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$perPage, $offset]);
$users = $stmt->fetchAll();
// Total count for pagination metadata
$total = (int)$pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
jsonResponse([
'data' => $users,
'meta' => [
'page' => $page,
'per_page' => $perPage,
'total' => $total,
'total_pages' => ceil($total / $perPage),
],
]);Rate Limiting
Rate limiting prevents API abuse. Use fixed window (simple) or sliding window (more accurate) algorithms. Store counters in Redis for distributed systems. Return X-RateLimit headers (Limit, Remaining, Reset) so clients can self-regulate. HTTP 429 with Retry-After tells clients when to retry. For production, use Redis or a dedicated rate limiter.
<?php
function checkRateLimit(int $userId, int $max = 100, int $window = 3600): bool
{
$file = sys_get_temp_dir() . "/rate_{$userId}_" . floor(time() / $window);
$count = file_exists($file) ? (int)file_get_contents($file) : 0;
if ($count >= $max) {
$reset = (floor(time() / $window) + 1) * $window;
header("X-RateLimit-Limit: $max");
header("X-RateLimit-Remaining: 0");
header("Retry-After: " . ($reset - time()));
http_response_code(429);
echo json_encode(['error' => 'Rate limit exceeded']);
return false;
}
file_put_contents($file, $count + 1);
header("X-RateLimit-Remaining: " . ($max - $count - 1));
return true;
}
if (!checkRateLimit($userId)) exit;API Versioning
API versioning strategies: URL prefix (/v1/) is most explicit and cache-friendly; Accept header is RESTful but harder to test. Document APIs with OpenAPI (Swagger) annotations. Generate interactive docs with tools like swagger-php. Version from the start; breaking changes require a new version. Deprecate old versions with Sunset header.
<?php
// Versioning via URL prefix: /v1/users, /v2/users
$version = $segments[0] ?? 'v1';
// Versioning via Accept header
// Accept: application/vnd.myapp.v2+json
preg_match('/vnd\.myapp\.(v\d+)\+json/',
$_SERVER['HTTP_ACCEPT'] ?? '', $m);
$version = $m[1] ?? 'v1';
// OpenAPI/Swagger documentation
/**
* @OA\Get(path="/api/users",
* @OA\Response(response=200, description="List users")
* )
*/Security Deep (XSS/CSRF)
XSS Prevention
XSS (Cross-Site Scripting) injects malicious scripts into web pages. Prevent it by encoding output based on context: htmlspecialchars for HTML, json_encode for JavaScript, urlencode for URLs. ENT_QUOTES escapes both single and double quotes. Content-Security-Policy (CSP) adds defense-in-depth by restricting script sources. Never trust user input.
<?php
// Output encoding (prevents XSS)
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');
// JavaScript string (use json_encode)
echo '<script>var name = ' . json_encode($name) . ';</script>';
// URL parameter
echo 'redirect=' . urlencode($url);
// Content-Security-Policy header
header("Content-Security-Policy: default-src 'self'; script-src 'self'");
// Disable inline scripts
header("X-XSS-Protection: 1; mode=block");CSRF Protection
CSRF (Cross-Site Request Forgery) tricks users into submitting unwanted actions. Prevent it with anti-CSRF tokens: generate a random token per session, include it in forms as a hidden field, and verify on POST/PUT/DELETE. Use hash_equals for timing-safe comparison. For AJAX, send the token in a custom header. SameSite=Strict cookies provide additional protection.
<?php
// Generate CSRF token
function csrfToken(): string {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// Verify CSRF token (timing-safe)
function verifyCsrf(string $token): bool {
return !empty($_SESSION['csrf_token'])
&& hash_equals($_SESSION['csrf_token'], $token);
}
// In the form
echo '<input type="hidden" name="csrf_token" value="' . csrfToken() . '">';
// On POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verifyCsrf($_POST['csrf_token'] ?? '')) {
http_response_code(403);
die('CSRF token validation failed');
}
}SQL Injection Prevention
SQL injection allows attackers to execute arbitrary SQL. Always use prepared statements with parameterized queries: the database separates SQL logic from data, making injection impossible. Never concatenate user input into SQL strings. For dynamic queries (IN clauses, ORDER BY), build the SQL structure with placeholders and pass values as parameters.
<?php
// BAD: string concatenation (SQL injection vulnerable)
$sql = "SELECT * FROM users WHERE name = '" . $_GET['name'] . "'";
// Attack: ?name=' OR '1'='1
// GOOD: prepared statements
$stmt = $pdo->prepare('SELECT * FROM users WHERE name = ? AND status = ?');
$stmt->execute([$_GET['name'], 'active']);
$users = $stmt->fetchAll();
// Named parameters
$stmt = $pdo->prepare(
'INSERT INTO users (name, email) VALUES (:name, :email)'
);
$stmt->execute([':name' => $name, ':email' => $email]);
// Dynamic IN clause (still safe)
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);Password Hashing
Never store plaintext passwords. password_hash uses bcrypt (or Argon2 if available) with automatic salt generation. The hash includes the algorithm, cost, and salt, so password_verify can check against any format. Use password_needs_rehash to upgrade hashes when you increase the cost factor or switch algorithms. Argon2 is recommended for new applications.
<?php
// Hash a password (uses bcrypt by default, auto-salts)
$hash = password_hash('myPassword123', PASSWORD_DEFAULT);
// $2y$10$... (bcrypt with cost 10)
// Verify a password
if (password_verify($inputPassword, $storedHash)) {
// Check if rehash is needed (algorithm upgrade)
if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
$newHash = password_hash($inputPassword, PASSWORD_DEFAULT);
// Update stored hash in database
}
// Log user in
}
// Argon2 (PHP 7.2+, requires libsodium)
$hash = password_hash('password', PASSWORD_ARGON2ID, [
'memory_cost' => 65536, // 64 MB
'time_cost' => 4, // iterations
'threads' => 2,
]);Input Validation
Validate input on the server side always (client-side validation is for UX only). Use filter_input with FILTER_VALIDATE_* for type checking and FILTER_SANITIZE_* for cleaning. For custom rules, use regex or dedicated validation libraries (Respect/Validation, Symfony Validator). Use a whitelist approach: only accept known fields, reject everything else.
<?php
// Validate email
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email === false) {
$errors[] = 'Invalid email';
}
// Validate integer with range
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [
'options' => ['min_range' => 1, 'max_range' => 150]
]);
// Validate URL
$url = filter_input(INPUT_POST, 'website', FILTER_VALIDATE_URL);
// Custom validation with regex
function validateUsername(string $u): ?string {
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $u)) {
return 'Username must be 3-20 alphanumeric chars';
}
return null;
}
// Whitelist approach for arrays
$allowed = ['name', 'email', 'age'];
$input = array_intersect_key($_POST, array_flip($allowed));Namespaces & Autoloading Deep
Namespace Declaration
Namespaces prevent class name collisions and organize code hierarchically. The namespace declaration must be the first statement. use imports classes, functions, and constants. Aliases (as) resolve conflicts. PHP namespacing uses backslashes. The PSR-4 standard maps namespace separators to directory separators: MyApp\Services\UserService -> src/Services/UserService.php.
<?php
// File: src/Services/UserService.php
namespace MyApp\Services;
use MyApp\Models\User;
use MyApp\Repositories\UserRepository;
use MyApp\Exceptions\NotFoundException;
class UserService
{
public function __construct(
private UserRepository $repo
) {}
public function find(int $id): User
{
$user = $this->repo->findById($id);
if (!$user) {
throw new NotFoundException("User {$id} not found");
}
return $user;
}
}PSR-4 Autoloading
PSR-4 is the standard autoloading specification: a namespace prefix maps to a base directory, and each namespace separator becomes a directory separator. Composer generates the autoloader that resolves class names to file paths automatically. Run composer dump-autoload after adding new classes. The autoloader only loads classes when they are first referenced (lazy loading).
// composer.json
{
"autoload": {
"psr-4": {
"MyApp\\": "src/",
"Tests\\": "tests/"
}
}
}
// After composer install:
require __DIR__ . '/vendor/autoload.php';
// Now all classes auto-load:
use MyApp\Services\UserService;
use MyApp\Controllers\UserController;
// PSR-4 rules:
// MyApp\User -> src/User.php
// MyApp\Services\Auth -> src/Services/Auth.php
// MyApp\Models\User\Profile -> src/Models/User/Profile.phpCustom Autoloader
spl_autoload_register adds a function to the autoloader stack. When a class is referenced but not loaded, PHP calls each registered autoloader in order until one loads the class. Multiple autoloaders can coexist (e.g., one for PSR-4, one for legacy classes). Always check if the file exists before requiring to avoid errors. Composer uses this mechanism internally.
<?php
spl_autoload_register(function (string $class): void {
$prefix = 'MyApp\\';
$baseDir = __DIR__ . '/src/';
// Check if class uses our namespace prefix
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return; // Not our class
}
// Get relative class name
$relativeClass = substr($class, $len);
// Replace namespace separators with directory separators
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
if (file_exists($file)) {
require $file;
}
});
// Multiple autoloaders can be registered (stack-based)
spl_autoload_register(function ($class) {
$path = __DIR__ . '/legacy/' . $class . '.php';
if (file_exists($path)) require $path;
});Classmap & Files Autoloading
classmap autoloading scans directories at dump-autoload time and builds an array mapping class names to file paths. This is faster than PSR-4 (one array lookup vs filesystem checks) and is recommended for production. files autoloads specific files on every request, useful for helper functions and constants that cannot be autoloaded as classes. Use --optimize for production classmap.
{
"autoload": {
"psr-4": { "MyApp\\": "src/" },
"classmap": ["src/Legacy/", "lib/OldClasses.php"],
"files": ["src/helpers.php", "src/constants.php"]
}
}
// classmap: scans directories and builds class-to-file map
// Faster than PSR-4 for production (no filesystem checks)
// files: always-loaded files (for functions and constants)
// src/helpers.php:
<?php
function dd($var) { var_dump($var); die; }
function env(string $key, $default = null) { /* ... */ }Namespace Resolution
In namespaced code, unqualified class names resolve through imports first, then the current namespace. Built-in classes (DateTime, PDO, Exception) live in the global namespace; reference them with a leading backslash or import them. Functions and constants fall back to the global namespace if not found locally. Use FQCN (leading backslash) for absolute references.
<?php
namespace MyApp\Services;
use MyApp\Models\User;
class UserService
{
public function create(): User
{
// User resolves to MyApp\Models\User (imported)
return new User();
}
public function find(): \MyApp\Models\User
{
// Fully Qualified Class Name (leading backslash)
return new \MyApp\Models\User();
}
public function date(): \DateTime
{
// Global classes need backslash or import
return new \DateTime();
}
}Generators & Yield
Basic Generator
Generators produce values lazily with yield, one at a time, without building the entire collection in memory. This is memory-efficient for large or infinite sequences. The function returns a Generator object that implements Iterator. Each yield pauses execution, resumes on the next iteration. Use generators for reading large files, database cursors, and computed sequences.
<?php
function rangeGen(int $start, int $end, int $step = 1): Generator
{
for ($i = $start; $i <= $end; $i += $step) {
yield $i;
}
}
// Iterate without loading all values into memory
foreach (rangeGen(1, 5) as $value) {
echo $value . ' '; // 1 2 3 4 5
}
// Memory efficient for large sequences
foreach (rangeGen(1, 1000000) as $value) {
if ($value % 100000 === 0) echo "Reached {$value}\n";
}Yield Key-Value Pairs
Generators can yield key-value pairs using yield key => value syntax, just like associative arrays. This preserves keys through transformations. To filter values, simply do not yield them. The Generator maintains its position in the iteration, so you can build pipeline-style processing where each generator transforms or filters the stream.
<?php
function mapGen(array $data): Generator
{
foreach ($data as $key => $value) {
yield $key => strtoupper($value);
}
}
foreach (mapGen(['a' => 'hello', 'b' => 'world']) as $key => $value) {
echo "{$key} => {$value}\n";
}
// a => HELLO
// b => WORLD
// Filter by not yielding unwanted values
function filterGen(array $data): Generator
{
foreach ($data as $value) {
if ($value > 0) yield $value;
}
}Sending Values to Generators
The send() method passes a value into the generator, which becomes the result of the yield expression. This enables bidirectional communication, useful for coroutines and state machines. current() starts the generator. getReturn() retrieves the return value after the generator completes. The finally block runs when the generator is destroyed, enabling resource cleanup.
<?php
function accumulator(): Generator
{
$total = 0;
while (true) {
$value = yield $total;
if ($value === null) break;
$total += $value;
}
return $total;
}
$gen = accumulator();
$gen->current(); // Start: 0
$gen->send(10); // Returns 10
$gen->send(20); // Returns 30
$gen->send(5); // Returns 35
$gen->send(null); // Triggers return
echo $gen->getReturn(); // 35Yield from (Delegation)
yield from delegates to another generator, array, or Traversable, flattening its values into the outer generator. The return value of the inner generator is available to the outer generator. This enables composition: build complex pipelines from simple generators. yield from is also more efficient than iterating and re-yielding manually.
<?php
function innerGen(): Generator
{
yield 1;
yield 2;
return 'inner done';
}
function outerGen(): Generator
{
yield 0;
$result = yield from innerGen();
echo "Inner returned: {$result}\n";
yield 3;
}
foreach (outerGen() as $value) {
echo $value . ' '; // 0 1 2 3
}
// Inner returned: inner done
// Yield from arrays
function mixedGen(): Generator {
yield from [10, 20, 30];
yield from new ArrayObject([40, 50]);
}Practical Use Cases
Generators excel at processing large or infinite data streams: reading files line by line, database cursor iteration, paginated API fetching, and mathematical sequences. The take() pattern limits an infinite generator. Generators compose well: pipe data through multiple generators for filtering, mapping, and reducing. Memory stays constant regardless of data size.
<?php
// 1. Read large files line by line (memory efficient)
function readLines(string $file): Generator {
$handle = fopen($file, 'r');
while (!feof($handle)) {
$line = fgets($handle);
if ($line !== false) yield rtrim($line);
}
fclose($handle);
}
foreach (readLines('large.log') as $line) {
if (str_contains($line, 'ERROR')) {
echo "{$line}\n";
}
}
// 2. Infinite sequence with take()
function naturals(): Generator {
$n = 1;
while (true) yield $n++;
}
function take(Generator $gen, int $n): Generator {
for ($i = 0; $i < $n; $i++) {
yield $gen->current();
$gen->next();
}
}
foreach (take(naturals(), 5) as $num) {
echo $num . ' '; // 1 2 3 4 5
}Security
SQL Injection Prevention
SQL injection occurs when user input is concatenated into SQL. Always use prepared statements with parameterized queries. PDO and MySQLi both support them. Never trust user input. Validate and sanitize all external data.
// BAD: vulnerable
$sql = "SELECT * FROM users WHERE name = '" . $_POST['name'] . "'";
// GOOD: prepared statements
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = :name");
$stmt->execute([':name' => $_POST['name']]);
$users = $stmt->fetchAll();XSS Prevention
XSS (Cross-Site Scripting) injects malicious scripts. htmlspecialchars converts special characters to HTML entities. ENT_QUOTES escapes both single and double quotes. Always escape when outputting user data to HTML. Use Content-Security-Policy headers for defense in depth.
// BAD: output without escaping
echo $_GET['name'];
// GOOD: escape output
echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');
// For HTML attributes
echo 'value="' . htmlspecialchars($value, ENT_QUOTES) . '"';Password Hashing
password_hash uses bcrypt (or argon2) with automatic salt generation. Never use md5 or sha1 for passwords. password_verify checks a password against a hash. password_needs_rehash allows upgrading hash algorithms. The salt is embedded in the hash string.
// Hash a password
$hash = password_hash('mypassword', PASSWORD_DEFAULT);
// Verify
if (password_verify('mypassword', $hash)) {
echo 'Valid password';
}
// Check if needs rehash
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$newHash = password_hash('mypassword', PASSWORD_DEFAULT);
}CSRF Protection
CSRF (Cross-Site Request Forgery) tricks users into unwanted actions. Generate a random token per session. Include it in forms as a hidden field. Verify on POST using hash_equals (timing-safe comparison). SameSite cookies provide additional protection.
session_start();
if (empty($_SESSION['token'])) {
$_SESSION['token'] = bin2hex(random_bytes(32));
}
// In form
echo '<input type="hidden" name="token" value="' . $_SESSION['token'] . '">';
// Verify
if (!hash_equals($_SESSION['token'], $_POST['token'] ?? '')) {
die('CSRF token mismatch');
}Session Security
cookie_httponly prevents JavaScript access. cookie_secure ensures HTTPS only. samesite=Strict prevents CSRF. use_strict_mode rejects uninitialized session IDs. session_regenerate_id prevents session fixation. Always regenerate after privilege changes.
// Secure session settings
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1); // HTTPS only
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);
session_start();
// Regenerate ID after login
session_regenerate_id(true);Related PHP snippets
Copy-paste ready code for common tasks.
Arrays and Array Functions in PHP
Create indexed, associative, and multidimensional arrays with map and filter.
String Functions in PHP
Manipulate strings with substr, replace, explode, and sprintf in PHP.
Read and Write Files in PHP
Read, write, append, and iterate files with PHP filesystem functions.
PDO Database Queries in PHP
Connect and run prepared statements safely with PDO in PHP.
Sessions and Cookies in PHP
Store user data across requests with sessions and cookies in PHP.
Classes and Inheritance in PHP
Define classes with constructors, visibility, and inheritance in PHP.
Was this helpful?