cheat sheets / php / Basics

PHP Cheat Sheet

A complete PHP cheat sheet covering every function, operator and pattern worth keeping close, updated for PHP 8.4. Search it, filter by version, copy what you need.

587snippets
13categories
95topic cards
8.4latest covered
/ to focus
version

Basics

Opening & Closing Tags

core
Standard opening tag, required at the top of every PHP file
<?php echo "Hello";
Hello
Omit the closing tag in pure PHP files to avoid stray whitespace
<?php // no ?> at the end of this file
Short echo tag, ideal inside templates
<p><?= $name ?></p>
<p>Ada</p>
Interleaving PHP with HTML markup
<?php if ($loggedIn): ?> <p>Welcome back</p> <?php endif; ?>

Output

core
echo prints one or more values and returns nothing
echo "Hello", " ", "world";
Hello world
print behaves like echo but returns 1, so it works in expressions
$ok = print "Saved";
Saved
printf writes a formatted string straight to output
printf("%s has %d points", "Ada", 42);
Ada has 42 points
sprintf returns the formatted string instead of printing it
$line = sprintf("%05.2f", 3.14159);
03.14
print_r renders a readable dump of arrays and objects
print_r(['a' => 1, 'b' => 2]);
Array ( [a] => 1 [b] => 2 )
var_dump reports the type and value, the go-to debugging call
var_dump(42, "42", true);
int(42) string(2) "42" bool(true)
var_export prints valid PHP source for the value
var_export([1, 2]);
array ( 0 => 1, 1 => 2, )
Capture output into a string with the buffer functions
ob_start(); echo "buffered"; $out = ob_get_clean();

Comments

core
Single line comment
// this line is ignored
Hash comment, identical behaviour to the double slash
# also a single line comment
Multi line comment
/* spans several lines */
Docblock, read by IDEs and documentation tools
/** * Adds two numbers. * * @param int $a * @param int $b * @return int */

Variables

core
Variables start with a dollar sign and need no declaration
$count = 10; $name = "Ada";
Names are case sensitive and may hold any type
$Total = 5; $total = 9; // a different variable
Assign by reference so both names point at one value
$a = 1; $b = &$a; $b = 7; // $a is now 7
Variable variables use the value of one name as another
$field = "email"; $$field = "a@b.co"; // sets $email
Unset removes a variable from the current scope
unset($temp);
Null coalescing reads a value with a fallback and never warns
$page = $_GET['page'] ?? 1;
7.0+

Constants

