Language Features

New Reserved Words

When parsing the source code, most compilers require a few words to be non-ambiguous. Consider the following piece of pseudo code, which just serves the purpose of confusing people (and compilers, for that matter):

for (for=integer to class) {
    ...
}

Nobody should write code like that, so disallowing to use keywords like integer or class should not be a serious limitation.

In PHP 7, a few additional words have been reserved, which means you cannot use them as class names, even if you could in PHP 5:

class String
{
}

This code, which works fine on PHP 5, directly leads to a fatal error in PHP 7:

PHP Fatal error:
Cannot use 'String' as class name as it is reserved in ...

It does not matter whether String is upper or lower case. PHP has never been case-sensitive about class names. Putting your class into a namespace will also not help, by the way.

PHP 7 will not allow you to use the following reserved words:

bool, true, false, null, object, string, int, float

If your code uses any of these words as class names, the only thing you can do is to rename those classes.

In addition, the words resource, mixed, and numeric should be considered as reserved for the future. This means that some few well-known projects will have to rename some classes, because terms like Object, or Resource were thought to be good and descriptive names. Without doubt, they were, but we should not use them anymore to avoid problems with future PHP versions.

On the upside, quite a few words that could not be used as method names in PHP 5 can now freely be used in PHP 7. We have already discussed this earlier.

Uniform Variable Syntax

What do you get if you divide six by three, and then add one? Simple enough: six divided by three equals two, and two plus one equals three. We can also use PHP to confirm that result:

php -r "var_dump(6/3+1);"
int(3)

However, if we were to add three and one first, and then divide, the result would be 1.5. But that is wrong, you say, point calculation goes before line calculation. This is called operator precedence. PHP knows all about this, of course, which makes its calculation results predictable and correct. The most basic precedence rule is “evaluate from left to right”.

By using brackets, we can force PHP to execute the calculations in a different order:

php -r "var_dump(6/(3+1));"
double(1.5)

With an expression as simple as in the example, all of this is really obvious. We know the basic operator precedence rules, because we learned them in school. But when it comes to more complex expressions, things can quickly get out of hand. Consider the following PHP code, for example:

$$foo['bar']['baz'];

Do you know the operator precedence rules for this one? To try and figure them out, let us have a look at what we have here. The example combines the usage of a variable variable, $$foo, and multi-dimensional array access. A variable variable is an “interesting” PHP construct (“interesting” meaning: you should better not use it) that has been around for a long time. Maybe it should have never been invented. Anyway: $$foo refers to the variable with the name contained in the variable $foo, as the following example shows:

$foo = 'bar';
$bar = 42;

var_dump($bar, $$foo);

$$foo refers to $bar, because $foo contains the string bar. By the way, if you hate foo/bar examples just like we do, you are in good company, but nobody should be using features like this, so we really had a hard time coming up with a more descriptive example.

So what does $$foo['bar']['baz']; mean after all, or to be more exact, what does it evaluate to in PHP? Is it ${$foo['bar']['baz']} (use $foo['bar']['baz'] to determine the name of the variable, then access a variable variable), or rather {$$foo}['bar']['baz'] (determine the variable variable first, then retrieve the value from the array)?

If your answer is: “Nobody knows, this is why you should never write such code”, then you hit the nail on the head. One could argue that it is okay to write code like that, because PHP knows the precedence rules, and applies them consistently. The problem is that no human being could possibly remember and understand the precedence rules in such cases – at least we can’t.

When evaluating expressions like $$foo['bar']['baz'], $foo->$bar['baz'], $foo->$bar['baz'](), or Foo::$bar['baz']() (yes, we can add function and method calls into the mix, to make everybody’s life even more interesting), PHP 5 would sometimes work from left to right, but not always.

PHP 7 will always evaluate from left to right. This means that some expressions will be evaluated differently in PHP 7:

Expression PHP 5 interpretation PHP 7 interpretation
$$foo['bar']['baz'] ${$foo['bar']['baz']} ($$foo)['bar']['baz']
$foo->$bar['baz'] $foo->{$bar['baz']} ($foo->$bar)['baz']
$foo->$bar['baz']() $foo->{$bar['baz']}() ($foo->$bar)['baz']()
Foo::$bar['baz']() Foo::{$bar['baz']}() (Foo::$bar)['baz']()

The easy fix is to add braces and brackets as needed. A better fix would be to rewrite the code so that such expressions are not needed anymore. We are sure all of your fellow developers would appreciate that.

Thanks to the uniform variable syntax, PHP 7 now understands more complex expressions than PHP 5. Some of them are:

$foo()['bar']()
[$obj1, $obj2][0]->prop
$foo::$bar::$baz
foo()()
(...)['foo']

We will leave it up to you to decide what the hell all of those mean, and whether it really makes you a good developer to write illegible code like this. We firmly believe that code should be easy to read, so just because you can use even more complex expressions with PHP 7, this does not mean you should.

Parameter Handling

PHP, by design, allows passing of an arbitrary amount of parameters to any function or method, even if they are not listed in the signature. To retrieve the parameters, the functions func_get_arg() and func_get_args() can be used.

Due to internal changes in PHP 7, when modifying those parameters within the called scope, func_get_arg() and func_get_args() will no longer return the original value, but the current value:

function doSomething($a)
{
    var_dump(func_get_args());
    $a = 'modified';
    var_dump(func_get_args());
}

doSomething('original');

The first var_dump() shows the original value, while the second one shows the modified one:

array(1) {
  [0] => string(8) "original"
}
array(1) {
  [0] => string(8) "modified"
}

If existing code uses func_get_arg() or func_get_args() after modifying any parameters, then modify the code to fetch the original parameters first, and then modifies them. This should be an easy fix.

Please note that in PHP 7, exception backtraces and the output of debug_backtrace() will also display the modified values rather than the original ones.

Calling Functions and Methods with too few Arguments

Before version 7.1, PHP would execute a function or method even when it was called with too few arguments. Consider the following example:

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

var_dump(add(1));

Executing the code shown above with PHP 7.0 produces the output shown below:

Warning: Missing argument 2 for add(), called in ...
and defined in ...

Notice: Undefined variable: b in ...

int(1)

Executing the code shown above with PHP 7.1 or later produces the output shown below:

Fatal error: Uncaught Error:
Too few arguments to function add(), 1 passed in ...
and exactly 2 expected in ...

Instead of executing the code without all parameters set, PHP since version 7.1 refuses to perform function and method calls with too few arguments. This change breaks backwards compatibility, if only for broken code.

Passing more arguments to a function or method than it expects still works, though:

function f() {
    var_dump(func_get_args());
}

f(true);

Executing the code shown above produces the output shown below:

array(1) {
  [0]=>
  bool(true)
}

Variable Variables are no Longer Allowed in global Declarations

Do you still remember the global keyword? Even though this relict of times past should have no relevance in the modern world of object-oriented PHP, it is still around and received an update restricting it to only simple variables. While this seems to be a rather logical thing at first glance, it breaks code that uses variable variables – another construct that should have been removed from PHP long ago – together with global. The following construct no longer works with PHP 7:

global $$foo->bar;

So in case such a construct would still be required by your codebase, it has to get adjusted using curly braces:

global ${$foo->bar};

instanceof and Literals

The instanceof operator is used to determine whether a variable holds an object of a specified class:

$o = new stdClass;

var_dump($o instanceof stdClass);
var_dump($o instanceof Exception);

Executing the code shown above will print the output shown below:

bool(true)
bool(false)

Previous versions of PHP triggered an error when a literal value of type bool, float, int, or string, for instance, was used with the instanceof operator:

var_dump(true instanceof stdClass);

Executing the code shown above used to print the error shown below:

Fatal error: instanceof expects an object instance,
constant given in ...

This behaviour has been changed and instead of triggering an error the instanceof operator now returns false when it is used with a literal value.

Executing the code shown above will print the output shown below:

bool(false)

Traits with Default Property Values

Introduced in PHP 5.4, traits provide an alternative mechanism for code reuse. A trait is a set of methods that can be embedded into one or more classes. There is no notion of traits at runtime: a trait cannot be instantiated, there is no difference between a method that is declared in a class and a method that comes from an embedded trait, and there is no type relationship between a trait and the classes that embed it.

In addition to methods, traits can also contain properties. Prior to PHP 7, defining a property in a class with the same name as in a trait triggered an E_STRICT notice even if the declarations used the same visibility and initial value.

trait T
{
    private $a = 'bar';
}

class C
{
    use T;

    private $a = 'bar';
}

Executing the code shown above with PHP 5.6 triggered the violation shown below:

Strict Standards: C and T define the same property ($a)
in the composition of C. This might be incompatible, to improve
maintainability consider using accessor methods in traits instead.
Class was composed in ...

Executing the same code with PHP 7 does not trigger that violation.

When both a trait and a class using that trait declare the same property, both declarations must be compatible with regard to their visibility, type, and default value.

trait T
{
    private $a = 'bar';
}

class C
{
    use T;

    private $a = 'baz';
}

