PHP 5.6 → 7.014 language changes15 min readEnd of life

PHP 7.0 language changes since PHP 5.6

A migration-focused tour of the PHP 7.0 language changes: scalar and return types, Error exceptions, new operators, foreach semantics and the first deprecations.

PHP 7.0 is the release where the language itself moved a long way from PHP 5.6. The parser now builds an abstract syntax tree, which allowed a more consistent grammar and also removed several special cases that older code may have depended on. This overview covers language-level behavior only, not new library functions, classes, constants or INI settings.

The headline additions are scalar type declarations and return type declarations, which let you state int, float, string and bool contracts directly in a signature. The default coercive mode keeps most existing calls working, while declare(strict_types=1) gives per-file strictness. Alongside them come the null coalescing operator ?? and the spaceship operator <=>, which remove a lot of ternary and comparison-callback boilerplate. Anonymous classes, grouped use declarations, generator return values and yield from round out the new syntax.

The changes most likely to break an upgrade are semantic. Many fatal and recoverable errors are now thrown as Error exceptions, so handlers typed against Exception and code that expected a fatal error may behave differently. foreach no longer moves the internal array pointer, iterates by-value loops over a copy, and follows appends during by-reference loops. Indirect variable expressions such as $$foo['bar'] are evaluated strictly left to right, and list() assigns in the order written. Several previously tolerated constructs are now errors, including assigning new by reference.

Two deprecations are worth fixing during the move: PHP 4 style constructors and static calls to non-static methods. Both still run in 7.0 but emit E_DEPRECATED, and both become stricter in later releases.

For an upgrade from 5.6, first run your test suite with error_reporting set to E_ALL and look for new deprecations and notices. Then search for exception handlers typed as Exception, list() calls that rely on order, variable-variable expressions with array or property access, and classes named after reserved scalar types. Adopt the new syntax gradually. Add types and ?? where they simplify code, and leave strict typing for a separate step once the coercive version is green. The examples below run on PHP 7.0 and on current PHP, and where a later release tightened the same feature, the explanation says so.

NEW

Scalar type declarations with optional strict mode

Parameters can be declared int, float, string or bool, with coercive behavior by default and per-file strictness through declare(strict_types=1).

In PHP 5.6 you could type-hint only classes, interfaces, array and callable, so scalar contracts lived in docblocks or manual casts. PHP 7.0 lets you declare int, float, string and bool on parameters. By default the engine uses coercive mode: a numeric string passed to an int parameter is converted, which keeps most legacy callers working.

Place declare(strict_types=1); at the very top of a file to make calls made from that file strict. The directive is per file, not global. It follows the caller, so a strict file calling a library function gets strict checking, while a coercive file calling your strict code does not. A mismatch raises TypeError.

Pitfalls: turning on strict mode for an old file can expose calls that passed strings from forms or databases. Also, coercive mode still accepts some lossy conversions such as a float with a fractional part to int; PHP 8.1 later deprecated that case, so do not depend on it. Types in a signature are also not a replacement for validating untrusted input.

Before: PHP 5.6: manual casts or docblocks
<?php
/**
 * @param int $a
 * @param int $b
 */
function add($a, $b)
{
    return (int) $a + (int) $b;
}

echo add(2, '3'), "\n";

Valid in PHP 5.6 and later; in 5.6 `int` as a parameter type would be read as a class name, so scalar contracts could not be enforced. Shown for comparison; not executed on this site.

After: Strict scalar types
<?php
declare(strict_types=1);

function add(int $a, int $b): int
{
    return $a + $b;
}

echo add(2, 3), "\n";
try {
    echo add(2, '3'), "\n";
} catch (TypeError $e) {
    echo "TypeError caught\n";
}
Output on PHP 8.5.10
5
TypeError caught

Official migration notes

NEW

Return type declarations

Functions, methods and closures can declare the type of the value they return, using the same types available for parameters.

PHP 7.0 adds a return type after the parameter list, written with a colon. The types available are the same as for parameters, so array, callable, class names and the new scalar types all work. The declaration applies to named functions, methods and closures.