core
define registers a constant at runtime
define("SITE", "example.com"); echo SITE;
example.com
const is resolved at compile time and reads more cleanly
const MAX_UPLOAD = 5_000_000;
Check whether a constant exists before using it
if (defined('DEBUG')) { // ... }
Class constants belong to the type, not an instance
class Http { public const OK = 200; } echo Http::OK;
200
Magic constants report where the code sits
echo __LINE__, __FILE__, __DIR__; echo __FUNCTION__, __CLASS__, __METHOD__;
Built in constants worth remembering
echo PHP_EOL; // newline for the platform echo PHP_VERSION; // 8.3.6 echo PHP_INT_MAX; // 9223372036854775807

Data Types

core
The four scalar types
$int = 42; $float = 3.14; $string = "text"; $bool = true;
Compound and special types
$array = [1, 2, 3]; $object = new stdClass(); $null = null; $fn = fn($x) => $x * 2;
gettype names the type, get_debug_type is shorter and more accurate
echo gettype(1.5); // double echo get_debug_type(1.5); // float
8.0+
Type checks read better than comparing gettype output
is_int($v); is_float($v); is_string($v); is_bool($v); is_array($v); is_object($v); is_null($v); is_callable($v); is_numeric($v);
Readable numeric separators for large literals
$budget = 1_250_000;
7.0+
Integer literals in other bases
$hex = 0xFF; // 255 $bin = 0b1010; // 10 $oct = 017; // 15, legacy leading zero
Explicit octal prefix, clearer than a leading zero
$oct = 0o17; // 15
8.0+

Casting & Juggling

core
Explicit casts
$n = (int) "42abc"; // 42 $f = (float) "3.5"; // 3.5 $s = (string) 99; // "99" $b = (bool) "0"; // false $a = (array) $object;
Conversion helpers that behave like casts
intval("0x1A", 16); // 26 floatval("1.5kg"); // 1.5 strval(true); // "1" boolval([]); // false
Values that evaluate as false
false, 0, 0.0, "", "0", [], null
PHP 8 compares numeric strings more predictably
0 == "foo"; // false in PHP 8, true before "1" == "01"; // true, both numeric "10" == "1e1"; // true
8.0+
Strict comparison checks value and type together
0 === "0"; // false 1 === 1.0; // false
Settype changes a variable in place
$v = "7"; settype($v, "integer");

Include & Require

core
include continues on failure with a warning
include 'header.php';
require halts the script with a fatal error if missing
require 'config.php';
The _once variants skip files already loaded
require_once __DIR__ . '/bootstrap.php';
Composer autoloading replaces manual includes in real projects
require __DIR__ . '/vendor/autoload.php';
An included file can return a value
// config.php return ['db' => 'mysql']; // index.php $config = require 'config.php';

Strings

Quoting

core
Single quotes are literal, nothing is parsed except \' and \\
echo 'Total: $sum';
Total: $sum
Double quotes parse variables and escape sequences
$sum = 12; echo "Total: $sum\n";
Total: 12
Braces remove ambiguity around a parsed variable
echo "{$user['name']}'s cart"; echo "{$obj->price} EUR";
Heredoc parses like a double quoted string
$html = <<<HTML <p>Hello $name</p> HTML;
<p>Hello Ada</p>
Nowdoc is the single quoted equivalent, nothing is parsed
$raw = <<<'SQL' SELECT * FROM users WHERE id = $id SQL;
SELECT * FROM users WHERE id = $id
PHP 7.3 lets you indent the closing marker
$t = <<<TXT indented body TXT;
7.0+
Escape sequences inside double quotes
\n newline \t tab \\ backslash \" double quote \$ literal dollar \u{1F600} unicode codepoint

Joining & Splitting

core
The dot operator concatenates
$full = $first . ' ' . $last;
Append in place with the dot equals operator
$log = "start"; $log .= " -> done";
start -> done
implode glues an array into a string
echo implode(', ', ['a', 'b', 'c']);
a, b, c
explode splits a string into an array
$parts = explode('/', 'usr/local/bin');
['usr', 'local', 'bin']
Limit the number of pieces explode returns
explode(':', 'a:b:c', 2); // ['a', 'b:c']
str_split cuts a string into fixed size chunks
str_split('abcdef', 2); // ['ab', 'cd', 'ef']
Join lines back together with the platform newline
echo implode(PHP_EOL, $lines);

Searching

core
str_contains reads better than comparing strpos to false
if (str_contains($haystack, 'php')) { }
8.0+
Prefix and suffix checks
str_starts_with($url, 'https://'); str_ends_with($file, '.php');
8.0+
strpos returns the offset or false, so compare strictly
if (strpos($s, 'x') !== false) { }
Case insensitive search and reverse search
stripos($s, 'PHP'); // ignores case strrpos($s, '/'); // last occurrence
substr_count tallies occurrences
substr_count('a-b-c', '-'); // 2
strstr returns the remainder from the first match
strstr('user@mail.com', '@'); // @mail.com strstr('user@mail.com', '@', true); // user

Slicing & Trimming

core
substr extracts part of a string
substr('abcdef', 1, 3); // bcd
bcd
Negative offsets count back from the end
substr('abcdef', -2); // ef
ef
trim removes whitespace from both ends
trim(" spaced "); // spaced
Trim only one side, or strip specific characters
ltrim($s); rtrim($s, "/\n"); trim($s, '"');
substr_replace swaps a section for new text
substr_replace('abcdef', 'XY', 2, 2); // abXYef
Mask all but the last four characters
$card = str_repeat('*', 12) . substr($num, -4);
************4242

Case & Padding

core
Case conversion
strtolower('ABC'); // abc strtoupper('abc'); // ABC ucfirst('hello'); // Hello lcfirst('Hello'); // hello ucwords('two words'); // Two Words
Pad a string to a fixed width
str_pad('7', 3, '0', STR_PAD_LEFT); // 007
Repeat a string
str_repeat('-', 20);
Reverse and shuffle
strrev('abc'); // cba str_shuffle('abc');
Word wrap long text at a column
echo wordwrap($text, 72, "\n", true);
The quick brown fox jumps over the lazy dog
Truncate with an ellipsis when needed
$short = mb_strimwidth($t, 0, 50, '...');

Replacing

core
str_replace swaps every occurrence
str_replace('cat', 'dog', 'a cat here');
a dog here
Pass arrays to run several replacements at once
str_replace(['a', 'b'], ['1', '2'], $s);
Count how many replacements happened
str_replace('x', 'y', $s, $count);
Case insensitive replace
str_ireplace('PHP', 'php', $s);
strtr maps characters or whole pairs
strtr('hi', 'hi', 'HI'); // HI strtr($s, ['{n}' => $name]); // template
nl2br turns newlines into HTML line breaks
echo nl2br("line one\nline two");
line one<br /> line two

Formatting Numbers & Text

core
number_format adds separators and fixes decimals
number_format(1234567.891, 2); // 1,234,567.89
Use custom separators for other locales
number_format(1234.5, 2, ',', '.'); // 1.234,50
sprintf placeholders
%s string %d integer %f float %05d zero padded %.2f two decimals %x hex %b binary %% literal %
Positional arguments repeat a value
sprintf('%1$s-%1$s', 'a'); // a-a
vsprintf takes the arguments as an array
vsprintf('%s is %d', ['Ada', 36]);
Ada is 36
Align text into columns
printf("%-10s|%5d\n", $name, $qty);
Espresso | 2 Cornetto | 1

Multibyte & Encoding

core
Use the mb_ family for UTF-8 text
mb_strlen('café'); // 4 strlen('café'); // 5 bytes
Multibyte equivalents of the common calls
mb_substr($s, 0, 10); mb_strtoupper($s); mb_strpos($s, 'é');
Split a UTF-8 string into characters rather than bytes
mb_str_split('café'); // ['c', 'a', 'f', 'é']
7.0+
Set the internal encoding once at bootstrap
mb_internal_encoding('UTF-8');
Detect and convert encodings
mb_detect_encoding($s, ['UTF-8', 'ISO-8859-1']); mb_convert_encoding($s, 'UTF-8', 'ISO-8859-1');
Escape for HTML output, always do this before echoing user data
echo htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
&lt;b&gt;bold&lt;/b&gt;
URL encoding for query strings
urlencode('a b&c'); // a+b%26c rawurlencode('a b&c'); // a%20b%26c

Comparing

core
strcmp is binary safe and returns an ordering integer
strcmp('a', 'b'); // negative strcasecmp('A', 'a'); // 0
Compare only the first n characters
strncmp($a, $b, 4);
Natural order comparison handles embedded numbers
strnatcmp('img12', 'img2'); // positive
Constant time comparison for secrets
hash_equals($knownToken, $userToken);
Fuzzy matching helpers
similar_text($a, $b, $percent); levenshtein('kitten', 'sitting'); // 3 soundex('Robert'); metaphone('Thompson');

Arrays

Creating

core
Short array syntax, the modern default
$nums = [1, 2, 3];
Associative array with string keys
$user = [ 'name' => 'Ada', 'email' => 'ada@example.com', ];
Nested and multidimensional data
$rows = [ ['id' => 1, 'tag' => 'php'], ['id' => 2, 'tag' => 'sql'], ];
Append with empty brackets
$list = []; $list[] = 'first';
range builds a sequence
range(1, 5); // [1,2,3,4,5] range('a', 'e'); // ['a'..'e'] range(0, 10, 2); // [0,2,4,6,8,10]
array_fill and array_fill_keys prefill a structure
array_fill(0, 3, null); array_fill_keys(['a', 'b'], 0);
Trailing commas keep diffs clean
$config = [ 'debug' => true, 'cache' => false, ];

Reading & Writing

core
Read by key
echo $user['name']; echo $rows[0]['tag'];
Safe read with a default
$role = $user['role'] ?? 'guest';
7.0+
Check for a key, including keys holding null
array_key_exists('role', $user); isset($user['role']); // false when the value is null
Count elements, recursively if needed
count($nums); count($rows, COUNT_RECURSIVE);
Check emptiness
if ($nums === []) { } if (empty($nums)) { }
First and last elements without moving the pointer
$first = $nums[array_key_first($nums)]; $last = $nums[array_key_last($nums)];
7.0+

Adding & Removing

core
Push and pop at the end
array_push($stack, 'a', 'b'); $top = array_pop($stack);
Shift and unshift at the start
array_unshift($queue, 'first'); $next = array_shift($queue);
Remove by key
unset($user['password']);
Reindex after removing keys
$clean = array_values($sparse);
array_splice removes or inserts in the middle
array_splice($a, 2, 1); // remove one array_splice($a, 2, 0, ['new']); // insert
array_slice copies a range without changing the source
array_slice($a, 1, 3); array_slice($a, -2); // last two array_slice($a, 1, 3, true); // keep keys

Searching

core
in_array checks for a value, always pass strict
in_array('2', $nums, true);
array_search returns the key or false
$key = array_search('php', $tags, true);
array_keys lists keys, optionally filtered by value
array_keys($user); array_keys($scores, 100);
array_column pulls one field out of a record set
array_column($rows, 'tag'); array_column($rows, 'tag', 'id'); // keyed by id
array_find returns the first matching element
$hit = array_find($rows, fn($r) => $r['id'] === 2);
8.0+
array_any and array_all answer yes or no questions
array_any($nums, fn($n) => $n > 10); array_all($nums, fn($n) => $n > 0);
8.0+

Transforming

core
array_map applies a callback to every element
$doubled = array_map(fn($n) => $n * 2, $nums);
[2, 4, 6]
Map over two arrays in parallel
array_map(fn($a, $b) => $a . $b, $x, $y);
array_filter keeps elements that pass the test
$evens = array_filter($nums, fn($n) => $n % 2 === 0);
[1 => 2]
Filter with access to the key
array_filter($a, $fn, ARRAY_FILTER_USE_KEY); array_filter($a, $fn, ARRAY_FILTER_USE_BOTH);
Drop falsy values by omitting the callback
$clean = array_filter($input);
array_reduce folds an array into one value
$total = array_reduce($items, fn($c, $i) => $c + $i['price'], 0);
24.90
array_walk mutates elements in place
array_walk($a, function (&$v, $k) { $v = trim($v); });

Combining

core
array_merge appends and renumbers integer keys
array_merge([1, 2], [3, 4]); // [1,2,3,4]
Later string keys overwrite earlier ones
array_merge(['a' => 1], ['a' => 9]); // ['a' => 9]
The plus operator keeps the left hand value
['a' => 1] + ['a' => 9, 'b' => 2]; // a stays 1
Spread operator unpacks arrays inline
$all = [...$first, ...$second];
7.0+
String keys can be spread since PHP 8.1
$merged = [...$defaults, ...$overrides];
8.0+
array_combine pairs keys with values
array_combine(['a', 'b'], [1, 2]);
Recursive merge for nested configuration
array_merge_recursive($base, $override); array_replace_recursive($base, $override);

Sorting

core
Sort values, discarding the original keys
sort($a); // ascending rsort($a); // descending
Sort while keeping key associations
asort($a); // by value arsort($a); // by value, descending
Sort by key
ksort($a); krsort($a);
usort takes a comparison callback
usort($rows, fn($a, $b) => $a['age'] <=> $b['age']);
7.0+
Sort by several fields in priority order
usort($rows, fn($a, $b) => [$a['dept'], $a['name']] <=> [$b['dept'], $b['name']] );
7.0+
uasort and uksort preserve or compare keys
uasort($a, $cmp); uksort($a, $cmp);
Natural and case insensitive ordering
natsort($files); natcasesort($files);
Reverse and shuffle
array_reverse($a); array_reverse($a, true); // keep keys shuffle($a);

Sets & Comparison

core
Remove duplicate values
array_unique($tags);
Difference and intersection by value
array_diff($a, $b); array_intersect($a, $b);
Compare keys instead of values
array_diff_key($a, $b); array_intersect_key($a, $b);
Compare both keys and values
array_diff_assoc($a, $b); array_intersect_assoc($a, $b);
Flip keys and values
array_flip(['a' => 1, 'b' => 2]); // [1 => 'a', 2 => 'b']
Check whether an array is a plain zero indexed list
array_is_list([1, 2, 3]); // true
8.0+

Maths & Chunking

core
Sum and product
array_sum([1, 2, 3]); // 6 array_product([2, 3, 4]); // 24
Highest and lowest
max($nums); min($nums); max(3, 9, 5); // 9
Split into fixed size groups
array_chunk($rows, 3); array_chunk($rows, 3, true); // preserve keys
[[1, 2, 3], [4, 5, 6], [7]]
Pad an array to a given length
array_pad([1, 2], 4, 0); // [1,2,0,0]
Pick random keys
$key = array_rand($a); $keys = array_rand($a, 2);
Count how often each value appears
array_count_values(['a', 'b', 'a']); // ['a' => 2, 'b' => 1]
['a' => 2, 'b' => 1]

Destructuring

core
Assign array elements to separate variables
[$x, $y] = [10, 20];
7.0+
$x is 10, $y is 20
Skip elements you do not need
[, , $third] = $parts;
7.0+
Destructure by key
['name' => $name, 'email' => $mail] = $user;
7.0+
$name is 'Ada', $mail is 'ada@example.com'
Destructure inside a loop
foreach ($rows as ['id' => $id, 'tag' => $tag]) { echo "$id: $tag"; }
7.0+
Swap two variables in one statement
[$a, $b] = [$b, $a];
7.0+
compact and extract convert between variables and arrays
$data = compact('name', 'email'); extract(['name' => 'Ada']); // creates $name
Nested destructuring
[[$a, $b], [$c, $d]] = [[1, 2], [3, 4]];
7.0+

SPL & Iterators

core
ArrayObject wraps an array in an object you can pass by handle
$bag = new ArrayObject(['a' => 1]); $bag['b'] = 2; foreach ($bag as $k => $v) { } count($bag);
SplStack is a last in, first out structure
$stack = new SplStack(); $stack->push('a'); $stack->push('b'); echo $stack->pop();
b
SplQueue is first in, first out
$queue = new SplQueue(); $queue->enqueue('first'); $queue->enqueue('second'); echo $queue->dequeue();
first
SplObjectStorage is a set of objects, useful for visited checks
$seen = new SplObjectStorage(); $seen->attach($node); if ($seen->contains($node)) { } $seen->detach($node);
SplFixedArray uses less memory for large fixed length lists
$grid = new SplFixedArray(1000); $grid[0] = 'x';
WeakMap lets the garbage collector reclaim keys
$cache = new WeakMap(); $cache[$object] = $expensiveResult; // entry disappears when $object is collected
8.0+
SplPriorityQueue pops the highest priority first
$q = new SplPriorityQueue(); $q->insert('low', 1); $q->insert('urgent', 10); echo $q->extract();
urgent
iterator_to_array materialises any iterator or generator
$all = iterator_to_array(counter(3)); $all = iterator_to_array($gen, false); // discard keys
array_multisort orders one array by another
array_multisort($scores, SORT_DESC, $names); // $names is reordered to match $scores
Implement Countable and IteratorAggregate for collection classes
class Cart implements Countable, IteratorAggregate { private array $lines = []; public function count(): int { return count($this->lines); } public function getIterator(): ArrayIterator { return new ArrayIterator($this->lines); } }
7.0+
ArrayAccess makes an object behave like an array
class Config implements ArrayAccess { public function offsetExists(mixed $k): bool { } public function offsetGet(mixed $k): mixed { } public function offsetSet(mixed $k, mixed $v): void { } public function offsetUnset(mixed $k): void { } }
8.0+
Stringable marks a class that can stand in for a string
class Slug implements Stringable { public function __toString(): string { return $this->value; } }
8.0+

Control Flow

Conditionals

core
Standard if, elseif and else
if ($score > 90) { $grade = 'A'; } elseif ($score > 80) { $grade = 'B'; } else { $grade = 'C'; }
Alternative syntax, far more readable inside templates
<?php if ($user): ?> <p>Hi <?= $user->name ?></p> <?php else: ?> <a href="/login">Sign in</a> <?php endif; ?>
Ternary for a short two way choice
$label = $count === 1 ? 'item' : 'items';
Short ternary returns the left side when it is truthy
$title = $input ?: 'Untitled';
Null coalescing only falls back on null or a missing key
$limit = $options['limit'] ?? 20;
7.0+
Null coalescing assignment writes only when unset
$config['cache'] ??= true;
7.0+
Nullsafe operator short circuits instead of erroring
$city = $user?->address?->city;
8.0+

Match & Switch

core
match compares strictly and returns a value
$name = match ($code) { 200, 201 => 'Success', 404 => 'Not found', default => 'Unknown', };
8.0+
Success // when $code is 200
match with no subject works as a clean condition chain
$band = match (true) { $n < 10 => 'low', $n < 100 => 'mid', default => 'high', };
8.0+
mid // when $n is 42
An unmatched value throws instead of failing quietly
// UnhandledMatchError when no arm matches $x = match ($v) { 1 => 'one' };
8.0+
switch uses loose comparison and needs break
switch ($role) { case 'admin': $level = 3; break; case 'editor': case 'author': $level = 2; break; default: $level = 1; }
Alternative switch syntax for templates
<?php switch ($tab): case 'home': ?> Home <?php break; endswitch; ?>

Loops

core
for loop with a counter
for ($i = 0; $i < 10; $i++) { echo $i; }
foreach over values
foreach ($items as $item) { echo $item; }
foreach with keys
foreach ($user as $field => $value) { echo "$field: $value"; }
Modify elements by taking a reference, then unset it
foreach ($prices as &$price) { $price *= 1.2; } unset($price);
while runs until the condition fails
while ($row = $stmt->fetch()) { process($row); }
do while always runs at least once
do { $attempt++; } while ($attempt < 3);
Alternative loop syntax for templates
<?php foreach ($rows as $row): ?> <li><?= $row['name'] ?></li> <?php endforeach; ?>

Loop Control

core
break leaves the loop
foreach ($rows as $row) { if ($row['id'] === $target) break; }
continue skips to the next iteration
foreach ($rows as $row) { if (!$row['active']) continue; render($row); }
Pass a level to break out of nested loops
foreach ($grid as $line) { foreach ($line as $cell) { if ($cell === 'x') break 2; } }
Loop with an index counter
foreach ($rows as $i => $row) { echo ($i + 1) . '. ' . $row['name']; }
Stripe alternate rows
$class = $i % 2 === 0 ? 'even' : 'odd';

Operators

core
Arithmetic
+ - * / % ** intdiv(7, 2); // 3, integer division 7 % 3; // 1 2 ** 10; // 1024
Assignment shorthands
$a += 5; $a -= 5; $a *= 2; $a /= 2; $a %= 3; $a **= 2; $s .= 'more';
Comparison
== equal after juggling === identical, value and type != <> not equal !== not identical < > <= >=
Spaceship returns -1, 0 or 1
1 <=> 2; // -1 2 <=> 2; // 0 3 <=> 2; // 1
7.0+
Logical operators
&& and || or ! not xor // and / or have lower precedence than =
Bitwise operators
& | ^ ~ << >> $flags | self::READ; $flags & self::WRITE;
Error suppression, avoid it in new code
$v = @$undefined; // hides the warning
avoid

Generators

core
yield produces values lazily without building an array
function counter(int $max): Generator { for ($i = 1; $i <= $max; $i++) { yield $i; } } foreach (counter(3) as $n) echo $n;
7.0+
123
Read a huge file line by line with almost no memory
function lines(string $path): Generator { $fh = fopen($path, 'r'); while (($line = fgets($fh)) !== false) { yield rtrim($line); } fclose($fh); }
7.0+
peak memory stays flat regardless of file size
Yield keys alongside values
yield $id => $row;
7.0+
Delegate to another generator
function all(): Generator { yield from first(); yield from second(); }
7.0+
A generator can return a final value
$gen = process(); foreach ($gen as $item) { } $summary = $gen->getReturn();
7.0+
Send values back into a generator
$gen->send('resume value');
7.0+

Functions

Declaring

core
A function with typed parameters and a return type
function add(int $a, int $b): int { return $a + $b; }
7.0+
Default values must follow required parameters
function greet(string $name, string $greeting = 'Hello'): string { return "$greeting, $name"; }
Nullable types accept the type or null
function find(?int $id): ?User { }
7.0+
Union types list several accepted types
function parse(int|string $input): array { }
8.0+
Intersection types require every listed interface
function save(Countable&Iterator $c): void { }
8.0+
void means the function returns nothing usable
function log(string $msg): void { file_put_contents('app.log', $msg, FILE_APPEND); }
7.0+
never marks a function that always throws or exits
function fail(string $m): never { throw new RuntimeException($m); }
8.0+
Enable strict typing at the very top of the file
declare(strict_types=1);
7.0+

Arguments

core
Named arguments skip the parameter order
setCookie(name: 'session', httpOnly: true, secure: true);
8.0+
Variadic functions collect the rest into an array
function sum(int ...$nums): int { return array_sum($nums); } sum(1, 2, 3); // 6
6
Spread an array into the argument list
$args = [1, 2, 3]; sum(...$args);
Pass by reference to modify the caller's variable
function addOne(array &$list): void { $list[] = 1; }
Static variables keep their value between calls
function counter(): int { static $n = 0; return ++$n; }
func_get_args reads every argument, useful in legacy code
$all = func_get_args();

Anonymous Functions

core
A closure assigned to a variable
$double = function (int $n): int { return $n * 2; }; echo $double(4);
8
use imports variables from the outer scope by value
$tax = 0.2; $withTax = function ($p) use ($tax) { return $p * (1 + $tax); };
Import by reference to write back to the outer variable
$total = 0; $add = function ($n) use (&$total) { $total += $n; };
Arrow functions capture the outer scope automatically
$withTax = fn($p) => $p * (1 + $tax);
7.0+
Arrow functions hold a single expression only
usort($rows, fn($a, $b) => $a['n'] <=> $b['n']);
7.0+
Immediately invoked closure
$config = (function () { return require 'config.php'; })();

Callables

core
Pass a function name as a string
array_map('strtoupper', $words);
Reference a method with the array form
usort($rows, [$sorter, 'compare']); usort($rows, ['SortHelper', 'compare']); // static
First class callable syntax, clean and checkable
$fn = strlen(...); $fn = $obj->handle(...); $fn = Sorter::compare(...);
8.0+
Bind a closure to an object instance
$bound = Closure::bind($fn, $object, Account::class);
Call any callable dynamically
call_user_func($fn, $arg); call_user_func_array($fn, $args);
An object becomes callable with __invoke
class Multiplier { public function __invoke(int $n): int { return $n * 3; } } $m = new Multiplier(); echo $m(4);
12

Recursion & Scope

core
A function that calls itself
function factorial(int $n): int { return $n <= 1 ? 1 : $n * factorial($n - 1); }
Walk a tree recursively
function flatten(array $tree): array { $out = []; foreach ($tree as $node) { $out[] = $node['name']; $out = array_merge($out, flatten($node['children'] ?? [])); } return $out; }
Functions do not see outer variables unless you pass them
$x = 5; function show() { // $x is not available here }
global pulls in a variable, prefer parameters instead
function show() { global $config; // works, but hard to test }
avoid
Check that a function exists before calling it
if (function_exists('bcadd')) { }

Fibers

modern
A fiber runs code that can pause and resume, added in PHP 8.1
$fiber = new Fiber(function (string $start): string { $received = Fiber::suspend($start . ' paused'); return $received . ' finished'; });
8.0+
Start it, then resume it with a value
$paused = $fiber->start('job'); // 'job paused' $result = $fiber->resume('job'); // returns from start echo $fiber->getReturn();
8.0+
job finished
Inspect the state before acting on it
$fiber->isStarted(); $fiber->isSuspended(); $fiber->isRunning(); $fiber->isTerminated();
8.0+
Fibers power async runtimes, you rarely write them by hand
// ReactPHP, Amp and Swoole build their // schedulers on top of Fiber
8.0+
Generators pause too, but only yield back to their caller
// Fiber : suspends the whole call stack // Generator: suspends one function
8.0+

Classes & OOP

Classes & Objects

core
A class with typed properties and a constructor
class User { public string $name; public int $age; public function __construct(string $name, int $age) { $this->name = $name; $this->age = $age; } } $u = new User('Ada', 36);
7.0+
Constructor property promotion removes the boilerplate
class User { public function __construct( public string $name, public int $age = 0, ) {} }
8.0+
Visibility controls access
public // anywhere protected // this class and children private // this class only
Access properties and methods with the arrow
echo $u->name; $u->rename('Grace');
Static members belong to the class
class Counter { public static int $count = 0; public static function bump(): void { static::$count++; } } Counter::bump();
readonly properties may be set once, in the constructor
class Money { public function __construct( public readonly int $amount, ) {} }
8.0+
A readonly class makes every property readonly
readonly class Point { public function __construct( public float $x, public float $y, ) {} }
8.0+
Clone copies an object, __clone fixes up nested references
$copy = clone $order; public function __clone(): void { $this->lines = array_map(fn($l) => clone $l, $this->lines); }

Inheritance

core
extend a parent class and call up with parent
class Admin extends User { public function __construct(string $name) { parent::__construct($name, 0); } }
Abstract classes cannot be instantiated
abstract class Shape { abstract public function area(): float; public function describe(): string { return static::class . ': ' . $this->area(); } }
final blocks further extension or overriding
final class Uuid { } class Base { final public function id(): string { } }
Late static binding resolves to the called class
class Model { public static function create(): static { return new static(); } }
8.0+
Check the type of an object
if ($obj instanceof Shape) { } get_class($obj); $obj::class;
8.0+

Interfaces & Traits

core
An interface defines a contract
interface Payable { public function total(): float; } class Invoice implements Payable { public function total(): float { return 99.0; } }
A class can implement several interfaces
class Cart implements Countable, IteratorAggregate { }
Interfaces may hold constants and extend one another
interface Status { const ACTIVE = 'active'; } interface Admin extends Status { }
Traits share method bodies across unrelated classes
trait Timestamps { public ?string $createdAt = null; public function touch(): void { $this->createdAt = date('c'); } } class Post { use Timestamps; }
Resolve conflicts when two traits share a method name
class Report { use A, B { A::render insteadof B; B::render as renderAlt; } }
Abstract methods in a trait force the user to implement them
trait Sluggable { abstract public function title(): string; }

Enums

modern
A pure enum lists a fixed set of cases
enum Status { case Draft; case Published; case Archived; } $s = Status::Draft;
8.0+
Backed enums carry a scalar value
enum Role: string { case Admin = 'admin'; case Editor = 'editor'; } echo Role::Admin->value;
8.0+
admin
Build an enum from its stored value
Role::from('admin'); // throws if unknown Role::tryFrom('nope'); // null if unknown
8.0+
Role::Admin null
List every case
foreach (Role::cases() as $case) { echo $case->name, '=', $case->value; }
8.0+
Admin=admin Editor=editor
Enums may hold methods and constants
enum Role: string { case Admin = 'admin'; public function label(): string { return match ($this) { Role::Admin => 'Administrator', }; } }
8.0+
Enums can implement interfaces
interface HasColour { public function colour(): string; } enum Level: int implements HasColour { case Low = 1; public function colour(): string { return 'green'; } }
8.0+

Magic Methods

core
__construct and __destruct bracket the object lifetime
public function __construct() { } public function __destruct() { }
__get and __set intercept inaccessible properties
public function __get(string $name): mixed { return $this->data[$name] ?? null; } public function __set(string $name, mixed $value): void { $this->data[$name] = $value; }
__isset and __unset complete the property overloading set
public function __isset(string $n): bool { return isset($this->data[$n]); } public function __unset(string $n): void { unset($this->data[$n]); }
__call and __callStatic catch undefined method calls
public function __call(string $m, array $args): mixed { } public static function __callStatic(string $m, array $args): mixed { }
__toString lets the object be used as a string
public function __toString(): string { return $this->name; } echo $user; // calls __toString
__invoke makes the object callable
public function __invoke(mixed $arg): mixed { }
__serialize and __unserialize control serialisation
public function __serialize(): array { return ['id' => $this->id]; }
7.0+

Namespaces & Autoloading

core
Declare a namespace as the first statement in the file
namespace App\Models; class User { }
Import a class with use
use App\Models\User; use App\Models\Post as BlogPost;
Group several imports from the same namespace
use App\Models\{User, Post, Comment};
Import functions and constants too
use function App\Helpers\slugify; use const App\Config\VERSION;
A leading backslash means the global namespace
$date = new \DateTimeImmutable();
The ::class constant resolves the fully qualified name
echo User::class; // App\Models\User
Register a PSR-4 autoloader in composer.json
{ "autoload": { "psr-4": { "App\\": "src/" } } }
Manual autoloader when Composer is not available
spl_autoload_register(function (string $class): void { $path = __DIR__ . '/src/' . str_replace('\\', '/', $class) . '.php'; if (is_file($path)) require $path; });

Newer Class Features

modern
new in initializers, allowed since PHP 8.1
class Service { public function __construct( private Logger $logger = new NullLogger(), ) {} }
8.0+
Attributes attach structured metadata to declarations
#[Route('/users', methods: ['GET'])] public function index(): Response { }
8.0+
Read attributes back through reflection
$r = new ReflectionMethod($obj, 'index'); foreach ($r->getAttributes(Route::class) as $attr) { $route = $attr->newInstance(); }
8.0+
Property hooks replace hand written getters, PHP 8.4
class Temp { public float $celsius = 0; public float $fahrenheit { get => $this->celsius * 9 / 5 + 32; set => $this->celsius = ($value - 32) * 5 / 9; } }
8.0+
Asymmetric visibility, public read and private write, PHP 8.4
class Order { public private(set) string $status = 'new'; }
8.0+
Chain a method straight off new without extra brackets, PHP 8.4
$name = new User('Ada')->name();
8.0+

Web & Requests

Superglobals

core
Query string values arrive in $_GET
$page = (int) ($_GET['page'] ?? 1);
Form posts arrive in $_POST
$email = trim($_POST['email'] ?? '');
$_SERVER carries request metadata
$_SERVER['REQUEST_METHOD']; // GET, POST $_SERVER['REQUEST_URI']; // /users?page=2 $_SERVER['HTTP_HOST']; $_SERVER['REMOTE_ADDR']; $_SERVER['HTTP_USER_AGENT']; $_SERVER['SCRIPT_NAME'];
Uploaded files land in $_FILES
$_FILES['avatar']['name']; $_FILES['avatar']['tmp_name']; $_FILES['avatar']['size']; $_FILES['avatar']['error'];
Cookies and session data
$_COOKIE['theme']; $_SESSION['user_id'];
Environment values
$_ENV['APP_ENV']; getenv('DATABASE_URL');
Avoid $_REQUEST, it hides where the value came from
$_REQUEST['id']; // mixes GET, POST and COOKIE
avoid

Reading Input Safely

core
Branch on the request method
if ($_SERVER['REQUEST_METHOD'] === 'POST') { handleForm(); }
Validate and cast rather than trusting input
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT); if ($id === false || $id === null) { http_response_code(400); exit('Bad id'); }
Common filter_var validators
filter_var($v, FILTER_VALIDATE_EMAIL); filter_var($v, FILTER_VALIDATE_URL); filter_var($v, FILTER_VALIDATE_INT); filter_var($v, FILTER_VALIDATE_FLOAT); filter_var($v, FILTER_VALIDATE_IP); filter_var($v, FILTER_VALIDATE_BOOLEAN);
Read a raw JSON request body
$raw = file_get_contents('php://input'); $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
7.0+
Parse a query string into an array
parse_str('a=1&b=2', $out);
['a' => '1', 'b' => '2']
Build a query string from an array
echo http_build_query(['page' => 2, 'q' => 'php']);
page=2&q=php
Split a URL into its parts
$parts = parse_url('https://a.com/p?x=1'); // scheme, host, path, query
['scheme' => 'https', 'host' => 'a.com', 'path' => '/p', 'query' => 'x=1']

Headers & Responses

core
Send a header before any output
header('Content-Type: application/json; charset=utf-8');
Redirect and stop the script
header('Location: /dashboard', true, 302); exit;
Set the status code on its own
http_response_code(404);
Return a JSON response
header('Content-Type: application/json'); echo json_encode(['ok' => true], JSON_THROW_ON_ERROR); exit;
7.0+
{"ok":true}
Force a file download
header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="report.csv"'); readfile($path);
Check whether headers were already sent
if (!headers_sent()) { header('X-Frame-Options: DENY'); }
Useful security headers
header('X-Content-Type-Options: nosniff'); header('Referrer-Policy: strict-origin-when-cross-origin'); header('Content-Security-Policy: default-src \'self\'');

Sessions

core
Start a session before writing to it
session_start(); $_SESSION['user_id'] = $user->id;
Read a session value with a fallback
$uid = $_SESSION['user_id'] ?? null;
7.0+
Harden the session cookie
session_start([ 'cookie_httponly' => true, 'cookie_secure' => true, 'cookie_samesite' => 'Lax', ]);
7.0+
Regenerate the id after login to block fixation
session_regenerate_id(true);
Log out completely
$_SESSION = []; session_destroy();
Check the session state
if (session_status() === PHP_SESSION_ACTIVE) { }

Cookies

core
Set a cookie with the options array
setcookie('theme', 'dark', [ 'expires' => time() + 86400 * 30, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax', ]);
7.0+
Read a cookie
$theme = $_COOKIE['theme'] ?? 'light';
Delete a cookie by expiring it
setcookie('theme', '', ['expires' => time() - 3600, 'path' => '/']);

File Uploads

core
Validate an upload before moving it
$f = $_FILES['avatar'] ?? null; if (!$f || $f['error'] !== UPLOAD_ERR_OK) { exit('Upload failed'); } if ($f['size'] > 2_000_000) { exit('File too large'); }
Detect the real type from the contents, never from the name
$mime = mime_content_type($f['tmp_name']); $allowed = ['image/jpeg', 'image/png', 'image/webp']; if (!in_array($mime, $allowed, true)) { exit('Unsupported type'); }
Move the file with a generated name
$ext = pathinfo($f['name'], PATHINFO_EXTENSION); $name = bin2hex(random_bytes(16)) . '.' . strtolower($ext); move_uploaded_file($f['tmp_name'], __DIR__ . "/uploads/$name");
Handle multiple files from one input
foreach ($_FILES['docs']['tmp_name'] as $i => $tmp) { $name = $_FILES['docs']['name'][$i]; }
Upload error constants
UPLOAD_ERR_OK // 0 UPLOAD_ERR_INI_SIZE // 1, larger than upload_max_filesize UPLOAD_ERR_FORM_SIZE // 2 UPLOAD_ERR_PARTIAL // 3 UPLOAD_ERR_NO_FILE // 4

HTTP Requests Out

core
A quick GET with the stream wrapper
$body = file_get_contents('https://api.example.com/items');
POST JSON with a stream context
$ctx = stream_context_create(['http' => [ 'method' => 'POST', 'header' => "Content-Type: application/json\r\n", 'content' => json_encode(['name' => 'Ada']), ]]); $body = file_get_contents($url, false, $ctx);
cURL gives full control over the request
$ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_TIMEOUT => 10, ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
Check for transport errors
if (curl_errno($ch)) { throw new RuntimeException(curl_error($ch)); }

Files & JSON

Reading

core
Read a whole file into a string
$text = file_get_contents(__DIR__ . '/notes.txt');
Read a file into an array of lines
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
Stream a large file line by line
$fh = fopen($path, 'r'); while (($line = fgets($fh)) !== false) { process($line); } fclose($fh);
Print a file straight to output
readfile($path);
Read a fixed number of bytes
$fh = fopen($path, 'rb'); $chunk = fread($fh, 8192);
Check the file before reading it
if (is_readable($path)) { }

Writing

core
Write a string, replacing the file
file_put_contents($path, $content);
Append instead of overwriting, with a lock
file_put_contents($log, "$line\n", FILE_APPEND | LOCK_EX);
Open, write and close a handle
$fh = fopen($path, 'w'); fwrite($fh, $data); fclose($fh);
File modes
'r' read only 'w' write, truncates or creates 'a' append, creates if missing 'x' create, fails if it exists 'r+' read and write 'b' binary safe, add to any mode
Lock a file while writing
$fh = fopen($path, 'c+'); if (flock($fh, LOCK_EX)) { ftruncate($fh, 0); fwrite($fh, $data); flock($fh, LOCK_UN); } fclose($fh);
Write atomically through a temporary file
$tmp = $path . '.tmp'; file_put_contents($tmp, $data); rename($tmp, $path);

Paths & Metadata

core
Directory of the current file, use it for every include
$root = __DIR__; require __DIR__ . '/../config.php';
Split a path into its parts
pathinfo('/var/www/app.min.js'); // dirname, basename, extension, filename
['dirname' => '/var/www', 'basename' => 'app.min.js', 'extension' => 'js', 'filename' => 'app.min']
Grab just the name or the folder
basename($path); // app.min.js basename($path, '.js'); // app.min dirname($path); // /var/www
Resolve a path and follow symlinks
$real = realpath('../uploads');
Existence and type checks
file_exists($p); is_file($p); is_dir($p); is_writable($p);
Size and modification time
filesize($p); date('Y-m-d', filemtime($p));
Free space on the volume
disk_free_space('/');

Directories

core
Create a directory tree
if (!is_dir($dir)) { mkdir($dir, 0755, true); }
List the entries in a directory
$items = array_diff(scandir($dir), ['.', '..']);
glob matches a pattern
foreach (glob($dir . '/*.php') as $file) { } glob($dir . '/{*.jpg,*.png}', GLOB_BRACE);
Walk a tree recursively
$it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS) ); foreach ($it as $file) { echo $file->getPathname(); }
Delete files and empty directories
unlink($file); rmdir($emptyDir);
Copy, move and rename
copy($from, $to); rename($from, $to);
Temporary files
$tmp = tempnam(sys_get_temp_dir(), 'rep'); $dir = sys_get_temp_dir();

JSON

core
Encode a value as JSON
echo json_encode(['id' => 1, 'ok' => true]);
{"id":1,"ok":true}
Readable output with unescaped slashes and unicode
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
{ "id": 1, "url": "https://a.com/p", "name": "café" }
Decode into an associative array
$data = json_decode($json, true);
Decode into objects by omitting the second argument
$obj = json_decode($json); echo $obj->name;
Throw on malformed JSON instead of returning null
try { $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $e) { // handle it }
7.0+
Check the last error the old way
if (json_last_error() !== JSON_ERROR_NONE) { echo json_last_error_msg(); }
Read and write a JSON file
$data = json_decode(file_get_contents($f), true); file_put_contents($f, json_encode($data, JSON_PRETTY_PRINT));
Control how an object serialises
class Point implements JsonSerializable { public function jsonSerialize(): array { return ['x' => $this->x, 'y' => $this->y]; } }

CSV

core
Read a CSV file row by row
$fh = fopen($path, 'r'); $head = fgetcsv($fh); while (($row = fgetcsv($fh)) !== false) { $record = array_combine($head, $row); } fclose($fh);
['id' => '1', 'name' => 'Ada']
Write rows to a CSV file
$fh = fopen($path, 'w'); fputcsv($fh, ['id', 'name']); foreach ($rows as $row) { fputcsv($fh, $row); } fclose($fh);
Stream a CSV download to the browser
header('Content-Type: text/csv'); header('Content-Disposition: attachment; filename="export.csv"'); $out = fopen('php://output', 'w'); fputcsv($out, ['id', 'name']);
Custom delimiters and enclosures
fgetcsv($fh, 0, ';', '"');
Parse a single CSV line held in a string
$fields = str_getcsv($line);

Streams & Wrappers

core
Built in stream wrappers
php://input raw request body php://output write straight to the browser php://memory in memory temp stream php://temp memory, spills to disk when large php://stdin command line input data:// inline data file:// http:// ftp:// zip:// phar://
Write to a memory stream instead of a temp file
$fh = fopen('php://memory', 'r+'); fwrite($fh, $csv); rewind($fh); echo stream_get_contents($fh);
Copy between two streams without loading it all
stream_copy_to_stream($source, $dest);
Set a timeout and other context options
$ctx = stream_context_create([ 'http' => ['timeout' => 5, 'ignore_errors' => true], ]);
Apply a filter as data passes through
stream_filter_append($fh, 'string.toupper'); stream_get_filters();
Read a gzipped file directly
$lines = file('compress.zlib://big.log.gz');
Disable remote includes unless you truly need them
allow_url_fopen = On // needed by file_get_contents on URLs allow_url_include = Off // never turn this on

Database

PDO Connection

core
Connect with sensible options
$pdo = 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, PDO::ATTR_EMULATE_PREPARES => false, ] );
DSN strings for the common drivers
mysql:host=127.0.0.1;dbname=app;charset=utf8mb4 pgsql:host=127.0.0.1;dbname=app sqlite:/var/data/app.sqlite sqlite::memory:
Wrap the connection so failures are handled
try { $pdo = new PDO($dsn, $user, $pass, $opts); } catch (PDOException $e) { error_log($e->getMessage()); exit('Database unavailable'); }

Queries

core
Prepared statement with positional placeholders
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?'); $stmt->execute([$id]); $user = $stmt->fetch();
Named placeholders read better in longer queries
$stmt = $pdo->prepare( 'SELECT * FROM posts WHERE status = :status AND author_id = :author' ); $stmt->execute(['status' => 'live', 'author' => $id]);
Fetch every row at once
$rows = $pdo->prepare('SELECT * FROM tags'); $rows->execute(); $all = $rows->fetchAll();
Fetch a single scalar value
$stmt = $pdo->prepare('SELECT COUNT(*) FROM users'); $stmt->execute(); $count = (int) $stmt->fetchColumn();
Loop the statement directly to save memory
foreach ($pdo->query('SELECT id, name FROM users') as $row) { echo $row['name']; }
Insert and read the new id
$stmt = $pdo->prepare( 'INSERT INTO users (name, email) VALUES (?, ?)' ); $stmt->execute([$name, $email]); $id = (int) $pdo->lastInsertId();
Count the rows an update or delete touched
$stmt = $pdo->prepare('DELETE FROM sessions WHERE expires < ?'); $stmt->execute([time()]); echo $stmt->rowCount();

Fetch Modes

core
Associative arrays, the usual choice
$stmt->fetch(PDO::FETCH_ASSOC);
Plain objects
$row = $stmt->fetch(PDO::FETCH_OBJ); echo $row->name;
Hydrate a class directly
$stmt->setFetchMode(PDO::FETCH_CLASS, User::class); $user = $stmt->fetch();
A single column as a flat list
$ids = $stmt->fetchAll(PDO::FETCH_COLUMN, 0);
Key value pairs from two columns
$map = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
Group rows under a shared first column
$grouped = $stmt->fetchAll(PDO::FETCH_GROUP);

Binding & Types

core
bindValue binds the value as it is now
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
bindParam binds a reference, read at execute time
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
Parameter types
PDO::PARAM_STR PDO::PARAM_INT PDO::PARAM_BOOL PDO::PARAM_NULL PDO::PARAM_LOB
LIMIT needs an integer bind when emulation is off
$stmt = $pdo->prepare('SELECT * FROM p LIMIT :n'); $stmt->bindValue(':n', $n, PDO::PARAM_INT); $stmt->execute();
Build an IN clause with the right number of placeholders
$in = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare("SELECT * FROM t WHERE id IN ($in)"); $stmt->execute($ids);
Escape LIKE wildcards in user input
$term = '%' . str_replace(['%', '_'], ['\%', '\_'], $q) . '%'; $stmt->execute([$term]);

Transactions

core
Wrap several writes so they succeed or fail together
$pdo->beginTransaction(); try { $pdo->prepare('UPDATE accounts SET bal = bal - ? WHERE id = ?') ->execute([$amount, $from]); $pdo->prepare('UPDATE accounts SET bal = bal + ? WHERE id = ?') ->execute([$amount, $to]); $pdo->commit(); } catch (Throwable $e) { $pdo->rollBack(); throw $e; }
Check whether a transaction is open
if ($pdo->inTransaction()) { $pdo->rollBack(); }
Insert many rows inside one transaction for speed
$pdo->beginTransaction(); $stmt = $pdo->prepare('INSERT INTO logs (msg) VALUES (?)'); foreach ($messages as $m) { $stmt->execute([$m]); } $pdo->commit();

MySQLi

core
Connect with mysqli and enable exceptions
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); $db = new mysqli('localhost', $user, $pass, 'app'); $db->set_charset('utf8mb4');
Prepared statement with bound parameters
$stmt = $db->prepare('SELECT name FROM users WHERE id = ?'); $stmt->bind_param('i', $id); $stmt->execute(); $result = $stmt->get_result(); $row = $result->fetch_assoc();
bind_param type characters
i integer d double s string b blob
Loop the result set
while ($row = $result->fetch_assoc()) { echo $row['name']; }
Insert id and affected rows
$db->insert_id; $db->affected_rows;
Close what you opened
$stmt->close(); $db->close();

