PHP 7.0 → 7.114 language changes15 min readEnd of life

PHP 7.1 language changes since PHP 7.0

A migration-focused tour of the syntax, type-system and engine behavior changes in PHP 7.1, with runnable examples and upgrade advice for code written against PHP 7.0.

PHP 7.1 is a language-focused release. Compared with PHP 7.0 it adds a handful of features that change how you write everyday code: nullable types, a void return type, the iterable pseudo-type, multi-exception catch blocks, class constant visibility, and a much more capable destructuring syntax. Most of these are additive, so existing 7.0 code keeps running, but they let you delete a lot of defensive boilerplate.

The risk in an upgrade sits in the backward-incompatible list, and a few of those items are easy to trip over. Calling a user-defined function with too few arguments now throws ArgumentCountError instead of emitting a warning and carrying on with a missing value. Dynamic calls to functions that inspect or modify another scope, such as compact(), extract() and func_get_args(), are forbidden. A bare return; inside a function with a non-void return type is now a compile-time error, even on a path that never runs. String handling also tightened up: the empty index operator no longer silently converts a string to an array, and writing to an offset of an empty string now behaves like writing to any other string. Numeric strings in scientific notation are honored by integer conversions.

There are also name restrictions: void and iterable can no longer be used as class, interface or trait names, and closures cannot bind a use variable that reuses a parameter name, $this or a superglobal.

This page deliberately concentrates on language-level behavior. The deprecations in this release concern extensions and library functions rather than syntax, and the many additions and changes to functions, classes, constants and INI settings are covered elsewhere.

For the upgrade itself, first run your test suite on 7.1 with all errors enabled and look for the new warnings and fatal errors listed above. Next, search for dynamic calls to the scope-inspecting functions and for return; in typed functions. Finally, adopt the new syntax gradually: nullable types and void are the best first targets because they make contracts explicit and are easy to review.

NEW

Nullable parameter and return types

Prefix a type with ? to allow null in addition to the type, for both parameters and return values.

Before 7.1 there was no way to say that a function returns either a string or null. Return types had to be left off, and parameters had to use a = null default, which also made the argument optional. PHP 7.1 lets you write ?string, meaning the declared type or null, for both parameters and return types.

A nullable parameter is still required: ?string $name accepts null only when you pass it explicitly. Calling the function with no argument at all still fails with an ArgumentCountError. If you want the argument to be omittable, you must still give it a default value.

The feature is most valuable for lookup-style functions that legitimately return nothing, because the signature now documents that callers must handle null, and the engine enforces that nothing else sneaks through. When migrating, replace docblock-only hints and the type $x = null trick with the explicit ?type form where the argument is genuinely required.

Before: PHP 7.0: no way to declare a nullable return
<?php
function label(string $name = null)
{
    return $name === null ? null : strtoupper($name);
}
var_dump(label('php'));

PHP 7.0 and 7.1 accept this; the null default makes the parameter optional and the return type cannot be declared. Shown for comparison; not executed on this site.

After: Nullable parameter and return type
<?php
function label(?string $name): ?string
{
    return $name === null ? null : strtoupper($name);
}
var_dump(label('php'));
var_dump(label(null));
Output on PHP 8.5.10
string(3) "PHP"
NULL

Official migration notes

NEW

The void return type

Functions declared with a void return type must return nothing, and using their result simply yields null.

PHP 7.1 introduces void as a return type for functions that exist purely for their side effects. A void function may omit return entirely or use an empty return;. Returning null explicitly, or returning any value, is not allowed.

The benefit is documentation that the engine checks: readers and static analysis tools know the result is meaningless, and an accidental return $value; is rejected. Reference parameters, as in a swap helper, are a typical use.

A subtle point is that using the result of a void function does not emit a warning; the expression just evaluates to null. The language avoids warnings here so that generic higher-order code, which may pass any callable around, keeps working. Also remember that void is only valid as a return type, not as a parameter type, and that it cannot be combined with a nullable marker.

Before: PHP 7.0: no return type for side-effect functions
<?php
function swap(&$left, &$right)
{
    $tmp = $left;
    $left = $right;
    $right = $tmp;
}

PHP 7.0 and 7.1 accept this; only 7.1 and later can declare the return type as void. Shown for comparison; not executed on this site.

