PHP 7.1: a tighter type system and the small wins around it
PHP 7.1 shipped December 1st. No 2x performance headline, no engine rewrite. It fills in the gaps that 7.0 left in the type system, and those gaps were genuinely annoying.
Nullable types
7.0 let you declare string $name as a parameter type. What it didn’t let you do was say “this can also be null”. You had to drop the type hint entirely or hack around it. 7.1 adds ? prefix:
function findUser(?int $id): ?User {
if ($id === null) return null;
return $this->repository->find($id);
}
This sounds minor. It’s not. Nullable types make the difference between a signature that tells you what a function does and one that lies by omission. Every codebase I’ve worked on has functions that can return null. Now you can actually say so instead of hiding it in a docblock.
Void return type
The complement to nullable: a function that intentionally returns nothing:
public function process(Order $order): void {
$this->dispatcher->dispatch(new OrderProcessed($order));
}
void makes the intent explicit and prevents accidentally returning a value from a function that shouldn’t. Combined with nullable types, PHP’s type system in 7.1 is quite a bit more expressive than 7.0.
Class constant visibility
A small but welcome fix. Constants in classes were always public before 7.1. Now:
class Config {
private const DB_PASSWORD = 'secret';
protected const VERSION = '2.0';
public const MAX_RETRIES = 3;
}
Keeping implementation details private matters. This should have existed from the start.
Catching multiple exceptions
try {
// ...
} catch (InvalidArgumentException | RuntimeException $e) {
// handle both
}
Saves a duplicate catch block when two exceptions need identical handling. Simple, useful.
Destructuring arrays without list()
list() has been in PHP since 4.0 and always felt slightly out of place syntactically. 7.1 adds a shorthand using [] that reads much more naturally:
[$first, $second] = $coordinates;
foreach ($rows as [$id, $name, $email]) {
// ...
}
It also gains key support, which makes destructuring associative arrays finally usable:
['id' => $id, 'name' => $name] = $user;
foreach ($records as ['id' => $id, 'status' => $status]) {
// ...
}
Before this, extracting named keys from an array meant either extract() (which dumps everything into scope and invites collisions) or a bunch of individual assignments. This is just cleaner.
The iterable type
If you write a function that accepts either an array or a generator, there was no clean type hint for that in 7.0. You either typed it as array and silently excluded generators, or dropped the hint entirely:
function processItems(iterable $items): void {
foreach ($items as $item) {
$this->handle($item);
}
}
iterable accepts anything you can foreach over: arrays and Traversable implementations. It also works as a return type. Not dramatic, but it closes a real gap.
Negative string offsets
String indexing with [] or {} now accepts negative values, counting from the end:
$str = 'hello';
echo $str[-1]; // "o"
echo $str[-2]; // "l"
Several string functions got the same treatment: strpos(), substr(), substr_count(), and others now accept a negative offset. Consistent with how Python has worked forever. Better late than never.
Closure::fromCallable()
Before this, converting a callable (like [$object, 'method'] or a function name string) to a proper Closure required Closure::bind() or bindTo() with awkward scope handling. 7.1 adds a static factory method:
class Processor {
private function transform(string $value): string {
return strtoupper($value);
}
public function getTransformer(): Closure {
return Closure::fromCallable([$this, 'transform']);
}
}
The resulting closure captures the correct $this and scope. It’s particularly useful when passing methods as callbacks to functions that expect callable, or when building pipelines.
ArgumentCountError
In PHP 7.0, calling a user-defined function with too few arguments generated a warning and execution continued with null-filled parameters. In 7.1, it throws an ArgumentCountError:
function connect(string $host, int $port): void { /* ... */ }
try {
connect('localhost'); // Throws ArgumentCountError
} catch (\ArgumentCountError $e) {
// ...
}
ArgumentCountError extends TypeError, which extends Error. Call sites that previously silently degraded now fail loudly. That’s a migration risk if you have code that relied on the permissive behavior, but honestly, it’s the right call.
7.1 is the kind of release that makes you trust a platform more. The core team was clearly paying attention to the friction, not just shipping headlines.