Errors

Try, Catch, Finally

core
Catch an exception and recover
try { $result = risky(); } catch (RuntimeException $e) { error_log($e->getMessage()); $result = null; }
Catch several types in one block
try { send(); } catch (NetworkException | TimeoutException $e) { retry(); }
7.0+
finally always runs, even after a return or a throw
try { $fh = fopen($path, 'r'); return process($fh); } finally { if (isset($fh)) fclose($fh); }
Catch anything throwable, both Error and Exception
try { boot(); } catch (Throwable $e) { http_response_code(500); }
7.0+
Omit the variable when you do not need the object
try { parse($v); } catch (JsonException) { $v = []; }
8.0+
Rethrow while keeping the original as the previous exception
catch (PDOException $e) { throw new StorageException('Save failed', 0, $e); }

Throwing

core
Throw a built in exception
throw new InvalidArgumentException('Age must be positive');
throw is an expression since PHP 8, so it fits inline
$user = find($id) ?? throw new NotFoundException("User $id");
8.0+
Use it in arrow functions and ternaries too
$fn = fn($v) => $v > 0 ? $v : throw new RangeException();
8.0+
Define a custom exception
class PaymentFailed extends RuntimeException { public function __construct( public readonly string $reference, string $message = 'Payment failed', ) { parent::__construct($message); } }
8.0+
Read the details off a caught exception
$e->getMessage(); $e->getCode(); $e->getFile(); $e->getLine(); $e->getPrevious(); $e->getTraceAsString();