This matters because the contract is now enforced by the engine rather than by a comment. A function declared to return int that returns a non-convertible value fails with a TypeError instead of silently passing bad data along. Return types are also checked under the file's strict_types setting, so strict files do not coerce returned values.

Pitfalls: in PHP 7.0 you cannot declare a nullable return type or void; those arrived in later versions, so an unrestricted return value is still needed for functions that may return null. Every return path must produce the declared type, including early returns, and a bare return; is not allowed in a function with a return type. Add types to leaf helpers first, where the behavior is easy to verify.

Before: PHP 5.6: documented but unenforced
<?php
/** @return int[] */
function lengths()
{
    return array_map('strlen', func_get_args());
}

Valid in PHP 5.6 and later; the docblock is only documentation and nothing checks the returned value. Shown for comparison; not executed on this site.

After: Return types on a function and a closure
<?php
function lengths(string ...$words): array
{
    return array_map(function (string $word): int {
        return strlen($word);
    }, $words);
}

echo json_encode(lengths('php', 'seven')), "\n";
Output on PHP 8.5.10
[3,5]

Official migration notes

CHANGED BEHAVIOR

Fatal errors become Error exceptions

Many fatal and recoverable errors are now thrown as Error objects implementing Throwable, so they can be caught and may bypass old error handlers.

In PHP 5.6 problems such as calling an undefined function ended the script with a fatal error that you could not catch. PHP 7.0 converts many of these into exceptions that extend Error. Both Error and Exception implement the new Throwable interface, so catch (Throwable $t) handles either family. Specific subclasses such as TypeError, ParseError and DivisionByZeroError exist too. The modulo operator with a zero divisor now throws DivisionByZeroError instead of warning and returning false.

The main pitfall is code that was written against the old behavior. A set_exception_handler() callback typed as Exception will itself fail with a fatal error when an Error arrives. Use Throwable for PHP 7-only code, or drop the type if the handler must also run on PHP 5. Custom error handlers set with set_error_handler() may no longer be triggered for conditions that now throw. Wrap eval() calls with a handler for ParseError.

Note that PHP 8.0 later made the / operator throw DivisionByZeroError as well, whereas in 7.0 it returns INF or NAN with a warning.

Before: PHP 5.6 handler typed as Exception
<?php
function handler(Exception $e)
{
    echo $e->getMessage(), "\n";
}
set_exception_handler('handler');

Accepted by PHP 5.6 and 7.0, but in PHP 7.0 and later it causes a fatal error when an uncaught Error is thrown. Shown for comparison; not executed on this site.

After: Catching Error subclasses
<?php
function remainder(int $divisor)
{
    return 10 % $divisor;
}

try {
    echo remainder(0), "\n";
} catch (DivisionByZeroError $e) {
    echo get_class($e), ': ', $e->getMessage(), "\n";
}

try {
    missing_function();
} catch (Throwable $t) {
    echo get_class($t), ': ', $t->getMessage(), "\n";
}
Output on PHP 8.5.10
DivisionByZeroError: Modulo by zero
Error: Call to undefined function missing_function()

Official migration notes

NEW

Null coalescing operator ??

The ?? operator returns its left operand when it exists and is not null, otherwise the right operand, replacing isset() ternaries.

The ?? operator is shorthand for the very common isset($x) ? $x : $default pattern. It returns the first operand if that operand exists and is not null, and otherwise evaluates and returns the second. Because it performs an isset-style check, reading an undefined variable, array key or property does not raise a notice.

Operators can be chained, and the first defined non-null value wins, which makes layered configuration lookups short and readable.

Pitfalls: ?? treats only null or missing as absent. A value of 0, '' or false is returned as is, unlike the short ternary ?:, which would replace those. If a default of 0 should apply to an empty string, ?? is the wrong tool. Also remember it checks existence, so a typo in a key name silently falls through to the default instead of warning. The right-hand side is evaluated only when needed, which is useful when the default is expensive to build.

Before: PHP 5.6 isset ternary
<?php
$config = ['host' => 'localhost'];
$host = isset($config['host']) ? $config['host'] : 'example.test';
echo $host, "\n";

