Evolution of PHP — v5.6 to v8.0

A condensed review of changes in PHP v7.x in under 15 minutes

PHP_v8.0

Once PHP 7.3 version has been released, I decided to take a detailed look into PHP development: what is being developed and what is the direction towards better understand its new potential and optimisations of this widely popular programming language.

After looking around for a condensed list of features that PHP implemented during development of PHP v7.x, I have decided to compile the list by myself — a nice catch up that I believe someone may find useful as well.

We will start at PHP 5.6 as a baseline and will look into what’s been added, changed. I’ve as well added links to official documentation for each of the mentioned things, so if you are interested into in depth reading — feel free.

PHP 7.0

Anonymous Class Support
An anonymous class might be used over a named class:

  • When the class does not need to be documented
  • When the class is used only once during execution
new class($i) {
public function __construct($i) {
$this->i = $i;
}
}

The Integer division function — safe way to divide (even by 0)
It returns the integer division of the first operand by the second. If the divisor (the second operand) is zero, it throws an E_WARNING and returns FALSE.

intdiv(int $numerator, int $divisor)

Added new null coalesce operator — the “??”

$x = NULL;
$y = NULL;
$z = 3;
var_dump($x ?? $y ?? $z); // int(3)

$x = ["c" => "meaningful_value"];
var_dump($x["a"] ?? $x["b"] ?? $x["c"]); // string(16) "meaningful_value"

Added new operator — space ship (<=>)
Used to optimise and simplify comparison operations.

// Before
f unction order_func($a, $b) {
return ($a < $b) ? -1 : (($a > $b) ? 1 : 0);
}
// Using <=> operator
function order_func($a, $b) {
return $a <=> $b;
}

Scalar type declarations
This is just the first step towards achieving stronger typed programming language benefits in PHP — v0.5.

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

add(1, 2); // float(3)

Return type declarations
Added ability to return types beyond scalar — classes including inheritance. Heh, still somehow totally missed ability to make it optional (introduced in v7.1 :) )

interface A {
static function make(): A;
}
class B implements A {
static function make(): A {
return new B();
}
}

Group Use Declarations

// Explicit use syntax:

use FooLibrary\Bar\Baz\ClassA;
use FooLibrary\Bar\Baz\ClassB;
use FooLibrary\Bar\Baz\ClassC;
use FooLibrary\Bar\Baz\ClassD as Fizbo;
// Grouped use syntax:

use FooLibrary\Bar\Baz\{ ClassA, ClassB, ClassC, ClassD as Fizbo };

Generator Delegation
The following new syntax is allowed in the body of generator functions:

yield from <expr>

Improved performance

PHP 7 is up to twice as fast as PHP 5.6.

Significantly reduced memory usage

As you can see from the charts, PHP 7.0 is a huge improvement in terms of performance and memory usage. For the page with the database queries, version 7.0.0 is more than 3 times faster than 5.6 with opcache enabled and 2.7 times faster without opcache! In terms of memory usage, the difference is also significant!

Throwable interface
Restructured exception classes to have a naming scheme that is unintuitive and will lead to less confusion, especially for newer users.

Errors and Exceptions are now implementing Throwable.

Here is Throwable hierarchy:

interface Throwable
|- Error implements Throwable
|- ArithmeticError extends Error
|- DivisionByZeroError extends ArithmeticError
|- AssertionError extends Error
|- ParseError extends Error
|- TypeError extends Error
|- ArgumentCountError extends TypeError
|- Exception implements Throwable
|- ClosedGeneratorException extends Exception
|- DOMException extends Exception
|- ErrorException extends Exception
|- IntlException extends Exception
|- LogicException extends Exception
|- BadFunctionCallException extends LogicException
|- BadMethodCallException extends BadFunctionCallException
|- DomainException extends LogicException
|- InvalidArgumentException extends LogicException
|- LengthException extends LogicException
|- OutOfRangeException extends LogicException
|- PharException extends Exception
|- ReflectionException extends Exception
|- RuntimeException extends Exception
|- OutOfBoundsException extends RuntimeException
|- OverflowException extends RuntimeException
|- PDOException extends RuntimeException
|- RangeException extends RuntimeException
|- UnderflowException extends RuntimeException
|- UnexpectedValueException extends RuntimeException

⚠ Caution! You can only implement Throwable through Error and Exception.

Unicode Codepoint Escape Syntax — “\u{xxxxx}”