Exception Hierarchy

core
Throwable sits at the top of everything you can catch
Throwable ├── Error engine level failures │ ├── TypeError │ ├── ValueError │ ├── ArgumentCountError │ ├── ArithmeticError │ │ └── DivisionByZeroError │ └── AssertionError └── Exception application level failures ├── ErrorException ├── JsonException ├── RuntimeException └── LogicException
7.0+
LogicException marks a bug in your own code
BadFunctionCallException BadMethodCallException DomainException InvalidArgumentException LengthException OutOfRangeException
RuntimeException marks a failure only visible at runtime
OutOfBoundsException OverflowException RangeException UnderflowException UnexpectedValueException
Division by zero throws rather than warning
try { $r = intdiv(1, 0); } catch (DivisionByZeroError $e) { }
7.0+

Error Reporting

core
Show everything while developing
declare(strict_types=1); error_reporting(E_ALL); ini_set('display_errors', '1');
Log rather than display in production
error_reporting(E_ALL); ini_set('display_errors', '0'); ini_set('log_errors', '1'); ini_set('error_log', '/var/log/php/app.log');
Write your own message to the log
error_log('Cache miss for key ' . $key);
Error level constants
E_ALL every message E_ERROR fatal runtime error E_WARNING non fatal warning E_NOTICE likely a bug E_DEPRECATED scheduled for removal E_USER_ERROR raised by trigger_error
Raise your own notice or warning
trigger_error('Legacy call used', E_USER_DEPRECATED);