Valid in PHP 5.6 and all later versions; `??` is the shorter equivalent. Shown for comparison; not executed on this site.

After: Defaults and chaining with ??
<?php
$config = ['host' => 'localhost', 'port' => null];

$host = $config['host'] ?? 'example.test';
$port = $config['port'] ?? 3306;
$user = $config['user'] ?? $config['login'] ?? 'guest';

echo "$host:$port:$user\n";
Output on PHP 8.5.10
localhost:3306:guest

Official migration notes

NEW

Spaceship operator <=>

The <=> operator returns -1, 0 or 1 for less than, equal or greater than, using PHP's normal comparison rules, which simplifies sort callbacks.

Comparison callbacks for usort() used to be written with nested if statements or a ternary that returned -1, 0 or 1. PHP 7.0 adds <=>, which does exactly that in one expression and follows the usual type comparison rules for integers, floats, strings and arrays.

It becomes especially useful with arrays: comparing two arrays element by element lets you sort by several keys at once. To sort a field in descending order, swap the operands for that field only.

Pitfalls: the operator inherits PHP's loose comparison rules for mixed types, so comparing a numeric string with an integer may not behave as you expect. In PHP 8.0 the string-to-number comparison rules changed, so keep sort keys type-consistent. The result is always exactly -1, 0 or 1, which is safe to return from a sort callback, but do not subtract values to build a comparison yourself, since float differences get truncated when converted to an integer return value.

Before: PHP 5.6 comparison callback
<?php
$ages = [31, 25, 40];
usort($ages, function ($a, $b) {
    if ($a == $b) {
        return 0;
    }
    return ($a < $b) ? -1 : 1;
});

Valid in PHP 5.6 and later; `<=>` replaces the whole body. Shown for comparison; not executed on this site.

After: Multi-key sorting with <=>
<?php
$people = [
    ['name' => 'Maya', 'age' => 31],
    ['name' => 'Ben', 'age' => 25],
    ['name' => 'Chloe', 'age' => 31],
];

echo json_encode([1 <=> 2, 2 <=> 2, 3 <=> 2]), "\n";

usort($people, function ($a, $b) {
    return [$b['age'], $a['name']] <=> [$a['age'], $b['name']];
});
echo implode(',', array_column($people, 'name')), "\n";
Output on PHP 8.5.10
[-1,0,1]
Chloe,Maya,Ben

Official migration notes

CHANGED BEHAVIOR

foreach no longer moves the array pointer and follows by-reference appends

foreach leaves the internal array pointer alone, by-value loops use a copy of the array, and by-reference loops now see elements added during iteration.

PHP 7.0 reworked how foreach interacts with arrays. In PHP 5.6 the loop advanced the array's internal pointer, so current() and next() inside or after a loop could return surprising values. In 7.0 the pointer is left untouched, and any code that read the pointer position to find out where the loop was must be rewritten.

A by-value loop now iterates over its own copy of the array, so changes made to the original during the loop never alter which values are visited. A by-reference loop is more precise at tracking changes: if you append to the array while iterating, the new elements are visited as well. In 5.6 the same loop would stop after the original elements.

Pitfalls: a by-reference loop that keeps adding elements can run forever, so give it a guard condition. Also unset the reference variable after the loop, because it still points at the last element and a later reuse of the same name will overwrite that element.

Before: PHP 5.6 result of the same loop
<?php
$array = [0];
foreach ($array as &$val) {
    echo $val, "\n";
    $array[1] = 1;
}

In PHP 5.6 this prints only 0; PHP 7.0 and later also visits the appended element and print 0 then 1. Shown for comparison; not executed on this site.

After: By-reference loop that grows the array
<?php
$array = [0];
foreach ($array as &$val) {
    echo $val, "\n";
    if ($val < 2) {
        $array[] = $val + 1;
    }
}
unset($val);

$nums = [10, 20, 30];
foreach ($nums as &$n) {
}
unset($n);
echo current($nums), "\n";
Output on PHP 8.5.10
0
1
2
10

Official migration notes

DEPRECATED

PHP 4 style constructors are deprecated

A method named after its class still acts as a constructor in 7.0 when it is the only one, but it triggers E_DEPRECATED and will stop working.

