What’s New in PHP 8 (Features, Updates & Changes)

PHP 8.0 is a major update that was released on November 26, 2020. It came with many new amazing features and optimizations such as named arguments, union types, attributes, constructor property promotion, match expression, nullsafe operator, JIT compiler, improvements in the type system, error handling, and consistency.
With 10Web, you can easily update your PHP to the PHP 8.0 version. If you don’t want to go into the details, scroll down to see how to enable it on 10Web. Others are welcome to learn about all the new features of PHP 8 and about the JIT compiler in particular.

New features

Let’s discover the new features introduced by this new major update. Here’s what we’ll cover in this article:

  • Named Arguments
  • Attributes v2
  • Constructor Property Promotion
  • Union Types 2.0
  • Match expression v2
  • Nullsafe operator
  • Saner string to number comparisons
  • Consistent type errors for internal functions
  • Type system and error handling improvements
  • New classes, interfaces, and functions
  • Just-in-time (JIT) compilation

Named Arguments

PHP 8.0 makes it possible to pass function parameters by their names instead of their position.
Here are some examples:

// Case of positional arguments:
array_fill(0, 100, 50);

// Case of named arguments:
array_fill(start_index: 0, num: 100, value: 50);

Note that the order of arguments does not matter, so this syntax is correct too:

array_fill(value: 50, num: 100, start_index: 0);

This new feature simplifies the coding process, as it allows you to skip function default values, to have a self-documenting code. It is especially great when you use it in object initialization.
As function-named arguments are order-independent, you can arbitrarily skip default values, which makes life easier. It is useful to use named arguments in class constructors, as constructors can have many arguments with default values. Named arguments make the code more readable as this makes the meaning of the argument self-documenting.
But it is important to remember some syntax rules when using named arguments. You can use named and positional arguments together, but do not pass positional arguments after named arguments. This syntax is correct:

htmlspecialchars($string, double_encode: false);

But this one is incorrect:

htmlspecialchars(double_encode: false, $string);

Another thing to avoid is using variables as argument names. The parameter name must be an identifier, it’s impossible to specify it dynamically:

my_function($ParamName: $value);

This syntax is not supported.
Read more about named parameters in PHP RFC: Named Arguments.

Attributes v2

PHP 8.0 offers a structured, syntactic form of metadata for declaring classes, class properties/methods, functions, parameters, and constants which are called attributes. Before this major update, PHP supported only doc-comments, an unstructured form of metadata.
Attributes syntax is very simple, attributes which are also called annotations are specially formatted text enclosed with “<<” and “>>.” They can be applied to

  • functions (including closures and short closures)
  • classes (including anonymous classes), interfaces, traits
  • class constants
  • class properties
  • class methods
  • function/method parameters

A couple of examples:

<<ExampleAttribute>>
class Foo
{
    <<ExampleAttribute>>
    public const FOO = 'foo';
    <<ExampleAttribute>>
    public $x;
    <<ExampleAttribute>>
    public function foo(<<ExampleAttribute>> $bar) { }
}
$object = new <<ExampleAttribute>> class () { };
<<ExampleAttribute>>
function f1() { }
$f2 = <<ExampleAttribute>> function () { };
$f3 = <<ExampleAttribute>> fn () => 1;

Much like doc-comments, attributes are added before the declaration they belong to. You can use attributes and doc-comments combination by adding attributes before or after a doc-block comment. Here’s an example:

<<ExampleAttribute>>
/** docblock */
<<AnotherExampleAttribute>>
function foo() {}

Note that each function, class, property or other declaration can have multiple attributes.

<<WithoutArgument>>
<<SingleArgument(0)>>
<<FewArguments('Hello', 'World')>>
function foo() {}

It is possible to use the same attribute name more than one time on the same declaration. You can also declare attributes in the same line:

<<WithoutArgument>><<SingleArgument(0)>><<FewArguments('Hello', 'World')>>
function foo() {} 