After: Swap helper declared void
<?php
function swap(&$left, &$right): void
{
    $tmp = $left;
    $left = $right;
    $right = $tmp;
}
$a = 1;
$b = 2;
var_dump(swap($a, $b));
echo $a, $b, "\n";
Output on PHP 8.5.10
NULL
21

Official migration notes

CHANGED BEHAVIOR

Too few arguments now throws ArgumentCountError

Calling a user-defined function with missing required arguments used to warn; it now throws an Error exception.

In PHP 7.0, calling a user-defined function with too few arguments produced a warning, and the function body still ran with the missing parameters unset. That meant bugs could slip through as logged warnings while the code carried on with null values.

Starting with 7.1 the warning is promoted to an Error exception, specifically ArgumentCountError, so the call stops immediately. Uncaught, it is a fatal error. This applies to user-defined functions only; internal functions were not changed by this item.

During migration, the practical risk is old code that was sloppy about arguments and relied on the warning being ignored or silenced. Turn on full error reporting in a 7.0 environment first and fix every "Missing argument" warning you find. Do not paper over the new exception by catching it broadly; supply the argument or give the parameter a default value.

Before: PHP 7.0: warning, then execution continues
<?php
function greet(string $name)
{
    return 'Hello, ' . $name;
}
echo greet();

PHP 7.0 emits a warning for the missing argument and continues; PHP 7.1 throws ArgumentCountError. Shown for comparison; not executed on this site.

After: Catching the ArgumentCountError
<?php
function greet(string $name)
{
    return 'Hello, ' . $name;
}
try {
    echo greet(), "\n";
} catch (ArgumentCountError $e) {
    echo get_class($e), "\n";
}
Output on PHP 8.5.10
ArgumentCountError

Official migration notes

NEW

Keys in list() and [] destructuring

Destructuring can now pull values out by key, so associative rows can be unpacked directly in assignments and foreach.

Before 7.1, list() only worked with positional integer keys starting at zero. Unpacking an associative row meant a series of manual array reads. PHP 7.1 lets you specify keys inside list() or the new bracket shorthand, which handles string keys and non-sequential integer keys.

This is especially pleasant in foreach loops over database-style rows, where you can name just the columns you care about and ignore the rest. Keys are looked up directly, so the order inside the pattern does not need to match the order in the array.

A pitfall: you cannot mix keyed and unkeyed entries in the same pattern. If a key is missing from the source array, the target variable becomes null, and PHP raises a notice or warning about the undefined key depending on the version, so only destructure keys you know exist.

Before: PHP 7.0: manual key lookups
<?php
$row = ['id' => 1, 'name' => 'Tom'];
$id = $row['id'];
$name = $row['name'];

Works in PHP 7.0 and later; keyed list() requires PHP 7.1. Shown for comparison; not executed on this site.

After: Keyed destructuring in foreach and assignment
<?php
$rows = [
    ['id' => 1, 'name' => 'Tom'],
    ['id' => 2, 'name' => 'Fred'],
];
foreach ($rows as ['id' => $id, 'name' => $name]) {
    echo "$id:$name\n";
}
['name' => $first] = $rows[0];
echo $first, "\n";
Output on PHP 8.5.10
1:Tom
2:Fred
Tom

Official migration notes

NEW

Symmetric array destructuring with []

The short array syntax can now appear on the left of an assignment or in foreach as an alternative to list().

PHP 7.1 makes array literals symmetric: the [] syntax you already use to build arrays can now also take them apart. It works in plain assignments and as the loop target in foreach, and list() remains fully supported, so there is no need to rewrite existing code.

The main benefit is readability and consistency. A pattern such as [$id, $name] mirrors the shape of the data, and swapping two variables becomes a one-liner because the right-hand array is built before the assignment happens.

A migration caution is that code using [] on the left side will not parse on PHP 7.0, so adopting it raises your minimum version. Also keep in mind that destructuring is positional unless you add keys, so the number and order of elements still matters.

Before: PHP 7.0: list() only
<?php
$pair = [1, 'Tom'];
list($id, $name) = $pair;
foreach ([[1, 'Tom']] as list($i, $n)) {
    echo $i, $n;
}

list() works in PHP 7.0 and later; the [] form on the left side needs PHP 7.1. Shown for comparison; not executed on this site.