Before __construct(), a constructor was a method with the same name as its class. PHP 7.0 still honors that style, but emits E_DEPRECATED when such a method is the only constructor in the class. Classes that define __construct() are unaffected, and if both exist, __construct() wins without a message.

The fix is mechanical: rename the method to __construct, and update any parent::OldName() calls to parent::__construct(). Do this in vendored legacy code too, since the deprecation notice appears at compile time and can flood logs.

This matters beyond tidiness. The source says the style "will be removed in the future", and PHP 8.0 did exactly that: a method with the class name becomes an ordinary method and is no longer called on new. Code that still relies on it would then silently skip its initialization logic, which is far harder to debug than a deprecation message today.

Before: PHP 4 style constructor
<?php
class Mailer
{
    private $transport;

    public function Mailer($transport)
    {
        $this->transport = $transport;
    }
}

Works in PHP 5.6; emits E_DEPRECATED in PHP 7.0; since PHP 8.0 it is no longer treated as a constructor. Shown for comparison; not executed on this site.

After: Use __construct()
<?php
class Mailer
{
    private $transport;

    public function __construct($transport)
    {
        $this->transport = $transport;
    }

    public function describe()
    {
        return "Mailer via {$this->transport}";
    }
}

echo (new Mailer('smtp'))->describe(), "\n";
Output on PHP 8.5.10
Mailer via smtp

Official migration notes

DEPRECATED

Static calls to non-static methods are deprecated

Calling a method not declared static with the Class::method() syntax emits E_DEPRECATED in PHP 7.0, and from an incompatible context $this is undefined.

PHP 5.6 let you write Foo::bar() even when bar() was an instance method, as long as it did not use $this. PHP 7.0 deprecates this: the call still runs but emits E_DEPRECATED, and the manual says the behavior may be removed in the future.

There is a stricter edge case. If the call is made from a context that is not compatible, for example from an unrelated class, the method runs with $this undefined. In 5.6 that situation was already deprecated and assumed the caller's $this. In 7.0 you get an undefined variable notice if the method touches it.

Resolve each case by choosing what the method really is. If it needs no object state, declare it static. If it uses state, create or inject an instance and call it normally. Be careful when making an existing method static, since other code may call it through an object, and any $this usage inside must be removed. PHP 8.0 later turned the non-static call into an error.

Before: Static call to an instance method
<?php
class Counter
{
    public function hello()
    {
        return 'instance';
    }
}

echo Counter::hello(), "\n";

Works in PHP 5.6 (with a strict standards notice); emits E_DEPRECATED in PHP 7.0; throws an Error in PHP 8.0 and later. Shown for comparison; not executed on this site.

After: Declare static, or call on an instance
<?php
class Counter
{
    public static function label()
    {
        return 'static';
    }

    public function hello()
    {
        return 'instance';
    }
}

echo Counter::label(), "\n";
echo (new Counter)->hello(), "\n";
Output on PHP 8.5.10
static
instance

Official migration notes

NEW

Anonymous classes

new class lets you define and instantiate a throwaway class inline, optionally extending a class or implementing interfaces.

PHP 7.0 adds new class { ... }, which declares and instantiates a class in a single expression. The class has no name you can reference, but it can extend a parent, implement interfaces and have properties and methods like any other class.

This is a good fit for one-off implementations: a test double, a small adapter, or a logger passed to a single service. It avoids declaring a named class that is used in exactly one place and keeps the behavior next to the code that needs it.

Pitfalls: because the class has no usable name, you cannot type-check it with instanceof against that name, though you can check the interface or parent it uses. Each place in the source creates one class, even if the expression runs many times. Constructor arguments are passed after the class keyword. If the same behavior is needed in several places, give it a real name, since anonymous classes are harder to reuse and to read in stack traces and debug output.

Before: PHP 5.6 named throwaway class
<?php
interface Logger
{
    public function log($msg);
}

class EchoLogger implements Logger
{
    public function log($msg)
    {
        echo $msg;
    }
}

Valid in PHP 5.6 and later; you had to name even a single-use class. Shown for comparison; not executed on this site.

