PHP 8 Features
PHP 8 was a landmark release that brought the language firmly into the modern era. Released in November 2020, it introduced a collection of features that make code more expressive, safer, and easier to reason about. Whether you are writing a simple script or a complex application, these additions reduce boilerplate and eliminate entire categories of bugs. This lesson walks through the most impactful changes you will encounter and use regularly.
Match Expressions
Before PHP 8, switch was the go-to for multi-branch logic, but it had quirks: loose comparison, fall-through behaviour, and no return value. The match expression fixes all of that.
match uses strict comparison (===), each arm returns a value, and there is no fall-through. It is an expression, so you can assign its result directly to a variable.
<?php
$status = 2;
$label = match($status) {
1 => "Active",
2 => "Pending",
3 => "Suspended",
default => "Unknown",
};
echo $label . "\n"; // Pending
// Multiple conditions per arm
$code = 404;
$message = match(true) {
$code >= 500 => "Server Error",
$code >= 400 => "Client Error",
$code >= 300 => "Redirect",
default => "Success",
};
echo $message . "\n"; // Client Error
// Match throws UnhandledMatchError if no arm matches and no default
// This prevents silent bugs that switch could hide
$role = "editor";
$permissions = match($role) {
"admin" => ["read", "write", "delete"],
"editor" => ["read", "write"],
"viewer" => ["read"],
default => [],
};
echo implode(", ", $permissions) . "\n"; // read, write
Prefer match over switch whenever you need a clean, strict, value-returning branch. Reserve switch only when you intentionally need fall-through.
Named Arguments
Named arguments let you pass values to a function by parameter name rather than position. This eliminates the need to remember argument order and makes call sites dramatically more readable, especially for built-in functions with many optional parameters.
<?php
// Without named arguments — what does true mean here?
$result = array_slice([1, 2, 3, 4, 5], 1, 3, true);
// With named arguments — intent is immediately clear
$result = array_slice(
array: [1, 2, 3, 4, 5],
offset: 1,
length: 3,
preserve_keys: true
);
print_r($result);
// Named arguments also let you skip optional parameters
function createUser(
string $name,
string $role = "viewer",
bool $active = true,
int $maxProjects = 10
): string {
$status = $active ? "active" : "inactive";
return "$name | $role | $status | projects: $maxProjects";
}
// Only supply what you care about, skip the rest
echo createUser(name: "Alice", maxProjects: 25) . "\n";
// Alice | viewer | active | projects: 25
echo createUser(name: "Bob", role: "admin", active: false) . "\n";
// Bob | admin | inactive | projects: 10
Named arguments work especially well when calling native PHP functions that have long, positional-only signatures. They make the code self-documenting at the call site.
Union Types and Nullsafe Operator
PHP 8 lets you declare that a parameter, property, or return value can hold one of several types using a union type — written as TypeA|TypeB. This replaces the informal convention of using mixed or omitting the type hint entirely.
The nullsafe operator (?->) addresses a separate but related pain point: safely traversing a chain of method calls where any link might return null.
<?php
// Union types in function signatures
function formatId(int|string $id): string {
if (is_int($id)) {
return str_pad((string)$id, 6, "0", STR_PAD_LEFT);
}
return strtoupper(trim($id));
}
echo formatId(42) . "\n"; // 000042
echo formatId(" abc ") . "\n"; // ABC
// int|null is so common PHP has ?int shorthand, but you can be explicit
function divide(int $a, int $b): int|null {
if ($b === 0) return null;
return intdiv($a, $b);
}
$result = divide(10, 0);
echo ($result ?? "division by zero") . "\n"; // division by zero
// Nullsafe operator: stops the chain and returns null if any step is null
class Address {
public function __construct(public string $city) {}
}
class User {
public function __construct(
public string $name,
public ?Address $address = null
) {}
public function getAddress(): ?Address {
return $this->address;
}
}
$userWithAddress = new User("Alice", new Address("Amsterdam"));
$userWithoutAddress = new User("Bob");
// Without nullsafe you would need: $user->getAddress() ? $user->getAddress()->city : null
echo $userWithAddress->getAddress()?->city . "\n"; // Amsterdam
echo ($userWithoutAddress->getAddress()?->city ?? "no city") . "\n"; // no city
Union types make implicit contracts explicit. The nullsafe operator replaces nested null checks with a clean, readable chain.
Constructor Property Promotion
Defining a class with several properties traditionally required three repetitions of each property name: in the property declaration, in the constructor parameter, and in the assignment. Constructor property promotion collapses all three into one.
<?php
// Old style — lots of repetition
class ProductOld {
public string $name;
public float $price;
public int $stock;
public function __construct(string $name, float $price, int $stock) {
$this->name = $name;
$this->price = $price;
$this->stock = $stock;
}
}
// PHP 8 — promoted properties declared directly in the constructor
class Product {
public function __construct(
public readonly string $name,
public float $price,
public int $stock = 0,
) {}
public function summary(): string {
return "$this->name — \$$this->price ($this->stock in stock)";
}
}
$item = new Product(name: "Keyboard", price: 89.99, stock: 12);
echo $item->summary() . "\n"; // Keyboard — $89.99 (12 in stock)
// readonly prevents reassignment after construction
// $item->name = "Mouse"; // TypeError: Cannot modify readonly property
// You can mix promoted and non-promoted parameters
class Order {
public float $total = 0.0;
public function __construct(
public readonly int $id,
public readonly string $customer,
private array $items = [],
) {}
public function addItem(Product $product): void {
$this->items[] = $product;
$this->total += $product->price;
}
public function itemCount(): int {
return count($this->items);
}
}
$order = new Order(id: 1001, customer: "Alice");
$order->addItem(new Product("Keyboard", 89.99));
$order->addItem(new Product("Mouse", 34.99));
echo "Order #{$order->id} for {$order->customer}\n";
echo "Items: {$order->itemCount()}, Total: \${$order->total}\n";
Try It Yourself
Combine the features you have learned to build a small product catalogue processor. Use match for categorisation, named arguments for clarity, union types for flexibility, and promoted properties for clean class definitions.
<?php
class CatalogueItem {
public function __construct(
public readonly string $sku,
public readonly string $name,
public float $price,
public int $stock = 0,
) {}
}
function categorise(float $price): string {
return match(true) {
$price >= 500 => "premium",
$price >= 100 => "standard",
$price >= 0 => "budget",
default => "unknown",
};
}
function formatItem(CatalogueItem $item, bool $showStock = false): string {
$category = categorise(price: $item->price);
$line = sprintf(
"[%s] %s — \$%.2f (%s)",
strtoupper($item->sku),
$item->name,
$item->price,
$category
);
if ($showStock) {
$availability = match(true) {
$item->stock === 0 => "out of stock",
$item->stock <= 5 => "low stock ({$item->stock})",
default => "in stock ({$item->stock})",
};
$line .= " — $availability";
}
return $line;
}
$catalogue = [
new CatalogueItem(sku: "KB-01", name: "Mechanical Keyboard", price: 149.99, stock: 8),
new CatalogueItem(sku: "MOU-02", name: "Ergonomic Mouse", price: 59.99, stock: 3),
new CatalogueItem(sku: "MON-03", name: "4K Monitor", price: 699.00, stock: 0),
new CatalogueItem(sku: "PAD-04", name: "Desk Mat", price: 24.99, stock: 50),
];
foreach ($catalogue as $item) {
echo formatItem(item: $item, showStock: true) . "\n";
}
Key Takeaways
matchexpressions use strict comparison, return values directly, and throw an error when no arm matches — safer and more concise thanswitch.- Named arguments let you pass values by parameter name, skip optional parameters freely, and make call sites self-documenting.
- Union types (
int|string,int|null) express multiple acceptable types in a single hint, replacing the informal use ofmixed. - The nullsafe operator (
?->) short-circuits a method chain the momentnullis encountered, eliminating nested null checks. - Constructor property promotion collapses property declaration, constructor parameter, and assignment into a single line, and pairs naturally with
readonlyfor immutable data. - Together these features push PHP toward more expressive, self-documenting, and type-safe code without requiring a complete rewrite of existing patterns.
Pro Tip: Adopt these features incrementally. Start with
matchin any new multi-branch logic you write, and add constructor property promotion the next time you create a new class. You do not need to rewrite existing code — just let the new style spread naturally as you touch files. Most static analysis tools (PHPStan, Psalm) and modern IDEs understand all PHP 8 features natively, so you will get immediate feedback if you misuse a union type or forget adefaultarm in amatch.
## Next Steps
You now understand match expressions, named arguments, union types, the nullsafe operator, and constructor property promotion -- the features that define modern PHP development.
Writing great code is only part of the picture. Real PHP projects depend on dozens of third-party libraries for everything from HTTP clients to testing frameworks, and managing those dependencies by hand would be chaos. **Composer and packages** teaches you PHP's dependency manager -- how to install packages, configure autoloading with PSR-4, manage version constraints, and structure your projects the way the professional PHP ecosystem expects. Composer is a tool you will use on every project from here on.
[Continue to Composer and Packages -->](/lessons/18-composer-and-packages)