After: Bracket destructuring
<?php
$pairs = [[1, 'Tom'], [2, 'Fred']];
[$id, $name] = $pairs[0];
echo "$id $name\n";
foreach ($pairs as [$id, $name]) {
    echo "$id=$name\n";
}
[$x, $y] = [10, 20];
[$x, $y] = [$y, $x];
echo "$x $y\n";
Output on PHP 8.5.10
1 Tom
1=Tom
2=Fred
20 10

Official migration notes

NEW

Class constant visibility

Class constants can be declared public, protected or private, finally letting classes hide internal constants.

Until 7.1, every class constant was effectively public. Anything you defined as a constant could be read from anywhere, so internal magic numbers and keys became part of the public API by accident. PHP 7.1 allows public, protected and private modifiers on constants.

A constant declared without a modifier remains public, so existing classes keep working. Private constants are visible only inside the declaring class, and protected ones also to subclasses. Reading a non-visible constant from outside throws an Error.

When upgrading, review classes that expose helper constants and decide which of them are really part of the contract. Tightening visibility is a breaking change for any caller that was relying on a constant, so do it deliberately, and remember that interface constants remain public.

Before: PHP 7.0: every constant is public
<?php
class Config
{
    const VERSION = '1.0';
    const SECRET = 'hidden';
}

PHP 7.0 accepts this and treats both constants as public; visibility modifiers need PHP 7.1. Shown for comparison; not executed on this site.

After: Public and private constants
<?php
class Config
{
    public const VERSION = '1.0';
    private const SECRET = 'hidden';
    public static function check(): string
    {
        return self::SECRET === 'hidden' ? 'ok' : 'bad';
    }
}
echo Config::VERSION, "\n";
echo Config::check(), "\n";
try {
    echo Config::SECRET;
} catch (Error $e) {
    echo "private\n";
}
Output on PHP 8.5.10
1.0
ok
private

Official migration notes

NEW

The iterable pseudo-type

iterable accepts arrays and Traversable objects in parameter and return types, and the name is now reserved.

Functions that just loop over their input often accept either an array or an iterator or generator, but 7.0 could not express that in a type declaration. PHP 7.1 adds iterable, a pseudo-type that matches arrays and any object implementing Traversable. It can be used for parameters and return types.

Inheritance rules follow the idea of substitution. A child class may widen a parent's array or Traversable parameter to iterable, and may narrow a parent's iterable return type to array or a Traversable implementation.

There is a compatibility catch: iterable (like void) can no longer be used as a class, interface or trait name. Search your codebase for any class with that name before upgrading. Also remember that iterable only guarantees you can loop once; generators cannot be rewound after they start.

Before: PHP 7.0: untyped parameter and a docblock
<?php
/** @param array|Traversable $items */
function total($items)
{
    $sum = 0;
    foreach ($items as $item) {
        $sum += $item;
    }
    return $sum;
}

Works in PHP 7.0 and later but the type is not enforced; iterable needs PHP 7.1. Shown for comparison; not executed on this site.

After: Accepting arrays and generators
<?php
function total(iterable $items): int
{
    $sum = 0;
    foreach ($items as $item) {
        $sum += $item;
    }
    return $sum;
}
function numbers(): Generator
{
    yield 1;
    yield 2;
    yield 3;
}
echo total([1, 2, 3]), "\n";
echo total(numbers()), "\n";
Output on PHP 8.5.10
6
6

Official migration notes

NEW

Multi-catch with the pipe character

One catch block can now handle several exception classes listed with |.

When two unrelated exception types need the same handling, PHP 7.0 forced you either to duplicate the catch body or to catch a broad common parent and risk swallowing more than intended. PHP 7.1 lets you list several classes in a single catch, separated by the pipe character.

The variable receives whichever exception was actually thrown, so you can still inspect its class or message. Matching is the usual instanceof check against each listed type, in any order.

This is purely additive, so it is safe to adopt incrementally. It is a good fit for cleanup, logging, or translating different low-level failures into one domain error. Avoid listing a class together with its own subclass, as that is redundant, and remember that code using the syntax will not parse on PHP 7.0.

Before: PHP 7.0: duplicated catch blocks
<?php
try {
    risky();
} catch (InvalidArgumentException $e) {
    echo get_class($e);
} catch (RuntimeException $e) {
    echo get_class($e);
}