After: Anonymous class implementing an interface
<?php
interface Logger
{
    public function log(string $msg);
}

class Service
{
    private $logger;

    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }

    public function run()
    {
        $this->logger->log('started');
    }
}

$service = new Service(new class implements Logger {
    public function log(string $msg)
    {
        echo "LOG: $msg\n";
    }
});
$service->run();
Output on PHP 8.5.10
LOG: started

Official migration notes

NEW

Group use declarations

Classes, functions and constants from one namespace can be imported with a single use statement using braces.

Large files that import many symbols from the same namespace used to repeat the namespace prefix on every line. PHP 7.0 allows grouping: use Vendor\Package\{ClassA, ClassB as Alias}; imports several classes at once, and aliases are still allowed inside the braces.

Functions and constants are grouped the same way by adding function or const after use, for example use function Vendor\Package\{fn_a, fn_b};. Each kind needs its own statement, because classes, functions and constants are imported separately.

This is purely a syntax convenience, so it changes no runtime behavior. The pitfall is compatibility: files using grouped imports will not parse on PHP 5.6, so apply it only to code that no longer needs to run there. Also keep the prefix before the opening brace complete, and watch for tools or older static analyzers that do not understand the syntax. The example uses braced namespace blocks so that the namespaced symbols and the importing code can live in one file.

Before: PHP 5.6 separate imports
<?php
use Lib\A;
use Lib\B as Other;
use function Lib\helper;
use const Lib\LIMIT;

Valid in PHP 5.6 and all later versions; grouping is only a shorter spelling. Shown for comparison; not executed on this site.

After: Grouped imports for classes, functions and constants
<?php
namespace Lib {
    class A { public function name() { return 'A'; } }
    class B { public function name() { return 'B'; } }
    function helper() { return 'helper'; }
    const LIMIT = 5;
}

namespace {
    use Lib\{A, B as Other};
    use function Lib\{helper};
    use const Lib\{LIMIT};

    echo (new A)->name(), (new Other)->name(), helper(), LIMIT, "\n";
}
Output on PHP 8.5.10
ABhelper5

Official migration notes

NEW

Generator return expressions and yield from

Generators can end with a return value fetched through getReturn(), and yield from delegates to another generator, array or Traversable.

PHP 5.5 introduced generators; PHP 7.0 extends them in two ways. First, a generator may use return expr; to provide a final value that is not part of the yielded sequence. Once the generator has finished, getReturn() retrieves it. Calling it earlier is an error, and returning by reference is not allowed.

Second, yield from delegates to another generator, an array or any Traversable. The inner values are passed straight through to the outer consumer, and when the inner source is a generator, the yield from expression itself evaluates to that generator's return value. This removes the foreach-and-re-yield boilerplate that composing generators required.

Pitfalls: the return value is invisible to foreach, so you must keep a reference to the generator object to read it. When delegating, keys from the inner source are preserved, which can produce duplicate keys if you collect everything into an array with the key-preserving behavior. Plan for that if keys matter to your consumer.

Before: PHP 5.6 manual delegation
<?php
function inner()
{
    yield 1;
    yield 2;
}

function outer()
{
    foreach (inner() as $value) {
        yield $value;
    }
}

Valid in PHP 5.6 and later; there was no way to return a final value from a generator. Shown for comparison; not executed on this site.

After: yield from with a return value
<?php
function inner()
{
    yield 1;
    yield 2;
    return 3;
}

function outer()
{
    $sum = yield from inner();
    yield $sum * 10;
    return 'done';
}

$gen = outer();
foreach ($gen as $value) {
    echo $value, "\n";
}
echo $gen->getReturn(), "\n";
Output on PHP 8.5.10
1
2
30
done

Official migration notes

CHANGED BEHAVIOR

Indirect variable access is evaluated left to right

Expressions like $$foo['bar'] are now read as ($$foo)['bar'] instead of the PHP 5 reading ${$foo['bar']}.