Handlers

core
Turn PHP errors into exceptions you can catch
set_error_handler(function (int $no, string $msg, string $file, int $line) { throw new ErrorException($msg, 0, $no, $file, $line); });
Catch anything that escapes to the top level
set_exception_handler(function (Throwable $e) { error_log($e->getMessage()); http_response_code(500); echo 'Something went wrong'; });
Run code when the script ends, including on a fatal error
register_shutdown_function(function () { $err = error_get_last(); if ($err && $err['type'] === E_ERROR) { error_log($err['message']); } });
Restore the previous handler
restore_error_handler(); restore_exception_handler();

Debugging

core
Dump and stop
var_dump($value); die();
Readable dump inside HTML
echo '<pre>'; print_r($data, false); echo '</pre>';
Print the current call stack
debug_print_backtrace(); $trace = debug_backtrace();
Measure elapsed time and memory
$start = hrtime(true); // work $ms = (hrtime(true) - $start) / 1_000_000; memory_get_usage(true); memory_get_peak_usage(true);
7.0+
Inspect a class at runtime
get_class_methods($obj); get_object_vars($obj); (new ReflectionClass($obj))->getProperties();

Dates & Maths

Formatting Dates

core
Format the current time
echo date('Y-m-d H:i:s');
2026-07-29 14:05:11
Format a specific timestamp
echo date('d/m/Y', 1735689600);
Common format characters
Y 2026 y 26 m 07 n 7 d 29 j 29 H 14 G 14 i 05 s 11 D Wed l Wednesday M Jul F July N 3 w 3 A PM a pm g 2 U timestamp
Standard formats
date(DATE_ATOM); // 2026-07-29T14:05:11+00:00 date('c'); // same as ATOM date('r'); // RFC 2822
Current timestamp
$now = time(); $ms = (int) (microtime(true) * 1000);
Build a timestamp from parts
$ts = mktime(9, 0, 0, 12, 25, 2026);
Parse a human readable string
strtotime('2026-07-29'); strtotime('+1 week'); strtotime('next monday'); strtotime('last day of this month');

DateTime Objects

core
Prefer the immutable class, it never changes underneath you
$d = new DateTimeImmutable('2026-07-29 10:00'); echo $d->format('D, d M Y');
Wed, 29 Jul 2026
Add and subtract intervals
$later = $d->modify('+3 days'); $prev = $d->sub(new DateInterval('P1M')); $next = $d->add(new DateInterval('PT2H30M'));
Interval format string
P1Y2M3D 1 year 2 months 3 days PT4H5M6S 4 hours 5 minutes 6 seconds P1W 1 week
Difference between two dates
$diff = $start->diff($end); echo $diff->days; // total days echo $diff->y, $diff->m, $diff->d; echo $diff->format('%a days');
94 0 3 4 94 days
Parse a non standard format explicitly
$d = DateTimeImmutable::createFromFormat('d/m/Y', '29/07/2026');
Compare dates with the normal operators
if ($start < $end) { } if ($due <= new DateTimeImmutable()) { }
Convert between an object and a timestamp
$ts = $d->getTimestamp(); $d = (new DateTimeImmutable())->setTimestamp($ts);

Time Zones

core
Set the default zone at bootstrap
date_default_timezone_set('Europe/Bucharest');
Create a date in a specific zone
$d = new DateTimeImmutable('now', new DateTimeZone('UTC'));
Convert an existing date to another zone
$local = $utc->setTimezone(new DateTimeZone('America/New_York'));
Store in UTC, display in the user's zone
$stored = new DateTimeImmutable($row['created_at'], new DateTimeZone('UTC')); $display = $stored->setTimezone(new DateTimeZone($user->tz));
List the available identifiers
DateTimeZone::listIdentifiers();

Date Helpers

core
Validate a calendar date
checkdate(2, 30, 2026); // false
Age in whole years
$age = (new DateTimeImmutable($dob)) ->diff(new DateTimeImmutable())->y;
36
Start and end of the current month
$first = new DateTimeImmutable('first day of this month 00:00'); $last = new DateTimeImmutable('last day of this month 23:59:59');
Loop over a range of days
$period = new DatePeriod( new DateTimeImmutable('2026-01-01'), new DateInterval('P1D'), new DateTimeImmutable('2026-01-08') ); foreach ($period as $day) { echo $day->format('Y-m-d'); }
2026-01-01 2026-01-02 2026-01-03 2026-01-04 2026-01-05 2026-01-06 2026-01-07
Relative time in words
$mins = (time() - $ts) / 60; $label = match (true) { $mins < 1 => 'just now', $mins < 60 => round($mins) . 'm ago', $mins < 1440 => round($mins / 60) . 'h ago', default => date('d M', $ts), };
8.0+
3h ago
Is it a weekend
$isWeekend = in_array((int) date('N', $ts), [6, 7], true);

