Core PHP
The complete language foundation — types, OOP, closures, match expressions, enums, generators, and fibers.
Types & Variables
01PHP is dynamically typed but supports strict type declarations. PHP 8.x dramatically improved the type system with union types, intersection types, and never.
| Type | Example | Notes |
|---|---|---|
int | 42, -7, 0xFF | 64-bit on modern systems |
float | 3.14, 1.2e3 | IEEE 754 double |
string | "hello", 'world' | Binary-safe, any length |
bool | true, false | Case-insensitive |
null | null | Absence of value |
int|float 8.0 | Union type | Accept multiple types |
A&B 8.1 | Intersection type | Must satisfy both |
never 8.1 | exit(), throw | Function never returns |
<?php
declare(strict_types=1); // Enforces all type declarations
// Union types (8.0+)
function format(int|float $n): string {
return number_format($n, 2);
}
// Nullable type — either User or null
function find(int $id): ?User { /* ... */ }
// Intersection types (8.1+): must be Countable AND Traversable
function process(Countable&Traversable $col): void { /* ... */ }
// First-class callable syntax (8.1+)
$fn = strlen(...); // Closure from built-in
$lengths = array_map(strlen(...), $strings);
Arrays
// List (sequential)
$fruits = ['apple', 'banana', 'cherry'];
// Associative (hash map)
$user = [
'name' => 'Alice',
'age' => 30,
'role' => 'admin',
];
// Spread & unpack
$a = [1, 2, 3];
$b = [0, ...$a, 4]; // [0,1,2,3,4]
// Destructuring
[$first, , $third] = $fruits;
['name' => $name] = $user;
// Functional pipeline
$result = array_reduce(
array_filter(
array_map(
fn($x) => $x * 2,
range(1, 10)
),
fn($x) => $x > 10
),
fn($carry, $item) => $carry + $item,
0
); // 60
// usort with spaceship operator
usort($users, fn($a, $b) => $a['age'] <=> $b['age']);
Strings
// PHP 8.0 str_* functions (replace strpos boilerplate)
str_contains($haystack, 'needle'); // bool
str_starts_with($url, 'https://'); // bool
str_ends_with($file, '.php'); // bool
// Heredoc — evaluates variables
$msg = <<
Control Flow
02match Expression 8.0
match uses strict === comparison, returns a value, and throws UnhandledMatchError when no arm matches.
// Old: switch (loose ==, no return)
switch ($status) {
case 1:
$label = 'Active';
break;
case 2:
$label = 'Inactive';
break;
default:
$label = 'Unknown';
}
// New: match (strict ===, returns value)
$label = match($status) {
1 => 'Active',
2 => 'Inactive',
3, 4 => 'Pending', // multiple arms
default => 'Unknown',
};
// Null-safe operator (8.0+)
$country = $user?->getAddress()?->getCountry();
?-> short-circuits the entire chain on null — none of the subsequent method calls execute.
Functions & Closures
03Closures
// Closure captures variables via `use`
$multiplier = 3;
$triple = function(int $n) use ($multiplier): int {
return $n * $multiplier;
};
// Capture by reference — mutates outer scope
$count = 0;
$inc = function() use (&$count) { $count++; };
$inc(); $inc(); // $count === 2
// Closure::bind — attach a closure to a private class context
class Foo { private int $secret = 42; }
$reader = Closure::bind(function() {
return $this->secret;
}, new Foo(), Foo::class);
$reader(); // 42
Arrow Functions 7.4
// Arrow functions implicitly capture outer scope — no `use` needed
$tax = 0.1;
$withTax = array_map(fn($price) => $price * (1 + $tax), $prices);
// Named arguments (8.0+) — skip positional order
array_slice($arr, offset: 2, length: 5, preserve_keys: true);
// First-class callable syntax (8.1+)
$fn = strlen(...); // Closure from built-in
$lengths = array_map(strlen(...), $strings);
Object-Oriented PHP
04Constructor Property Promotion 8.0
class User
{
// Constructor property promotion — no boilerplate property declarations
public function __construct(
public readonly int $id,
public string $name,
private string $email,
public Role $role = Role::User,
) {}
// Fluent interface using clone
public function withName(string $name): static
{
$clone = clone $this;
$clone->name = $name;
return $clone;
}
public function __toString(): string
{
return "{$this->name} <{$this->email}>";
}
}
Interfaces
interface Repository
{
public function find(int $id): ?Model;
public function findAll(): array;
public function save(Model $model): void;
public function delete(int $id): bool;
}
// A class can implement multiple interfaces
class UserRepository implements Repository, Countable
{
public function count(): int { return count($this->items); }
// ... implement Repository methods
}
Traits
trait Timestampable
{
private ?DateTime $createdAt = null;
private ?DateTime $updatedAt = null;
public function touch(): void
{
$this->updatedAt = new DateTime();
$this->createdAt ??= new DateTime(); // only set once
}
}
trait SoftDeletes
{
private ?DateTime $deletedAt = null;
public function delete(): void { $this->deletedAt = new DateTime(); }
public function restore(): void { $this->deletedAt = null; }
public function isDeleted(): bool { return $this->deletedAt !== null; }
}
class Post
{
use Timestampable, SoftDeletes; // compose both traits
}
Enums 8.1
// Pure enum — no backing value
enum Status
{
case Active;
case Inactive;
case Banned;
public function label(): string
{
return match($this) {
Status::Active => 'Active',
Status::Inactive => 'Inactive',
Status::Banned => 'Banned',
};
}
}
// Backed enum — has a scalar value (int or string)
enum Role: string
{
case Admin = 'admin';
case User = 'user';
case Guest = 'guest';
}
$role = Role::from('admin'); // throws on invalid value
$role = Role::tryFrom('unknown'); // returns null
$val = Role::Admin->value; // 'admin'
Readonly Classes 8.2
// readonly class — ALL properties are readonly, great for value objects
readonly class Money
{
public function __construct(
public float $amount,
public string $currency,
) {}
public function add(Money $other): static
{
// Cannot mutate — must return a new instance
return new static($this->amount + $other->amount, $this->currency);
}
}
$price = new Money(9.99, 'USD');
$price->amount = 5.00; // Fatal error: Cannot modify readonly property
Error Handling
05// Exception hierarchy
class AppException extends RuntimeException {}
class NotFoundException extends AppException {
public function __construct(string $resource, int $id) {
parent::__construct("$resource #$id not found", 404);
}
}
// Multi-catch (7.1+) — catch multiple types in one block
try {
$user = $repo->findOrFail($id);
} catch (NotFoundException | AccessDeniedException $e) {
return response()->error($e->getMessage(), $e->getCode());
} catch (Throwable $e) {
// Catches exceptions AND PHP engine errors (TypeError, etc.)
logger()->error('Unexpected', ['exception' => $e]);
throw $e;
} finally {
// Always runs — clean up resources here
$db->close();
}
Throwable (not just Exception) when you need to intercept PHP engine errors like TypeError, ParseError, and Error.
Generators
06Generators produce values lazily — only one value is in memory at a time, making them ideal for large datasets or infinite sequences.
// Without generator — loads ALL rows ❌
function getAllUsers(): array
{
return DB::query('SELECT * FROM users')
->fetchAll(); // 1M rows → OOM
}
// Generator — yields one at a time ✅
function streamUsers(): Generator
{
$stmt = DB::query('SELECT * FROM users');
while ($row = $stmt->fetch()) {
yield $row;
}
}
foreach (streamUsers() as $user) {
process($user); // constant memory
}
// yield from — delegate to sub-generator
function flatten(array $items): Generator
{
foreach ($items as $item) {
if (is_array($item)) {
yield from flatten($item); // recurse
} else {
yield $item;
}
}
}
// send() — two-way communication with generator
function accumulator(): Generator
{
$total = 0;
while (true) {
$n = yield $total;
if ($n === null) break;
$total += $n;
}
}
$gen = accumulator(); $gen->current(); // init
$gen->send(10); $gen->send(5); // total = 15
Fibers 8.1
07Fibers are stackful coroutines — they can pause execution at any point in the call stack, unlike generators which only pause at the top level.
$fiber = new Fiber(function(): void {
$value = Fiber::suspend('first'); // pauses here, yields 'first'
echo "Resumed with: $value\n"; // prints "hello"
$value2 = Fiber::suspend('second'); // pauses again
echo "Resumed again: $value2\n";
});
$yielded = $fiber->start(); // 'first'
$yielded2 = $fiber->resume('hello'); // 'second', prints "Resumed with: hello"
$fiber->resume('world'); // prints "Resumed again: world"
Fiber directly — you interact through an event loop abstraction.