Executing the code shown above with PHP 7 triggers the error shown below:

Fatal error: C and T define the same property ($a)
in the composition of C. However, the definition differs
and is considered incompatible. Class was composed in ...
trait T
{
    public $a = 'bar';
}

class C
{
    use T;

    private $a = 'bar';
}

Executing the code shown above with PHP 7 triggers the error shown below:

Fatal error: C and T define the same property ($a)
in the composition of C. However, the definition differs
and is considered incompatible. Class was composed in ...

Prior to PHP 7.2, a loose comparison with automatic type casting was used to check the type compatibility of default values. However, this was not the intent of the original RFC for traits.

trait T
{
    private $a = true;
}

class C
{
    use T;

    private $a = 1;
}

Executing the code shown above with PHP 7.2 produces the output shown below:

PHP Fatal error: C and T define the same property ($a) in the
composition of C. However, the definition differs and is
considered incompatible. Class was composed in ...

The same code did not trigger an error in earlier versions of PHP.

DateTime, DateTimeImmutable, and Microseconds

Prior to PHP 7.1, objects of the DateTime and DateTimeImmutable classes did not include microsecond information when constructed for the current time or using a string such as “first day of next month”. This means that naive comparisons of two newly created DateTimeImmutable objects, for instance, may yield a wrong result.

var_dump(new DateTimeImmutable == new DateTimeImmutable);

When executed with PHP 5.6 and PHP 7.0, the code shown above, will – at least most of the time – output bool(true). If both objects are not created in the same second, the comparison will fail and bool(false) will be printed.

With PHP 7.1, however, the same code usually prints bool(false). If both objects happen to be created with a microsecond, then bool(true) would be printed.

This affects your code when you mistakenly assume that two DateTime or DateTimeImmutable objects, one created after the other, represent the exact point in time with microsecond resolution.

Even if you got away with this on PHP 5.6 or PHP 7.0, you will run into trouble with PHP 7.1, because the clock now advances a million times faster!

Comparison of DateInterval Objects

Trying to compare DateInterval objects using operators such as ==, for example, now triggers a warning:

$a = new DateInterval('PT1S');
$b = new DateInterval('PT2S');

var_dump($a == $b);

Executing the code shown above prints the warning shown below:

PHP Warning: Cannot compare DateInterval objects in ...
bool(false)

Previous versions of PHP considered all DateInterval objects to be equal, unless they had dynamically defined properties:

$a = new DateInterval('PT1S');
$b = new DateInterval('PT2S');

var_dump($a == $b);

$a->foo = 'bar';

var_dump($a == $b);

The code shown above used to generate the following output:

bool(true)
bool(false)

Dynamic Calls to Scope Introspection Functions

There are a couple of functions built into PHP that inspect or modify the parent scope or stack frame, make unexpected scope modifications, or generally have unclear behavior when called dynamically, for example by using call_user_func(). Allowing these functions to be called dynamically not only makes it possible to write code that behaves “interestingly” but also causes problems for further work on PHP’s bytecode optimizer.

As of PHP 7.1, the functions assert() (with string argument), compact(), extract(), func_get_args(), func_get_arg(), func_num_args(), get_defined_vars(), mb_parse_str() (with one argument), and parse_str() (with one argument) can no longer be dynamically called. The code shown below contains two common examples for dynamic calls:

$function = 'get_defined_vars';
$function();

call_user_func($function);

Executing the shown code above with PHP 7.1 or newer will result in the errors shown below:

Warning: Cannot call get_defined_vars() dynamically in ...
Warning: Cannot call get_defined_vars() dynamically in ...

Static Methods and ReflectionMethod::invoke()

When you use ReflectionMethod::invoke() to invoke a static method then the first argument passed, $object, is ignored. This makes sense because there is no object to invoke the method on.

Prior to PHP 7.1 you could pass a string containing the name of the class as the first argument to ReflectionMethod::invoke():

class C
{
    public static function m($p)
    {
        print $p . PHP_EOL;
    }
}

$method = new ReflectionMethod(C::class, 'm');
$method->invoke('C', 'example');

Executing the code shown above with PHP 5.6 or PHP 7.0 has the expected result:

example

Executing the code shown above with PHP 7.1 or newer, however, leads to a different result:

Warning:
ReflectionMethod::invoke() expects parameter 1 to be object,
string given in ...

The PHP runtime warns us that the first argument passed to ReflectionMethod::invoke() must be an object and does not invoke the static method.

This breaking change was introduced to make the handling of arguments for ReflectionMethod::invoke() consistent with that of ReflectionMethod::invokeArgs(). The first argument passed to both methods must be null now when used with static methods.