Numbers & Maths

core
Rounding
round(3.456, 2); // 3.46 floor(3.9); // 3 ceil(3.1); // 4 round(2.5); // 3, half away from zero
Absolute value and sign
abs(-7); // 7 $sign = $n <=> 0;
7.0+
Integer division and remainder
intdiv(17, 5); // 3 17 % 5; // 2 fmod(17.5, 5); // 2.5
7.0+
Powers and roots
2 ** 8; // 256 pow(2, 8); // 256 sqrt(144); // 12 hypot(3, 4); // 5
Logarithms and constants
log(M_E); log10(1000); exp(1); M_PI; M_E; PHP_FLOAT_EPSILON;
Compare floats with a tolerance, never with equals
if (abs($a - $b) < PHP_FLOAT_EPSILON) { }
7.0+
Clamp a value into a range
$safe = max($min, min($max, $value));
Precise decimal maths for money
bcadd('0.1', '0.2', 2); // 0.30 bcmul($price, $qty, 2); bccomp($a, $b, 2);

Random Values

core
Cryptographically secure integers, use these for anything sensitive
$n = random_int(1, 100);
7.0+
Secure random bytes as a hex token
$token = bin2hex(random_bytes(32));
7.0+
c4f1a9e07b2d5836af10c7e94db2635f8a0e17c3b95d24618f7a0c3e5b91d476
Fast pseudo random numbers for non security work
mt_rand(1, 6); mt_rand(); random_int(0, PHP_INT_MAX); // prefer this
Pick a random element
$pick = $items[array_rand($items)];
Shuffle an array in place
shuffle($deck);
Generate a UUID v4
$b = random_bytes(16); $b[6] = chr((ord($b[6]) & 0x0f) | 0x40); $b[8] = chr((ord($b[8]) & 0x3f) | 0x80); $uuid = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4));
7.0+
9f8c2d41-6b3e-4a17-b0d5-72e1c4a8f930

Number Formatting

core
Thousands separators and fixed decimals
number_format(1234.5, 2); // 1,234.50 number_format(1234.5, 2, ',', ' '); // 1 234,50
Currency with the intl extension
$f = new NumberFormatter('en_GB', NumberFormatter::CURRENCY); echo $f->formatCurrency(1234.5, 'GBP');
Percentages
echo round($part / $total * 100, 1) . '%';
42.5%
Human readable file sizes
$units = ['B', 'KB', 'MB', 'GB']; $i = $bytes > 0 ? (int) floor(log($bytes, 1024)) : 0; echo round($bytes / 1024 ** $i, 2) . ' ' . $units[$i];
2.44 MB
Convert between bases
base_convert('ff', 16, 2); // 11111111 bindec('1010'); // 10 dechex(255); // ff decbin(10); // 1010
Check a value is numeric before casting
is_numeric('12.5'); // true is_numeric('12abc'); // false ctype_digit('123'); // true, string of digits only

Security & Regex

Passwords

core
Hash a password, never store it in plain text
$hash = password_hash($plain, PASSWORD_DEFAULT);
$2y$12$Km9r1XqYzL4pN8vTc.WdOeH3sB7uJ2aF5gRxE0iQ6nMlA8yZbCdKu
Verify a password against the stored hash
if (password_verify($plain, $user->hash)) { session_regenerate_id(true); $_SESSION['user_id'] = $user->id; }
Upgrade the hash when the algorithm moves on
if (password_needs_rehash($user->hash, PASSWORD_DEFAULT)) { $new = password_hash($plain, PASSWORD_DEFAULT); saveHash($user->id, $new); }
Tune the work factor to your hardware
password_hash($plain, PASSWORD_BCRYPT, ['cost' => 12]); password_hash($plain, PASSWORD_ARGON2ID);
Inspect an existing hash
password_get_info($hash);

Output & Input Safety

core
Escape every value you echo into HTML
echo htmlspecialchars($comment, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
&lt;script&gt;alert(1)&lt;/script&gt;
A short helper keeps templates readable
function e(?string $v): string { return htmlspecialchars($v ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } // <p><?= e($name) ?></p>
Escape differently depending on the context
htmlspecialchars($v) // HTML body and attributes rawurlencode($v) // URL path segment json_encode($v) // inside a script block
Strip tags when you want no markup at all
$plain = strip_tags($html); $some = strip_tags($html, '<p><a><strong>');
Sanitise then validate, in that order
$email = trim($_POST['email'] ?? ''); if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors[] = 'Enter a valid email address'; }
Always use prepared statements against the database
// safe $stmt = $pdo->prepare('SELECT * FROM u WHERE email = ?'); $stmt->execute([$email]); // never do this $pdo->query("SELECT * FROM u WHERE email = '$email'");

Tokens & Hashing

core
Generate a CSRF token once per session
if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); }
7.0+
Add the token to every state changing form
<input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
Compare tokens in constant time
if (!hash_equals($_SESSION['csrf'], $_POST['csrf'] ?? '')) { http_response_code(419); exit('Session expired, reload the page'); }
Hash data for integrity, not for passwords
hash('sha256', $payload); hash_file('sha256', $path);
a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e
Sign a payload with a shared secret
$sig = hash_hmac('sha256', $payload, $secret);
7d4e2a15c893bf60e1a7d4c2b98f5063e2a71c4d8b590f3e6a1c7d2b4f80e953
Encode and decode safely for URLs
$enc = rtrim(strtr(base64_encode($raw), '+/', '-_'), '=');
md5 and sha1 are fine for checksums, never for passwords
md5($file); // checksum only sha1($file); // checksum only
avoid

Regex Basics

core
preg_match tests a pattern and captures groups
if (preg_match('/^(\d{4})-(\d{2})$/', $s, $m)) { [$all, $year, $month] = $m; }
$year is '2026', $month is '07'
Named capture groups read far better
preg_match('/(?<year>\d{4})-(?<month>\d{2})/', $s, $m); echo $m['year'];
2026
preg_match_all collects every occurrence
preg_match_all('/#(\w+)/', $text, $m); $tags = $m[1];
['php', 'backend', 'pdo']
preg_replace rewrites matches
preg_replace('/\s+/', ' ', $messy);
one space between every word
Reference groups in the replacement
preg_replace('/(\w+)@(\w+)/', '$2 at $1', $s);
example at user
preg_replace_callback runs code per match
preg_replace_callback('/\d+/', fn($m) => $m[0] * 2, 'a1 b2' ); // a2 b4
7.0+
preg_split cuts on a pattern
preg_split('/[\s,]+/', 'a, b c');
['a', 'b', 'c']
Escape user input before putting it in a pattern
$safe = preg_quote($input, '/');

Regex Reference

core
Character classes
. any character except newline \d digit \D non digit \w word char \W non word \s whitespace \S non whitespace [abc] one of a b c [^a] anything but a [a-z] range
Quantifiers
* zero or more + one or more ? zero or one {3} exactly three {2,} two or more {2,5} between two and five *? lazy, matches as little as possible
Anchors and boundaries
^ start of subject or line $ end of subject or line \b word boundary \A start of subject only \z end of subject only
Groups and alternation
(abc) capturing group (?:abc) non capturing (?<n>abc) named group a|b either a or b
Pattern modifiers go after the closing delimiter
/pattern/i case insensitive /pattern/m multiline, ^ and $ per line /pattern/s dot matches newline too /pattern/u UTF-8 mode, use it for user text /pattern/x ignore whitespace in the pattern
Handy ready made patterns
'/^[\w.+-]+@[\w-]+\.[\w.]+$/' email shape '/^\+?[0-9\s-]{7,15}$/' phone shape '/^[a-z0-9]+(?:-[a-z0-9]+)*$/' slug '/^#?([a-f0-9]{6}|[a-f0-9]{3})$/i' hex colour

Environment & Config

core
Read configuration from the environment
$dsn = getenv('DATABASE_URL') ?: throw new RuntimeException('DATABASE_URL missing');
8.0+
Change a setting at runtime
ini_set('memory_limit', '512M'); ini_get('upload_max_filesize');
Check the running version
echo PHP_VERSION; if (PHP_VERSION_ID < 80200) { exit('PHP 8.2 or newer required'); }
Check that an extension is loaded
if (!extension_loaded('pdo_mysql')) { exit('pdo_mysql is required'); }
Give long running scripts more time
set_time_limit(0); ignore_user_abort(true);
Detect whether you are on the command line
if (PHP_SAPI === 'cli') { $args = $argv; }
Settings worth locking down in production
display_errors = Off expose_php = Off session.cookie_httponly = 1 session.cookie_secure = 1 allow_url_include = Off

Serialization Safety

core
unserialize on untrusted input can trigger object injection
// dangerous, attacker controls which classes get built $data = unserialize($_COOKIE['prefs']);
avoid
Restrict which classes may be built, or forbid all of them
$data = unserialize($raw, ['allowed_classes' => false]); $data = unserialize($raw, ['allowed_classes' => [Prefs::class]]);
7.0+
Prefer JSON for anything crossing a trust boundary
$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
7.0+
Validate a JSON string without building the value
if (!json_validate($raw)) { throw new UnexpectedValueException('Bad JSON'); }
8.0+
Sign data you hand to the client and verify it on the way back
$payload = base64_encode(json_encode($data)); $sig = hash_hmac('sha256', $payload, $secret); // on return if (!hash_equals($sig, hash_hmac('sha256', $payload, $secret))) { exit('Tampered payload'); }

Request Forgery & Redirects

core
Never pass a user supplied URL straight to a fetch
// SSRF: the attacker can reach your internal network $body = file_get_contents($_GET['url']);
avoid
Allow list the host before fetching anything
$host = parse_url($url, PHP_URL_HOST); if (!in_array($host, ['api.partner.com'], true)) { exit('Host not allowed'); }
Block private and loopback ranges
$ip = gethostbyname($host); if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { exit('Blocked address'); }
Stop cURL following redirects into somewhere private
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
Validate an internal redirect target so it cannot leave your site
$next = $_GET['next'] ?? '/'; if (!str_starts_with($next, '/') || str_starts_with($next, '//')) { $next = '/'; } header('Location: ' . $next, true, 303); exit;
8.0+
Header injection, strip newlines from anything you put in a header
$name = str_replace(["\r", "\n"], '', $userValue); header('X-Requested-Name: ' . $name);

filter_var Reference