Semantically the attribute declaration should be read as instantiating a class with the attribute name and passing arguments to the constructor.
Attributes are better than doc-block comments and are introduced as a new language construct. The complexity of checking for attribute existence is O(1) compared to doc-block parsing or performance of strstr. As attributes are mapped to classes it ensures that attributes are correctly typed, which reduces the major source of bugs in reliance on docblocks at runtime.
You can read more about attributes in PHP RFC: Attributes v2.

Constructor Property Promotion

One of the amazing new features provided by PHP 8.0 is constructor property promotion. Before going into what is this, let’s take a look at this example:

class MyClass {
    public int $prop1;
    public int $prop2;
 
    public function __construct(
        int $prop1 = 0,
        int $prop2 = 0
    ) {
        $this->prop1 = $prop1;
        $this->prop2 = $prop2;
    }
}

As we see in this simple example the properties are repeated:

  1. In the property declaration
  2. The constructor parameters
  3. In the property assignment (twice)

PHP 8.0 offers new short syntax for the above example, here’s what it looks like:

class MyClass {
public function __construct(
public int $prop1 = 0,
public int $prop2 = 0
) {
}
}

It is exactly the same as the example above, but more concise.
Now let’s discuss what a promoted parameter is. We call a method parameter “promoted” when it is prefixed with one of the visibility keywords: public, protected or private. So for each promoted parameter, the property with the same name will be added and the forwarding assignment will be done to that property in the body of the class constructor.
When using promoted properties you have to take account of these restrictions. It is not allowed to use them in non-constructors, in abstract constructors, in interface constructors, you can use them only in non-abstract constructors. Also, it is illegal to use the “var” keyword instead of the visibility keywords public, protected, and private. These examples are all not allowed:

// Error: Not a constructor.
function my_function(private $prop) {}

abstract class MyClass {
// Error: Abstract constructor.
abstract public function __construct(private $prop);
}

interface MyInterface {
// Error: Abstract constructor.
public function __construct(private $prop);
}

class MyClass {
// Error: "var" keyword is not allowed.
public function __construct(var $prop) {}
}

You can’t declare class property and constructor promoted property with the same name, so this example is illegal, too:

class MyClass {
public $prop;

// Error: Redeclaration of property.
public function __construct(public $prop) {}
}

It is impossible to use callable types as promoted property types:

class MyClass {
// Error: Callable type not supported for promoted properties.
public function __construct(public callable $prop) {}
}

And be careful when declaring nullable properties:
It is incorrect to use null default on non-nullable property:

class MyClass {
// Error: using null on non-nullable property
public function __construct(public MyPropType $prop = null) {}

}

Instead, you must use:

class MyClass {
public function __construct(public ?MyPropType $prop = null) {}
}

And one last thing, variadic parameters cannot be used as promoted properties:

class MyClass {
// Error: Variadic parameter.
public function __construct(public string ...$props) {}
}

These were the restrictions that you should take into consideration when using constructor promoted properties.

Promoted properties follow a simple desugaring, where the following transformation is applied for all promoted parameters:

// From:
class MyClass {
    public function __construct(public MyPropType $prop = DEFAULT) {}
}
 
// To:
class MyClass {
    public MyPropType $prop;
 
    public function __construct(MyPropType $prop = DEFAULT) {
        $this->prop = $prop;
    }
}

What about inheritance? Constructor promotion can be used in conjunction with inheritance, but has no special interaction with it beyond what is implied by the desugaring:

class MyBaseClass {
public function __construct(
protected float $prop1 = 0.0,
protected float $prop2 = 0.0,
) {}
}

class MyChildClass extends MyBaseClass {
public function __construct(
public int $prop = 0,
float $prop1 = 0.0,
float $prop2 = 0.0,
) {
parent::__construct($prop1, $prop2);
}
}

You can read more about constructor property promotion here: PHP RFC: Constructor Property Promotion.

Union Types 2.0

Another powerful new feature is union types. PHP already allows the use of two special union types, some type or null (the syntax: ?Type) and array or traversable (the syntax: iterable). You could use multiple types declaring them in PHP doc-blocks, for example:

class Number {
    /**
     * @var int|float $number
     */
    private $number;
 
    /**
     * @param int|float $number
     */
    public function setNumber($number) {
        $this->number = $number;
    }
 