The new abstract-syntax-tree parser removed the mix of special cases that PHP 5 used for indirect access. In PHP 7.0, variable-variables, property names taken from variables and static property access are all evaluated strictly left to right. So $$foo['bar']['baz'] now means ($$foo)['bar']['baz'], whereas PHP 5 treated it as ${$foo['bar']['baz']}. The same applies to $foo->$bar['baz'] and to Foo::$bar['baz']().

The risk is silent: code that used the old reading still parses, and may fail only with a notice or return a different value. Search for variable-variable expressions that combine $$, ->$ or ::$ with array brackets or calls.

To keep the old meaning, add curly braces around the part that must be evaluated first, as in ${$foo['bar']['baz']}. That form means the same thing in PHP 5 and 7, so it is a safe cross-version fix. The global keyword is affected too, so use the curly form there. When writing new code, prefer arrays over variable-variables, which avoid the ambiguity altogether.

Before: PHP 5 reading of $$name['bar']
<?php
$name = 'data';
$data = ['bar' => 'ok'];
echo $$name['bar'];

In PHP 5.6 this was read as ${$name['bar']}; PHP 7.0 and later read it as ($$name)['bar'] and print ok. Shown for comparison; not executed on this site.

After: Left-to-right reading and explicit braces
<?php
$name = 'data';
$data = ['bar' => 'ok'];
$keys = ['bar' => 'other'];
$other = 'legacy';

echo $$name['bar'], "\n";
echo ${$keys['bar']}, "\n";
Output on PHP 8.5.10
ok
legacy

Official migration notes

CHANGED BEHAVIOR

list() assigns in written order and rejects empty lists

list() now fills variables left to right instead of in reverse, and empty list() constructs and string unpacking are no longer allowed.

In PHP 5.6, list() assigned values in reverse order. This was invisible for plain variables but visible when several targets used the append operator on the same array. With list($a[], $a[], $a[]) = [1, 2, 3], PHP 5 produced [3, 2, 1], while PHP 7.0 produces [1, 2, 3].

Two related restrictions came with it. Empty list() constructs such as list() = $a or list(,,) = $a are no longer permitted, so remove any placeholders that unpack nothing. list() also cannot unpack strings any more; use str_split() first and unpack the resulting array.

The manual advises not to rely on assignment order at all, since it is an implementation detail that could change again. If the order of resulting elements matters, assign into explicit keys or build the array in a separate step. When migrating, grep for list( combined with [] targets, and run tests that check the resulting order.

Before: PHP 5.6 reversed order
<?php
list($a[], $a[], $a[]) = [1, 2, 3];
var_dump($a);

Under PHP 5.6 the array ends up as 3, 2, 1; PHP 7.0 and later produce 1, 2, 3. Shown for comparison; not executed on this site.

After: Left-to-right list() assignment
<?php
list($a[], $a[], $a[]) = [1, 2, 3];
echo json_encode($a), "\n";
Output on PHP 8.5.10
[1,2,3]

Official migration notes

REMOVED

Assigning new by reference is removed

The $c =& new C form, deprecated since PHP 5.3, is a parse error in PHP 7.0; use a plain assignment.

Early PHP 5 code often wrote $obj =& new SomeClass; out of habit from PHP 4, where it avoided copying the object. Objects have been handles for a long time, so the reference never did anything useful. PHP 5.6 already warned about it with a deprecation message, and PHP 7.0 removes it: the statement is now a parse error.

Because it is a parse error, the whole file fails to compile, not just the line. A single leftover in an old library can therefore break loading entirely, which makes it one of the easier things to find. A search for =& new or = & new across your code and your vendored dependencies will find them all.

The replacement is a normal assignment: $c = new C;. The behavior is identical for objects, so there is no semantic migration to worry about, only the syntax. If a dependency still contains the pattern, update it rather than patching around it, since that same package likely contains other PHP 4 era constructs.

Before: Reference assignment of new
<?php
class C
{
}
$c =& new C;

Accepted in PHP 5.6 with a deprecation message; a parse error in PHP 7.0 and later. Shown for comparison; not executed on this site.

After: Plain assignment
<?php
class C
{
}

$c = new C;
echo get_class($c), "\n";
Output on PHP 8.5.10
C

Official migration notes

PHP 7.1 changes →