core
Validation filters return the value or false
FILTER_VALIDATE_EMAIL FILTER_VALIDATE_URL FILTER_VALIDATE_INT FILTER_VALIDATE_FLOAT FILTER_VALIDATE_IP FILTER_VALIDATE_MAC FILTER_VALIDATE_REGEXP FILTER_VALIDATE_DOMAIN FILTER_VALIDATE_BOOLEAN
Sanitising filters strip characters instead of rejecting
FILTER_SANITIZE_EMAIL FILTER_SANITIZE_URL FILTER_SANITIZE_NUMBER_INT FILTER_SANITIZE_NUMBER_FLOAT FILTER_SANITIZE_SPECIAL_CHARS FILTER_SANITIZE_FULL_SPECIAL_CHARS FILTER_UNSAFE_RAW
Useful flags
FILTER_FLAG_ALLOW_FRACTION FILTER_FLAG_ALLOW_THOUSAND FILTER_FLAG_IPV4 / FILTER_FLAG_IPV6 FILTER_FLAG_NO_PRIV_RANGE FILTER_FLAG_NO_RES_RANGE FILTER_FLAG_PATH_REQUIRED FILTER_NULL_ON_FAILURE
Constrain an integer to a range
$age = filter_var($raw, FILTER_VALIDATE_INT, [ 'options' => ['min_range' => 0, 'max_range' => 130], ]);
Boolean validation understands the usual spellings
filter_var('yes', FILTER_VALIDATE_BOOLEAN); // true filter_var('off', FILTER_VALIDATE_BOOLEAN); // false filter_var('maybe', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); // null
Filter a whole array of input in one call
$clean = filter_var_array($_POST, [ 'email' => FILTER_VALIDATE_EMAIL, 'age' => FILTER_VALIDATE_INT, ]);

Tooling & Testing

Composer

core
Start a new project and add dependencies
composer init composer require monolog/monolog composer require --dev phpunit/phpunit
Install and update
composer install # from composer.lock, use in CI and production composer update # resolve fresh, updates the lock file composer update vendor/pkg composer install --no-dev --optimize-autoloader
Version constraints
^1.2.3 >=1.2.3 <2.0.0 any compatible release ~1.2.3 >=1.2.3 <1.3.0 patch level only 1.2.* >=1.2.0 <1.3.0 >=1.2 no upper bound, avoid this 1.2.3 pinned exactly
Inspect what you depend on
composer show composer show --tree composer why vendor/package composer outdated composer audit # known security advisories
Autoload configuration
{ "autoload": { "psr-4": { "App\\": "src/" }, "files": ["src/helpers.php"] }, "autoload-dev": { "psr-4": { "Tests\\": "tests/" } } }
Regenerate the autoloader after changing the mapping
composer dump-autoload composer dump-autoload --optimize # for production
Define scripts you can run by name
{ "scripts": { "test": "phpunit", "lint": "php-cs-fixer fix --dry-run --diff", "check": ["@lint", "@test"] } }
Load the autoloader as the first line of your entry point
require __DIR__ . '/vendor/autoload.php';

PHPUnit

core
A basic test class
use PHPUnit\Framework\TestCase; final class MoneyTest extends TestCase { public function test_it_adds_amounts(): void { $sum = (new Money(100))->plus(new Money(50)); $this->assertSame(150, $sum->amount); } }
The assertions you will use most
$this->assertSame($expected, $actual); // strict, prefer this $this->assertEquals($expected, $actual); // loose $this->assertTrue($v); $this->assertFalse($v); $this->assertNull($v); $this->assertNotNull($v); $this->assertCount(3, $array); $this->assertContains('a', $array); $this->assertInstanceOf(User::class, $obj); $this->assertStringContainsString('php', $text);
Assert that something throws
$this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Age must be positive'); new Person(age: -1);
Data providers run one test against many inputs
#[DataProvider('slugCases')] public function test_it_slugifies(string $in, string $out): void { $this->assertSame($out, slugify($in)); } public static function slugCases(): array { return [ 'spaces' => ['Hello World', 'hello-world'], 'accents' => ['Café Noir', 'cafe-noir'], ]; }
8.0+
Set up and tear down around each test
protected function setUp(): void { parent::setUp(); $this->pdo = new PDO('sqlite::memory:'); } protected function tearDown(): void { unset($this->pdo); parent::tearDown(); }
Test doubles for collaborators
$mailer = $this->createMock(Mailer::class); $mailer->expects($this->once()) ->method('send') ->with($this->equalTo('ada@example.com')) ->willReturn(true);
Run the suite
vendor/bin/phpunit vendor/bin/phpunit --filter test_it_adds_amounts vendor/bin/phpunit --testsuite unit vendor/bin/phpunit --coverage-html build/coverage
Minimal phpunit.xml
<?xml version="1.0"?> <phpunit bootstrap="vendor/autoload.php" colors="true"> <testsuites> <testsuite name="unit"> <directory>tests</directory> </testsuite> </testsuites> </phpunit>

Command Line

core
Check the version and loaded extensions
php -v php -m php -i | grep memory_limit
Run the built in development server
php -S localhost:8000 -t public
Check a file for syntax errors without running it
php -l index.php
Run a one liner or an interactive shell
php -r 'echo PHP_VERSION;' php -a
Read arguments passed to a script
// php import.php users.csv --dry-run $script = $argv[0]; $file = $argv[1] ?? null; $dryRun = in_array('--dry-run', $argv, true); echo $argc . ' arguments';
Parse options properly
$opts = getopt('f:v', ['file:', 'verbose', 'dry-run']); $file = $opts['file'] ?? $opts['f'] ?? null;
Read from standard input
while (($line = fgets(STDIN)) !== false) { echo strtoupper($line); }
Write to the right stream and exit with a status code
fwrite(STDERR, "Import failed\n"); exit(1); // non zero tells the shell it failed
Only run when invoked from the terminal
if (PHP_SAPI !== 'cli') { exit('Command line only'); }

Static Analysis & Style

core
PHPStan finds bugs without running the code
composer require --dev phpstan/phpstan vendor/bin/phpstan analyse src --level 8
phpstan.neon configuration
parameters: level: 8 paths: - src - tests ignoreErrors: - '#Call to an undefined method#'
Psalm is the main alternative, with its own type syntax
composer require --dev vimeo/psalm vendor/bin/psalm --init vendor/bin/psalm
PHP-CS-Fixer rewrites code to match a style
composer require --dev friendsofphp/php-cs-fixer vendor/bin/php-cs-fixer fix --dry-run --diff vendor/bin/php-cs-fixer fix
PHP_CodeSniffer reports violations instead of fixing them
vendor/bin/phpcs --standard=PSR12 src vendor/bin/phpcbf --standard=PSR12 src
Rector automates upgrades between PHP versions
composer require --dev rector/rector vendor/bin/rector process src --dry-run
Annotate types the engine cannot express
/** @param list<array{id:int, name:string}> $rows */ function render(array $rows): string { } /** @var non-empty-string $slug */

PSR Standards

core
The ones worth knowing by number
PSR-1 basic coding standard PSR-3 logger interface PSR-4 autoloading from namespace to path PSR-6 caching interface PSR-7 HTTP message interfaces PSR-11 container interface PSR-12 extended coding style PSR-14 event dispatcher PSR-15 HTTP server request handlers PSR-18 HTTP client
PSR-4 maps a namespace prefix onto a directory
// App\Service\Mailer -> src/Service/Mailer.php { "autoload": { "psr-4": { "App\\": "src/" } } }
PSR-3 gives you a logger you can swap out
use Psr\Log\LoggerInterface; public function __construct( private LoggerInterface $logger, ) {} $this->logger->error('Payment failed', ['order' => $id]);
8.0+
PSR-3 log levels, highest severity first
emergency alert critical error warning notice info debug

Performance

core
OPcache settings that matter in production
opcache.enable=1 opcache.memory_consumption=256 opcache.max_accelerated_files=20000 opcache.validate_timestamps=0 # requires a deploy reload opcache.jit_buffer_size=100M
Confirm it is actually running
var_dump(opcache_get_status()['opcache_enabled']); opcache_reset(); // clear after a deploy
Preload hot classes once at server start, PHP 7.4
// opcache.preload=/app/preload.php $files = glob(__DIR__ . '/src/**/*.php'); foreach ($files as $file) { opcache_compile_file($file); }
7.0+
The N plus 1 query, the most common real bottleneck
// slow: one query per row foreach ($posts as $post) { $post->author = findAuthor($post->author_id); } // fast: one query for all of them $ids = array_column($posts, 'author_id'); $authors = findAuthorsById($ids);
Measure before you optimise
$t = hrtime(true); $work(); printf("%.2f ms\n", (hrtime(true) - $t) / 1e6); printf("%.1f MB\n", memory_get_peak_usage(true) / 1048576);
7.0+
Xdebug and profiling
XDEBUG_MODE=profile php script.php XDEBUG_MODE=debug php -S localhost:8000 # read the cachegrind file in KCachegrind or PhpStorm

0 results

Nothing matches that. Try one word instead of several, or reset the version filter to All.

What PHP Is and Why It Still Runs the Web

PHP is a server side scripting language built for the web. Your browser never sees it. The server runs the code, produces HTML or JSON, and sends only the result down the wire.

Rasmus Lerdorf released the first version in 1995. Three decades later PHP powers WordPress, Laravel, Symfony, Drupal, Magento and a very large share of every site you visit.

The modern language is not the PHP of 2005. Typed properties, enums, readonly classes, match expressions, attributes and a genuinely fast engine landed between PHP 7.0 and PHP 8.4. If your mental model dates from the mysql_* era, the snippets above are worth a slow read.

This PHP cheat sheet covers the whole surface: syntax, strings, arrays, control flow, functions, objects, requests, files, databases, errors, dates, security and tooling. Search it, filter it by version, copy what you need.


PHP Syntax Basics

Tags and Statements

Every block of PHP opens with <?php. In files that contain only PHP, leave the closing tag off. A stray newline after ?> gets sent to the browser and breaks header calls later.

<?php
declare(strict_types=1);

echo "Hello, world";

Statements end with a semicolon. Blocks use braces. Whitespace is free, so format for the reader.

Echoing Into Templates

Inside a template, the short echo tag keeps the markup readable:

<ul>
<?php foreach ($items as $item): ?>
  <li><?= htmlspecialchars($item->name) ?></li>
<?php endforeach; ?>
</ul>

The alternative syntax with endforeach, endif and endwhile exists precisely for this. Braces buried in HTML are hard to match by eye.

Comments

Use // for a line, /* */ for a block, and a docblock when a function needs explaining. Modern editors read docblocks for autocomplete, so the effort pays back.


Variables, Types and Type Juggling

Variables start with $ and need no declaration. PHP infers the type from the value, which is convenient and occasionally dangerous.

Strict Types

Add declare(strict_types=1); as the first statement in every file. Without it, passing the string "5" to a function expecting int silently converts. With it, you get a TypeError at the boundary where the bug actually is.

Comparison Traps

Use === unless you have a specific reason not to. Loose comparison applies conversion rules that surprise people:

0 == "foo";     // false in PHP 8, true before it
"1" == "01";    // true, both read as numbers
null == false;  // true
[] == false;    // true
"abc" === "abc" // true, and unambiguous

PHP 8 rewrote string to number comparison so that a non numeric string is compared as a string rather than being cast to zero. That single change fixed a whole family of authentication bugs.

Falsy Values

These evaluate as false in a boolean context: false, 0, 0.0, "", "0", [] and null. Note that the string "0" is falsy while "0.0" is truthy, which is exactly the kind of thing === saves you from.


Working With Strings

Quotes Matter

Single quotes are literal. Double quotes parse variables and escape sequences. For anything with no interpolation, single quotes are marginally faster and always clearer about intent.

$name = 'Ada';
echo 'Hi $name';   // Hi $name
echo "Hi $name";   // Hi Ada
echo "Hi {$user['name']}"; // braces for complex expressions

Heredoc for Blocks

Heredoc keeps multi line templates readable without a pile of concatenation. Nowdoc, with the marker in single quotes, parses nothing at all and is ideal for raw SQL or code samples.

Multibyte Safety

