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.

:white_check_mark: 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.

:no_entry: 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.

:unlock: 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.

:fishing_pole_and_fish: Catching multiple exceptions

try {
    // ...
} catch (InvalidArgumentException | RuntimeException $e) {
    // handle both
}

Saves a duplicate catch block when two exceptions need identical handling. Simple, useful.

:scissors: 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.

:arrows_counterclockwise: 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.

:dart: 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.

:electric_plug: 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.

:no_entry_sign: 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.