PHP 8.0: What's New and Changed

Version StatusFuture Release
Release Date2020-12-03

Union Types

TypeNew Feature

PHP has come a long way with types. We now have scalar types, return types, nullable types, and even property types in PHP 7.4!

PHP 8.0 comes with support for Union Types!

In versions prior to PHP 8.0, you could only declare a single type for properties, parameters, and return types. PHP 7.1 and newer versions have nullable types, which means you can declare the type to be null with a type declaration similar to ?string.

From PHP 8.0, you can declare more than one type for arguments, return types, and class properties.

class Example {
    private int|float $foo;
    public function squareAndAdd(float|int $bar): int|float {
        return $bar ** 2 + $foo;

Similar to types enforced in previous PHP versions, PHP will now make sure the function parameters, return types, and class properties belong to one of the types declared in the definition.

Union types prior to PHP 8.0

Nullable types

PHP already has support for nullable types; You can declare a type as nullable by prefixing a ? sign to the type. For example, if a certain property can be a string or a null, you can declare it as ?string. With PHP 8.0 Union Types, string|null will be functionally equivalent to ?string.

iterable type

There is also the iterable pseudo-type that is functionally equivalent to array|Traversable.

PHPDoc comments

PHPDoc standard had support for Union Types. You would have used a PHPDoc comment to declare the types allowed for a certain code block:

class Example {  

  * @var int|float  
  private $foo;  

   * @param int|float $bar  
   * @return int|float  
  public function squareAndAdd(int $bar): int {  
    return $bar ** 2 + $this->foo;  

Union Types in PHP 8.0

From PHP 8.0, you can now declare any number of arbitrary types to for properties, arguments, and return types. There are a few exceptions that don't make sense to be used together, such as string|void.

You can completely get rid of the @var, @param, and @return PHPDoc comments now in favor of the types enforced in the code itself. This will certainly help clean the boilerplate comments that were there purely because they could not be declared in code before.

Existing nullable types are not removed, nor deprecated. You can continue to use ?string as a shorthand to string|null.

The iterable type is not going to be removed either, and will be functionally equal to array|Traversable.

The RFC further explains certain restrictions and other improvements to Union Types:

The void type is not allowed in a Union

PHP already has support for void type. It is only allowed as a return type because using void anywhere else wouldn't make any sense.
Either the function must not return any value, or call return; without specifying a value.
In PHP 8.0 Union Types, the void type is not allowed to be combined with any other type.

For example, the following is not allowed:

function foo(): void|null {}

Special false type

PHP core and many legacy libraries return false to indicate negative result.

For example, if you have a function that loads use account by its ID, it might return false to indicate that the user does not exists.

To help the adoption of union types, it is allowed to use false as part of the union types.

function user_load(int $id): User|false {

The snippet above mimics Drupal's user_load() function, where it returns a User object if the user account is found, or false otherwise. This special false type can be quite helpful in this situation.

There are several PHP core functions that follow the same pattern too. For example, strpos() function returns an int of the position where the needle bytes were found in the string, or false if it is not found anywhere. The special false type can come handy here too.

  • false cannot be used as a standalone type. This means a type declaration like public false $foo is not allowed. You can still use bool type for it.
  • There is no true pseudo type. The aim of the false pseudo type is to accommodate the (mostly legacy) code that returns false on failure. Use bool type there.
  • false pseudo type is allowed anywhere types are allowed: class properties, function arguments, and return types all support false type.
  • *If bool is used, false cannot be used in same type declaration.

Nullable types (?TYPE) and null must not be mixed.

While ?string is a shorthand for string|null, the two notations must not be mixed.

If your type declaration has more than one type and null, it must be declared as the following:


It must not be declared as ?TYPE_1|TYPE_2 as this would be an ambiguous declaration.

Compile-time error checking

Union types does not allow duplicate or redundant types in type declaration. This check will happen at compile-time without autoloading classes/interfaces.

Duplicate types are not allowed.

You cannot declare int|int or int|INT as this essentially is the same. Furthermore, int|?int is not allowed either.
The latter will result in a syntax error, and the former will throw this error:

Fatal error: Duplicate type ... is redundant in ... on line ...

Redundant types are not allowed.

Union types does not allow redundant class types. However, it is important to note that this only applies to a few special special types only.

  • bool|false is not allowed because false is a type of bool.
  • `object cannot be used with a class name because all class objects are of type object too.
  • iterable cannot be used with array or Traversable because iterable is a union type array|Traversable already.
  • Class names can be used even if one extends another.

Because the Union types declarations are validated at compile-time, there will be no errors thrown if you use a parent class and a child class in union because that would require class hierarchy resolution.

Here are some examples:

function foo(): bool|false {} // Fatal error: Duplicate type false is redundant in ... on line ...
function foo(): DateTime|object {} // Fatal error: Type DateTime|object contains both object and a class type, which is redundant in ... on line ...
function foo(): iterable|array {} // Fatal error: Type iterable|array contains both iterable and array, which is redundant in ... on line ...

Because the type declaration is only checked at compile-time, the following is valid:

class A{}
class B extends A{}
function foo(): A|B {}

Type variance

Union type variance follows LSP. This is to ensure that all sub classes and implementations of the interface must not change the behavior of the program and still adhere to the contract. This is enforced in PHP even without Union types.

  • Parameter are contra-variant: Types can be widened with a super type.
  • Return types are covariant: Types can be restricted to a sub type.
  • Property types invariant: Types cannot be changed to a sub or super type.

Adding or removing types to a Union

class A{
    public function foo(string|int $foo): string|int {}
class B extends A{
    public function foo(string|int|float $foo): string {}

In the snippet above, parameter types are widened (with the addition of float type). This is allowed because all programs that use class B expect them to accept all types class A accepts. Class B still fulfills this contract.

Return type in B::foo is restricted further, but this still follows LSP because class B fulfils the return type declared in A::foo.

If you were to change the type in a way that class B does not fulfill the contract, this will trigger a fatal error:

class A{
    public function foo(string|int $foo): string|int {}
class B extends A{
    public function foo(string|float $foo): string|int {}
Fatal error: Declaration of B::foo(string|float $foo): string|int must be compatible with A::foo(string|int $foo): string|int in ... on line ...

Variance of individual types in a Union

Same variance rules are followed when you change individual members a union type.


class ParentClass {}
class ChildClass extends ParentClass{}

class FooParent {
    public function foo(string|ChildClass $a): string {}
    public function bar(string $b): string:ParentClass {}

class FooChild extends FooParent{
    public function foo(string|ParentClass $a): string|ChildClass {}    
    public function bar(string $b): string:ChildClass {}

This is perfectly, because in the FooChild::foo functions parameters are expanded. All existing code that works with FooParent class will work because ChildClass is a sub-type of ParentClass with same functionality.

In FooChild::bar function, its return type is further restricted. This is also valid because FooChild::bar still fulfills its contract by return a sub-type of string or ChildClass which inherits ParentClass.


class FooParent {
    public function foo(string $a): int|null {}

class FooChild extends FooParent{
    public function foo(string|null $b): int|null {}    

Return types are allowed to lose nullable type, and parameters are allowed to make they accept null.

bool and false

In a Union type, PHP considers false to be a sub-type for bool. This allows variance like this:

class FooParent {
    public function foo(int|false $a): int|bool {}

class FooChild extends FooParent{
    public function foo(int|bool $b): int|false {}  

Backwards compatibility impact

Union Types is a new feature in PHP, and its new syntax does not break any existing functionality. Because this is a syntax change in the engine, this functionality cannot be brought to older versions with a poly-fill.

RFC discussion Implementation

New `str_contains` function

TypeNew Feature

One of the usability improvements that comes with PHP 8.0 is the new str_contains function. As the name suggests, it checks if the given haystack string contains a given string needle.

Without this function, the usual way to find if a given string contains another string is to use to the strpos() function:

if (strpos('Foo Bar Baz', 'Bar') !== false) {
  echo 'Found';

strpos() function returns the position of the needle string, or false if the needle is not found. This is error-prone, because if the needle is found at the position 0 of the haystack, it evaluates to false unless strict comparison (===) used.

To explain further, the following snippet is not correct:

if (strpos('Foo Bar Baz', 'Foo')) {
  echo 'Found';

Because Foo is found at the beginning of the haystack, the return value of strpos() call will be 0, which evaluates to false, and the if block will not run.

With the new str_contains function, it is easy to do this:

if (str_contains('Foo Bar Baz', 'Foo')) {
  echo 'Found';

Case sensitivity

str_contains() function is case-sensitive. There is no case-insensitive variant of this function. There is no technical reason to not have a case-insensitive str_icontains() function, but there isn't one for now to keep things simple.

Multi-byte strings

For strpos() function, there is a multi-byte safe mb_strpos() variant. However, for str_contains() function, there is no mb_str_contains() function. This is because internally, PHP strings are streams of bytes, and an mb_str_contains() function will be identical to the functionality of str_contains() as it would be checking for a sequence of bytes in another sequence of bytes anyway.

Conflicts with user-land implementations

There are several user-land implementations of this functionality, often with the exact same name. As of this moment, there are over 192K str_contains() matches on Github, and over 6K search results of str_contains() function declarations.

Most notably, Laravel offers a helper function str_contains(), but this function accepts an array of needles for the second parameter as well, which is not compatible with PHP core implementation.

Empty strings

If you search for an empty needle (""), PHP will always return true. To quote Nikita:

As of PHP 8, behavior of '' in string search functions is well defined, and
we consider '' to occur at every position in the string, including one past
the end. As such, both of these will (or at least should) return true. The
empty string is contained in every string.

This means the following will always returns true:

str_contains('Foo', ''); // true
str_contains('', ''); // true


A PHP 7.0+ compatible polyfill is straight forward and simple:

if (!function_exists('str_contains')) {
    function str_contains(string $haystack, string $needle): bool {
        return '' === $needle || false !== strpos($haystack, $needle);

Backwards compatibility impact

str_contains() is a new function; Unless you already have a str_contains() function declared, there should be no BC impact.

Most user-land implementations only declare its own str_contains() function only if that function does not exists. You might run into obscure bugs if the user-land implementations are incompatible with PHP core's. See the section above about user-land implementations.

RFC discussion Implementation

New `fdiv` function

TypeNew Feature

PHP 8 adds a new fdiv() function to fit along functions such as fmod() and intdiv().

As of now, attempting a divide a number by zero results in inconsistent behavior.

Division operator triggers warnings

$num = 1 / 0;
// Warning: Division by zero in ... on line ...

intdiv() function throws DivisionByZeroError exceptions

$num = intdiv(1, 0);
// Fatal error: Uncaught DivisionByZeroError: Division by zero in ...:...

Modulo operator throws DivisionByZeroError exceptions

$mod = 1 % 0;
// Fatal error: Uncaught DivisionByZeroError: Modulo by zero in ...:...

Semantically, the division operator should throw a DivisionByZeroError exception as well, but due to backwards compatibility issues, it is still a warning.

In an attempt to streamline this, Nikita suggested to add the new fdiv() function.

This function will follow the IEEE-754 standard on Floating-Point Arithmetic, and return NAN, or ±INF as float. The return value of fdiv will be identical to the current division operator's behavior.

## +INF
fdiv(1, 0); // float(INF)

$num = 1 / 0; // float(INF)
// Warning: Division by zero in ... on line ...

## -INF
fdiv(-1, 0); // float(-INF)

$num = -1 / 0; // float(-INF)
// Warning: Division by zero in ... on line ...

The division operator will eventually throw DivisionByZeroError when this function is well-established in the future.


A PHP 7.0+ polyfill that mimics the new fdiv() function:

if (!function_exists('fdiv')) {
    function fdiv(float $dividend, float $divisor): float {
        return @($dividend / $divisor);

Backwards compatibility impact

fdiv() is a new function. A quick GitHub search did not reveal any positive results for existing code that declares a function with the same name.

It is recommended that you use this function whenever possible (see the polyfill above) because division by zero using division operator is already triggering a PHP warning in your existing code, which will escalate to a DivisionByZeroError exception in the future. discussion Implementation

New `get_debug_type` function

TypeNew Feature

get_debug_type() function that comes with PHP 8.0 is an attempt to improve gettype() function which returns inconsistent values and does not expand to reveal the class names of objects.

One of the use cases of this function is to use it as an easy way to explain an unexpected variable in error logs or exception messages because the return type of gettype() is often not verbose enough to be meaningful.

get_debug_type() vs gettype()

The following sections will explain the differences between the new get_debug_type() function and current gettype() function return values.

Scalar types

Type Example value gettype() get_debug_type()
String "Foo" string string
Arrays [1, 2] array array
Null null NULL null
Integers 123 integer int
Float 3.141 double float
Boolean true boolean bool
Boolean false boolean bool

Notice how the new get_debug_type() function returns the exact types that you use in scalar typing.

Class objects and anonymous functions

Type Example value gettype() get_debug_type()
Class object new stdClass() object stdClass
Class object new DateTime() object DateTime
Class object new Foo\Bar() object Foo\Bar
Closure function() {} object Closure
Anonymous class new class {} object class@anonymous
Anonymous subclass new class extends Foo{} object Foo@anonymous

As you can see, get_debug_type() is starting to get more helpful here, because it helpfully reports the name of the class, or further details about anonymous classes.

If a closure is passed (which is internally an object), get_debug_type() returns it as Closure.

When an anonymous class object is passed, it returns class@anonymous for classes without a parent/interface, or parent/interface name, followed by @anonymous.

This is where the get_debug_type() function gets quite helpful because instead of calling get_class() and get_parent_class(), get_debug_type() directly returns a useful name for the given object. This can help minimize the boilerplate code and code bloat in when exceptions are thrown and errors are logged.


Type Example value gettype() get_debug_type()
Streams tmpfile() resource resource (stream)
Curl handler curl_init() resource resource (curl)
Closed Curl handler curl_close($ch) resource (closed) resource (closed)
XML document xml_parser_create() resource resource (xml)
... ... resource resource (TYPE)

For all resource types in PHP, get_debug_type() function will return the type of that resource too.

Use cases

When you want to output a verbose error message on an unexpected type, instead of doing an ugly type lookup, which often overlooks other variable types, you can now easily get the type of a passed variable:


if (!($foo instanceof Foo)) { 
    throw new TypeError(
            'Parameter 1 is expected to be of type "%s", got "%s" instead.',
            (is_object($foo) ? get_class($foo) : gettype($foo))


if (!($foo instanceof Foo)) { 
    throw new TypeError(
            'Parameter 1 is expected to be of type "%s", got "%s" instead.',


if (!function_exists('get_debug_type')) {
    function get_debug_type($value): string {
        switch (true) {
            case null === $value: return 'null';
            case \is_bool($value): return 'bool';
            case \is_string($value): return 'string';
            case \is_array($value): return 'array';
            case \is_int($value): return 'int';
            case \is_float($value): return 'float';
            case \is_object($value): break;
            case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class';
                if (null === $type = @get_resource_type($value)) {
                    return 'unknown';

                if ('Unknown' === $type) {
                    $type = 'closed';

                return "resource ($type)";

        $class = \get_class($value);

        if (false === strpos($class, '@')) {
            return $class;

        return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous';

Above is a polyfill that you can use in your code if you want to bring the get_debug_type() function to your own code that cannot require PHP 8.0 to run. This snippet is from Symfony's PHP 8.0 polyfill.

Backwards compatibility impact

get_debug_type() is a new function. Unless you have declared a function with an identical name in the global namespace, this should not bring any BC issues.

RFC discussion Implementation

New `preg_last_error_msg` function

TypeNew Feature

Some of the legacy PHP functionality does not throw exceptions on errors, and it is up to the caller to check if an operation was successful or not.

When you run a regular expression using PHP's preg_ functions, these functions do not throw an exception if something went wrong. It is up to the caller to retrieve any error messages using preg_last_error function, which returns an error code if there were any errors.

PHP's core json_encode() and json_decode() functions follow a similar pattern, and unless the exception behavior is explicitly requested (PHP 7.3+), you have to call json_last_error function to retrieve any errors occurred during the last operation.
However, JSON functionality comes with a json_last_error_msg() function to retrieve the human-friendly error message of the JSON encode/decode error.

PREG functionality did not come with a function to retrieve the human-friendly error message. PHP 8.0 brings one!

The new preg_last_error_msg() returns a human-friendly error message, or "No error" (as string) if there were no errors.

preg_match('/(?:\D+|<\d+>)*[!?]/', 'foobar foobar foobar');
var_dump(preg_last_error()); // 2

With the new preg_last_error_msg() function, you can directly get the error message:

preg_match('/(?:\D+|<\d+>)*[!?]/', 'foobar foobar foobar');
var_dump(preg_last_error()); // 2
var_dump(preg_last_error_msg()); // Backtrack limit was exhausted


The new preg_last_error_msg() function simply returns a human-friendly text error message instead of the error code. This can be polyfilled with a hardcoded list of error codes mapped to error messages:

if (!function_exists('preg_last_error_msg')) {
    * Returns the error message of the last PCRE regex execution.
    * @return string
    function preg_last_error_msg(): string {
        switch (preg_last_error()) {
            case PREG_NO_ERROR:
                return 'No error';
            case PREG_INTERNAL_ERROR:
                return 'Internal error';
                return 'Backtrack limit exhausted';
                return 'Recursion limit exhausted';
            case PREG_BAD_UTF8_ERROR:
                return 'Malformed UTF-8 characters, possibly incorrectly encoded';
            case PREG_BAD_UTF8_OFFSET_ERROR:
                return 'The offset did not correspond to the beginning of a valid UTF-8 code point';
                return 'JIT stack limit exhausted';

                return 'Unknown error';

Backwards compatibility impact

preg_last_error_msg() is a new function, and unless you have declared a function with the same name in global namespace, there should be no BC issues. discussion Implementation

`::class` magic constant is now supported on objects

TypeNew Feature

PHP has a magic constant ::class that resolves a class name to its fully-qualified class name. When used with a class name, use and use as statements will be resolved, or the current namespace will be prefixed which makes it a fully-qualified class name.


namespace App\Demos;

use Foo\Bar;
use Bar\Baz as BBaz;

class Demo {}

// `use` statement is resolved:
echo Bar::class; // "Foo\Bar"

// `use` X `as` Y is resolved:
echo BBaz::class; // "Bar\Baz"

// Current namespace is resolved:
echo Demo::class; // "App\Demos\Demo"

Until PHP 8.0, the ::class magic constant was not allowed on objects.

$object = new Foo\Bar();
echo $object::class;

// Fatal error: Cannot use ::class with dynamic class name.

With PHP 8.0, now you can use ::class constant on objects, and it will be correctly resolved at run time:

$object = new Foo\Bar();
echo $object::class;

// PHP 8.0+:
// "Foo\Bar"

Same result as get_class()

The ::class constant on an instantiated object will return the exact same return value as a get_class() call.

get_class($object) === $object::class;

Non-objects are not allowed

Using ::class on a non-object is not allowed:

$object = array();

// Fatal error: Uncaught TypeError: Cannot use ::class on value of type array in ...:...

If you need to get the type of any variable, PHP 8.0 comes with a new get_debug_type() function that you can call to get the class name, scalar types, resource type, etc all from one handy function.

Backwards compatibility impact

In PHP versions prior to 8.0, using ::class on an object triggers a fatal error. Because PHP 8.0 is relaxing this error, this should not create any new BC issues. However, this will make your code require PHP 8.0 to run without a way to polyfill this functionality to older versions.

RFC discussion Implementation

New `ValueError` Error Exception

TypeNew Feature

\ValueError is a new Exception type that extends \Exception class, and from PHP 8.0, you will be seeing lots of them! \ValueError exception is thrown when a value encountered is of correct type, but it cannot be used to continue the operation.

What about other exceptions?

PHP already has exceptions such as \InvalidArgumentException, \OutOfRangeException and \LengthException exceptions that convey a more precise error message.

However, exceptions is the keyword here: The new \ValueError exception extends \Error, instead of \Exception. While you can throw \ValueError exceptions in user-land code, PHP core functions will throw \ValueError exceptions except for a few specific cases (such as sprintf() function throwing \ArgumentCountError exceptions instead of the legacy Warning: sprintf(): Too few arguments warning).

  ├── Error
  │     ├── TypeError
  │     ├── ValueError
  └── Exception
        ├── LogicException
                ├── DomainException
                ├── InvalidArgumentException
                ├── LengthException
                └── OutOfRangeException

This is a simplified chart of PHP core \Error and \Exception. You can take a look at full PHP Exception hierarchy in this post.

From PHP 8.0 and forward, \ValueError errors will be thrown when the value passed to a function is of a valid type, but is not valid for the operation.


  • strpos() attempting to set a offset longer than the haystack length

    Before PHP 8.0:

    $a = strpos("s", "small", 16);
    // Warning: strpos(): Offset not contained in string in ... on line ...
    // bool(false)

    From PHP 8.0

    $a = strpos("s", "small", 16);
    // Uncaught ValueError: Offset not contained in string in ...:...

  • range() with non-positive steps

    Before PHP 8.0:

    $a = range(1, 2, 0);
    // Warning: range(): step exceeds the specified range in ... on line ...
    // bool(false)

    From PHP 8.0

    $a = range(1, 2, 0);
    // Uncaught ValueError: Step exceeds the specified range ...:...

  • array_rand() with an empty array

    Before PHP 8.0:

    $a = array_rand(array(), 0);
    // Warning: array_rand(): Array is empty in ... on line ...
    // NULL

    From PHP 8.0

    $a = array_rand(array(), 0);
    // Uncaught ValueError: Array is empty in ...:...

But why?

The new \ValueError exception is introduced as part of the major change Internal function warnings now throw TypeError and ValueError exceptions, where you can find detailed information.


It is possible to polyfill this exception class by simply declaring a user-land class with the same name.

if (!class_exists('\ValueError')) {
    class ValueError extends \Error {

Note that this will not make internal PHP functions throw \ValueError exceptions when appropriate. However, if you have user-land code that needs to throw \ValueError exceptions, it is now possible with the polyfill.

Backwards compatibility impact

\ValueError is a new class, and unless you have user-land implementations, there should be no BC impact.

RFC Class introductionIndividual changes

New `str_starts_with` and `str_ends_with` functions

TypeNew Feature

PHP 8.0 comes with two new functions to help you easily assert if a given string is present at the beginning or ending of a haystack string. This goes nicely with str_contains() in PHP 8.0.

  • str_starts_with(): Check if a given haystack string starts with the given needle string
  • str_ends_with: Check if a given haystack string ends with the given needle string
str_starts_with(string $haystack, string $needle): bool;
str_ends_with(string $haystack, string $needle): bool;

PHP 8 finally brings the total number of string-related functions in PHP to over 100, and the new functions can be easily mimicked with existing functions such as strpos, substr, strncmp, and substr_compare. However, these new functions were well-received due to possible engine-level optimizations and their frequent use-cases.

Case sensitivity

Both str_starts_with() and str_ends_with() functions are case-sensitive. There are no flags or other functions to make them case-insensitive. This is the same pattern with str_contains

Multi-byte strings

Multi-byte (mb_*) variants for str_starts_with() and str_ends_with() are not currently planned.

Empty strings

Similar to str_contains, PHP now considers empty string ("") to be present in everywhere in a string. To quote Nikita:

As of PHP 8, behavior of '' in string search functions is well defined, and
we consider '' to occur at every position in the string, including one past
the end. As such, both of these will (or at least should) return true. The
empty string is contained in every string.

The following calls will be always true:

str_starts_with('Foo', ''); // true
str_starts_with('', ''); // true

str_ends_with('Foo', ''); // true
str_ends_with('', ''); // true


Here is a polyfill for PHP 7.0 and later. Be sure to wrap them with function_exists() calls where you use them. These functions pass the exact same tests in PHP core.

function str_starts_with(string $haystack, string $needle): bool {
    return \strncmp($haystack, $needle, \strlen($needle)) === 0;

function str_ends_with(string $haystack, string $needle): bool {
    return $needle === '' || $needle === \substr($haystack, - \strlen($needle));

Conflicts with current user-land implementations

Starts-with and ends-with functionality is often provided as helper functions in various frameworks. This includes Symfony String component, Laravel Str helper, and Yii StringHelper.

There are over 4,000 str_starts_with() matches on GitHub, for PHP, most of which appear to be already namespaced.

Case-insensitivity support Empty strings at every position
PHP 8.0
No Yes
Polyfill (above)
No Yes
Symfony String
With ::ignoreCase()
No No
Yes (default)
With parameter

Backwards compatibility impact

Both str_starts_with and str_ends_with functions are new functions. Unless you already have a str_contains() function declared, there should be no BC impact.

PHP 8's new behavior that it considers there is an empty string at every position of a string can be tricky. Note that Laravel helpers and Symfony String component, among many others return false when you search for an empty string needle ("") at the start and end of strings, although PHP core returns true.

RFC discussion Implementation


TypeNew Feature

One of the biggest new changes in PHP 8 is the Attributes support. Attributes help to add meta-data to PHP functions, parameters, classes, class methods, constants, properties, closures, and even anonymous classes. This meta-data can be fetched programmatically, and provides a pragmatic approach to resolve the attributes in elsewhere in the code.

An in-depth post about the history, different approaches, and practical use cases of Attributes are in the post Attributes in PHP 8.

Syntax and Usage

Attributes are declared with << and >> signs.

<<ExampleAttribute('foo', 'bar')>>
function example() {}

After several suggestions such as @@ and @: for the prefix, the <<Foo>> syntax was chosen due to implementation details in PHP parser.

These tokens will yield a parse error in older versions of PHP, which makes this feature impossible to back-port to older PHP versions.

Attributes can resolve to class names

Although not required, PHP 8 provides functionality to resolve the attribute names to class names. You can use use statements to clean-up the code. Standard rules of class name resolving will be followed.

It is optional to match the Attribute name to a class name.

Attributes can have parameters

Each attribute can have zero or more parameters. They will be passed to the Attribute class constructor if attempted to get an instantiated object of the attribute.

Parameter can be simple scalar types, arrays, or even simple expressions such as mathematical expressions, PHP constants, class constants (including magic constants). Any expression that can be used as a class constant can be used as Attribute parameter.

More than one attribute allowed

Each item that receives Attributes can have zero or many attributes, each in its own <<>> brackets.

Each Attribute can be separated by a white-space (either a new line or a space(s)).

Before and After DocBlock comments

Attributes can appear before and after DocBlock comments. There is no standard recommendation for the code style, but this surely will be ironed out in a future PSR code-style recommendation.

Attribute Syntax Example

use App\Annotations\FooAttribute;
use App\Annotations\ClassAttrib as FooClassAttrib;
use App\Annotations\FooParamAttrib;
use External\Attr\BarClassAttrib;

function foo_func(<<FooParamAttrib('Foo1')>> $foo) {}

class Foo {
    private const FOO_CONST = 28;
    private const BAR_CONST = 28;

    <<PropAttr(Foo::BAR_CONST, 'string')>>
    private string $foo;

    public function getFoo(<<FooClassAttrib(28)>>): string{}

// Declare Attributes

 * Attributes are declared with `<<PhpAttribute>>` attribute. So meta!

namespace App\Annotations;

class FooAttribute {
    public function __construct(?string $param1 = null) {}

class ClassAttrib {
    public function __construct(int $index) {}

Looking for fine-grained examples ?
Attributes in PHP 8 contains several more examples with edge-cases demonstrated.

Fetching Attributes

Attributes are retrieved using the Reflection API. When PHP engine parses code that contains Attributes, they are stored in internal structures for future use. Opcache support included. It does not execute any code or call the constructors of the attributes unless an instance of the Attribute is requested (see examples below).

Using the Reflection API, the Attributes can be retrieved either as strings that contain the Attribute name (with class names resolved), and its optional arguments.

Reflection API can also instantiate an instance of the Attribute class, with class names resolved, autoloaded, and the optional parameters passed to the class constructor. Failure to instantiate the class will throw \Error exceptions that can be caught at the caller level.

New Reflection*::getAttributes() method

$reflector = new \ReflectionClass(Foo::class);

All Reflection* classes get a new method getAttributes method, that returns an array of ReflectionAttribute objects. A synopsis of this new method would be similar to the following:

 *  @param string $name Name of the class to filter the return list
 *  @param int $flags Flags to pass for the filtering process.
 *  @return array ReflectionAttribute[]
public function getAttributes(?string $name = null, int $flags = 0): array {}

ReflectionAttribute class synopsis

final class ReflectionAttribute {
     * @return string The name of the attribute, with class names resolved.
    public function getName(): string {}

     * @return array Arguments passed to the attribute when it is declared.
    public function getArguments(): array {}

     * @return object An instantiated class object of the name, with arguments passed to the constructor.
    public function newInstance(): object {}

Attribute filtering

Reflection*::getAttributes() optionally accepts a string of class name that can be used to filter the return array of attributes by a certain Attribute name.

$attrs = $reflector->getAttributes(FooAttribute::class);

$attrs array would now be only ReflectionAttribute objects or FooAttribute Attribute name.

A second optional parameter accepts an integer to further fine tune the return array.

$attrs = $reflector->getAttributes(BaseAttribute::class, \ReflectionAttribute::IS_INSTANCEOF);

At the moment, only \ReflectionAttribute::IS_INSTANCEOF is available.

If \ReflectionAttribute::IS_INSTANCEOF is passed, the return array will contain Attribute with same class name or classes that extends or implements the provided name (i.e all classes that fulfull instanceOf $name).

Retrieving Attribute Objects

ReflectionAttribute::newInstance method returns an instance of the Attribute class, with any parameters passed to the Attribute object class constructor.

A complete example

use My\Attributes\ExampleAttribute;

<<ExampleAttribute('Hello world', 42)>>
class Foo {}

class ExampleAttribute {
    private string $message;
    private int $answer;
    public function __construct(string $message, int $answer) {
        $this->message = $message;
        $this->answer = $answer;

$reflector = new \ReflectionClass(Foo::class);
$attrs = $reflector->getAttributes();

foreach ($attrs as $attriubute) {

    $attribute->getName(); // "My\Attributes\ExampleAttribute"
    $attribute->getArguments(); // ["Hello world", 42]
        // object(ExampleAttribute)#1 (2) {
        //  ["message":"Foo":private]=> string(11) "Hello World"        
        //  ["answer":"Foo":private]=> int(42) 
        // }

Read in-depth: This is a short summary of the new Attributes feature in PHP 8. For a detailed guide, see Attributes in PHP 8

Backwards Compatibility Impact

Due to the syntax change in Attributes, any code that uses Attributes will not work in older PHP versions.

Attempting to do so will trigger a parse error:

Parse error: syntax error, unexpected '<<' (T_SL), expecting end of file in ... on line ...

RFC Discussion Implementation

New `mixed` pseudo type

TypeNew Feature

mixed is a pseudo type added in PHP 8 that conveys the type of the parameter/return/property can be of any type. mixed type includes all scalar types in PHP, null, all class objects, callable, and even resource.

mixed is equivalent to a Union Type of:


With mixed, it is now possible to declare mixed as the type when the parameters, returns, and class properties can be of any type.

class Example {
    public mixed $exampleProperty;
    public function foo(mixed $foo): mixed {}

mixed is a pseudo type

mixed represents any type PHP can handle, and thus you cannot cast a variable to mixed because it simply doesn't make sense.

$foo = (mixed) $bar;

Further, there is no is_mixed() function for the same reasons.

gettype()and get_debug_type() functions will never return mixed as the type of a variable either.

mixed in union with other types

Because mixed represents all types, mixed cannot be used in union with other types:

function (mixed|FooClass $bar): int|mixed {}

Both union types above are not allowed, and will result in a fatal error:

Fatal error: Type mixed can only be used as a standalone type in ... on line ...

mixed is assumed when no type is declared

When a function parameter or a class property has no explicit type declared, the type is now assumed to be mixed.

Be mindful when you add mixed type to all your existing code; PHP 8 has Union Types that might be a better fit because Union Types allow you to be more specific.

For return types, lack of an explicit return type is equal to mixed|void.

However, note that you cannot declare mixed|void as a return type because mixed is not allowed in a Union Type.

Type variance

When a class method, return type, or a property type is overridden by a sub-class, Liskov Substitution Principle is respected.

Contravariance: mixed parameters types

Function parameter types can be "widened" at a child class or an interface implementation because the widened type still fulfills the contract of the interface/parent class. This means child class parameters can declare a type with a Union Type that includes more types, or a class name that is a parent of the current class.

When a parameter type is declared as mixed, this practically prevents further contravariance because mixed includes all types PHP works with. If possible, always opt for more specific types because once you mark a parameter type as mixed in a public API, all child classes must be capable to deal with mixed types.

Covariance: mixed return types

If a parent class method has declared a return type other than mixed, child classes will not be allowed to declare mixed because it widens the return type scope, thus breaking LSP.

class A {
    public function foo(): mixed {}
class B extends A{
    public function foo(): void {}

The above will result in a fatal error:

Fatal error: Declaration of B::foo(): void must be compatible with A::foo(): mixed

This is because mixed does not include void type. If a return type is not explicitly declared, it is assumed to be mixed|void.

All following declarations are allowed:

class A {
    public function foo() {}
    public function bar() {}
    public function baz(): mixed {}
class B extends A{
    public function foo(): mixed {}
    public function bar(): void {}
    public function baz(): string {}
  • B::foo: Allowed: Narrows down the assumed mixed|void return type of A::foo.
  • B::bar: Allowed: Narrows down the assumed mixed|void return type of A::bar.
  • B::baz: Allowed: Narrows down the declared mixed type.

Invariance: mixed property types

If a property type is declared as mixed, this type cannot be omitted or changed at all.

Usage with void

PHP supports void pseudo return type to indicate that the function will not return anything. This is equivalent to lack of a return statement, or return; expression without setting an explicit value.

void type and mixed cannot be in a union. Further mixed does not include void.

Nullable mixed types

It is not allowed to declare mixed type as nullable because mixed includes null.

All of the following declarations are not allowed:

function foo(mixed|null $foo) {}
function foo(?mixed $foo) {}
function foo($foo): mixed|null {}
function foo($foo): ?mixed {}

All declarations above will raise a fatal error:

Fatal error: Type mixed can only be used as a standalone type in ... on line ...

Practical Usage

Many PHP internal functions accept various types, which can now be declared with the mixed type. However, for user-land functions, it is often better to use a specific type or a Union Type.

Functions such var_dump or get_debug_type() can declare its parameters as mixed because these functions accept any type by definition.

If you declare a parameter/return type/class property as mixed type, be mindful that mixed includes types such resource and callable, which are not easily stored, serialized, sanitized, or displayed.

Most user-land functions that need to "accept anything", such as logging functions are better off with a Union Type such as string|int|float|bool|null|object|array.

Backwards compatibility

mixed is soft-reserved since PHP 7. Until PHP 8, it is technically possible to declare a class with name mixed, and it will not raise any errors, warnings, or notices. PHPDoc standard widely used mixed as a type declaration, so it is highly unlikely that even the wildest code base out there declares a class with name mixed.

Attempting to declare a class with name mixed in PHP 8 results the following error:

Fatal error: Cannot use 'mixed' as class name as it is reserved in ... on line ...


It is not possible to polyfill this functionality because it's an internal type. If you use mixed type anywhere in your code, be sure that it will always run on a PHP 8+ platform.

RFC Discussion Implementation

New `get_resource_id` function

TypeNew Feature

PHP resources, such as Curl handlers, open files, database connections, can be cast to int. PHP 8 adds a new get_resource_id function that is essentially a (int) $resource cast to make it easier to retrieve the resource ID.

The advantage of the new get_resource_id function is the type safety, that arguments and return type are checked to be a resource and an int.

get_resource_id() Function synopsis

function get_resource_id($res): int {}

Note that PHP does not have a resource type that can be enforced yet.


function get_resource_id($res): int {
    if (!\is_resource($res) && null === @get_resource_type($res)) {
        throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res)));

    return (int) $res;

From Symfony/Polyfill package. Supports PHP 7.0 and later.

Backwards Compatibility Impact

get_resource_id is a new function, and it can be easily polyfilled in previous PHP versions. Unless you have declared a function with exact same name in the global namespace, there should not be any problems.


Class constructor property promotion

TypeNew Feature

Constructor Property Promotion is a new syntax in PHP 8 that allows class property declaration and constructor assignment right from the constructor.

A typical class that declares a property, and then assigns a value to it in the class constructor is quite verbose.

class User {
    private string $name;
    public function __construct(string $name) {
        $this->name = $name;

With the Constructor Property Promotion syntax, the class declaration above can be minimized to avoid boilerplate:

  class User {
-   private string $name;
-   public function __construct(string $name) {
+   public function __construct(private string $name) {
-       $this->name = $name

This results in a much simplified and minimal syntax that is functionally identical to the verbose class declaration:

class User {
    public function __construct(private string $name) {}

What it does

Constructor Property Promotion is a shorthand syntax to declare and assign class properties from the constructor. This avoids having to type the class property name from 4 times to just once, and property type from twice to just once.

Your constructor can still run additional code within the constructor. Properties will be assigned before the rest of the constructor code is executed.

class User {
    public function __construct(private string $name) {
        echo $this->name;

new User('Ayesh');
// echoes "Ayesh"

If you change the constructor argument, you need to re-assign it to the class property:

new User('Ayesh');

class User {
    public function __construct(public string $name) {
        echo $this->name; // "Ayesh"
        $name = 'foo' . $name;
        echo $this->name; // "Ayesh"
        $this->name = $name;
        echo $this->name; // "fooAyesh"

AST and Reflection

Constructor Property Promotion is not internally handled as an Abstract Syntax Tree transformation. This means if you inspect the AST (with an extension such as php-ast), you will still see the code as written with property visibility modifiers right in the constructor.

However, as for Reflection API, it will seamlessly return information about both standard and constructor-promoted properties all the same.

In Reflection API, ReflectionProperty and ReflectionParameter classes will have a new method isPromoted to determine if the parameter/property was declared in a constructor.


You can mix Constructor Properties with standard properties

Mixing Constructor Properties and standard properties can lead to a messy code, but it is technically possible to do so:

class User {
    private int $uid;
    private string $name;
    public function __construct(public string $name, int $uid) {
        $this->uid = $uid;

Property Type is not required

It is possible to use Typed Properties and untyped properties (although it's often a good idea to always strict type class properties).

class User {
    public function __construct(private string $name, private $uid) {}

In the example above $uid will be a class property, but without a type declared to it.

Nullable types are support, but not implicitly

Nullable properties can be promoted at constructor, however, the PHP 7.0-style nullable types are not supported.

class User {
    public function __construct(
        public ?string $name,
        public ?string $fullname = null,
    ) {}

While the syntax above is allowed, implicit declaration of types (without the ? prefix, but = null default value) is not allowed:

class User {
    public function __construct(
        public string $name = null,
        public string $fullname = null,
    ) {}
Fatal error: Cannot use null as default value for parameter $name of type string in ... on line ...

Both property promotions above are not allowed because they are implicitly declaring nullable types, but without the ?string syntax. Although it works, this practice is frowned upon.

No duplicate properties

It is not allowed to duplicate a property with explicit declaration and constructor property promotion.

The following code is not allowed:

class User {
    public string $name;
    public function __construct(public string $name) {}
Fatal error: Cannot redeclare User::$name in ... on line ...

Not supported in interfaces and abstract classes, allowed in traits

Constructor Properties cannot be used in interfaces because they simply don't allow properties in the first place.

Not allowed:

interface User {
    public function __construct(public string $name);

Abstract classes can have constructor properties promoted, but not if the constructor itself is marked abstract.

abstract class User {
    abstract public function __construct(public string $name) {}
Fatal error: Cannot declare promoted property outside a constructor in ... on line ...

Traits can can contain both properties and methods, including constructors. This means it is possible for a trait for make use of Constructor Property Promotion too.

trait UserTrait {
    public function __construct(public string $name) {}

var is not allowed

The legacy syntax of declaring a property with var keyword is not allowed. All constructor-promoted properties must be declared with public, protected, or private visibility modifiers.

The following code is not valid:

class User {
    public function __construct(var $username) {}
Parse error: syntax error, unexpected 'var' (T_VAR), expecting variable (T_VARIABLE) in ... on line ...

callable type is not allowed

callable is a valid type in PHP, but it is not allowed as a property type. Thus, it is not allowed to declare a constructor property with type callable either:

class User {
    public function __construct(public callable $logoutFn) {}
Fatal error: Property User::$logoutFn cannot have type callable in ... on line ...

Only constructor supports property promotion

It probably goes without saying, but only the __construct method can have properties promoted. Other methods cannot:

class User {
    public function setUser(public string $name) {}
Fatal error: Cannot declare promoted property outside a constructor in ... on line ...

Backwards compatibility impact

Constructor Property Promotion is a syntax change in PHP 8, and so it's not possible to backport this feature to older PHP versions. If you use Constructor Property Promotion, that code will not be compatible with PHP 7 and older versions.

Attempting to run constructor-promoted classes will throw a parse error:

Parse error: syntax error, unexpected 'public' (T_PUBLIC), expecting variable (T_VARIABLE) in ... on line ...

Derick Rethans interviewed Nikita Popov (author of the RFC) in PHP Internals News podcast, which you can list here.

RFC Discussion Implementation

Match Expressions

TypeNew Feature

Match expression syntax is one of the nicest features in PHP 8 that improves the switch syntax in multiple ways.

$status = match($request_method) {
    'post' => $this->handlePost(),
    'get', 'head' =>  $this->handleGet(),
    default => throw new \Exception('Unsupported'),

Functionality from the match expression above, compared to a switch block:

- switch ($request_method) {
+ $status = match($request_method) {
-   case 'post':
-       $status = $this->handlePost();
-       break;
+   'post' => $this->handlePost(),
-   case 'get':
-   case 'head':
-       $status = $this->handleGet();
-       break;
+   'get', 'head' =>  $this->handleGet(),
-   default:
-       throw new \Exception('Unsupported'); 
+   default => throw new \Exception('Unsupported'),
- }
+ };

match expressions can return a value

The return value of the expression used in each "arm" (similar to each case in switch blocks) is can be assigned to a variable.

$name = match(2) {
    1 => 'One',
    2 => 'Two',

echo $name; // "Two"

It is not necessary to assign the return value to anything, the return value of the matched arm will be returned from the match expression.

Multiple matching conditions allowed

It is possible for a match expression to contain one or more matching conditions, and they will behave similar to multiple cascading case keys in a switch block.

match($request_method) {
    'post' => $this->handlePost(),
    'get', 'head' =>  $this->handleGet(),

$request_method === 'get' and $request_method === 'head' both conditions will be handled with $this->handleGet().

Each matching case must only contain one expression

Unlike switch blocks that can contain any number of expressions, a match arm can only contain only one expression.

match($name) {
    'foo' => 

Syntax above is not allowed. Each arm must contain only a single expression.

Implicit break

Each matched "arm" of a match expression only allows a single expression, and it will not fall-through, as it does in a switch block.

switch ('test') {
    case 'test':
    case 'send':

It is easy to overlook the missing break call in each of the switch case, which allows the code to fall-through to the next case. In the switch block above, missing break; statement makes the code fall-through and execute $this->sendNuclearAlert() as well, although it is unlikely the outcome you expect.

match ('test') {
    'test' => $this->sendTestAlert(),
    'send' => $this->sendNuclearAlert(),

match expressions work without explicit break statements. It only executes one matching arm, and immediately returns the value, making it imply a break call right after the expression the matched arm executes.

default case

match statement supports a default arm that will work similar to the default case in switch blocks.

A default arm will catch all expressions if none of the other conditions matched.

match ('Qux') {
    'foo' => ...,
    'bar' => ...,
    default => echo 'Unknown: ' . $name,

// "Unknown: Qux"

match expression MUST match a condition

switch blocks silently proceeds the code flow if there are no matching case keys. match expressions do not.

In a match expression, there must be condition that matches the expression, or a default case to handle it. If there are no matches, match expression throws an \UnhandledMatchError exception.

$value = 3;
match($value) {
    1 => 'One',
    2 => 'Two',

match expression above throws error:

Fatal error: Uncaught UnhandledMatchError in ...

UnhandledMatchError exception

match expressions throw an \UnhandledMatchError exception if there are no matches in within the expression.

\UnhandledMatchError is a new exception class in PHP 8, and it extends \Error. For a full hierarchy of all PHP core exception classes, including the ones added in PHP 8, see Hierarchy of PHP exceptions

This class can be easily poly-filled:

class UnhandledMatchError extends \Error {}

Strict matches without type coercion

One of the most important design choices in match expression is that it matches without type coercion.

function read(mixed $key): string {
    return match ($key) {
        1 => 'Integer 1',
        '1' => 'String 1',
        true => 'Bool true',
        [] => 'Empty array',
        [1] => 'Array [1]',

read(1); // "Integer 1"
read('1'); // "String 1"
read(true); // "Bool true"

In a typical switch block, its cases are matched loosely, i.e with ==. In match expressions, all matching arms are matched with strict comparison (===), leaving possible bugs in switch blocks out.

In the snippet above, each individual arm will be matched for the value and type.

Match against arbitrary expressions

match expression allows a given value to be matched against an expression.

    404 => 'Page not found',
    Response::REDIRECT => 'Redirect',
    $client->getCode() => 'Client Error',
    $response->getCode() => 'Response Error',
    default => 'Unknown error'

The expressions will be evaluated in the order they are laid out.

match expression will try to match $foo against in this order:

  1. $foo === 404
  2. $foo === Response::REDIRECT
  3. $foo === $client->getCode()
  4. $foo === $response->getCode()
  5. default

If it finds a positive match, the rest of the code will not be evaluated.

match vs switch

switch match
Requires PHP ^8.0 No Yes
Returns value No Yes
default condition support Yes Yes
Multiple conditions in single arm Yes Yes
Multiple expressions in code block Yes No
Implicit break No Yes
Falls-through without break Yes No
Throws an exception on no matches No Yes
Match against arbitrary expressions Yes Yes
Strict type-safe comparison No Yes

Backwards compatibility impact

match expressions are a new syntax in PHP 8. Code that uses match expressions will not work in older PHP versions.

The \UnhandledMatchError exception class can be backported.

Trying to run code that uses this expression will fail with a parse error in older PHP versions:

Parse error: syntax error, unexpected '=>' (T_DOUBLE_ARROW) in ... on line ...

RFC RFC (v1) Discussion Implementation

Internal function warnings now throw `TypeError` and `ValueError` exceptions


In PHP 8, internal function parameters have types and value validations enforced, and will throw \TypeError or \ValueError exceptions if the expected type or value is not allowed.

Prior to PHP 8, this resulted in a PHP warning.

Not all PHP warnings emitted by internal functions are transformed to exceptions, but majority of the functions will throw \TypeError or \ValueError exceptions if the provided type is not allowed, or the provided value is invalid. This includes functions that accept multiple types (such as a string or an array) because PHP 8 comes with Union Types.

Out of all PHP 8 changes, this will likely be the biggest pain-point when you upgrade existing code.

Reasons behind this decision

Many of the PHP internal functions gracefully handle unexpected values by raising a PHP warning, but still returning a "false-ish" value such as null, false, or 0. This can lead to subtle bugs that are later discovered, if discovered at all, in different parts of the program.

For example, json_decode() function accepts a $depth parameter that must be a positive integer. This was not enforced with an exception prior to PHP 8. If you call json_decode() with an invalid $depth, json_decode() function will raise a warning, but still return null, which is an acceptable return type of the original json-encoded value is also null.

With types and values enforced, json_decode() function throws an exception when it encounters an unexpected type or a value.

This can result in applications that dismissed the warning prior to PHP 8 to fail due to the unexpected exception. However, this results in fewer bugs once fixed because PHP makes sure to defend aggressively against invalid values.

\TypeError Examples

Warning to Exception

Most of the PHP internal functions that accept a typed parameter now throw \TypeError exceptions instead of warnings. This can eliminate a lot of subtle bugs because most of these string functions return either false or null on such unexpected types, which can result in a bug somewhere else.

substr('foo', []);

PHP versions prior to 8 will raise a warning and return null instead of throwing a \TypeError and refusing to go forward. The type is now enforced and throws exceptions in PHP 8.

- Warning: substr() expects parameter 2 to be int, array given in ... on line ...
+ Fatal error: Uncaught TypeError: substr(): Argument #2 ($start) must be of type int, array given in ...:...

New \TypeError without prior warnings

Some functions, such as method_exists() did not throw exceptions on unexpected values, but returned a value that fulfills the semantic return values of the function.

Union types are used when they are deemed necessary. For example, method_exists() function accepts either a class name (string) or an object (object). This is enforced as a Union Type of string|object.

method_exists([], 'getName');

This will now throw a \TypeError. Prior to PHP 8, it returned false if the provided parameter is not a string or an object, but did not raise any warnings.

+ Fatal error: Uncaught TypeError: method_exists(): Argument #1 ($object_or_class) must be of type object|string, array given in ...:...

\ValueError examples

PHP throws \ValueError exceptions if the provided value is of correct type, but not acceptable in the context.

json_decode('"foo"', true, -1);

Prior to PHP 8, setting a depth less than 0 resulted in a warning, which is now promoted to a \ValueError exception:

- Warning: json_decode(): Depth must be greater than zero in ... on line ...
+ Fatal error: Uncaught ValueError: json_decode(): Argument #3 ($depth) must be greater than 0 in ...:...

Many SPL functions, mbstring_ functions, password_* functions, etc now throw \ValueError exceptions when the provided values cannot be used to go further.

Backwards compatibility impact

Except for certain cases such as method_exists, the \TypeError and \ValueError exceptions PHP 8 and later throws resulted in a warning prior to PHP 8.

Unless you went out of your way to silent (e.g @strlen([])) the warning, or dismissed in from the error log, you should not encounter any major problems.

Except for the few exceptional cases, all the changes you make because of the new \TypeError and \ValueError exceptions will work all the same in prior PHP versions as well.

The new \ValueError exception class can be polyfilled, but that does not mean internal functions will throw exceptions instead of raising warnings.

class ValueError extends Error {}

It is possible to have a try/catch block that accepts \ValueError exceptions without having the \ValueError exception class declared. It will not be thrown in prior versions, which makes the try/catch block a harmless no-op in older PHP versions.

try {}
catch (\ValueError $exception) {}

Implementations (multiple pull-requests)

Expressions can now `throw` Exceptions


Prior to PHP 8.0, it was not allowed to throw an exceptions in when a single expression is expected. It is now possible to throw an exception in arrow functions, ternary expressions, or anywhere else the PHP parser expects a single expression.

Arrow Functions:

$fn = fn() => throw new \Exception('oops');

Ternary expressions:

$value = isset($_GET['value'])
    ? $_GET['value']
    : throw new \InvalidArgumentException('value not set');

$value ??= throw new \InvalidArgumentException('value not set');

$foo = $bar ?: throw new \InvalidArgumentException('$bar is falsy');

$foo = $bar ?? throw new \InvalidArgumentException('$bar is not set');

All of the snippets above are allowed since PHP 8.0.

Versions prior to 8.0 trigger a parse error:

Parse error: syntax error, unexpected 'throw' (T_THROW) in ... on line ...

Backwards Compatibility Impact

Using throw in an expression previously triggered a parse error, so there should be no practical breaking changes when you upgrade to PHP 8.

However, note that there is no way to make any code that throw in an expression work seamless in PHP versions prior to PHP 8. If compatibility with older versions is a concerns hold off using throw in expressions.

RFC GitHub PR Discussion Implementation

JSON extension is always available


A subtle yet healthy change in PHP 8.0 is that the JSON extension is always included in PHP.

Prior to PHP 8.0, a compilation flag ./configure --disable-json allowed disabling the JSON extension.

Debian, Ubuntu, CentOS/RHEL distributions, and Ondrej's builds included JSON extension by default. In these distributions, it was possible to disable JSON extension (although not a sensible decision due to how widely JSON is used out there). Windows builds on are statically bundled with JSON, which means it is not even possible to disable JSON extension.

For PHP builds that had the unlikely flag --disable-json in the configuration step, you will need update the build scripts to not use this flag for PHP 8.0 and forwards.

Because the JSON extension is already included in PHP core, you will no longer need to check the extension availability with extension_loaded('json'), or function_exists('json_decode') functions because they will be always available. These function calls are not broken; they will always return true in PHP 8.

For composer.json file that had ext-json in the require section, this requirement is no longer necessary if already requires php ^8.0.

    "require": {  
        "php": "^8.0",  
-       "ext-json": "*",  

RFC Discussion Implementation

`catch` exceptions only by type


PHP 8.0 and later allows to use try/catch blocks where the catch() statement does not catch the exception itself to a variable.

Prior to PHP 8.0, a typical PHP try/catch block must capture the exception in the catch statement:

try {
  // try something
catch (\InvalidArgumentException $ex) { // "$ex" is required
  // handle the exception

Sometimes, the exception type (such as \InvalidArgumentException) is enough to determine the way the exception is handled, and capturing the exception to a variable (such as $ex in the example above), PHP 8.0 allows to drop the exception capturing.

  try {
- catch (NotFoundExceptionInterface $exception) {
+ catch (NotFoundExceptionInterface) {
    $logger->log('API key not set');

A word of caution

Make sure that the exception type you catch is granular enough to convey the meaning of the exception. For example, catching a wildcard \Exception or \Throwable without capturing the exception might be a bad idea if you intend to log the event.

Backwards Compatibility Impact

PHP versions prior to 8.0 will raise a fatal error with the new syntax:

Parse error: syntax error, unexpected ')', expecting '|' or variable (T_VARIABLE) in ... on line ...

It is not possible back-port this functionality to earlier PHP versions.

RFC Discussion Implementation

`+`/`-` operators take higher precedence when used with concat (`.`) operator


PHP 7.3 deprecated unparenthesized expressions containing '.' and '+'/'-'. From PHP 8.0, operator precedence is enforced, and this deprecation notice is no longer raised.

When an expression contains the contact operator (.) and +/- operators, +/- operators take precedence.

For example, consider the following snippet:

echo 35 + 7 . '.' . 0 + 5;

Prior to PHP 8, this snippet will be evaluated in the order of the operators.
From PHP 8.0 and later, the + and - operators take a high a precedence. If you have been ignoring the deprecation notices since PHP 7.3, you can have different results:

Pre-PHP 8

echo 35 + 7 . '.' . 0 + 5;
// Deprecated: The behavior of unparenthesized expressions containing both '.' and '+'/'-' will change in PHP 8: '+'/'-' will take a higher precedence in /in/n3lVl on line 3 
// 47

PHP 8 and later

echo 35 + 7 . '.' . 0 + 5;
// 42.5

This is equivalent to (35 + 7) . '.' . (0 + 5).

Backwards compatibility impact

If you have any existing code that raised a deprecation notice due to expressions that contained . and +/- operators in same expression without parenthesis, note that the output of in PHP 8 can be different.

Make sure to properly add paranthesis to your existing expressions to express their meaning.

- echo 35 + 7 . '.' . 0 + 5;
+ echo (35 + 7) . '.' . (0 + 5); 

`CurlHandle` class objects replace curl handlers


One of the long-term goals in PHP is to convert resource type to appropriate class objects. In PHP 8, the Curl functionality is transformed into class object based resources.

Init methods return objects

Curl init functions curl_init, curl_multi_init, and curl_share_init functions returned PHP resources prior to PHP 8. It was not possible enforce typing in those functions, or any other user-land functionality that used them.

From PHP 8, these functions return objects. You still have to use init functions to create these objects; it is not allowed to new CurlInit() (or any other objects for that matter).

curl_init() now returns a \CurlHandle object

Prior to PHP 8, when you call curl_init(), it returned a PHP resource of type Curl.

From PHP 8 and forward, curl_init function returns an instance of \CurlHandle class.

- /**
-  * @return resource|false 
-  */
- function curl_init() {}
+ function curl_init(): \CurlHandle|false {}
final class CurlHandle {}

curl_multi_init() now returns a CurlMultiHandle object

Similar to curl_init() in PHP 8, curl_multi_init() now returns a \CurlMultiHandle object instead of a resource with type curl_multi.

- /**
-  * @return resource 
-  */
- function curl_multi_init() {}
+ function curl_multi_init(): \CurlMultiHandle {}
final class CurlMultiHandle {}

curl_share_init() now returns a \CurlShareHandle object

curl_share_init() now returns a \CurlShareHandle object instead of a resource with type curl_share.

- /**
-  * @return resource
-  */
- function curl_share_init() {}
+ function curl_share_init(): \CurlShareHandle {}
final class CurlShareHandle {}

All curl_* functions accept Curl* objects instead of resources

PHP does not have a resourcetype, so it was not possible to enforce a parameter or return type for any of the curl_ functions. This conveniently makes this resource to Curl* object transform not introduce backwards-incompatibilities.

is_resource() returns false on Curl* objects

Because the Curl init function return values are standard PHP objects, is_resource function returns false which is the correct bahavior for that function.

This might break your existing code because it is common to check if the curl_init() call was successful with a is_resource($handle) call.

Note that in PHP 8 and in old versions, curl_(multi_|share_)init() functions return false on failed resource creation.

To make sure the code is compatible with both PHP 8 and older versions, you can change is_resource($handle) calls with $handle !== false calls.

  $handle = \curl_init();
- if (!\is_resource($handle)) {
+ if ($handle === false) {
    throw new \Exception();

curl_close no longer closes the resource

Because Curl resources are now objects, Curl handles are closed when the object is no longer referenced, or explicitly destroyed.

You can still call curl_close($handle), but this function no longer has any effect.

In your existing code, you can explicitly unset() the CurlHandle object. PHP will automatically do it following standard object garbage collection mechanisms.

  $handle = curl_init();
  // ... use it ... 
+ unset($handle);

Backwards compatibility impact

Calling is_resource() on Curl init function return values will now return false, which breaks existing code. For cross-version compatibility, always use $handle === false which will correctly evaluate in all PHP versions.

PHP has no resource type declared in a way that you can enforce it in user-land code. This mitigates any possible typed functions.

Furthermore, curl_close no longer effectively closes the Curl handler. Because the handlers are now objects, the resources will be destroyed during garbage collection, unless the handler is explicitly closed with an unset() call. This unset() call can be used in older PHP versions without breaking the functionality. See the example in curl_close section above

It is not possible to backport this functionality to older PHP versions. This means that if you start to enforce CurlHandle, CurlShareHandle, and CurlShareHandle types, that code will not work in older PHP versions.


Fatal errors on incompatible method signatures


Prior to PHP 8, PHP was inconsistent the way it handled method signature checks when a parent class was extended.

Extending class methods

class Foo {
    public function process(stdClass $item): array{}

class SuperFoo extends Foo{
    public function process(array $items): array{}
    //                      ^^^^^ mismatch

In the snippet above, SuperFoo::process method has a mismatching signature to its parent Foo. This is a clear violation of Liskov Substitution Principle (LSP), but PHP only raised warning:

Warning: Declaration of SuperFoo::process(array $items): array should be compatible with Foo::process(stdClass $item): array in ... on line ...

In PHP 8, such signature mismatches result in fatal error.

Fatal error: Declaration of SuperFoo::process(array $items): array must be compatible with Foo::process(stdClass $item): array in ... on line ...

Note that this fatal error will only be triggered if it violates LSP. Changing the type of method signatures is allowed as long it follows LSP.

Implementing abstract trait methods

Prior to PHP 8, PHP did not enforce any signature checking when a trait method is extended. In PHP 8.0 and later, abtract methods with mismatching signatures will fail with a fatal error.

trait Foo {
    abstract public function inParams(stdClass $item): array;
    abstract public function inReturn(stdClass $item): int;

class SuperFoo{
    use Foo;
    public function inParams(array $items): array{}
    //                       ^^^^^ Mismatch
    public function inReturn(stdClass $item): int{}
    //                                        ^^^ Mismatch

In PHP 8, the snippet above will result in a fatal error:

Fatal error: Declaration of SuperFoo::inParams(array $items): array must be compatible with Foo::inParams(stdClass $item): array in ... on line ...

Note that this fatal error will only be triggered if it violates LSP. Changing the type of method signatures is allowed as long it follows LSP.

PHP LSP enforcement

This change will finally bring PHP 8 to actively enforce signature checks on all extent/implement patterns:

The following chart describes how PHP versions prior to 8.0 enforced signature mismatches, and how it changes in PHP 8

PHP <8 PHP >=8
class implements interface: method parameters Fatal Error Fatal Error
class implements interface: return type Fatal Error Fatal Error
class extends abstract method: method parameters Fatal Error Fatal Error
class extends abstract method: return type Fatal Error Fatal Error
class extends class: Method parameters Warning Fatal Error
class extends class: Method return type Fatal Error Fatal Error
trait use and extend: Method parameters none none
trait use and extend: Method return type none none
trait use and implement: abstract Method parameters none Fatal Error
trait use and implement: abstract Method return type none Fatal Error

Backwards compatibility impact

If you had classes that extended a parent class with mismatching signatures, they will now fail with a fatal error. Prior PHP 8, those errors raised a warning.

If your PHP 7 code base does not raise such warnings, you will be able to upgrade to PHP 8 without problems. Fixing the code for PHP 8 will fix the warning in PHP 7 as well.

abtract trait method mismatches did not emit any warnings whatsoever in any PHP versions prior to PHP 8, and they will fail with a fatal error in PHP. As pointed in a Reddit discussion, this change can introduce unexpected errors that are not currently detected by any code analysis tools because that was technically allowed.

Symfony, for example, used this pattern that needed to be fixed.

Class method fatal errors

RFC Discussion Implementation

abstract trait method fatal errors

RFC Discussion Implementation

Disabled functions behave as if they do not exist


PHP has a feature to disable certain functions and classes with disable_functions and disable_classes INI directives. This is often used as a security measure to disable potentially unsafe functions.

This functionality still exists in PHP 8, but the disabled functions behave as if they are not declared at all.

Prior to PHP 8, attempting to use a disabled functions resulted in a warning:

Warning: substr() has been disabled for security reasons in ... on line ...

In PHP 8 and later, attempting to use a disabled function will throw a standard error for an undeclared function:

Fatal error: Uncaught Error: Call to undefined function substr() in ...:...

This change makes the disable feature transparent to consumers, and it even allows the function to be redefined. Prior to PHP 8, attempting to redefine a disabled function resulted in a fatal error.

if (!function_exists('substr')) {
    function substr() {}
Fatal error: Cannot redeclare substr() in ... on line ...

function_exists() returns false for disabled functions

function_exists() function now returns false for functions disabled with disable_functions INI directive. This is because internally, disabled functions do not even make to the internal functions table.

Prior to PHP 8, to check if a function is disabled, one would need to query the INI setting, or use the return value of get_defined_functions() function.

Disabled functions can be redefined

In PHP 8, it is possible to redefine a disabled function.

This enables a whole new set of ways to polyfill and mock disabled function. Test frameworks and mocking frameworks will be able to dynamically disable a function, and redefine it user-land as a mock, and polyfill libraries will be able to provide a user-land implementation for functions that are disabled.

// test.php
if (function_exists('random_int')) {
    return 4;

If you want to mock the random_int function, now you can execute the test script with:

php -d disable_functions=random_int test.php

random_int will now always return 4 as if it was chosen by fair dice roll.

Function re-declaration and mock/polyfill exampls are covered in PHP 8: Override internal functions with disable_functions.

Deprecation notices

Related to this change, there are two new deprecation notices:

  1. ReflectionFunction::isDisabled()
  2. get_defined_functions() with $exclude_disabled has no effect

These changes are detailed in Disabled functions: Reflection and get_defined_functions() deprecations

Backwards compatibility impact

Redefining a disabled function in user-land code was not allowed in PHP versions prior to 8.0. This means you will be able to upgrade existing code, but any code that intentionally redefined a function to work-around disabled_functions feature will not work in older PHP versions.


Deprecate required parameters after optional parameters in function/method signatures


When declaring a function or a method, adding a required parameter after optional parameters is deprecated since PHP 8.0.

This means the following function signature triggers a deprecation notice:

function foo($param_optional = null, $param_required) {
//           ^^ optional parameter , ^^ required parameter

From PHP 8.0 onwards:

Deprecated: Required parameter $param_required follows optional parameter $param_optional in ... on line ...

If you have a require parameter (i.e. a parameter without a default value set in its signature) after an optional one (i.e. a parameter with a default value), it makes all parameters before it essentially required because the caller has to explicitly pass a value for optional parameters as well.

To quote a bug submitted to back in 2010:

PHP does not emit a notice of any kind when defining a function with a required
parameter after an optional parameter. For example:
function foo($optional = 1, $required) {}

It doesn't make sense to define a required parameter after an optional one,
since that effectively makes all preceding optional parameters required. Since this is
an error that can produce bugs and other warnings if one is not careful (calling
the above function with less than two parameters will cause warnings to be
emitted for the missing $required parameter), PHP should emit a warning of some kind
when functions like this are defined.

PHP documentation already explains that having required parameters after optional parameters is incorrect. There was no deprecation notice until PHP 8.0, even though it hints a likely issue in the code architecture.

Nullable parameters

If you have used typed parameters with a default value set to null, you can use nullable types instead. To provide an example:

function foo(string $param_optional = null, $param_required) {
//           ^^ poormans nullable param   , ^^ required parameter

This "trick" was used with PHP 7.0, there was no nullable type support until PHP 7.1. The snippet above can be replaced with the following:

function foo(?string $param_optional, $param_required) {
//           ^^ optional parameter  , ^^ required parameter

The deprecation does not apply if there is a type declared on the optional parameter, and the default value is null.

The following will not trigger a deprecation notice:

function foo(string $param_optional = null, $param_required) {}

This is because there is a type (string) declared for the optional $param_optional parameter, and its default value is null. Setting any other default value (such as $param_optional = 'bar'), or not declaring the type will trigger the deprecation notice.

Thanks to M1keSkydive and the_alias_of_andrea for the insight to extend this part.

Backwards compatibility impact

If you have any functions/methods that have required parameters after optional parameters, you will get a deprecation notice similar to the one in the example. It is most likely an indication of poorly defined architecture. The deprecation notice is triggered at the compile time, and it will be raised even if it is not called.

You can work around it by removing the default value in all optional parameters before the last required parameter. This should not break your existing code because all callers must pass a value to the optional parameters before proving values for the required parameters that appear later.

RFC discussion Implementation

`ReflectionParameter::getClass())`, `::isArray()`, and `::isCallable()` methods deprecated


PHP 8 introduces several improvements in PHP type systems such as the introduction of Union Types, mixed type, and a few more.

With these changes, certain methods in Reflection API's ReflectionParameter yield incorrect results.

In PHP 8, the following methods from ReflectionParameter class is deprecated:

ReflectionParamter::getType() is the recommended way to replace the deprecated methods. This method is available in PHP 7.0 and later.

ReflectionParameter::getClass() deprecation

Trying to use ReflectionParameter::getClass() will emit a deprecation notice in PHP 8:

Deprecated: Function ReflectionParameter::getClass() is deprecated in ... on line ...

ReflectionParameter::getClass() returns the name of the class of a parameter. However, when a Union Type is used, it returns null if difference class types are in a single Union.

ReflectionParameter::getType() supersedes getClass() method. Note that getClass() method returns a ReflectionClass() object, which implements __toString(). If you are interested in just the name, you can use $param->getType()->getName() to return the name.

A full replacement with same functionality:

- $name = $reflectionParam->getClass();
+ $name = $param->getType() && !$param->getType()->isBuiltin() 
+   ? new ReflectionClass($param->getType()->getName())
+   : null;

ReflectionParameter::isArray() deprecation

ReflectionParameter::isArray() is deprecated in PHP 8 because it only works with array and ?array types, but not with a Union type.

ReflectionParameter::getType() result can be used to mimic the result of this method:

- $isArray = $param->isArray();
+ $isArray = $param->getType() && $param->getType()->getName() === 'array'; 

ReflectionParameter::isCallable() deprecation

ReflectionParameter::isCallable() is deprecated in PHP 8 too, and similar to isArray(), it only works for callable and ?callable parameters, but not when it's used in a Union Type.

ReflectionParameter::getType() result can be used to mimic the result of this method for isCallable as well:

- $isCallable = $param->isCallable();
+ $isCallable = $param->getType() && $param->getType()->getName() === 'callable'; 

Backwards compatibility impact

All 3 deprecated methods can be easily replaced with the result of getType() method, which is more robust and accurate in PHP 8. getType() method is available in PHP 7.0 and later.

If you need your code to work in PHP 5, through 8, conditional code branches depending on the PHP version can be used. For all other cases, replacing the deprecated method with getType() approaches will be backwards compatible to all PHP 7 versions as well.


Disabled functions: Reflection and `get_defined_functions()` deprecations


In PHP 8, disabled functions are not added to PHP's internal functions table. This makes PHP behave as if disabled functions are not defined at all.

Related to this change, there are two new deprecations in PHP 8.

ReflectionFunction::isDisabled() is deprecated

ReflectionFunction::isDisabled() method is deprecated because it no longer provides any useful information to the caller.

Deprecated: Function ReflectionFunction::isDisabled() is deprecated in %s on line %d

This method will always return false even if the function is disabled with the INI directive.

There is no need to check if a function is disabled because function_exists() will return true only if the function is declared and not disabled.

get_defined_functions($exclude_disabled = false) is deprecated

For the same reasons that disabled functions are not included in functions list, get_defined_functions() function with the first parameter $exclude_disabled set to false is disabled.

get_defined_functions() will always exclude disabled functions in PHP 8, and even if you try to get all functions including the excluded ones, it will not return disabled functions.

If you redefine a disabled function, that function will be in the user key of the get_defined_functions() return array, even if it is defined internally and disabled via the disable_functions INI directive.

Backwards compatibility impact

The main use-case for disable_functions INI directive is to disable potentially unsafe functions such as system(). The deprecation notices are unlikely to occur, but it does not introduce any loss of functionality.

A fuzzy GitHub code search does not yield any uses this pattern.


XMLRPC extension is moved to PECL


XMLRPC is an extension that was bundled in PHP that brought XML RPC server and client features to PHP.

This extension was relatively unused, and was marked "experimental" all along. This extension relied on some of the libraries that were not maintained for several years.

xmlrpc extension is unbundled in PHP 8.0. You can still install the extension from PECL if your code or any of the dependencies require.

Backwards Compatibility

Because xmlrpc is no longer bundled, you will need to make sure the extension is available.

For composer projects, make sure to add ext-xmlrpc as a dependency to ensure Composer checks the availability of the extension in its platform checks.

Alternative Libraries

Note that the xmlrpc library and its dependencies were not updated in several years, and a more apt approach would be to replace it with more modern libraries that rely in xml extension or work with its own XML implementation:

RFC Discussion



filter_var() function used to accept two flags that were deprecated in PHP 7.3:


filter_var() function, when used with FILTER_VALIDATE_URL already assumed these two flags, and it was not necessary to use these flags explicitly.

Deprecated in PHP 7.3, these two flags are now removed in PHP 8.0.

  $var = '';
+ filter_var($var, FILTER_VALIDATE_URL);

If attempted to use these two flags, a fatal error will be thrown:

Fatal error: Uncaught Error: Undefined constant 'FILTER_FLAG_SCHEME_REQUIRED' in ...:...
Fatal error: Uncaught Error: Undefined constant 'FILTER_FLAG_HOST_REQUIRED' in ...:...

Backwards Compatibility Impact

Because the FILTER_FLAG_HOST_REQUIRED and FILTER_FLAG_SCHEME_REQUIRED constants are removed in PHP 8, attempting to use them will now throw a fatal error.

The easiest fix is to simply remove these two flags. filter_var() function when used with FILTER_VALIDATE_URL will always enforce the host-required and scheme-required condions regardless of the presence of these two flags.