If your text can contain accents, emoji or any non ASCII character, use the mb_ family. strlen('café') returns 5 because it counts bytes. mb_strlen('café') returns 4 because it counts characters. Set mb_internal_encoding('UTF-8') once at bootstrap.


Arrays: The PHP Workhorse

A PHP array is an ordered map. The same structure serves as a list, a dictionary, a stack and a queue, which is why almost every PHP function seems to take or return one.

Functional Style

Three functions cover most transformations:

  • array_map changes every element and keeps the count the same.
  • array_filter keeps the elements that pass a test and drops the rest.
  • array_reduce collapses the whole array into a single value.
$total = array_reduce(
    array_filter($orders, fn($o) => $o['paid']),
    fn($sum, $o) => $sum + $o['amount'],
    0
);

One thing to watch: array_filter preserves keys. If you plan to json_encode the result and expect a JSON array rather than an object, wrap it in array_values.

Sorting

The sort family is easy to mix up, so here it is in one place:

FunctionSorts byKeeps keys
sort / rsortValueNo
asort / arsortValueYes
ksort / krsortKeyYes
usortCallbackNo
uasortCallback on valueYes
uksortCallback on keyYes

The spaceship operator makes comparison callbacks trivial: fn($a, $b) => $a['age'] <=> $b['age']. Compare arrays of fields to sort by several columns at once.


Control Flow and the Match Expression

match arrived in PHP 8 and replaces most switch blocks. It compares strictly, returns a value, needs no break, and throws if nothing matches instead of falling through silently.

$label = match (true) {
    $bytes < 1024        => $bytes . ' B',
    $bytes < 1048576     => round($bytes / 1024) . ' KB',
    default              => round($bytes / 1048576, 1) . ' MB',
};

Reach for switch only when you deliberately want fall through behaviour or loose comparison.

Null Handling

Three operators cover almost every null case. ?? reads a value with a fallback and never warns about a missing key. ??= assigns only when the target is null. ?-> stops a method chain at the first null rather than throwing.

$page  = $_GET['page'] ?? 1;
$opts['retries'] ??= 3;
$city  = $order?->customer?->address?->city;

Functions and Closures

Type Everything

Parameter types, return types and property types are checked by the engine and understood by static analysis tools. They are the cheapest bug prevention available.

function discount(float $price, float $percent = 10.0): float
{
    return round($price * (1 - $percent / 100), 2);
}

Named Arguments

Since PHP 8 you can pass arguments by name, which removes the need to remember positions and makes long signatures readable at the call site.

createUser(name: 'Ada', admin: true, notify: false);

Arrow Functions

Arrow functions capture the outer scope automatically and hold a single expression. They are made for callbacks. Use a full closure with use when you need several statements or want to be explicit about what is captured.

Generators

A function containing yield returns a generator instead of an array. Values are produced one at a time, so you can iterate a two gigabyte log file inside a few kilobytes of memory. This is the single biggest memory win available in PHP, and it costs one keyword.


Object Oriented PHP

Constructor Promotion

PHP 8 collapsed the old four line pattern of declaring a property, documenting it, accepting it and assigning it into one declaration:

final class Money
{
    public function __construct(
        public readonly int $amount,
        public readonly string $currency = 'EUR',
    ) {}
}

readonly means the property can be written once, from inside the class. Combined with promotion it gives you a real value object in six lines.

Enums

Before PHP 8.1 people used class constants and hoped nobody passed a bad string. Enums make the invalid state unrepresentable:

enum OrderStatus: string
{
    case Pending  = 'pending';
    case Shipped  = 'shipped';
    case Refunded = 'refunded';

    public function isFinal(): bool
    {
        return $this === self::Refunded;
    }
}

Type hint OrderStatus and the engine guarantees the value is one of three. Use tryFrom() when parsing untrusted input and from() when a bad value should throw.

Interfaces Versus Traits

An interface is a promise about behaviour with no implementation. A trait is shared implementation with no promise. Use interfaces for the contract your callers depend on, and traits sparingly to share mechanical code such as timestamps or event dispatch.

Property Hooks

PHP 8.4 added hooks, which let a plain property run code on read or write. Boilerplate getters and setters mostly disappear, and you can add validation to an existing public property without changing a single caller.


Handling Requests, Forms and Sessions

Never Trust Input

Everything in $_GET, $_POST, $_COOKIE and the request headers is attacker controlled. Validate the shape, cast to the type you expect, and escape at the point of output.

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false || $id === null) {
    http_response_code(400);
    exit('Invalid id');
}

The Post Redirect Get Pattern

After a successful form submission, redirect instead of rendering. It stops the browser from resubmitting on refresh:

if ($_SERVER['REQUEST_METHOD'] === 'POST' && save($data)) {
    header('Location: /orders/' . $id, true, 303);
    exit;
}

Always call exit after a redirect. Without it the script keeps running and can still emit output or write to the database.

Sessions Done Properly

Configure the session cookie as HTTP only, secure and SameSite. Call session_regenerate_id(true) immediately after a successful login to prevent session fixation. On logout, clear the array and destroy the session rather than just unsetting one key.


Databases With PDO

PDO is the portable database layer. Configure it once, correctly, and most classes of database bug simply stop happening:

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

Exception mode means a failed query throws rather than returning false that you forget to check. Turning off emulated prepares sends real prepared statements to the server, so types are respected and injection is structurally impossible.

Prepared Statements Are Not Optional

String interpolation into SQL is the root of SQL injection. A placeholder is never parsed as SQL, so no input can change the meaning of the query.

// vulnerable
$pdo->query("SELECT * FROM users WHERE email = '$email'");

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

Transactions

Any operation that writes to more than one table belongs in a transaction. Wrap it in try and catch, commit on success, roll back on any throwable, and rethrow so the caller knows.


Errors and Exceptions

PHP has two throwable branches. Exception covers application problems you expect to handle. Error covers engine failures such as a type mismatch or a call to a missing method. Both implement Throwable, so catching Throwable catches everything.

try {
    $report = $generator->build($input);
} catch (InvalidArgumentException $e) {
    $errors[] = $e->getMessage();
} catch (Throwable $e) {
    error_log($e->getMessage());
    http_response_code(500);
}

In production, log errors and show nothing. A stack trace on screen tells an attacker your directory structure, framework version and database driver. Set display_errors to Off and log_errors to On.


Security Checklist

  • Passwords: password_hash and password_verify, nothing else. Never md5, never sha1, never your own salt scheme.
  • SQL: prepared statements with bound parameters on every query that touches user input.
  • Output: htmlspecialchars with ENT_QUOTES on every value echoed into HTML.
  • CSRF: a random per session token in every state changing form, compared with hash_equals.
  • Sessions: HTTP only, secure, SameSite cookies, and regenerate the id on login.
  • Uploads: check the size, verify the type from the file contents, rename the file, and store it outside the web root.
  • Randomness: random_bytes and random_int for anything security related. rand and mt_rand are predictable.
  • Config: keep credentials in environment variables, never in a file inside the document root.

PHP Version Reference

Knowing which release introduced a feature saves you from writing code your server cannot run. Every snippet in the PHP cheat sheet above is tagged with the version that introduced it, so the version filter narrows the whole reference down to what your server will actually run. This is what each release brought:

VersionHeadline features
7.0Scalar and return types, spaceship operator, null coalescing, big speed gains
7.1Nullable types, void return, class constant visibility, multi catch
7.4Typed properties, arrow functions, spread in arrays, null coalescing assignment
8.0Named arguments, attributes, constructor promotion, match, nullsafe operator, union types, JIT
8.1Enums, readonly properties, fibers, first class callables, never return type
8.2Readonly classes, disjunctive normal form types, standalone null and false types
8.3Typed class constants, dynamic class constant fetch, json_validate
8.4Property hooks, asymmetric visibility, new without parentheses, array find and any and all

Check what you are running with php -v on the command line or PHP_VERSION in code. Anything below 8.1 no longer receives security patches, so upgrading is the highest value maintenance work available.


Performance Notes That Actually Matter

Micro optimising loops is rarely where the time goes. These four changes are:

  • OPcache: on by default in production builds. Verify it is enabled. It compiles your code once instead of on every request.
  • The N plus 1 query: loading a list then querying inside the loop. Fetch the related rows in one query keyed by id instead.
  • Generators over arrays: any time you build a large array only to loop over it once, yield instead.
  • Persistent connections and indexes: most slow PHP pages are actually slow SQL. Profile the query before rewriting the PHP.

Bookmark this PHP cheat sheet and use the search box rather than scrolling. Typing two words narrows nearly six hundred snippets down to the handful you actually want, and the copy button on every one of them means you never retype a signature from memory.


Frequently Asked Questions

Is PHP still worth learning in 2026?

Yes, on the evidence. PHP runs a large share of the web, WordPress alone accounts for a significant portion of all sites, and Laravel and Symfony remain in heavy commercial use. The job market for maintaining and extending PHP systems is substantial and unlikely to shrink quickly.

The language people dismiss is usually PHP 5. Modern PHP is typed, fast and pleasant to work in.

What is the difference between echo and print?

echo accepts multiple comma separated arguments and returns nothing. print takes one argument and returns 1, which means it can be used inside an expression. In practice echo is the one to use.

When should I use == instead of ===?

Almost never. Use === by default so that both the value and the type must match. Loose comparison converts operands before comparing, and the rules are subtle enough that they have caused real security bugs. If you need a numeric comparison of a string, cast explicitly instead.

PDO or MySQLi?

PDO, in most cases. It supports a dozen database drivers with the same API, has named placeholders, and its exception mode plus fetch mode configuration is cleaner. MySQLi is MySQL only and is worth using when you specifically need a MySQL feature PDO does not expose.

How do I stop SQL injection?

Use prepared statements with bound parameters for every query containing user input, and never build SQL by concatenating strings. Placeholders are sent to the database separately from the query, so input cannot change what the query does. Escaping functions are not a substitute.

What does declare(strict_types=1) actually do?

It changes type checking from coercive to strict for the file it appears in. Without it, passing "5" where an int is expected silently converts the value. With it, that call throws a TypeError. It only affects the file containing the declaration, so add it to every file.

Should I use readonly or a getter?

Use readonly for value objects that should never change after construction. It is enforced by the engine and needs no extra code. Reach for a getter or a property hook when the value is computed, needs validation on write, or the storage format differs from what callers should see.

Why does my header call say headers already sent?

Something was output before the call. Common causes are a blank line before <?php, whitespace after a closing ?> in an included file, or a stray echo. Omit closing tags in pure PHP files and check every include in the chain.


Official Documentation for Every Category on This Sheet

Every snippet above is checked against the PHP manual rather than written from memory. These are the primary sources behind each category, worth bookmarking alongside the sheet itself:

For what shipped in each release, see the official PHP release notes.


How This Cheat Sheet Is Kept Current

Last updated 29 July 2026. The sheet is re-checked after every PHP release and whenever a snippet is reported as drifting from the manual.

What that check involves:

  • Function signatures and behaviour are read from the PHP manual for the version in question, not from memory or old notes.
  • Version tags (7.0+, 8.0+, and so on) are set from the release that introduced the feature, cross-checked against the official release notes.
  • Security guidance follows current PHP core recommendations, not habits carried over from PHP 5.
  • Anything deprecated or removed in a supported release is flagged rather than left to look current.

Written and maintained by Bogdan Sandu at TMS Outsource. If a snippet looks wrong or out of date, the fastest fix is to check it against the linked manual page above and let us know - corrections get applied to the sheet, and the date at the top of this section moves with them.