echo "\u{202E}Reversed text"; // outputs ‮Reversed text
echo "mañana"; // "ma\u{00F1}ana"
echo "mañana"; // "man\u{0303}ana" "n" with combining ~ character (U+0303)

Context Sensitive Lexer
With this globally reserved words became semi-reserved:

callable  class  trait  extends  implements  static  abstract  final  public  protected  private  const
enddeclare endfor endforeach endif endwhile and global goto instanceof insteadof interface
namespace new or xor try use var exit list clone include include_once throw array
print echo require require_once return else elseif default break continue switch yield
function if endswitch finally for foreach declare case do while as catch die self parent

Except it’s still forbidden to define a class constant named as class because of the class name resolution ::class.

Generator return expressions

Uniform variable syntax

Level support for the dirname() function

PHP 7.1

Nullable Types

function answer(): ?int  {
return null; //ok
}

function answer(): ?int {
return 42; // ok
}

function answer(): ?int {
return new stdclass(); // error
}
function say(?string $msg) {
if ($msg) {
echo $msg;
}
}

say('hello'); // ok -- prints hello
say(null); // ok -- does not print
say(); // error -- missing parameter
say(new stdclass); //error -- bad type

Void Returns

function should_return_nothing(): void {
return 1; // Fatal error: A void function must not return a value
}

Unlike other return types which are enforced when a function is called, this type is checked at compile-time, which means that an error is produced without the function needing to be called.

A function with a void return type, or void function, may either return implicitly, or have a return statement without a value:

function lacks_return(): void {
// valid
}

Iterable pseudo type
It is common for a function to accept or return either an array or an object implementing Traversable to be used with foreach. However, because array is a primitive type and Traversable is an interface, there currently is no way to use a type declaration on a parameter or return type to indicate that the value is iterable.

function foo(iterable $iterable) {
foreach ($iterable as $value) {
// ...
}
}

iterable can also be used as a return type to indicate a function will return an iterable value. If the returned value is not an array or instance of Traversable, a TypeError will be thrown.

function bar(): iterable {
return [1, 2, 3];
}

Parameters declared as iterable may use null or an array as a default value.

function foo(iterable $iterable = []) {
// ...
}

Closure from callable

class Closure {
...
public static function fromCallable(callable $callable) : Closure {...}
...
}

Square bracket syntax for array destructuring assignment

$array = [1, 2, 3];
// Assigns to $a, $b and $c the values of their respective array elements in $array with keys numbered from zero
[$a, $b, $c] = $array;

// Assigns to $a, $b and $c the values of the array elements in $array with the keys "a", "b" and "c", respectively
["a" => $a, "b" => $b, "c" => $c] = $array;

Square bracket syntax for list()

$powersOfTwo = [1 => 2, 2 => 4, 3 => 8];
list(1 => $oneBit, 2 => $twoBit, 3 => $threeBit) = $powersOfTwo;

Class constant visibility

class Token {
// Constants default to public
const PUBLIC_CONST = 0;

// Constants then also can have a defined visibility
private const PRIVATE_CONST = 0;
protected const PROTECTED_CONST = 0;
public const PUBLIC_CONST_TWO = 0;

//Constants can only have one visibility declaration list
private const FOO = 1, BAR = 2;
}

Catching Multiple Exception Types

try {
// Some code...
} catch (ExceptionType1 | ExceptionType2 $e) {
// Code to handle the exception
} catch (\Exception $e) {
// ...
}

PHP 7.2

Parameter Type Widening

<?php

class ArrayClass {
public function foo(array $foo) { /* ... */ }
}


// This RFC proposes allowing the type to be widened to be untyped aka any
// type can be passed as the parameter.
// Any type restrictions can be done via user code in the method body.
class EverythingClass extends ArrayClass {
public function foo($foo) { /* ... */ }
}

Counting of non-countable objects
Calling count() on a scalar or object that doesn't implement the Countable interface (http://php.net/manual/en/class.countable.php) returns 1 (illogical).
In this version added a warning when calling count() with a parameter that is a scalar, null, or an object that doesn't implement Countable.

Trailing Commas In List Syntax in namespace uses

use Foo\Bar\{ Foo, Bar, Baz, };

Argon2 Password Hash
The existing password_* functions provided a forward compatible, simplified interface for hashing passwords. This RFC proposes the implementation of Argon2i (v1.3) within the password_* functions for use as a secure alternative to Bcrypt.