To make the code shown above compatible with PHP 7.1 and above, we need to change the first argument passed to the ReflectionMethod::invoke() method to null:

class C
{
    public static function m($p)
    {
        print $p . PHP_EOL;
    }
}

$method = new ReflectionMethod(C::class, 'm');
$method->invoke(null, 'example');

Executing this code prints the same output for all versions of PHP since PHP 5.6:

example

Removed PHP Extensions

As of PHP 7, some extensions are gone. The most notable one is the mysql extension, which in the early years of PHP was the default way of accessing a MySQL database. Years ago, the mysql extension has been superseded by the mysqli extension, to enable PHP developers to use features of MySQL database servers that had been introduced after version 3.

The mysql extension has been marked as deprecated for years now. If you have not switched to mysqli yet, now is the time. We would recommend against moving to PDO, by the way. In many cases, adjusting your code to use mysqli is almost as simple as adding an i to the mysql_ function calls. However, you will have to pass the database connection to each mysqli_ function or method call, which was optional for the mysql_ calls. If this is something you need to change, do not use global variables or similar to get to the database connection, but wrap the calls into an object that receives the database connection as a constructor parameter, and pass around this object.

The mssql extension, which provided access to Microsoft SQL Server database servers, has also been removed from PHP. The new sqlsrv and pdo_sqlsrv extensions should be used to access Microsoft SQL Server database servers. The former provides a procedural API whereas the latter provides a PDO driver. Both extensions are supported on UNIX-based operating systems as well as on Microsoft Windows and allow accessing data in all editions of Microsoft SQL Server 2012 and later. This includes accessing data that is stored in Microsoft Azure SQL Database instances.

Another extension that has been removed from PHP is ereg. It provided regular expression functions that were not capable of processing strings with binary data. In the C programming language, a string is stored as a sequence of characters, terminated by a so-called null character, the ASCII character encoded as 0. A zero byte. Whenever a string is being processed in C, it thus knows that it has reached the end when it encounters a null character, and stops. When binary data, for example an image file, is being passed as a string, it might contain zero bytes that are not end-of-string markers. Still, code written to deal with strings would stop processing. Such code is said to be not binary safe, as it is not safe to be applied to binary data.

Non-binary safe code usually implies serious security issues. Should your code still use any ereg_() function, replace them by preg_() functions, and start doing so as quickly as possible. The so-called Perl Regular Expressions are binary safe.

In older PHP versions, a sybase extension existed. In PHP 5.3, this extension had been renamed to sybase_ct. In PHP 7, this extension has been removed. Researching the web, we were unable to find any substantial information on either the sybase or the sybase_ct extension. We therefore assume that nobody was ever really using it.

The PHP core developers have decided to not remove the imap extension, even though it depends on an unmaintained library. You should not rely on this extension anymore, since it cannot be considered as maintained and might become incompatible with PHP at any given point in the future.

The mhash extension is no longer available in PHP 7. Instead, the hash extension should be used. If your application uses mhash_() functions, namely mhash_count(), mhash_get_block_size(), mhash_get_hash_name(), or mhash_keygen_s2k(), you have to replace them.

The mcrypt extension was deprecated in PHP 7.1 and has been removed from the standard PHP distribution in PHP 7.2. You can still install it from PECL. Since the mcrypt extension usually is not installed by default, but has to be installed using the operating systems’s package manager, most PHP users might not even notice. After all, you do not see where the sources that an additional PHP package was built from were obtained from (which is, in fact, more of a curse than a blessing, but that is a different story).

This does not mean, however, that mcrypt should still be used. On the contrary: cryptographic software that is outdated always poses a security risk. And the underlying library, libmcrypt, has not been updated since 2007. This also means that modern cryptographic algorithms are not supported, neither by libmcrypt nor by PHP’s extension for it. Even AES, a widely used standard, is not available under its name. You have to fall back to using Rijndael, which was the algorithm’s original name before it was standardized as AES. Still, performing AES encryption or decryption has always been cumbersome using the mcrypt extension.

If you need cryptographic functionality in PHP, use the functions provided by the sodium or openssl extensions instead. There should be no need for PHP to work with old or exotic cryptographic algorithms. Since most of the world wide web has finally realized that using HTTPS is a good idea, web servers “automagically” tend to handle most encryption and decryption for us. And if you use an encrypted file system (which you probably should), the filesystem itself will take care of cryptography, again freeing PHP from the need to deal with it.

Optional Parameter of mktime() Removed