    /**
     * @return int|float
     */
    public function getNumber() {
        return $this->number;
    }
}

Now with new union types this has become possible. The syntax is Type1|Type2 … and can be used everywhere that type is allowed. Here’s an example:

class Number {
    private int|float $number;
 
    public function setNumber(int|float $number): void {
        $this->number = $number;
    }
 
    public function getNumber(): int|float {
        return $this->number;
    }
}

Here again you have to remember some rules. You can’t use union types in these ways:

function foo(): int|void {} // Disallowed
function foo(): ?T1|T2, T1|?T2 {} // Disallowed

function foo(): int|INT {} // Disallowed
function foo(): bool|false {} // Disallowed

use A as B;
function foo(): A|B {} // Disallowed ("use" is part of name resolution)

class_alias('X', 'Y');
function foo(): X|Y {} // Allowed (redundancy is only known at runtime)

There are variance rules for using union types. We know that return types are covariant, parameter types are contravariant, and property types are invariant. You can’t remove union types in return position and add union types in parameter position:

class Test {
public function param1(int $param) {}
public function param2(int|float $param) {}

public function return1(): int|float {}
public function return2(): int {}
}

class Test2 extends Test {
public function param1(int|float $param) {} // Allowed: Adding extra param type
public function param2(int $param) {} // FORBIDDEN: Removing param type

public function return1(): int {} // Allowed: Removing return type
public function return2(): int|float {} // FORBIDDEN: Adding extra return type
}

For more about union types, check out PHP RFC: Union Types 2.0.

Match expression v2

With the new update we have a new match expression similar to switch. Here’s the existing switch syntax:

switch ($task) {
case TASK_ADD:
add_function();
break;

case TASK_UPDATE:
update_function();
break;

case TASK_DELETE:
delete_function();
break;

default:
error_function('ADD, UPDATE or DELETE');
break;
}

Now this syntax is allowed:

$statement = match ($task) {
    TASK_ADD => add_function(),
    TASK_UPDATEE => update_function(),
    TASK_DELETE => delete_function(),
    default => error_function('ADD, UPDATE or DELETE',
};

The main difference between switch and match is that the switch statement loosely compares to (==) the value given to the case values. This behavior can lead to unexpected results. The match expression uses strict comparison (===) instead. Another advantage of match expression is that there’s no need for a break keyword, which you can forget to add in switch statements and face strange results.
Read more about match expression here: PHP RFC: Match expression v2.

The Nullsafe operator

PHP 8.0 offers a new Nullsafe operator ?-> with full short circuiting. Now you have to write long nested code to check nulls:

$name =  null;
 
if ($payload !== null) {
    $user = $payload>user;
 
    if ($user !== null) {
        $data = $user->getData();
 
        if ($data !== null) {
            $name = $data->name;
        }
    }
}
echo $name;

Instead of these checks you can now use the Nullsafe operator and make your code significantly shorter:

$name = $payload?->user?->getData()?->name;
echo $name;

Note that when the evaluation of one element in the chain fails, the execution of the entire chain is aborted and the entire chain evaluates to null.
Read more about the Nullsafe operator here: PHP RFC: Nullsafe operator.

Saner string to number comparisons

It is known that PHP supports two types of comparisons: the strict comparisons (=== and !==) , and the non-strict comparisons (==, !=). The difference between them is that in the first case comparison takes into account the variable type while the second one doesn’t. Non-strict comparisons currently work by casting the string to a number, so if you consider 0 == “foo”, you will get true, which can have very strange logical consequences. Another pain point is the usage of in_array function or switch statement:

$validValues = ["foo", "bar", "baz"];
$value = 0;
var_dump(in_array($value, $validValues));
// bool(true)

But there are many cases where the true result of 12 == “12” is very useful. Now PHP offers more reasonable behavior for string to number comparisons: When comparing a number to a numeric string, convert the numeric string to a number and use number comparison. Otherwise, convert the number to a string and use a string comparison.
Let’s take a look at these tables:

Comparison | Before | After
------------------------------
0 == "0" | true | true
0 == "0.0" | true | true
0 == "foo" | true | false
0 == "" | true | false
42 == " 42" | true | true
42 == "42foo" | true | false

String to string comparisons haven’t changed with this update.

Comparison | Result
------------------------
"0" == "0" | true
"0" == "0.0" | true
"0" == "foo" | false
"0" == "" | false
"42" == " 42" | true
"42" == "42foo" | false

Read more about saner string to number comparisons here: PHP RFC: Saner string to number comparisons.

Consistent type errors for internal functions

This new feature is about internal functions’ behavior when you pass a parameter of illegal type. In PHP, in the case of a user-defined function when you pass parameters with improper types you’ll get TypeError, but for internal functions, while the behavior depends on many factors, the default behavior is to throw a warning and return null. So for a user-defined function:

function foo(int $bar) {}
foo("not an int");
// TypeError: Argument 1 passed to foo() must be of the type int, string given

For an internal function:

var_dump(strlen(new stdClass));
// Warning: strlen() expects parameter 1 to be string, object given
// NULL

Note that you can meet about 150 functions which will return false instead of null. So when you enable strict_types the result will be TypeError:

declare(strict_types=1);
var_dump(strlen(new stdClass));
// TypeError: strlen() expects parameter 1 to be string, object given

You can read more about this topic here: PHP RFC: Consistent type errors for internal functions.

Type system and error handling improvements

With this new major update, there are amazing improvements in the type system and error handling.

Now PHP8 will throw a TypeError when an arithmetic or bitwise operator is applied to an array, resource or (non-overloaded) object. Scalar operands’ behavior remains unchanged. Read about type checks in more detail here: PHP RFC: Stricter type checks for arithmetic/bitwise operators.

Incompatible method signatures during inheritance either throw a fatal error or a warning depending on the cause of the error and the inheritance hierarchy. Here’s an example:

interface I {
public function method(array $a);
}
class C implements I {
public function method(int $a) {}
}
// Fatal error: Declaration of C::method(int $a) must be compatible with I::method(array $a)

class C1 {
public function method(array $a) {}
}
class C2 extends C1 {
public function method(int $a) {}
}
// Warning: Declaration of C2::method(int $a) should be compatible with C1::method(array $a)

As we saw in the examples, in the case of interface implementation, incompatible method signatures throw a fatal error, but in the case of class inheritance incompatible method signatures return a warning. But return type signature errors always result in fatal errors. More about this topic here: PHP RFC: Always generate fatal error for incompatible method signatures.

With the new update, the @ operator no longer silences fatal errors.

Besides unions, which we’ve already discussed, there’s a new mixed type added in the type system. Previously this was supported only in doc-blocks. The mixed type is equivalent to array|bool|callable|int|float|null|object|resource|string. You can use it this way:

function foo(mixed $value) {}

More about mixed types here: PHP RFC: Mixed Type v2.

New Classes, Interfaces, and Functions

Let’s discover what new classes, interfaces, and functions PHP8 offers. This is the full list:

We’ll go over weak maps and the Stringable interface first.

Weak Map

Weak maps make it possible to create a map with objects as keys without preventing keys from being garbage collected. If an object key is garbage collected, it will simply be removed from the map. From PHP 7.4 we know about weak reference (weak references allow the programmer to retain a reference to an object which does not prevent the object from being destroyed). Keys of weak maps are weakly referenced. Here’s an example:

$map = new WeakMap;
$obj = new stdClass;
$map[$obj] = 42;
var_dump($map);
// object(WeakMap)#1 (1) {
//   [0]=>
//   array(2) {
//     ["key"]=>
//     object(stdClass)#2 (0) {
//     }
//     ["value"]=>
//     int(42)
//   }
// }
 
// The object is destroyed here, and the key is automatically removed from the weak map.
unset($obj);
var_dump($map);
// object(WeakMap)#1 (0) {
// }

You can find out more about weak maps here: PHP RFC: Weak maps.

Stringable

The Stringable interface was automatically added to classes that implement the __toString() method.

interface Stringable
{
public function __toString(): string;
}

You can find out more about this interface here: PHP RFC: Add Stringable interface.

With this new update, you can find other syntax tweaks and improvements, such as, for example, allowing a trailing comma in parameter lists RFC and closure use lists RFC. The throw is now an expression RFC which means you can write the code this way:

$condition &amp;&amp; throw new Exception();
$condition || throw new Exception();
$condition and throw new Exception();
$condition or throw new Exception();

Learn more about other improvements here.

Just-In-Time compilation

Now let’s discuss one of the most amazing features provided by PHP8: just-in-time (JIT) compilation. Before defining what it is, let’s go under the hood and explore how PHP executes source code. During code execution PHP goes through 4 stages:

  1. Lexing or tokenizing
  2. Parsing
  3. Compilation
  4. Interpretation

Let’s have a look at each stage separately.

Lexing

In this stage, the PHP interpreter turns PHP source code which is string into a sequence of tokens. Tokens look like these:
T_VARIABLE, T_WHITESPACE, T_OPEN_TAG…

Parsing

In this stage, the token set is taken from lexer, and it is ensured that the tokens had formed valid language constructs. The parser checks tokens’ order and matches them to syntax rules. After grammar rules verification, the parser generates an abstract syntax tree (AST) which will be used in the next stage.

Compilation

In this stage, the PHP interpreter recursively traverses the AST and generates opcodes. An opcode is the numeric identifier of a single operation that is performed by the Zend Virtual Machine (VM).

Interpretation

In this final stage, opcodes are interpreted by Zend VM. It translates PHP byte codes or opcodes into machine code and executes it. The output is the same that our PHP code outputs via echo, print, var_dump, and other functions.

Now let’s talk about the opcache which came with PHP 5.5. When opcache is enabled, PHP takes opcodes and before passing them to Zend VM stores them in shared memory. Storing PHP byte codes or opcodes in opcache removes the need for PHP to load and parse scripts upon each request. So opcache improves PHP performance. With enabled opcache PHP source code execution skips the first 3 stages described above. It goes through all 4 stages only when the script runs for the first time. You can easily manage opcache configs via php.ini file. You can enable it by setting opcache.enable=1 in the php.ini file.
Here is the image that describes PHP source code execution stages:
PHP source code execution stages

What is a JIT compiler?

Now after understanding the PHP source code execution process and opcodes caching, let’s have a look at the just-in-time compiler.

In computing, just-in-time compilation is a way of executing computer code that involves compilation during the execution of a program rather than before execution.

With enabled opcache, we saw that PHP skips lexing, parsing, and compiling but the Zend VM still has to compile opcodes from opcache to native machine code. So here comes JIT which translates PHP byte codes to machine code and creates a new cache layer for native machine code and makes it possible to run the code by CPU and not Zend VM.
PHP8 comes with two JIT compilation engines, tracing and function JIT. Tracing is the most promising of the two because it gives better performance results. The difference between function and tracing JIT is that the first one optimizes single function code, while the second one checks the whole stack trace to optimize hot code.
It is possible to easily enable or disable JIT. Just like opcache, you can do that in a php.ini file. There are 3 directives for JIT: opcache.jit_buffer_size, opcache.jit and opcache.jit_debug. Before setting these directives make sure that opcache is enabled: opcache.enable=1.

opcache.jit_buffer_size
With this directive you define the shared memory buffer size for storing generated native code. Example: opcache.jit_buffer_size=100M

opcache.jit
This directive determines JIT control options, it is four decimal digits, CRTO.
Let’s explore each separately.

C – CPU-specific optimization, possible values are
0 – No optimization whatsoever
1 – Enable AVX instruction generation

R – sets register allocation modes, possible values are
0 – Never perform register allocations
1 – Use local linear-scan register allocation
2 – Use global linear-scan register allocation

T – determines JIT trigger, possible values are
0 – JIT everything on script load
1 – JIT functions when they execute
2 – Profile first request and compile hot functions on second requests
3 – Profile and combine hot functions all the time
4 – Compile functions with a @jit in doc blocks

O – determines optimization level, possible values are
0 – Never JIT
1 – Minimal JIT
2 – Selective VM handler inlining
3 – Optimized JIT based on static type interface of individual function
4 – Optimized JIT based on static type interface and call tree
5 – Optimized JIT based on static type interface and onner procedure analysis

So use opcache.jit=1205 for JIT everything. The best default is 1255, it will do maximum jitting. Note that this directive is optional, if you skip it the default value is opcache.jit=tracing. Also for function JIT you can set opcache.jit=function.

opcache.jit_debug
This directive specifies the JIT debug control options. The default is 0.

How does the JIT compiler impact WordPress performance?

As with the JIT compiler, the code is run by CPU and that causes significantly better performance for mathematical computations. Real-life web applications like WordPress websites work with large datasets, make graphic manipulations, and that’s why there is no significant performance improvement with JIT. During tests there were increases from 315 requests to 326 requests per second for WordPress applications. Though JIT doesn’t have a large impact on real-life web applications’ performance, it moves PHP to the next level, making it possible to use PHP in machine learning and the big data world.
JIT compiler impact

Should I upgrade to PHP 8?

As we see, PHP 8 has some amazing new features, like union types, nullSafe operator, named arguments which will make your developing life much more interesting and pleasing. Also it has optimizations and performance improvements. It is shown that PHP8 is 18.47% faster than PHP 7.4. So yes, you should update to PHP 8. But before upgrading check your application capability with PHP8. WordPress 5.7 is more stable with PHP 8.0 as this version already has PHP 8 compatibility fixes, such as, for example, removal of the @ operator, which no longer silences fatal errors. The most popular WordPress plugins already have compatibility fixes.
10Web supports PHP 8.0 and you can easily upgrade your PHP. Some compatibility fixes have already been made in 10Web plugins so they fully support PHP 8.0 and you can freely use 10Web plugins with PHP 8.0. Read the next section for more information on how to upgrade to PHP 8 on 10Web.

How do I upgrade to PHP 8?

With 10Web it is very easy to switch to the new PHP version. All you have to do is to go to Hosting Services>Tools and choose PHP 8.0 from the list.

updating PHP in 10Web

Is PHP 8 backwards compatible?

One of the important backwards incompatible changes is the string to number comparison. As we talked about it in this article, non-strict comparisons between numbers and non-numeric strings now work by casting the number to string and comparing the strings. You should check all non-strict comparisons in your code to avoid unexpected results.
Other incompatible changes are that match is now a keyword, methods with the same name as the class are no longer interpreted as constructors, and the ability to define case-insensitive constants has been removed. You can find the full list here: Backward Incompatible Changes.

What is deprecated in PHP 8?

Here are deprecations in PHP core:

  • If a parameter with a default value is followed by a required parameter, the default value has no effect. One exception to this rule are parameters of the form Type $param = null, where the null default makes the type implicitly nullable.
  • Calling get_defined_functions() with exclude_disabled explicitly set to false is deprecated and no longer has an effect. get_defined_functions() will never include disabled functions.

You can find the full list with deprecations in Enchant, LibXML, PGSQL / PDO PGSQL, Zip, Standard library, and Reflection here: Deprecated Features.

Conclusion

In this article we explored the new world of PHP 8.0. We described new powerful features and improvements. We have become familiar with JIT compilers and discovered new opportunities for PHP.

If you haven’t upgraded your PHP to 8.0 it is the right time for you to start your journey into the new world of PHP full of amazing features and improvements.

Share article

Leave a comment

Your email address will not be published. Required fields are marked *

Your email address will never be published or shared. Required fields are marked *

Comment*

Name *

4 comments

  • adruby

    opcache.jit=1255 with opcache.jit_buffer_size=16m works just fine.
    Thanks for the 1255 tip. The default jitt setting take a lot more ram.

    Loading
    Copy
  • John

    PHP 8.0 is a major update of the PHP language. It contains many new features and optimizations including named arguments, union types, attributes, constructor property promotion, match expression, nullsafe operator, JIT, and improvements in the type system, error handling, and consistency.

    Loading
    Copy
  • Sam Bashir

    Very well explained and informative post. Thanks for sharing it with us.

    Loading
    View all 1 replies
    Copy
    • Anna Alaverdian

      Glad you enjoyed the article, Sam 🙂

      Loading
      Copy