Debugging PDO Prepared Statement Emulation

$db = new PDO(...);

// works with statements without bound values
$stmt = $db->query('SELECT 1');
var_dump($stmt->activeQueryString()); // => string(8) "SELECT 1"

$stmt = $db->prepare('SELECT :string');
$stmt->bindValue(':string', 'foo');

// returns unparsed query before execution
var_dump($stmt->activeQueryString()); // => string(14) "SELECT :string"

// returns parsed query after execution
$stmt->execute();
var_dump($stmt->activeQueryString()); // => string(11) "SELECT 'foo'"

PHP 7.3

JSON_THROW_ON_ERROR
Not having an adequate way to handle errors when using JSON has been a problem for quite a long time, and web developers all over the world have seen this as a huge downside of the language.

Until PHP v7.2 we needed to use a workaround to get an error from JSON and it was neither reliable, nor proficient in its job;

Here is an example:

json_decode("{");
json_last_error() === JSON_ERROR_NONE // the result is false
json_last_error_msg() // The result is "Syntax error"

So let’s see how we could employ this new sugar:

use JsonException;

try {
$json = json_encode("{", JSON_THROW_ON_ERROR);
return base64_encode($json);
} catch (JsonException $e) {
throw new EncryptException('Could not encrypt the data.', 0, $e);
}

As you can see from the previous code the json_encode function has now an optional parameter JSON_THROW_ON_ERROR — this will catch the error and display it using the following exception methods:

$e->getMessage(); // like json_last_error_msg()
$e->getCode(); // like json_last_error()

Added is_countable function

// Before:
if (is_array($foo) || $foo instanceof Countable) {
// $foo is countable
}
// After
if (is_countable($foo)) {
// $foo is countable
}

Added array functions array_key_first(), array_key_last()

$firstKey = array_key_first($array);
$lastKey = array_key_last($array);

Added native “same site” cookie support
There are now two possibilities for a cookie that is using the samesite flag: “Lax” and “Strict”. The difference between Lax and Strict is the accessibility of the cookie in requests originating from another registrable domain using the HTTP GET method. Cookies using Lax will be accessible in a GET request originated from another registrable domain, whereas cookies using Strict will not be accessible in a GET request originated from another registrable domain. There is no difference with POST methods: the browser should not allow the cookie to be accessed in a POST request originating from another registrable domain.

Set-Cookie: key=value; path=/; domain=example.org; HttpOnly; SameSite=Lax|Strict

Migrated PCRE to PCRE2

Argon2 Password Hash Enhancements
The existing password_* functions provided a forward compatible, simplified interface for hashing passwords. This RFC proposes the implementation of Argon2id within the password_* functions for use as a secure alternative to the originally proposed Argon2i.

Allow a Trailing Comma in Function Calls

$newArray = array_merge(
$arrayOne,
$arrayTwo,
['foo', 'bar'], // comma is allowed in function calls
);

list() Reference Assignment

$array = [1, 2];
list($a, &$b) = $array;

This is equivalent to the following:

$array = [1, 2];
$a = $array[0];
$b =& $array[1];

Deprecated case-insensitive constants

PHP 7.4 (In development)

Typed properties

class User {
public int $id;
public string $name;

public function __construct(int $id, string $name) {
$this->id = $id;
$this->name = $name;
}
}

Foreign Function Interface
FFI is one of the features that made Python and LuaJIT very useful for fast prototyping. It allows calling C functions and using C data types from pure scripting language and therefore develop “system code” more productively. For PHP, FFI opens a way to write PHP extensions and bindings to C libraries in pure PHP.

Null Coalescing Assignment Operator

// The folloving lines are doing the same
$this->request->data['comments']['user_id'] = $this->request->data['comments']['user_id'] ?? 'value';
// Instead of repeating variables with long names, the equal coalesce operator is used
$this->request->data['comments']['user_id'] ??= 'value';

Preloading
PHP has been using opcode caches for ages (APC, Turck MMCache, Zend OpCache). They achieve significant performance boost by ALMOST completely eliminating the overhead of PHP code recompilation. Preloading is going to be controlled by just a single new php.ini directive — opcache.preload. Using this directive we will specify a single PHP file — which will perform the preloading task. Once loaded, this file is then fully executed — and may preload other files, either by including them or by using the opcache_compile_file() function.