Dealing with dates and times is a complex issue. One day, for instance, does not always have 24 hours or 86400 seconds), as is frequently assumed. Let aside the fact that a day in general has a bit less than 24 hours, there are usually two days in a year when a day has 23 or 25 hours, respectively. This happens on the days when the clock is being switched back or forth between regular time and daylight saving time.

Unfortunately, this switch is not being handled consistently across platforms. Some systems switch to daylight saving time between 2am and 3am in the morning, others do so at midnight.

PHP has a built-in function to create a Unix timestamp from a given date:

$timestamp = mktime(12, 0, 0, 7, 1, 2016);

The parameters are $hour, $minute, $seconds, $month, $day, and $year. Up to PHP 5, there was a seventh optional parameter $is_dst. The name might suggest that the parameter was boolean, but in fact it was an integer.

When 0 was being passed, no daylight saving time was assumed, while 1 denoted daylight saving time. The default value -1 made PHP guess whether daylight saving time applied or not.

As you can imagine, guessing in combination with inconsistent behavior between different platforms is never a good idea. Thus, the seventh parameter has now been removed in PHP 7. If you try to pass seven parameters to mktime() in PHP 7, a warning is being issued:

PHP Warning:
mktime() expects at most 6 parameters, 7 given in ...

When working with time zones, you should use the built-in class DateTimeZone, which can tell you about all daylight saving time transitions in a given time zone:

$timezone = new DateTimeZone('Europe/Berlin');

var_dump(
    $timezone->getTransitions(
        mktime(0, 0, 0, 1, 1, 2020),
        mktime(0, 0, 0, 1, 1, 2021)
    )
);

This will output all transitions in 2020:

array(3) {
  [0]=>
  array(5) {
    ["ts"]     => int(1577833200)
    ["time"]   => string(24) "2019-12-31T23:00:00+0000"
    ["offset"] => int(3600)
    ["isdst"]  => bool(false)
    ["abbr"]   => string(3) "CET"
  }
  [1]=>
  array(5) {
    ["ts"]     => int(1585443600)
    ["time"]   => string(24) "2020-03-29T01:00:00+0000"
    ["offset"] => int(7200)
    ["isdst"]  => bool(true)
    ["abbr"]   => string(4) "CEST"
  }
  [2]=>
  array(5) {
    ["ts"]     => int(1603587600)
    ["time"]   => string(24) "2020-10-25T01:00:00+0000"
    ["offset"] => int(3600)
    ["isdst"]  => bool(false)
    ["abbr"]   => string(3) "CET"
  }
}

The first array element tells us that the year started without daylight saving time. On March 29th, daylight saving time had started, ending on October 25th.

stdClass Objects and var_export()

$o = new stdClass;

$o->property = 'value';

print var_export($o);

Executing the code shown above printed the output shown below with previous versions of PHP:

stdClass::__set_state(array(
   'property' => 'value',
))

As you can see above, var_export() used to generate code for stdClass objects that calls the __set_state() method which is not available on these objects.

Now var_export() generates code for stdClass objects that uses the (object) operator to cast an array to object instead:

(object) array(
   'property' => 'value',
)

<?php at the End of a File

If a source file ends with <?php without a trailing newline then previous versions of PHP behaved differently depending on the short_open_tag configuration directive. With short_open_tag=1 it was treated as <? php which then triggered a syntax error. With short_open_tag=0 it was treated a literal string.

Now <?php at the end of a source file is always treated as an opening PHP tag regardless of the short_open_tag configuration directive’s setting.

Heredoc and Nowdoc Syntax

The so-called heredoc syntax allows for in-line definition of multiline content:

<?php
$string = <<<EOT
    String
    with
    indentation
EOT;

var_dump($string);

This will output:

string(35) "    String
    with
    indentation"

Heredoc supports escape sequences and will expand variables just like a double-quoted string. When enclosing the EOT identifier in single quotes, the behaviour changes analogous to a
single-quoted string (this is the so-called nowdoc syntax).

Up to PHP 7.3, the heredoc/nowdoc syntax was very strict, for example, the closing marker EOT could not be indented. Since PHP 7.3, the syntax has been relaxed. The closing marker can now be indented, which means that you should choose a closing marker that appears somewhere in your heredoc/nowdoc block, because this will now be considered a closing marker.

In addition, the indentation of the closing marker will determine how many whitespace characters will be removed from the heredoc/nowdoc content:

$string = <<<EOT
    String
    with
    indentation
  EOT;

var_dump($string);

This will take two whitespace off:

string(29) "  String
  with
  indentation"

Previously, a new line character was required after the closing marker. This requirement was also dropped.