Works in PHP 7.0 and later; the combined catch syntax needs PHP 7.1. Shown for comparison; not executed on this site.

After: One handler for two exception types
<?php
function risky(int $n): void
{
    if ($n === 1) {
        throw new InvalidArgumentException('a');
    }
    throw new RuntimeException('b');
}
foreach ([1, 2] as $n) {
    try {
        risky($n);
    } catch (InvalidArgumentException | RuntimeException $e) {
        echo get_class($e), "\n";
    }
}
Output on PHP 8.5.10
InvalidArgumentException
RuntimeException

Official migration notes

NEW

Negative string offsets

String indexing, including inside interpolated strings, now accepts negative offsets counted from the end.

PHP 7.1 interprets a negative string offset as a position counted from the end of the string, so -1 is the last character. This works with square-bracket indexing and also in the simple variable syntax inside double-quoted strings. The release also extends it to string functions that take offsets, which belong to the library side of the release.

Previously you had to compute the position yourself from the string length, which was both noisy and a common source of off-by-one mistakes. With the new behavior, the last or second-to-last character is a direct read.

Two pitfalls remain. An offset that reaches before the start of the string is still out of range, so do not assume negative offsets are always safe. Also, the older curly-brace form of string offsets was later removed from the language in PHP 8.0, so prefer square brackets.

Before: PHP 7.0: compute the offset by hand
<?php
$s = 'abcdef';
echo $s[strlen($s) - 1];

Works in PHP 7.0 and later; negative offsets for indexing require PHP 7.1. Shown for comparison; not executed on this site.

After: Negative offsets in indexing and interpolation
<?php
$s = 'abcdef';
echo $s[-1], "\n";
echo $s[-2], "\n";
echo "Last: $s[-1]\n";
Output on PHP 8.5.10
f
e
Last: f

Official migration notes

CHANGED BEHAVIOR

Bare return in typed functions is a compile error

A return statement with no value inside a function that has a non-void return type now fails at compile time.

In PHP 7.1, a return; statement without a value inside a function that declares a return type triggers a compile-time fatal error, unless the declared type is void. The check happens when the file is compiled, so it applies even if that particular return statement could never be reached at runtime.

This matters because such code could previously sit unnoticed in a rarely used branch. After upgrading, the whole file fails to load, which makes the problem impossible to miss but also means a single bad line can take down a request.

The fix depends on intent. If the function should sometimes produce nothing, declare a nullable return type and write return null; explicitly. If it never produces a value, declare it void. Scan typed functions for bare return; before deploying.

Before: PHP 7.1+: fatal at compile time
<?php
function firstEven(array $values): int
{
    foreach ($values as $v) {
        if ($v % 2 === 0) {
            return $v;
        }
    }
    return;
}

The bare return was not rejected at compile time before 7.1; PHP 7.1 and later report a compile error for it. Shown for comparison; not executed on this site.

After: Explicit null with a nullable return type
<?php
function firstEven(array $values): ?int
{
    foreach ($values as $v) {
        if ($v % 2 === 0) {
            return $v;
        }
    }
    return null;
}
var_dump(firstEven([1, 3, 4]));
var_dump(firstEven([1, 3]));
Output on PHP 8.5.10
int(4)
NULL

Official migration notes

REMOVED

Dynamic calls to scope-introspection functions are forbidden

Calling functions such as func_get_args(), compact() or extract() through variable names or callbacks no longer works.

Some built-in functions look at or modify the variables of the calling scope. Calling them indirectly, for example through $func() or by passing the name to array_map(), makes it unclear which scope they would act on, so PHP 7.1 forbids it. The affected functions include compact(), extract(), func_get_args(), func_get_arg(), func_num_args() and get_defined_vars(). A few others are restricted only in certain forms: assert() with a string argument, and parse_str() and mb_parse_str() with one argument.

Attempting a dynamic call now yields a warning that the function cannot be called dynamically, rather than doing something unreliable.

The replacement is simply to call the function directly, by name, in the scope that you want it to inspect. If you were building generic helpers around these functions, restructure them to pass arrays explicitly instead of relying on the caller's scope.

Before: PHP 7.0: dynamic call to func_num_args()
<?php
(function () {
    $func = 'func_num_args';
    return $func();
})();