Always available hash extension
This will make hash extension (`ext/hash`) always available, similar to the `date`. The hash extension provides a very rich utility with many hashing algorithms which is extremely useful in modern day applications, not only in userland code but also very much in internals.

On the way to PHP 8.0

JIT.

In short. When you start a PHP program, the Zend Engine will parse the code into an abstract syntax tree (AST) and translate it to opcodes. The opcodes are execution units for the Zend Virtual Machine (Zend VM). Opcodes are pretty low-level, so much faster to translate to machine code than the original PHP code. PHP has an extension named OPcache in core, to cache these opcodes.

“JIT” is a technique that will compile parts of the code at runtime, so that the compiled version can be used instead.

This is one of the last and biggest PHP optimisations strategies still on the table. PHP engineers are looking forwards to seeing how much this new approach can squeeze out of their applications. Really keen on seeing this myself.

Consistent type errors for internal functions
Make the internal parameter parsing APIs always generate a TypeError if parameter parsing fails. It should be noted that this also includes ArgumentCountError (a child of TypeError) for cases where too few/many arguments were passed.


Performance comparison

I have composed a simple test to help easily compare performance of different PHP versions (using Docker). This would even allow to easily check performance of new PHP versions just by adding new container names.

Running on Macbook pro, 2,5 GHz Intel Core i7.

PHP version : 5.6.40
--------------------------------------
test_math : 1.101 sec.
test_stringmanipulation : 1.144 sec.
test_loops : 1.736 sec.
test_ifelse : 1.122 sec.
Mem: 429.4609375 kb Peak mem: 687.65625 kb
--------------------------------------
Total time: : 5.103

PHP version : 7.0.33
--------------------------------------
test_math : 0.344 sec.
test_stringmanipulation : 0.516 sec.
test_loops : 0.477 sec.
test_ifelse : 0.373 sec.
Mem: 421.0859375 kb Peak mem: 422.2109375 kb
--------------------------------------
Total time: : 1.71

PHP version : 7.1.28
--------------------------------------
test_math : 0.389 sec.
test_stringmanipulation : 0.514 sec.
test_loops : 0.501 sec.
test_ifelse : 0.464 sec.
Mem: 420.9375 kb Peak mem: 421.3828125 kb
--------------------------------------
Total time: : 1.868

PHP version : 7.2.17
--------------------------------------
test_math : 0.264 sec.
test_stringmanipulation : 0.391 sec.
test_loops : 0.182 sec.
test_ifelse : 0.252 sec.
Mem: 456.578125 kb Peak mem: 457.0234375 kb
--------------------------------------
Total time: : 1.089

PHP version : 7.3.4
--------------------------------------
test_math : 0.233 sec.
test_stringmanipulation : 0.317 sec.
test_loops : 0.171 sec.
test_ifelse : 0.263 sec.
Mem: 459.953125 kb Peak mem: 460.3984375 kb
--------------------------------------
Total time: : 0.984

PHP version : 7.4.0-dev
--------------------------------------
test_math : 0.212 sec.
test_stringmanipulation : 0.358 sec.
test_loops : 0.205 sec.
test_ifelse : 0.228 sec.
Mem: 459.6640625 kb Peak mem: 460.109375 kb
--------------------------------------
Total time: : 1.003

If interested in testing yourself, you can find code in repository below.


Benchmarks from PHP 5.6 and up

I really liked visual performance compilation from servebolt.com of all major versions from 5.6 and up. See the results in the tables below.

Performance summary

PHP 7.0.0 was a major milestone with significantly improved performance and lower memory usage but PHP maintainers are simply running out of ideas to improve it. One of the remaining points is JIT (Just in time) compilation. And it’s coming with PHP 8.0.

Development direction

Throughout PHP versions 7.x there is a visible path towards more typed (and a bit more objective) and modern programming language. Still PHP likes to adopt neat and useful features from other programming languages.

Soon we may see some more nice features, like:

With these in the pipeline, PHP developers would join the pool of modern language adopters. No language is perfect, but PHP is paving it’s way to the future.

TL;DR

To shorten even more, I have chosen most important changes to my arbitrary opinion given the latest version of PHP 7.3. Here they are:

References

https://wiki.php.net/rfc
https://www.cloudways.com/blog/php-5-6-vs-php-7-symfony-benchmarks/
https://servebolt.com/articles/wordpress-5-0-php-7-2-vs-php-7-3-performance-and-speed-benchmark/