PHP 8.x Reference
PHP Reference
Modern PHP 8.x patterns: named arguments, match expressions, fibers, enums, readonly classes, nullsafe operator, array functions, PDO, Composer, and the type system upgrades from 8.0 to 8.4.
PHP 8.x — what actually changed
# PHP 8.0
# Named arguments — pass by parameter name, skip optional params
htmlspecialchars($str, double_encode: false);
array_slice($array, offset: 2, length: 3);
# Match expression — strict equality (===), no fall-through, exhaustive
$status = match($code) {
200, 201 => 'success',
301, 302 => 'redirect',
404 => 'not found',
500 => 'server error',
default => 'unknown',
};
# Nullsafe operator — short-circuits on null
$city = $user?->getAddress()?->getCity();
# Union types
function process(int|string $value): int|float { }
# Match + no-match throws UnhandledMatchError (unlike switch)
// match($x) { 1 => 'one' } → throws if $x is 2
# PHP 8.1
# Enums
enum Status {
case Active;
case Inactive;
}
enum Color: string { // backed enum (int or string)
case Red = 'red';
case Green = 'green';
case Blue = 'blue';
public function label(): string {
return ucfirst($this->value);
}
}
Color::Red->value; // "red"
Color::from('red'); // Color::Red — throws ValueError if not found
Color::tryFrom('invalid'); // null — safe version
// Enum in match
$response = match($status) {
Status::Active => 'Account is active',
Status::Inactive => 'Account is inactive',
};
# Readonly properties
class User {
public function __construct(
public readonly int $id,
public readonly string $email,
) {}
}
$user = new User(1, 'alice@example.com');
// $user->id = 2; → Error: Cannot modify readonly property
# Intersection types
function process(Iterator&Countable $data): void {}
# Fibers (cooperative multitasking)
$fiber = new Fiber(function(): void {
$value = Fiber::suspend('first');
echo "Got: $value\n";
});
$result = $fiber->start(); // "first"
$fiber->resume('hello'); // "Got: hello"
# PHP 8.2
# Readonly classes — all properties become readonly
readonly class Point {
public function __construct(
public float $x,
public float $y,
) {}
}
# Disjunctive Normal Form (DNF) types
function process((Iterator&Countable)|array $data): void {}
# PHP 8.3+
# Typed class constants
class Config {
const string VERSION = '1.0.0';
const int MAX_RETRIES = 3;
}
# json_validate() — validate without decoding
json_validate('{"name":"Alice"}'); // true
json_validate('{invalid}'); // false
# Override attribute
class Child extends Base {
#[\Override] // compile-time check: parent method must exist
public function process(): void {}
}
Array functions — the ones worth memorising
# array_map — transform
$doubled = array_map(fn($n) => $n * 2, [1, 2, 3]); // [2, 4, 6]
$upper = array_map('strtoupper', ['a', 'b']); // ['A', 'B']
// With keys (use callback with two args):
array_map(null, [1, 2], [3, 4]); // [[1,3],[2,4]] — zip
# array_filter — keep matching (preserves keys!)
$evens = array_filter([1, 2, 3, 4], fn($n) => $n % 2 === 0);
$truthy = array_filter([0, 1, '', 'a', null, false]); // [1, 'a'] (keys preserved)
$reset = array_values($truthy); // reindex keys
# array_reduce — fold to single value
$sum = array_reduce([1, 2, 3, 4], fn($carry, $n) => $carry + $n, 0);
# usort / uasort / uksort — sort with callback
usort($items, fn($a, $b) => $a['price'] <=> $b['price']); // ascending
usort($items, fn($a, $b) => $b['price'] <=> $a['price']); // descending
// Multi-column sort:
usort($items, function($a, $b) {
return [$a['type'], $a['name']] <=> [$b['type'], $b['name']];
});
# array_column — extract a single column
$names = array_column($users, 'name'); // ['Alice', 'Bob']
$by_id = array_column($users, null, 'id'); // reindex by id
$lookup = array_column($users, 'name', 'id'); // id => name map
# array_combine — merge two arrays as key => value
$map = array_combine(['a', 'b', 'c'], [1, 2, 3]); // ['a'=>1, 'b'=>2, 'c'=>3]
# array_unique — deduplicate
array_unique([1, 2, 2, 3, 1]); // [1, 2, 3] (keys preserved)
# array_flip — swap keys and values
array_flip(['a'=>1, 'b'=>2]); // [1=>'a', 2=>'b']
# in_array, array_search — search
in_array('needle', $haystack); // bool
in_array('needle', $haystack, strict: true); // === comparison
$key = array_search('needle', $haystack); // key or false
# array_key_exists vs isset
array_key_exists('key', $arr); // true even if value is null
isset($arr['key']); // false if value is null
# array_merge vs + (union)
array_merge(['a'], ['b']); // reindex: ['a', 'b']
$arr1 + $arr2; // keeps first array's values for duplicate keys
# Spread operator in arrays (PHP 8.1 with string keys)
$defaults = ['color' => 'red', 'size' => 'L'];
$custom = ['color' => 'blue'];
$merged = [...$defaults, ...$custom]; // ['color'=>'blue', 'size'=>'L']
# Useful functions
count($arr);
array_push($arr, $val); // append (or $arr[] = $val)
array_pop($arr); // remove and return last
array_shift($arr); // remove and return first
array_unshift($arr, $val); // prepend
array_slice($arr, 2, 3); // offset, length
array_splice($arr, 1, 2, $replace); // in-place replace
array_chunk($arr, 3); // split into chunks of 3
array_reverse($arr);
array_sum($arr);
array_keys($arr);
array_values($arr);
Type system and type juggling
# Type declarations (PHP 7.0+, required in modern code)
function add(int $a, int $b): int {
return $a + $b;
}
# Nullable types
function find(?int $id): ?User { // ?T = T|null
if ($id === null) return null;
return User::find($id);
}
# Return types
function process(): void {} // no return value
function makeArray(): array {}
function getUser(): User {} // class type
function getItems(): iterable {} // array or Traversable
# Strict mode (declare at top of file — recommended for all new code)
declare(strict_types=1);
// Without strict_types: add("1", "2") works → 3
// With strict_types: add("1", "2") → TypeError
# Type juggling gotchas — always use === in PHP
var_dump(0 == "foo"); // true in PHP 7 (!!), false in PHP 8
var_dump(0 == ""); // true in PHP 7, false in PHP 8
var_dump("1" == "01"); // true (numeric string comparison)
var_dump("10" == "1e1"); // true (scientific notation!)
var_dump(100 == "1e2"); // true
// Golden rule: use === (strict) always
if ($value === null) {}
if ($value === false) {}
if ($value === 0) {}
# Type checking
is_int($v); is_float($v); is_string($v); is_bool($v);
is_array($v); is_null($v); is_object($v); is_callable($v);
gettype($v); // "integer", "double", "string", "boolean", "NULL", "array", "object"
# Casting
(int) "42abc"; // 42
(string) 42; // "42"
(bool) 0; // false
(bool) ""; // false
(bool) "0"; // false (gotcha!)
(bool) []; // false
(bool) "false"; // TRUE (non-empty string)
# intval / floatval / strval
intval("0x1A", 16); // 26 (hex parse)
intval("0b1010", 2); // 10 (binary parse)
# instanceof and type checking
$user instanceof User;
$user instanceof UserInterface;
is_a($user, User::class); // same as instanceof
# Class constants for type safety
class HttpMethod {
const string GET = 'GET';
const string POST = 'POST';
const string DELETE = 'DELETE';
}
// Or use Enums for exhaustive matching (PHP 8.1+)
PDO — database access
# Connect
$pdo = new PDO(
'mysql:host=localhost;dbname=myapp;charset=utf8mb4',
'user',
'password',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // throw on error
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // array by column name
PDO::ATTR_EMULATE_PREPARES => false, // real prepared statements
]
);
# Query (always use prepared statements for user input)
# NEVER: "SELECT * FROM users WHERE id = " . $_GET['id']; // SQL injection!
# fetch single row
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->execute([':email' => $email]);
$user = $stmt->fetch(); // associative array or false
# fetch all rows
$stmt = $pdo->prepare("SELECT * FROM users WHERE status = :status");
$stmt->execute([':status' => 'active']);
$users = $stmt->fetchAll();
# fetch as class instances
$stmt->fetchAll(PDO::FETCH_CLASS, User::class);
# fetch single column
$stmt->fetchColumn(); // first column of first row
$stmt->fetchAll(PDO::FETCH_COLUMN); // all values of first column
# Insert / update / delete
$stmt = $pdo->prepare("INSERT INTO users (name, email) VALUES (:name, :email)");
$stmt->execute([':name' => $name, ':email' => $email]);
$id = $pdo->lastInsertId();
$stmt = $pdo->prepare("UPDATE users SET status = :status WHERE id = :id");
$stmt->execute([':status' => 'inactive', ':id' => $id]);
$affected = $stmt->rowCount();
# Transactions
try {
$pdo->beginTransaction();
$pdo->prepare("UPDATE accounts SET balance = balance - :amount WHERE id = :id")
->execute([':amount' => $amount, ':id' => $from]);
$pdo->prepare("UPDATE accounts SET balance = balance + :amount WHERE id = :id")
->execute([':amount' => $amount, ':id' => $to]);
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
throw $e;
}
# Positional placeholders
$stmt = $pdo->prepare("SELECT * FROM items WHERE user_id = ? AND active = ?");
$stmt->execute([$userId, 1]);
# Dynamic IN clause (parameterised)
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);
OOP patterns in modern PHP
# Constructor property promotion (PHP 8.0)
class User {
public function __construct(
private readonly int $id,
private string $name,
private ?string $email = null,
) {}
public function name(): string { return $this->name; }
}
# First-class callables (PHP 8.1) — replaces Closure::fromCallable
$fn = strlen(...); // Closure from built-in
$fn = $obj->method(...); // Closure from method
$fn = Foo::static(...); // Closure from static method
# Interfaces
interface Repository {
public function find(int $id): ?User;
public function save(User $user): void;
public function delete(int $id): void;
}
class UserRepository implements Repository {
public function find(int $id): ?User {
// ...
}
// must implement all methods
}
# Abstract classes
abstract class BaseController {
abstract protected function authorize(): void;
public function handle(Request $request): Response {
$this->authorize(); // subclass must implement
return $this->process($request);
}
protected function process(Request $request): Response {
// default implementation
}
}
# Traits — horizontal code reuse
trait Timestampable {
private DateTime $createdAt;
private DateTime $updatedAt;
public function setTimestamps(): void {
$this->createdAt ??= new DateTime();
$this->updatedAt = new DateTime();
}
}
class Post {
use Timestampable;
use SoftDeletable; // multiple traits
}
# Conflict resolution
class Article {
use TraitA, TraitB {
TraitA::hello insteadof TraitB; // use TraitA's hello
TraitB::hello as helloFromB; // alias TraitB's hello
}
}
# Magic methods
class MagicExample {
private array $data = [];
public function __get(string $name): mixed { return $this->data[$name] ?? null; }
public function __set(string $name, mixed $v): void { $this->data[$name] = $v; }
public function __isset(string $name): bool { return isset($this->data[$name]); }
public function __toString(): string { return json_encode($this->data); }
public function __invoke(mixed ...$args): mixed { return $this->process(...$args); }
public function __clone(): void { // called when cloning
$this->data = array_map('clone', $this->data);
}
}
// Fluent builder pattern
class QueryBuilder {
private array $wheres = [];
private ?int $limit = null;
public function where(string $condition): static {
$this->wheres[] = $condition;
return $this; // return static, not self — preserves subclass type
}
public function limit(int $n): static {
$this->limit = $n;
return $this;
}
}
$query = (new QueryBuilder)
->where('active = 1')
->where('age > 18')
->limit(10);
Composer and project structure
# composer.json
{
"name": "vendor/package",
"description": "My application",
"require": {
"php": "^8.2",
"guzzlehttp/guzzle": "^7.0",
"monolog/monolog": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^11.0",
"phpstan/phpstan": "^1.0",
"squizlabs/php_codesniffer": "^3.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit --configuration phpunit.xml",
"stan": "phpstan analyse src --level 8",
"cs": "phpcs --standard=PSR12 src/",
"cbf": "phpcbf --standard=PSR12 src/"
}
}
# Commands
composer install # install from composer.lock
composer install --no-dev # production (skip require-dev)
composer update guzzlehttp/guzzle # update one package
composer update # update all (semver-constrained)
composer require monolog/monolog # add a new dependency
composer remove package/name # remove
composer dump-autoload -o # regenerate optimised autoloader
composer outdated # show outdatable packages
# Version constraints
"~1.2.3" >=1.2.3 <1.3.0 (patch-level)
"~1.2" >=1.2 <2.0.0 (minor-level)
"^1.2.3" >=1.2.3 <2.0.0 (compatible — most common)
"^0.3.0" >=0.3.0 <0.4.0 (0.x = no backwards compat)
# PHPUnit
composer require --dev phpunit/phpunit
./vendor/bin/phpunit tests/
# Test example
use PHPUnit\Framework\TestCase;
class UserTest extends TestCase {
public function testCanCreateUser(): void {
$user = new User(1, 'Alice');
$this->assertSame('Alice', $user->name());
}
public function testThrowsOnEmptyName(): void {
$this->expectException(\InvalidArgumentException::class);
new User(1, '');
}
}
# PHPStan (static analysis)
./vendor/bin/phpstan analyse src --level 8 # 0=loose, 9=strictest
# PHP_CodeSniffer (PSR-12)
./vendor/bin/phpcs --standard=PSR12 src/
./vendor/bin/phpcbf --standard=PSR12 src/ # auto-fix
Track PHP EOL dates and releases at ReleaseRun.
🔍 Free tool: HTTP Security Headers Analyzer — after building your PHP app, check it returns the right HTTP security headers — HSTS, CSP, X-Frame-Options.
🔍 Free tool: PHP Composer Package Health Checker — check any Composer package for abandonment status and latest version before composer require.
🔍 Free tool: composer.json Batch Health Checker — paste your entire composer.json and grade all packages at once via Packagist.
Founded
2023 in London, UK
Contact
hello@releaserun.com