PHP 7.4: typed properties and the arrow function you actually want
PHP 7.4 landed November 28th. It’s the last 7.x release before PHP 8.0, and it feels like it. The features are substantial enough to stand on their own, but they also read as groundwork for what’s coming.
Typed properties
This is the one. Since PHP 7.0, you could type function parameters and return values. But class properties? Still untyped:
class User {
public int $id;
public string $name;
public ?DateTimeInterface $deletedAt;
}
7.4 changes that. Typed properties enforce types at assignment, not just at call sites. Classes become self-documenting in a way that docblocks never quite managed, and the engine catches type errors before they propagate through half your stack.
One subtlety: typed properties are uninitialized by default (not null). Accessing an uninitialized property throws an Error. This trips people up: ?string doesn’t imply a default of null. You still need an explicit = null for that.
Arrow functions
Closures in PHP have always required explicitly importing outer scope variables with use:
$multiplier = 3;
$fn = fn($x) => $x * $multiplier; // no use() needed
Arrow functions capture the enclosing scope automatically. Single expression, implicit return, no boilerplate. They don’t replace full closures for complex logic, but for short callbacks they eliminate a class of noise that had been accumulating for years.
Opcache preloading
For long-lived PHP-FPM setups, preloading allows a script to load and compile PHP files into opcache memory at server startup. Those files are available to all requests without compilation overhead.
The gain varies by application. On large frameworks where the same files are loaded on every request, it’s real. On smaller apps, negligible. Worth benchmarking before adding the configuration complexity.
The small ones that add up
The features mentioned in passing deserve more than a line. The null coalescing assignment operator ??= solves a pattern that was annoying enough to write every single time but never annoying enough to bother abstracting:
$config['timeout'] ??= 30;
// equivalent to: $config['timeout'] = $config['timeout'] ?? 30;
Spread operator in array literals does what you’d expect from the function call version — unpack an iterable into an array literal:
$defaults = ['color' => 'blue', 'size' => 'M'];
$options = ['size' => 'L', ...$defaults, 'weight' => 1.2];
// ['size' => 'M', 'color' => 'blue', 'weight' => 1.2]
Note: string keys weren’t supported in 7.4 for array unpacking. That came later.
Covariant return types and contravariant parameter types close a gap that made some inheritance patterns needlessly awkward. A child class can now narrow its return type to a subtype of the parent’s, without hitting a fatal error:
class Producer {
public function get(): Iterator {}
}
class ChildProducer extends Producer {
public function get(): ArrayIterator {} // ArrayIterator implements Iterator
}
Reading numbers at 3am
The numeric literal separator is one of those features you don’t know you wanted until the first time you write a large constant and immediately lose track of the magnitude:
$earthMass = 5_972_168_000_000_000_000_000_000; // kg
$lightSpeed = 299_792_458; // m/s
$planck = 6.626_070_15e-34; // J·s
$hexMask = 0xFF_EC_D5_08;
$binaryFlags = 0b0001_1111_0010_0000;
The underscore is purely syntactic. The engine strips it before parsing the value. You can put it anywhere between digits, though convention follows the natural grouping of the number system you’re working in.
Holding without owning
WeakReference lets you hold a reference to an object without preventing the garbage collector from destroying it. The use case is caches and registries: you want to know an object is alive, but you don’t want to be the reason it stays alive:
$object = new HeavyObject();
$ref = WeakReference::create($object);
var_dump($ref->get()); // object(HeavyObject)
unset($object);
var_dump($ref->get()); // NULL — GC collected it
Before 7.4 you had WeakRef via an extension, and some frameworks were doing SplObjectStorage tricks that didn’t quite behave the same way. The native class is just straightforward.
Serialization without surprise
Custom object serialization before 7.4 went through the Serializable interface: implement serialize() and unserialize(), return a string, reconstruct from it. The problem is that serialize() triggered __sleep(), unserialize() triggered __wakeup(), and the interaction between these hooks was fragile, especially in inheritance hierarchies.
7.4 introduces __serialize() and __unserialize(), which work with arrays instead of strings and don’t interact with the old hooks:
class Session {
private string $token;
private \DateTime $createdAt;
public function __serialize(): array {
return ['token' => $this->token, 'created' => $this->createdAt->getTimestamp()];
}
public function __unserialize(array $data): void {
$this->token = $data['token'];
$this->createdAt = (new \DateTime())->setTimestamp($data['created']);
}
}
When both the new and old methods exist on the same class, __serialize() wins. The old Serializable interface gets deprecated in 8.1.
What the standard library quietly got
mb_str_split() does what str_split() does but correctly for multibyte strings. The gap was genuinely embarrassing for a language used in as many locales as PHP:
mb_str_split('héllo', 1); // ['h', 'é', 'l', 'l', 'o']
str_split('héllo', 1); // ['h', 'Ã', '©', 'l', 'l', 'o'] — broken
strip_tags() now accepts an array of allowed tags, which is cleaner than the string format it used to require:
strip_tags($html, ['p', 'br', 'strong']); // was: '<p><br><strong>'
proc_open() now accepts an array command, bypassing shell interpretation entirely. Same idea as Python’s subprocess with shell=False. Worth knowing whenever you’re passing user-supplied arguments to an external process.
The FFI chapter
The Foreign Function Interface extension landed in 7.4 after spending time in a feature branch. It lets PHP call native C functions by loading a shared library and declaring the signatures:
$ffi = FFI::cdef("int strlen(const char *s);", "libc.so.6");
var_dump($ffi->strlen("hello")); // int(5)
The practical applications are narrow but real: calling platform APIs with no PHP binding, wrapping performance-critical C code without writing a full extension, or just poking at native libraries directly. Not a replacement for proper extensions in production, but it removes the “write a C extension” barrier for exploration.
What got deprecated
A few things that should have been cleaned up a while ago finally got the deprecation treatment in 7.4.
Nested ternaries without parentheses have been ambiguous forever. PHP evaluated them left-to-right while pretty much every other language with a ternary evaluates right-to-left:
// Was ambiguous, now deprecated:
$a ? $b : $c ? $d : $e;
// Make it explicit:
($a ? $b : $c) ? $d : $e;
Curly brace offset access for strings and arrays — $str{0} instead of $str[0] — is deprecated and gone in 8.0. It was always an alias, never a separate feature.
implode() with reversed argument order (array first, glue second) is deprecated. The function has accepted both orders since the beginning, which was a mistake. The correct order is implode(string $separator, array $array).
What comes next
7.4 is the last 7.x release. The deprecations are mostly ground-clearing for removals in 8.0. The RFC backlog for 8.0 is substantial: JIT, attributes, named arguments, match expressions. 7.4 is a solid place to land while waiting for all that to arrive.