Skip to content

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