PHP 7.0 allowed this; PHP 7.1 emits a warning that the function cannot be called dynamically. Shown for comparison; not executed on this site.

After: Direct calls inside the function
<?php
function countArgs()
{
    return func_num_args();
}
function sumArgs()
{
    return array_sum(func_get_args());
}
echo countArgs('a', 'b', 'c'), "\n";
echo sumArgs(1, 2, 3), "\n";
Output on PHP 8.5.10
3
6

Official migration notes

CHANGED BEHAVIOR

Offset assignment on an empty string

Writing to an offset of an empty string now pads the string like any other string instead of turning it into an array.

In PHP 7.0, an empty string was silently treated like an empty array when you assigned to an offset, so $a = ''; $a[10] = 'foo'; produced an array with one element. PHP 7.1 removes that special case. The empty string now behaves like any other string: writing beyond its end pads it with spaces, non-integer offsets are converted to integers, and only the first character of the assigned value is used.

This can quietly change data shapes. Code that started from an empty string and relied on getting an array out will now produce a string, and later array functions or foreach loops may misbehave.

If you want an array, initialize it with []. If you want to build a string, use concatenation. Assign a single character when you really mean to write one position, because later PHP versions warn when a longer string is assigned to an offset.

Before: PHP 7.0: empty string becomes an array
<?php
$a = '';
$a[10] = 'foo';
var_dump($a);

PHP 7.0 produces an array with key 10; PHP 7.1 produces a padded string. Shown for comparison; not executed on this site.

After: Padding behavior shown with visible dots
<?php
$a = '';
$a[10] = 'f';
echo strlen($a), "\n";
echo str_replace(' ', '.', $a), "\n";
Output on PHP 8.5.10
11
..........f

Official migration notes

REMOVED

The empty index operator on strings is an error

Using $str[] = $x on a string now causes a fatal error instead of silently converting the string to an array.

The empty index operator, written [] with no key, appends to an array. In PHP 7.0, applying it to a string quietly converted the variable into an array, which was almost never what the author intended and could hide type bugs. PHP 7.1 turns this into a fatal error.

The fix depends on what you wanted. To add text to a string, use concatenation with .=. To collect a list of items, start from an array and append with []. Because the failure is fatal rather than a warning, it is worth grepping for [] = on variables that might hold strings, especially ones initialized as '' and later appended to.

Running the test suite on 7.1 with full error reporting will surface the remaining cases quickly, because the engine now stops instead of changing the variable's type.

Before: PHP 7.0: string silently becomes an array
<?php
$str = '';
$str[] = 'x';
var_dump($str);

PHP 7.0 converts the string to an array; PHP 7.1 and later throw a fatal error. Shown for comparison; not executed on this site.

After: Concatenate for strings, append for arrays
<?php
$str = 'ab';
$str .= 'c';
$list = [];
$list[] = 'c';
echo $str, "\n";
echo count($list), "\n";
Output on PHP 8.5.10
abc
1

Official migration notes

CHANGED BEHAVIOR

Numeric strings honor scientific notation

Integer conversions of numeric strings such as '1e3' now take the exponent into account.

Integer operations and conversions on numerical strings now respect scientific notation. This covers the (int) cast, intval() with base 10, settype(), and the base-conversion helpers decbin(), decoct() and dechex().

In PHP 7.0, a string like '1e3' converted to the integer 1, because parsing stopped at the letter. In 7.1 it converts to 1000. That is arguably what a reader expects, but it is a silent behavior change: values read from forms, CSV files or configuration that happen to look like exponent notation will produce different integers after the upgrade.

When you migrate, check places where strings that may contain an e are cast to int, such as product codes or identifiers that are all digits with a single letter e in the middle. If such values are identifiers rather than numbers, keep them as strings and do not cast them.

Before: PHP 7.0: exponent ignored
<?php
echo (int) '1e3', "\n";
echo intval('2.5e2'), "\n";

PHP 7.0 prints 1 and 2; PHP 7.1 and later print 1000 and 250. Shown for comparison; not executed on this site.

After: Exponent respected in integer conversion
<?php
echo (int) '1e3', "\n";
echo intval('2.5e2'), "\n";
Output on PHP 8.5.10
1000
250

Official migration notes

← PHP 7.0 changes PHP 7.2 changes →