PHP 8.0: What's New and Changed

Version StatusSupported (Latest)
Release Date2020-11-26

PHP 8.0 is a major version update and a remarkable milestone in PHP, as it brings several new features to type system, syntax, error handling, strings, object-oriented programming, and more.

It is the efforts of hundreds of people coming together to shape the future of a programming language that powers a significant portion of the Internet web sites and applications.

PHP tries to be conservative with changes that can break a majority of the applications, and yet, it brings several new major features to PHP 8.0.

Features such as Named Parameters, JIT, Attributes, and Constructor Properties bring major improvements and syntax changes, while several minor improvements such as resource to object migrations, improved error handling, and changes and improvements in operators and engine comparisons help seamlessly reduce the chances of overlooked bugs.

Thank you for all the contributors, of whom there are hundreds, for all your efforts, in PHP core, documentation, libraries, testing tools, extensions, authors, package maintainers, PHP developers, and everyone else including you ❤.

Major New Features

Named Parameters

PHP 8.0 allows named parameters in function/method calls in addition to traditional positional parameters.

str_contains(needle: 'Bar', haystack: 'Foobar');

This makes the function/method parameter names part of the public API. The non-standardized DocBlock @no-named-arguments expresses that the library does not provide backwards-compatibility for named parameters.


Attributes allows declaring meta-data for functions, classes, properties, and parameters. Attributes map to PHP class names (declared with an Attribute itself), and they can be fetched programmatically with PHP Reflection API.

class Foo {
    public function bar(): void {}

Attributes makes it easy and performant to declare Attributes/annotations that previously required storing them in DocBlock comments, and parsing the string to infer them.

Constructor Properties

A new syntax to declare class properties right from the class constructor (__construct magic method).

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

In the constructor, PHP 8.0 supports declaring the visibility (public, private, or protected) and type. Those properties will be registered as class properties with same visibility and type they are declared in the constructor.

This backwards-incompatible feature can help reduce boilerplate code when declaring value-object classes.

Just-In-Time Compilation

PHP Opcache supports JIT. It's disabled by default, and if enabled, JIT compiles and caches native instructions. It does not make a noticeable difference in IO-bound web applications, but provides a performance boost for CPU-heavy applications.

# Enabling JIT in php.ini

Note that JIT is still new, and had bug fixes as late as a day before PHP 8.0.0 release. It makes debugging and profiling more difficult with the added extra layer.

Union Types

Union Types extend type declarations (return types, parameters, and class properties) to declare more than one type.

function parse_value(string|int|float): string|null {}

It also supports false as a special type (for Boolean false), a trait that's prevalent in legacy code that did not use Exceptions.

Null-safe Operator

Null-safe operator provides safety in method/property chaining when the return value or property can be null.

return $user->getAddress()?->getCountry()?->isoCode;

The ?-> null-safe operator short-circuits the rest of the expression if it encounters a null value, and immediately returns null without causing any errors.

match expressions

Match expressions are similar to switch blocks, but match blocks provide type-safe comparisons, supports a return value, does not require break statements to break-out, and supports multiple matching values. it also guarantees that at least one branch is matched, ensuring all cases are accounted for.

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

Not all switch blocks might convert well to match blocks. Code that requires backwards-compatibility, switch blocks with multiple statements (as opposed to single-line expressions), or expects fall-through functionality still fits the switch statements.


A WeakMap allows to store and associate arbitrary values for object keys, but without preventing the garbage collector from clearing it the object falls out of scope in everywhere else.

A WeakMap is similar to SplObjectStorage, as in both WeakMap and splObjectStorage use objectss as the key, and allows storage of arbitrary values. However, a WeakMap does not prevent the object from being garbage collected.

New Functions and Classes

PHP 8.0 introduces a few new functions to ease string inspections (contains, starts with substring, or ends with substring) to replace the meticulous strpos() !== false calls that are less readable, and error-prone due to weak type comparisons.

Function Description Example
str_contains Haystack contains the needle somewhere str_contains('Foobar', 'Foo')
str_starts_with Needle starts with haystack string str_starts_with('PHP 8.0', 'PHP)
str_ends_with Needle ends with haystack string str_ends_with('PHP 8.0', '8.0)

PHP 8.0 also brings functions such as fdiv, get_resource_id, get_debug_type, and preg_last_error_msg

Function Description
fdiv Float division supporting IEEE-754 standard on Floating-Point Arithmetic.
get_resource_id Returns the internal ID for a given PHP resource
get_debug_type Returns the internal type of a passed variable
preg_last_error_msg Returns a human-friendly error message for the last preg operation.

New Stringable interface

The new Stringable interface is automatically added to all classes that implement __toString method, and those explicitly declare that they implements Stringable.

With the Stringable interface, it is now easy to declare types as string|Stringable for on functions that can accept/return strings or objects with a __toString() method.

New PhpToken Tokenizer class

The new PhpToken class provides a more fluent Object-Oriented interface as an alternative to the legacy array-based to token_get_all function.

Type System Improvements

PHP 8.0 improves typing system with the addition of Union Types and the mixed type.

Union Types

Union Types extend type declarations (return types, parameters, and class properties) to declare more than one type.

function parse_value(string|int|float): string|null {}

It also supports false as a special type (for Boolean false), a trait that's prevalent in legacy code that did not use Exceptions.

New mixed pseudo type

PHP 8.0 brings mixed type, that was already being widely used in DocBlock comments.

function dd(mixed $var): void {

mixed type can be used to indicate that it accepts any type, or can return any type. In a class/interface context, mixed type plays by the same rules of Liskov Substitution Principle.

static return type for class methods

static return type, already support as a DocBlock return type, is now supported in PHP 8.0. The static return type declares an object of the called class will be returned.

class Foo {
    public static function getInstance(): static {
        return new static();

Error Handling Improvements

A major and backwards-incompatible change in PHP is that internal functions now throw exceptions on type errors or value errors.

This corrects PHP's historical behavior of emitting a warning, and returning null when it encounters a value that it cannot use. This behavior is often undesired because PHP warnings does not halt the execution of the remaining block.

Internal function warnings now throw TypeError and ValueError exceptions

Almost entirety of PHP internal functions now enforce type checking, and instead of warnings, PHP now throws TypeError or ValueError exceptions. On legacy code bases, this change can cause issues, now that the errors are more boldly and unforgivably handled.

throw in expressions

Prior to PHP 8.0, it was not possible to throw exceptions from an expression (e.g a ternary statement). This is now allowed in PHP 8.0.

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

catch exceptions only by type

It is possible to catch exceptions by their type, without capturing the exception object.

try {}
catch(TypeError) {
  // Did not catch the $exception object

Default error reporting is set to E_ALL

PHP 8.0 default configuration is to show all error messages by default. It was configured to hide deprecation and strict warnings in older versions.

PHP Startup Errors are displayed by default

PHP now displays startup-errors (failure to load dynamic extensions, invalid INI configurations, etc) by default.

Assertions throw exceptions by default

PHP assertions (assert()) now throw exceptions on assertion failures. Prior to PHP 8.0, it required an explicit configuration that was disabled by default.

@ Error Suppression operator does not silent fatal errors

PHP 8.0 corrects the behavior of @ error suppression operator, where it silenced fatal errors, which lead to a script failure, because the @ operator does not prevent fatal errors, but rather hide the error message display.

Resource to Object Migration

One of the long-term efforts in PHP development was to move away from the resource types. They were difficult to deal with, and even in PHP 8.0, does not offer typing support.

PHP resource objects do not play well with garbage-collector well either, which resulted in memory leaks in resource objects such as xml.

In PHP 8.0, some of the most used extensions changes moves away from the traditional resource objects to standard PHP classes.

In PHP 8.0, they are work as value-objects as opposed to fully-features classes with methods in them. Majority of these classes do not allow instantiating with the new Foo() construct either, and must be instantiated with the existing functions that returned resource objects in prior versions.

PHP 8.0's resource to object migration is quite seamless, as in all functions return and accept the new objects, and behave by the same semantics of the previous resource objects.

Extension resource (PHP < 8.0) object (PHP >= 8.0)
Curl Curl CurlHandle
Curl curl_multi CurlMultiHandle
Curl curl_share CurlShareHandle
GD gd GdImage
Sockets Socket Socket
Sockets AddressInfo AddressInfo
OpenSSL OpenSSL key OpenSSLAsymmetricKey
OpenSSL OpenSSL X.509 OpenSSLCertificate
OpenSSL OpenSSL X.509 CSR OpenSSLCertificateSigningRequest
XMLWriter xmlwriter XMLWriter
XML xml XMLParser

PHP Object Oriented Programming changes

PHP 8.0 is the first major version to be strict on Liskov Substitution Principle violations. Prior to PHP 8.0, PHP was not consistent on how it handled incompatible method signatures.

In PHP 8.0, all signature mismatches, including abstract traits, result in a fatal error. Further, it enforces signatures for PHP magic methods.

Fatal errors on incompatible method signatures

PHP 8.0 throws fatal errors when Liskov Substitution Principle is not followed when classes are extended, or interfaces are implemented.

Prior to PHP 8.0, incompatible signatures only emitted a warning.

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

class SuperFoo extends Foo{
    public function process(array $items): array{}
    //                      ^^^^^ mismatch
Fatal error: Declaration of SuperFoo::process(array $items): array must be compatible with Foo::process(stdClass $item): array in ... on line ...

Class magic method signatures are strictly enforced

From PHP 8.0 and later, magic methods (e.g __toString(), __get(), etc), if they declare types, must implement the signature PHP expects. This is to avoid the smallest chance of the user declaring a magic method that doesn't follow the semantic meaning.

class Foo {
    public function __toString(): object {

Declarations like Foo::__toString(): object was allowed in previous PHP versions, but PHP 8.0 and throws an exception if the signature does not meet the requirements.

Calling non-static class methods statically result in a fatal error

PHP 8.0 no longer allows calling class methods as a static method.

class Foo {
    public function bar() {}

Previous versions emitted a deprecation notice, but from PHP 8.0 and later, this results in a fatal error.

Inheritance rules are not applied to private class methods

PHP 8.0 relaxes the signature, abstract, and static flag enforcement for private class methods. This change comes from the rationale that private methods are just that: Private.

From PHP 8.0, it is now allowed for the child classes to declare abstract methods, and change static/flags for private methods.

::class magic constant is now supported on objects

The ::class magic constant returns the fully-qualified class name. This was only allowed on class names (such as Foo\Bar::class), but in PHP 8.0, the ::class magic constant works on instantiated objects too.

String-related changes

In PHP 8.0, there are several subtle changes that might be not obvious at first, but can result in quite unexpected results.

A major difference in PHP 8.0 is that, PHP now considers there is an empty string between every character in a given string.

Prior to PHP 8.0, checking for an empty string needle ("") was not allowed, but in PHP 8.0, PHP will happily accept it, and return that there is indeed an empty string between each character.

The multi-byte handling, or functions like strlen still returns same values as the older versions, but all functions that check for a substring in a given string are changed.

In addition, PHP 8.0 changes how the string concatenation operator priorities, and supports new modifiers in sprintf functions such as %h and %H modifiers and * width and precision modifier.

substr, iconv_substr, grapheme_substr return empty string on out-of-bound offsets

These functions clamp the needle and offset parameters to the string length, and returns an empty string instead of returning false.

substr('FooBar', 42, 3); // "" in PHP >=8.0, false in PHP < 8.0
mb_substr('FooBar', 42, 3); // "" in PHP >=8.0, false in PHP < 8.0
iconv_substr('FooBar', 42, 3); // "" in PHP >=8.0, false in PHP < 8.0
grapheme_substr('FooBar', 42, 3); // "" in PHP >=8.0, false in PHP < 8.0

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

When the mathematical + and - operators are used in the same expression with the concatenation operator (.), the + and - operators take higher precedence. This resulted in a deprecation notice in PHP versions prior to 8.0, but now it happens silently and as per the warning.

echo 35 + 7 . '.' . 0 + 5;
// 42.5 in PHP >= 8.0
// 47 in PHP <= 8.0

Locale-independent float to string casting

When a float value is coerced to string, PHP no longer uses the locale. Prior to PHP 8.0, PHP considered the current locale (which is not thread-safe) or system locale when doing so. This resulted in inconsistent string outputs because certain locales, specially European ones, swap the thousand separator and decimal sign from the US locale.
For locale-aware float to string cast, the %f modifier in printf class of functions can be used.

In addition, the new %h and %H modifiers for printf class of functions provide locale-indepdent variants of %g and %G modifiers.

$thanks ❤

PHP 8.0 is an amazing effort from hundreds of awesome people. It's a major milesone in PHP's history. Thank you to everyone who helped from code, to documentation, to conferences, to articles to all of the developers.

PHP 8 Thanks name cloud
Download posters

New Features

Syntax/Functionality Changes


Removed Features and Functionality

Named Parameters

TypeNew Feature

PHP 8.0 supports optionally calling functions and class methods by specifying the parameter name, instead of calling them on the order of parameters that they are declared.

PHP, and many other programming languages, support positional parameters: The caller passes the parameters in the same order the function/method declares its parameters.

In PHP 8.0, Named Parameters support is added, which means it's now possible to call a function/method by setting the parameters by their name.

Positional Parameters

function str_contains(string $haystack, string needle): bool {}
str_contains('FooBar', 'Foo');

Named Parameters

function str_contains(string $haystack, string $needle): bool {}
str_contains(haystack: 'FooBar', needle: 'Foo');

With Named Parameters, the call-site names the parameter value, and passes it to the called function; The order of the parameters are not important, as long as all the required parameters are passed.

For example, both of these statements are correct and functionally identical:

str_contains(haystack: 'FooBar', needle: 'Foo');
str_contains(needle: 'Foo', haystack: 'FooBar');

Named parameter feature offers more readability when calling a function with parameters that do not require to have a particular order, or cannot be made more intuitive.

    name: "Awesome",
    value: 'Yes',
    path: "/",
    domain: "",
    secure: true,
    httponly: true

The name of the parameter in declaration, without the $ sign will be the parameter name.

Optional Parameters and Default Values

When a function/method declares a parameter with a default value, and if the named parameter call-site does not set a value for that parameter, the default value will be used, just like a positional parameter call.

function greet(string $name, string $greeting = 'Hello') {
    echo "$greeting, $name";

greet('Simon');        //"Hello, Simon"
greet(name: 'Simon'); // "Hello, Simon"

Skipping Parameters

Named parameters inherit the default values if not explicitly set at the call-site. This makes it possible to skip optional parameters for a function/method call, which was not possible with positional parameters without assuming the default values.

function greet(string $name, string $greeting = 'Hello', bool $warm = false) {
    echo "$greeting, $name";
    if ($warm) {
        echo "You are awesome";

With positional parameters, it is not possible to set a value for the third $warm parameter without assuming the default value for the second $greeting parameter.

greet('Simon', 'Hello', true);

With named parameters, the default value is inherited if it is not set at the call-site:

greet(name: "Simon", warm: true);

Named parameters require all non-optional parameters to be passed. If there are not enough parameters, an `ArgumentCountError exception will be thrown, just like it does with positional parameters.

str_replace(search: 'foo');
Fatal error: Uncaught ArgumentCountError: str_replace() expects at least 3 arguments, 1 given in ...

Unknown Parameters

It is not allowed to pass additional parameters or unknown parameters in a named parameter call-site, and will result in an error:

str_replace(time_limit: "mi");
Fatal error: Uncaught Error: Unknown named parameter $time_limit in ...:...

Overwriting Parameters

It is not allowed to overwrite a parameter that's set before:

function test($a, $b, ...$args) {}
test(a: 28, b: 15, a: 45);
Fatal error: Uncaught Error: Named parameter $a overwrites previous argument in ...:...

Variable Named Parameters

The parameter name cannot be a variable, and will result in a syntax error.

str_replace($key: "Inevitable", $value: "Iron Man", subject: "I am Inevitable");
Parse error: syntax error, unexpected token ":", expecting ")" in ... on line ...

For call-sites with variable parameter names, it is possible to use call_user_func_array() function for this, but be cautious when accepting a user-input with this function.

$key = 'search';
$value = 'replace';
$params = [
    $key => 'Inevitable',
    $value => 'Iron Man',
    'subject' => 'I am Inevitable',

call_user_func_array('str_replace', $params);

See call_user_func_array Considerations for caveats when using this pattern.

Mixing Named and Positional Parameters

It is possible to use named parameters and positional parameters in the same call. Named parameters must be used after positional parameters.

setcookie("Awesome", "Yes",
    path: "/",
    domain: "",
    secure: true,
    httponly: true

In the snippet above, the first two parameters are positional parameters, as they are passed in the same order they are declared. The last four parameters use named parameters, and the call passes all required parameters, making it a valid call.

However, it is not allowed to use named parameters before positional parameters, if they are used at all:

    name: "Awesome",
    path: "/",
    domain: "",
    secure: true,
    httponly: true
Fatal error: Cannot use positional argument after named argument in ... on line ...

The snippet above has a named parameter for the $name parameter, but uses positional parameter for the second parameter. This is not allowed because the parser cannot correctly infer the position of the parameter once named parameters are used in a call-site.

call_user_func_array Considerations

The call_user_func_array() function will use they array keys as parameter names, and this can be a breaking-change in certain situations, where the parameter array is an associative array with non-numeric keys.

$params = [
    'replace' => 'Iron Man',
    'search' => 'Inevitable',
    'subject' => 'I am Inevitable',

echo call_user_func_array('str_replace', $params);

In the snippet above, only PHP 8.0 and later will consider named parameters, and in prior versions, the arguments will be passed as positional parameters.

The snippet below will return different values in PHP < 8.0 and PHP >= 8.0:

$params = [
    'replace' => 'Iron Man',
    'search' => 'Inevitable',
    'subject' => 'I am Inevitable',

echo call_user_func_array('str_replace', $params);

PHP < 8.0

I am Inevitable

PHP >= 8.0

I am Iron Man

Due to PHP's back-porting policy, it is unlikely that PHP 7.4 will emit any warnings of potentially different interpretations in PHP 8.0.

In practice, this means all call_user_func_array() function calls must be aware that PHP 8.0 will interpret associative arrays and numeric arrays different.

As a precautionary measure, call_user_func_array() calls can use array_values if the parameters array might contain non-numeric keys.

$params = [
    'replace' => 'Iron Man',
    'search' => 'Inevitable',
    'subject' => 'I am Inevitable',

echo call_user_func_array('str_replace', array_values($params));

With array_values call, PHP will always use the positional calling pattern, ensuring the outcome will be the same in PHP 8.0 and later.

call_user_func_array() behaves differently if the parameters array is an associative array. There is a non-zero backwards-compatible impact due to this, and is the only potential backwards-compatibility issue related to named parameters.

Argument Unpacking

PHP supports argument unpacking feature since PHP 5.6, but it did not allow arrays with non-numeric keys, to ensure there will be no backward compatibility issues when named parameters are implemented.

With PHP 8.0 named parameters, it is now possible to use argument unpacking operator with names parameters, although directly using named parameters might be more readable.

    'replace' => 'Iron Man',
    'search' => 'Inevitable',
    'subject' => 'I am Inevitable',

Named Parameter with Inheritance Chains

PHP's Named Parameters feature allows renaming the parameter names.

class Foo {
    public function doSomething(string $param) {}
class SubFoo extends Foo{
    public function doSomething(string $myParam) {}

When using named parameter, the parameter of the called class must be used, and parameter name of the parent class/classes are not considered.

For example, in the simple inheritance chain above, SubFoo::doSomething methods renames the param parameter to myParam. This is allowed, but when they are used, the renamed parameter name must be used.

$foo = new SubFoo();
$foo->doSomething(myParam: "test");

Calling methods with the previous parameter names will cause an error, as if an unknown parameter is passed.

$foo = new SubFoo();
$foo->doSomething(param: "test");
Fatal error: Uncaught Error: Unknown named parameter $param in ...:...

Behavior of func_get_arg(s) and func_num_args()

With positional parameters, it is possible to pass more parameters that what the function declaration accepts. This pattern is now allowed with named parameters, due to the unknown parameter rule. To collect excessive parameter values without unknown parameter restrictions, use [variadic parameters]{#named-params-variadic-params}

function test(){}
test(a: 28, b: 15);
Fatal error: Uncaught Error: Unknown named parameter $a in ...:...

func_get_arg() and func_get_args() still work with named parameters, but they behave as if positional parameters were used at call-site.

func_get_arg() returns the values in the order they are declared in the function; not the order they were used in a named parameter call-site.

function test($a, $b){
test(b: 15, a: 28); // 28

func_get_args() returns a numeric array in the order parameters are declared, not called, which is the exact same behavior in prior PHP versions.

function test($a, $b){
test(b: 15, a: 28);
array(2) {
    [0]=> int(28)
    [1]=> int(15)

It is not possible to get a parameter by the named parameter from call-site: e.g. func_get_arg('c').

Named Parameters with Variadic Parameters

A variadic parameter can collect parameters not assigned to a previous parameter (if any), and their names will be preserved.

function test($a, $b, ...$args) {

test(28, 15, june: 6, september: 15, january: "01");
array(3) {
    ["june"]=> int(6)
    ["september"]=> int(15)
    ["january"]=> string(2) "01"

This behavior aligns with argument unpacking, that it is possible to pass a variadic arguments list to a callback.

function run_hook(callable $callable, ...$args): mixed {
    return $callable($args);
function run_hook(callable $callable, ...$args): mixed {
    return $callable(...$args);

    search: 'Inevitable',
    replace: 'Iron Man',
    subject: 'I am Inevitable'
); // "I am Iron Man"

Named Parameters in Public APIs

With named parameters, the name of the function/method parameters become part of the public API, and changing the parameters of a function will be a semantic versioning breaking-change. This is an undesired effect of named parameters feature.

Opting-out of Named Parameters

Except for a variadic parameter containing named parameter, both positional and named parameters will behave similarly within the called function.

Some PHP projects have started to use @no-named-parameters DocBlock attribute to indicate that the function does not prefer to be called with named parameters, and does not provide backwards-compatibility for parameter named.

Backwards Compatibility Impact

The only backwards-compatibility impact is with call_user_func_array(), that it considers an associative array as a named parameter call. This can be mitigated by "normalizing" the parameters array with array_values call:

$params = [
    'replace' => 'Iron Man',
    'search' => 'Inevitable',
    'subject' => 'I am Inevitable',

echo call_user_func_array('str_replace', array_values($params));

This ensures the code behaves exactly the same in all PHP versions.

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 ] braces.

#[ExampleAttribute('foo', 'bar')]
function example() {}

Attributes syntax was initially implemented as <<Attribute>>, followed by @@Attribute, and finally as #[Attribute].

In all PHP versions, # starts a comment for the rest of the line. When an attribute is used in its own line, it will not cause a syntax error in PHP versions prior to 8.0. This makes code that uses PHP 8.0 attributes somewhat backwards compatible, as long as each attribute is attributed in its own line, which makes PHP ignore those attributes.

Attributes may 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.

If an attribute does not map to a class, the Reflection API does not allow to fetch an instance of the instantiated attribute.

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, or inside the same brackets separated by a comma.

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

function foo(){}

#[Attr, FooAttr]
function bar(){}

By default, the same attribute is not allowed to be used more than once. However, it is possible to declare an attribute that explicitly allows repeated use.

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.

Declaring Attributes

In order to use attributes, the attribute itself may be declared as a class. This is validated only when the attribute is fetched, and not immediately when the code is parsed.

A PHP attribute is a standard PHP class, declared with #[Attribute] attribute.

class FooAttribute{


By default, a declared attribute can be used on any item that accepts attributes. This includes classes, class methods, closures, functions, parameters, and class properties.

When declaring the attribute, it is possible to declare the targets the attribute must be used.

class Foo {}

When the attribute is attributed with the targets it supports, PHP does not allow the attribute to be used on any other targets. It accepts a bit-mask to allow the attribute in one or more targets.

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Foo {}

It allows the following targets:

  • Attribute::TARGET_ALL
  • Attribute::TARGET_FUNCTION
  • Attribute::TARGET_METHOD
  • Attribute::TARGET_PROPERTY
  • Attribute::TARGET_METHOD

TARGET_ALL is the OR of all other targets.

Attribute class is declared final

The Attribute class is declared final, which prevents it from being extended.

Repeatable Attributes

By default, it is not allowed to use the same attribute on the same target more than once. The attribute must explicitly allow it:

class MyRepeatableAttribute{}

Attribute Syntax Example

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)] $a): string{}

// Declare Attributes

 * Attributes are declared with `#[Attribute]`.

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, auto-loaded, 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

#[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) {

    $attriubute->getName(); // "My\Attributes\ExampleAttribute"
    $attriubute->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

The final syntax of Attributes feature is not backwards-compatible. However, because the # character is a valid symbol to start a comment, an attribute declared on its own line can make the code parse and run on earlier PHP versions.

#[ExampleAttribute('Hello world', 42)]
class Foo {}

If an attribute is used within the same line, the parser of prior PHP versions will not work:

function foo(#[TestAttr] $bar){
Parse error: syntax error, unexpected '}', expecting variable (T_VARIABLE) in ... on line ...

RFC Discussion Implementation

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 four 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.

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.


Mixing constructor properties with standard properties

A class is allowed to have constructor-promoted properties and standard properties:

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

However, note that the same property must not be declared as a standard property and a constructor-promoted property.

Property Type is not required

It is possible to use Typed Properties and un-typed 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 supported, but not implicit syntax

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 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


TypeNew Feature

PHP 8.0 brings support for Just-In-Time Compilation (JIT).

Pure interpreted programming languages has no compilation step, and directly executes the code in a virtual machine. Most of the interpreted languages including PHP, in fact, has a light-weight compilation step to improve its performance.

Programming languages with Ahead-Of-Time (AOT) compilation, on other hand requires the code to be compiled first before it runs.

Just-In-Time compilation is a hybrid model of interpreter and Ahead-of-Time compilation, that some or all of the code is compiled, often at run-time, without requiring the developer to manually compile it.

PHP was historically an interpreted language, that all of the code was interpreted by a virtual machine (Zend VM). This was changed with the introduction of Opcache and Opcodes, which were generated from the PHP code, and can be cached in memory. PHP 7.0 added the concept of AST (Abstract Syntax Tree), that further separated the parser from the compiler.

PHP's JIT internally uses DynASM from LuaJIT, and as implemented as part of the Opcache.

JIT In Depth
A detailed guide on optimal JIT configuration, benchmarks, and how JIT works in detail

PHP JIT chart

Opcache can inspect the used code, commonly called hot code, and store the compiled versions of them within the shared Opcache memory. When the code should be compiled, and what code should be compiled is configurable.

Platform Support

JIT is currently enabled on Linux and Windows systems running on x86 and x64 processor instruction sets. Apple M1, and ARM CPUs are currently not supported.

DynASM, which is the underlying assembler used in PHP JIT, supports ARM instructions as well, but it is not clear if PHP JIT can be run on ARM processors yet.

JIT also makes use of AVX if it is supported on the CPU. Most consumer and server grade processors from 2011 and later support AVX instructions. cat /proc/cpuinfo | grep avx run on most POSIX systems should reveal if the processor supports it.

Configuring JIT

In PHP 8.0, JIT is enabled by default, but turned off. Enabling JIT is easy, and only requires minimal INI configuration similar to this:


Enable Opcache (opcache.enable)

JIT is implemented as part of Opcache, and requires the Opcache extension to be enabled.


opcache.enable=1 directive does not enable Opcache for CLI, and requires opcache.enable_cli=1. It is possible to turn off Opcache (and this JIT) for other SAPIs (such as FPM), and enable Opcache only for CLI with opcache.enable_cli directive.

Set Buffer Size (opcache.jit_buffer_size)

The effective toggle for JIT is opcache.jit_buffer_size. It accepts how much memory JIT is allowed to use for its buffer.

The default value 0, which effectively disables JIT.

To enable and set a buffer size, set a positive value in bytes, or with standard PHP data size suffixes (M, G, etc.)


Additional JIT Configuration Options

The following configuration options are available, but they are set to appropriate defaults.

Set JIT flags (opcache.jit)

opcode.jit is a somewhat complicated configuration value. It accepts disable, on, off, trace, function, and a 4-digit value (not a bitmask) of 4 different flags in the order.

  • disable: Completely disables JIT feature at start-up time, and cannot be enabled run-time.
  • off: Disabled, but it's possible to enable JIT at run-time.
  • on: Enables tracing mode.
  • tracing: An alias to the granular configuration 1254.
  • function: An alias to the granular configuration 1205.

Granular Configuration

In addition to the tracing and function aliases, the opcache.jit directive accepts a 4-digit configuration value as well. it can further configure the JIT behavior.

The 4-digit configuration value is in the form of CRTO, where each position allows a single digit value for the flag designated by the letter.

For the full configuration options, see JIT Flags.

The default value is tracing, which compiles hot code (call-graph) while the code is being run (tracing), and enables use of CPU registers and AVX extension.

JIT In Depth
A detailed guide on optimal JIT configuration, benchmarks, and how JIT works in detail

Debugging JIT (opcache.jit_debug)

PHP JIT provides a way to emit JIT debug information by setting an INI configuration. When set, it outputs the assembly code for further inspection.


The opcache.jit_debug directive accepts a bit-mask value to toggle certain features. It currently accepts a value in the range of 1 (0b1) to 20 bits binary. A value of 1048576 represents the maximum level of debug output.

php -d opcache.jit_debug=1 test.php
TRACE-1$/test.php$7: ; (unknown)
    sub $0x10, %rsp
    mov $EG(jit_trace_num), %rax
    mov $0x1, (%rax)
    cmp $0x4, 0x58(%r14)
    jnz jit$$trace_exit_0
    cmp $0x4e20, 0x50(%r14)

More Configuration Options

JIT supports several other configuration options to tune how many function calls makes it a "hot" function, which JIT then compiles, and a threshold to consider which functions to JIT, based on the percentage of overall function calls.

For a full list, see Opcache Configuration.

JIT Flags

The opcache.jit directive accepts a 4-digit value to control the JIT behavior, in the form of CRTO, and accepts following values for C, R, T, and O positions.

C: CPU-specific Optimization Flags

  • 0: Disable CPU-specific optimization.
  • 1: Enable use of AVX, if the CPU supports it.

R: Register Allocation

  • 0: Don't perform register allocation.
  • 1: Perform block-local register allocation.
  • 2: Perform global register allocation.

T: Trigger

  • 0: Compile all functions on script load.
  • 1: Compile all functions on first execution.
  • 2: Profile first request and compile the hottest functions afterwards.
  • 3: Profile on the fly and compile hot functions.
  • 4: Currently unused.
  • 5: Use tracing JIT. Profile on the fly and compile traces for hot code segments.

O: Optimization Level

  • 0: No JIT.
  • 1: Minimal JIT (call standard VM handlers).
  • 2: Inline VM handlers.
  • 3: Use type inference.
  • 4: Use call graph.
  • 5: Optimize whole script.

The option 4 under Triggers (T=4) did not make it to the final version of JIT implementation. It was trigger JIT on functions declared with @jit DocBlock comment attribute. This is now unused.

Backwards Compatibility Impact

None. JIT is a new feature added in PHP 8.0, and should not cause any issues. PHP does not raise any warnings or errors when it encounters unknown INI directives, which means setting JIT INI directives would not cause any issues.

RFC Discussion Implementation

New `%h` and `%H` `printf` specifiers

TypeNew Feature

PHP 8.0 adds new %h and %H format specifiers for printf class of functions. They work similar to the existing %g and %G modifiers, but are locale-independent.

printf('%h', 3.141592653589793238); // 3.14159
printf('%H', 3.141592653589793238); // 3.14159

Prior to PHP 8.0, float to string conversions were locale-aware. This is changed in PHP 8.0. The existing %g and %G specifiers remain locale-aware, and the new %h and %H are their locale-independent counterparts.

setlocale(LC_ALL, "fr_FR");

// Locale-aware, with fr_FR comma decimal separator
printf('%g', 3.141592653589793238); // 3,14159
printf('%G', 3.141592653589793238); // 3,14159

// Locale-independent
printf('%h', 3.141592653589793238); // 3.14159
printf('%H', 3.141592653589793238); // 3.14159

Backwards Compatibility Impact

If %h and %H specifiers are used in PHP versions prior to 8.0, they will be processed to empty strings without raising any errors.

Related Changes


New `*` precision and width modifiers in `printf`

TypeNew Feature

printf and its related functions (such as sprintf) in PHP 8.0 support specifying the width/precision modifiers as a function parameter rather than hard-coding it to the format string itself.

  • %f, the float formatter supports specifying the precision with %.3f, where the float is represented with a precision of 3. Similarly, %.5f is for precision 5, etc.
  • %s, the string formatter supports specifying the length of the string. %.5 would limit the string to 5 bytes, %.10s is for the first 10 bytes, etc.

In PHP 8, the width and precision modifiers can be set as a parameter to the printf and other related functions. The width/precision modifier is indicated with *, and should follow the same sequential order the printf functions otherwise accept.

printf('%.*f', 3, 1.61803); // "1.618"
printf('%.3f', 1.61803); // "1.618"

In the snippet above, the first printf call uses %.*f format, and the precision is set as the second parameter. The result is identical to the second printf call, where the precision is already specified in the format itself.

Positional parameters support the * modifiers as well:

printf('%.*2$f', 1.61803, 3);           // "1.618"
      //   ^^             ^

printf('%3$.*4$f', 42, 42, 1.61803, 3); // "1.618"
      //     ^^                     ^

Precision must me an integer

Note that the precision must be an integer, and there is no type coercion even with strict typing is off. Attempting to set a precision with any value other than an integer will throw a ValueError exception.

printf('%.*f', (string) 3, 1.618);
// Fatal error: Uncaught ValueError: Precision must be an integer in ...:...

Precision -1 is allowed in %g/%G and [%h/%H]

%g/%G and %h/%H specifiers allow -1 as the precision. For width and precision modifiers, only 0 and positive integers are allowed otherwise.

Backwards Compatibility Impact

Precision modifier and width modifiers are not understood in PHP versions prior to 8.0. They will not raise any warnings or exceptions.

Both width (%*) and precision (%.*) will be evaluated to empty strings.

PHP < 8.0 PHP >= 8.0
printf('%.*f', 3, 1.61803) f 1.618
printf('%.3f', 1.61803) 1.618 1.618
printf('%*.*f', 7, 3, 1.61803) .*f   1.618
printf('%7.3f', 1.61803)   1.618   1.618
printf('%*d', 5, 17) d    17
printf('%5d', 17)    17    17

The * modifiers are merely syntactic changes, and hardcoding the precision/width values in the format string itself ensures backwards compatibility with PHP versions prior to 8.0.

Related Changes


New `Stringable` interface

TypeNew Feature

PHP 8.0 adds a new interface named Stringable, that indicates any classes implementing the Stringable interface implement a __toString(): string magic method.

To provide forward compatibility, PHP engine automatically adds the Stringable interface to all classes that implement a __toString() method.

interface Stringable {
    public function __toString(): string;

The most common use case would be a strict type inside a function/method before it attempts to use a string function.

class Foo {
    public function __toString(): string {
        return 'FooBar';

function safePrint(string|Stringable $input): void {
    echo htmlspecialchars((string) $input);

The safePrint function can use Union Types to indicate and enforce that it accepts scalar string types, or an object of a class that implements Stringable interface.

Optional to declare implements \Stringable

If a class implements a __toString method, PHP automatically considers that it implements the Stringable interface. It is not required, but possible to explicitly declare it.

class Foo implements \Stringable{
    public function __toString(): string {
        return 'FooBar';

Checking string capabilities

With both scalar string type and the new Stringable interface, it is now possible to safely use string functions in an application that types are enforced.

$foo instanceof \Stringable

instanceof checks will return true for classes that implement __toString method with or without explicit implements \Stringable declaration.

class Foo {
    public function __toString(): string {
        return 'FooBar';
$foo = new Foo();
var_dump($foo instanceof Stringable); // true

Note that instanceof currently requires an object, and does not accept scalar variables yet.


class_implements function will correctly return that a given object from a class that has a __toString() method with or without an explicit implements \Stringable declaration will return Stringable as one of the interfaces the class implements.

class Foo {
    public function __toString(): string {
        return 'FooBar';
var_dump(class_implements(new Foo())); 
// array("Stringable" => "Stringable")


The is_string function checks the type of the variable and return true only if the type of the parameter provided is string. Because objects from classes that implement __toString are objects, is_string() function returns false on objects even if they comply with Stringable interface with or without explicit declaration.

class Foo {
    public function __toString(): string {
        return 'FooBar';
var_dump(is_string(new Foo())); // false

strict_types = 1 behavior

Stringable objects will be evaluated to strings with __toString when strict types are not enforced. If strict typing is enforced (with declare(strict_types=1) at the top of the file), an explicit string cast is required.

class Foo {
    public function __toString(): string {
        return 'FooBar';

function safePrint(string|Stringable $input): void {
    echo htmlspecialchars($input);
                        //^^^ No string cast 
safePrint(new Foo());
// Fatal error: Uncaught TypeError: htmlspecialchars(): Argument #1 ($string) must be of type string, Foo given in ...:...

In the snippet above, strict types are enforced. htmlspecialchars function only accepts string parameters. In case an object of Stringable interface is passed, PHP will refuse to accept it even though the object implements Stringable interface.

When strict types are enabled, make sure to cast string|Stringable values to strings with (string) $input.

Backwards Compatibility Impact

It is trivial to bring the new Stringable interface to PHP versions prior to 8.0. However, note that unless the class explicitly declares implements \Stringable, PHP versions prior to 8.0 will not provide the string-capability checks as mentioned above.

interface Stringable {
    public function __toString(): string;

class MyStringCapableClass implements Stringable {
    public function __toString(): string {
        return 'Hello World';

For the compatibility for prior PHP versions, the Stringable class must be added, and all classes providing string capabilities must explicitly declare that they implements \Stringable.

Related Changes

RFC Discussion (2017) Discussion (2020) Implementation

Built-in web server supports dynamic port selection

TypeNew Feature

The built-in server in PHP 8.0 supports dynamic port selection.

php -S localhost:0

With the port 0 specified, an unused port is picked by the operating system, and the built-in web server will be bound to that port number.

> php -S localhost:0
[Thu Oct 1 02:35:59 2020] PHP 8.0.0 Development Server (http://localhost:11583) started

For every run of this command, a random unused port will be selected by the operating system. Note that this behavior can be different from one operating system to another.

Obtain the allocated port number

To obtain the selected port number within the application being run, use $_SERVER['SERVER_PORT'] super global variable.

Port 0

Port 0 is an IANA reserved port number. While not a standard, many operating systems support binding a service to port 0, which requests the next available port from the system. This functionality is available in most operating systems TCP/IP stacks, including Windows and Linux.

PHP historically disallowed binding to port 0, but from PHP 8.0 and forward, it is now possible to specify port 0 for the built-in server, and it will use an available unoccupied non-system (> 1024) port number.

In other languages

Built-in servers such as Node JS server and Python server already support this pattern to pick a port number dynamically.

Backwards Compatibility Impact

"Binding" to port 0 is not allowed in PHP versions prior to 8.0, and result in an error:

> php -S localhost:0
Invalid address: localhost:0

Sockets extension can bind to port 0 in all PHP versions, and it might help to find a free port, after which it can be used to start a PHP server.

$socket = \socket_create_listen(0);
\socket_getsockname($socket, $addr, $port);

echo $port;

Note that in PHP 8, socket extension uses \Socket class objects instead of resource data types for its primary data type.


Stack trace as string - Parameter max length is configurable

TypeNew Feature

Exceptions thrown in PHP can optionally print the exception information on screen, or return a string with the exception, including the stack trace.

function test() {
    try {
        throw new \Exception();
    catch (Exception $ex) {
        echo $ex;

Exception in ...:...
Stack trace:
#0 ...(...): test('123456789012345...')

By default, the length of the parameters in the stack trace is truncated to 15 bytes, followed by .... Notice how the line is truncated to 15 bytes, followed by an ellipsis.


In PHP 7.4, a new INI setting named zend.exception_ignore_args was added, which can completely hide the parameter information from stack trace.


An INI directive like above will completely hide the parameters in stack trace.

In PHP 8.0, there is a new INI directive to limit the length of the parameters shown in stack trace. Note that the zend.exception_ignore_args directive can still completely hide the parameter information from stack traces.


This somewhat lengthy but unambiguous directive can expand the stack traces to show more information in the stack trace. They can help ease debugging because a maximum length of 15 bytes is often not enough for URLs (https://www. for example is already 12 bytes, leaving only three bytes for the remaining of it), database queries, etc.

To prevent a misconfigured configuration from writing or outputting too much data due this change, there is a hard limit of 1000000 bytes to this directive.

Note that this change only affects when a Throwable (see hierarchy of PHP exceptions) is coerced to a string, explicitly retrieved as a string, or when an uncaught exception is printed on screen.

  1. When a \Throwable is coerced to string, such as (string) $exception or echo $exception.
  2. When a \Throwable is retrieved as string, with $exception->getTraceAsString(), i.e. Throwable::getTraceAsString().
  3. Default errors printed on screen when an exception is uncaught.

This change does not affect debug_print_backtrace function or Throwable::getTrace method, as they both return the full stack trace.

Backwards Compatibility Impact

The default configuration value of zend.exception_string_param_max_len remains 15 bytes. This means unless this value is not updated, the stack traces would be the same.

Related Changes

RFC Discussion Implementation


TypeNew Feature

Weak Maps is a new feature added in PHP 8.0.

A WeakMap allows to store and associate arbitrary values for object keys, but without preventing the garbage collector from clearing the object if it falls out of scope in everywhere else.

Weak Maps in PHP 8.0 is different from the WeakMap class from the weakref PECL extension. See PHP WeakMap Practical Use Cases: PECL extension for more information.

A WeakMap is similar to SplObjectStorage, as in both WeakMap and splObjectStorage use objectss as the key, and allows storage of arbitrary values. However, a WeakMap does not prevent the object from being garbage collected.

$map = new splObjectStorage();

$object = new stdClass();
$map[$object]  = 'Foo';

var_dump(count($map)); // int(1)


var_dump(count($map)); // int(1)

In the snippet above, splObjectStorage is used to store additional data about objects. Even though there is an unset($object) call, PHP's garbage collector does not clear it from memory because of the reference within the $map splObjectStorage.

With WeakMap, the references are weak, and the reference inside the WeakMap does not prevent the garbage collector from clearing the object if there are no other references to the object.

- $map = new splObjectStorage();
+ $map = new WeakMap();

$object = new stdClass();
$map[$object]  = 'Foo';

var_dump(count($map)); // int(1)


var_dump(count($map)); // int(0)

A more simplified example would be:

$map = new splObjectStorage();
$map[new stdClass()] = 'Foo';
count($map); // 1

$map = new WeakMap();
$map[new stdClass()] = 'Foo';
count($map); // 0

new stdClass is used as the key, and it immediately falls out of scope. WeakMap allows the object to be cleared, while splObjectStorage does not.

Weak Maps can help associate transient data for objects without changing the state of the object itself. If the object falls out of scope, so does the associated data.

Complete Example

class CommentRepository {
    private WeakMap $comments_cache;
    public function getCommentsByPost(Post $post): ?array {
        if (!isset($this->comments_cache[$post])) {
            $this->comments_cache[$post] = $this->loadComments($post);
        return $this->comments_cache[$post]

In the example above, the CommentRepository class uses WeakMap class to temporarily store comments in memory, stored as typed property $comments_cache. When the getCommentsByPost method is called with a Post object, the $comments_cache is checked and returned first, before loading them from the database with private loadComments method.

If the CommentRepository class were to store the comments in array (indexed by the post ID), or in SplObjectStorage, this cache will not be cleared even if the Post object falls out of scope or even explicitly unset with unset($post).

With help from WeakMap, CommentRepository class can now cache data (array of comments) without "holding onto" the Post objects, but still associate the comments with Post objects. The Post object state is not changed (e.g. by setting $post->comments property), and the life-cycle of the comments managed automatically alongside the garbage clearance of the Post object itself.

WeakMap class synopsis

final class WeakMap implements ArrayAccess, Countable, IteratorAggregate, Traversable {
    public function offsetGet(object $object);
    public function offsetSet(object $object, mixed $value): void;
    public function offsetExists(object $object): bool;
    public function offsetUnset(object $object): void;
    public function count(): int;

WeakMap keys must be objects

Weak Maps only allow objects as keys, and the associated allows arbitrary values.

Attempting to store any other data type as the key will throw a type error.

$map = new WeakMap();
$map['Foo'] = 'Bar';

// Fatal error: Uncaught TypeError: WeakMap key must be an object in ...:...

Appending to a WeakMap is not allowed either.

$map = new WeakMap();
$map[] = 'Baz';

// Fatal error: Uncaught Error: Cannot append to WeakMap in ...:...

Error on non-existing keys

If the requested object does not exist in the weakmap object, an \Error exception is thrown.

$map = new WeakMap();
$map[new stdClass()];

// Fatal error: Uncaught Error: Object stdClass#2 not contained in WeakMap in ...:...

This can be avoided by checking if the index exists in the map.

$map = new WeakMap();
isset($map[new stdClass()]); // false

WeakMap does not allow properties

$map = new WeakMap();
$map->foo = 'Bar';

// Fatal error: Uncaught Error: Cannot create dynamic property WeakMap::$foo in ...:...

WeakMap does not support Serialization/Unserialization

Weak Maps are not allowed to be serialized or unserialized. Given the WeakMap class is declared final, this can be changed either.

$map = new WeakMap();

// Fatal error: Uncaught Exception: Serialization of 'WeakMap' is not allowed in ...:...
$serialized_str = 'C:7:"WeakMap":0:{}';

// Fatal error: Uncaught Exception: Unserialization of 'WeakMap' is not allowed in ...:...

Iterating Weak Maps

WeakMap class implements Traversable interface, it can be used iterated with foreach construct.

Further, WeakMap implements IteratorAggregate that brings a WeakMap::getIterator method.

$map = new WeakMap();

$obj1 = new stdClass();
$map[$obj1] = 'Object 1';

foreach ($map as $key => $value) {
    var_dump($key); // var_dump($obj1)
    var_dump($value); // var_dump('Object 1');

The iterator itself can be extracted with getIterator method, and the return value is an iterable.

$map = new WeakMap();

$obj1 = new stdClass();
$map[$obj1] = 'Object 1';

$iterator = $map->getIterator();

foreach ($iterator as $key => $value) {
    var_dump($key); // var_dump($obj1)
    var_dump($value); // var_dump('Object 1');

Practical Use Cases

Weak Maps can help with temporary storage of objects, but without affecting the memory usage due to the temporary storage itself.

When used as a cache storage, the object keys can be used to store a primitive state of the object, and reduce subsequent calls to an expensive function that is supposed to process or enhance the object.

Another use case would be storage of completion data for a collection of objects, but without mutating the object itself.

For more examples of Weak Maps in PHP 8.0) along with code examples, see PHP WeakMap Practical Use Cases.

Backwards Compatibility Impact

Weak Maps is a new feature in PHP 8.0, and unless there is a class with name WeakMapin the global namespace, there should not be any issues when upgrading an existing code base to PHP 8.0.

Because the WeakMap is an internal class, this functionality cannot be back-ported to prior PHP versions and make it work with the native garbage collector. A user-land implementation exists, but at a significant performance penalty.

A PECL extension named weakref provided similar functionality, but it is no longer maintained.

RFC Discussion Implementation

Null-safe operator

TypeNew Feature

Null-safe operator is a new syntax in PHP 8.0, that provides optional chaining feature to PHP.

The null-safe operator allows reading the value of property and method return value chaining, where the null-safe operator short-circuits the retrieval if the value is null, without causing any errors.

The syntax is similar to the property/method access operator (->), and following the nullable type pattern, the null-safe operator is ?->.


Null safe operator silently returns null if the expression to the left side evaluates to null.

class Customer {
    public function getAddress(): ?Address {}
class Address {
    public function getCountry(): string {}

$country = $customer->getAddress()->getCountry();

In the snippet above, the Customer::getAddress() method return value is nullable; It can return a null value, or an object of Address class.

The $customer->getAddress()->getCountry() chain is not "null safe", because the return value of getAddress can be null, and PHP throws an error when it tries to call getCountry() method:

Fatal error: Uncaught Error: Call to a member function getCountry() on null in ...:...

To safely access the address, it was necessary to check the null return values before further accessing the return value.

$address = $customer->getAddress();
$country = $address ? $address->getCountry() : null;
$address = $customer->getAddress();
if ($address) {
    $country = $address->getCountry();
else {
    $country = null;

The null-safe operator solves this by short-circuiting the property/method access, and returning null immediately if the left side of the operator is null, without executing the rest of the expression.

- $address = $customer->getAddress();
- $country = $address ? $address->getCountry() : null;
+ $country = $customer->getAddress()?->getCountry(); 

The ?-> null-safe operator can help reduce excessive isset() and ternary checks.

Null-safe operator was added to PHP 8.0 between alpha 3 release and first beta release, right around the time PHP 8.0 reached its feature-freeze.


The null-safe operator is read-only. It cannot write/assign values from it.

class Customer {
    private ?Address $address;

    public function getAddress(): ?Address {
        return $this->address;

class Address {
    public string $country;

$customer->getAddress()?->country = 'NL';

This snippet attempts to write to the $country property of the Address object. This is not allowed with the null-safe operator.

Fatal error: Can't use nullsafe operator in write context in ... on line ...


The null-safe operator can be chained, and the expression as a whole will be short-circuited from the first null-safe operator that encounters null.


This chain of calls are null-safe. PHP will stop and immediately return null if any of the null-safe return operators evaluate to null.

{code-ok} Null-safe operator must be present at every step the chain can be short-circuited. Using the ?-> operator will not make the whole chain null-safe.

Evaluated from left to right

The null-safe operator is evaluated from left to right. Most importantly, it does not prioritize other function calls or other access patterns such as array-access.


class Customer {
    public function getAddress(): ?Address {}

class Address {
    public function setCountry(string $country_code): void {}


If the Customer::getAddress method returns a valid Address object, the GeoIP::getCountry() will be executed, and passed to the Address::setCountry() method call. Because the null-safe operators are evaluated from left to right, the GetIP::getCountry() method will never be called if the Customer::getAddress() method returns null.

Note that using braces will make the expression inside the braces execute altogether, as opposed to the left-to-write pattern.


If Customer::getAddress() returns a value other than null, the (GeoIP::getAddress()?->getCountry()) chain will be executed, and the return value will be passed to the Address::setCountry() call.

No References

The null-safe operator cannot be used with references.

$country = &$customer->getAddress()?->getCountry();
Fatal error: Cannot take reference of a nullsafe chain in ... on line ...

Backwards Compatibility Impact

Null-safe operator is a new syntax, and cannot be back-ported to older PHP versions.

Running code that uses null-safe operator will raise a parse error:

Parse error: syntax error, unexpected '->' (T_OBJECT_OPERATOR) in ... on line ...

RFC Discussion Implementation

`static` return type for class methods

TypeNew Feature

PHP 8.0 allows static as a return type for class methods.

class Foo {
    public static function getInstance(): static {
        return new static();

PHP class methods can return self and parent in previous versions, but static was not allowed in PHP versions prior to 8.0. The newly allowed static return type allows to narrow down the return type to the called class.

The static return type helps classes with fluent methods (i.e the ones with return $this), immutable classes (i.e return clone $this) or static methods that return an instance of the class itself.

Without the static type allowed in return types, one would have to use self as the return type, which may not be the ideal one. PHP DocBlock already allowed @return static in its DocBlocks to indicate that the methods return the object itself, or an instance of the same class.

With PHP 8.0's static return type support, it is now possible to replace DocBlock @return static statements with a return type declaration.

class Foo {
-   /**
-    * @return static
-    */
-   public static getInstance() {
+   public static getInstance(): static {
        return new static();


static return type follows Liskov Substitution Principle. A child class method can return a narrower class object than the parent methods return type.

Because static always refer to the class name of the called object (i.e. same as get_class($object)), static is a subset of self, which in turn is subset of parent.

It is possible for a child class to return static, even if the parent methods return type is self, or parent.

For example, the entirety of the following inheritance is valid:

Excessive inheritance chains are almost always a bad idea, and leads to unmanageable code. The snippet below is just for demonstration.

class Foo {
    public function getInstance(): mixed {}
class Bar extends Foo {
    public function getInstance(): object|null {}
class Baz extends Bar {
    public function getInstance(): object {}
class Qux extends Baz {
    public function getInstance(): parent {}
class Quux extends Qux {
    public function getInstance(): self {}
class Corge extends Quux {
    public function getInstance(): static {}

The snippet above uses other PHP 8.0 features

Attempting to "widen" the return type scope with self, parent, or any other return type will cause error:

class Corge {
    public function getInstance(): static {}
class Grault extends Corge {
    public function getInstance(): parent {}
Fatal error: Declaration of Grault::getInstance(): Corge must be compatible with Corge::getInstance(): static in ... on line ...

In fact, it is also not allowed to replace the static return type with the class name of the child class or parent class either.

Return type only

static return type is only allowed as a return type. It is not allowed as a property type or a parameter type.

This is because the static return type is always narrowing the scope, which is not allowed in typed properties and parameter types.

Not allowed outside class context

Only class methods can declare static return type. Standard functions or closures are not allowed to declare static return type.

function get_instance(): static {}
Fatal error: Cannot use "static" when no class scope is active in ... on line ...

Backwards Compatibility Impact

Code with static return type will not be backwards-compatible with older PHP versions prior to 8.0. Doing so will result in a parse error:

Parse error: syntax error, unexpected 'static' (T_STATIC) in ... on line ...

It is not possible to back-port this feature to older PHP versions either.

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.


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 `PhpToken` Tokenizer class

TypeNew Feature

token_get_all function from tokenizer extension parsers a given PHP source code, and returns an array of tokens. Its array return value is not quite convenient to use.

PHP 8.0 comes with a new class PhpToken that provides an object-oriented approach to tokenizer results.

$code = "<?php echo "Hello world"; ?>";
$tokens = PhpToken::tokenize($code);

Prior to PHP 8.0 rc4, the PhpToken::tokenize() method was called PhpToken::getAll().

The return value is an array of PhpToken objects, that provides more fluent methods and public properties to retrieve information about each token.

PhpToken class synopsis

class PhpToken implements Stringable {

     * One of the T_* constants, or an integer < 256 representing a single-char token.
    public int $id;

    /** The textual content of the token. */
    public string $text;

    /** The starting line number (1-based) of the token. */
    public int $line;

    /** The starting position (0-based) in the tokenized string. */
    public int $pos;

    /** @return static[] */
    public static function tokenize(string $code, int $flags = 0): array {}

    final public function __construct(int $id, string $text, int $line = -1, int $pos = -1) {}

     * Whether the token has the given ID, the given text,
     * or has an ID/text part of the given array.
    public function is(int|string|array $kind): bool {}

    /** Whether this token would be ignored by the PHP parser. */
    public function isIgnorable(): bool {}

    /** Get the name of the token. */
    public function getTokenName(): ?string {}

    /** Returns $text property */
    public function __toString(): string {}

Related Changes from the snippet

Each returned PhpToken object will contain public properties $token->id, $token->text, $token->line, and $token->pos to retrieve information about the token.

  • $token->id: One of the T_* constants, or an integer < 256 representing a single-char token.
  • $token->text: The textual content of the token.
  • $token->line: The starting line number (1-based) of the token.
  • $token->pos: The starting position (0-based) in the tokenized string.

The methods $token->is(), $token->getTokenName(), $token->isIgnorable(), and $token->__toString() can return additional information as well.

  • PhpToken::is(int|string|array $kind): bool: Returns whether the token is of a given T_ token (integer), a string token, or an array of int|string tokens to match against the token ID.
  • $token->getTokenName: The textual content of the token.
  • $token->isIgnorable(): Whether this token would be ignored by the PHP parser.
  • $token->__toString(): Returns the $token->text value.

Backwards Compatibility Impact

PhpToken is a new class added in PHP 8.0, and unless there is no such class in user-land PHP code, there should not any upgrading issues.

The functionality can be backported to other PHP versions as well. See phpwatch/phptoken-polyfill

RFC Discussion Implementation

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

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.

Related Changes

RFC Discussion Implementation

New `p` date format for UTC `Z` time zone designation

TypeNew Feature

There is a new date format introduced in PHP 8.0: p. This new lower case "p" date format behaves similar to the upper case P, which shows the time zone offset. The difference between P and p format is that the new p format uses Z for UTC time, while the P format uses +00:00.

The ISO 8601 date format permits UTC time with +00:00 format, or with Z. This means both date formats below are valid:

  • 2020-09-09T20:42:34+00:00
  • 2020-09-09T20:42:34Z

With the new p date formatter, it is now possible to create a date with the second pattern above as well:


Here is a quick list of PHP ISO8601-related date formats:

<PHP 8.0 >=PHP 8.0
Valid ISO 8601 date format.
2020-09-09T20:42:34+00:00 2020-09-09T20:42:34+00:00
Valid ISO 8601 date format.
2020-09-09T20:42:34+00:00 2020-09-09T20:42:34+00:00
P +00:00 +00:00
p is new in PHP 8.0
p Z
Valid ISO 8601 date format. Same as c and DATE_ATOM
2020-09-09T20:42:34+00:00 2020-09-09T20:42:34+00:00
p is new in PHP 8.0
2020-09-09T20:42:34p 2020-09-09T20:42:34+00:00

Dates formatted with +00:00 to indicate UTC times are perfectly valid. Quoting from Wikipedia:

An offset of zero, in addition to having the special representation "Z", can also be stated numerically as "+00:00", "+0000", or "+00".

Backwards Compatibility Impact

UTC offsets indicated with +00:00 is perfectly valid ISO 8601 date formats. If it is absolutely necessary to use Z instead of of +00:00 for UTC time, the new p format can be helpful.

While it is not possible to backport the formatter itself, a simple string-replace hack could achieve the same results as PHP 8.0 in all PHP versions since 5:

str_replace('+00:00', 'Z', date('c'));


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

New `DateTime/DateTimeImmutable::createFromInterface()` methods

TypeNew Feature

PHP's DateTime and DateTimeImmutable classes have methods to create one instance from another.

Both DateTime and DateTimeImmutable classes implement DateTimeInterface

  ├── DateTime
  └── DateTimeImmutable

In PHP 8.0, both DateTime and DateTimeImmutable classes get a new method ::createFromInterface. They behave exactly as they would in createFromMutable and createFromImmutable methods, but the new createFromInterface method, as its name suggests, accepts any DateTimeInterface implementation.

  • DateTime::createFromInterface(DateTimeInterface $object): DateTime
  • DateTimeImmutable::createFromInterface(DateTimeInterface $object): DateTimeImmutable

Backwards compatibility impact

This is a rather simple improvement in PHP 8.0, and it wouldn't be too difficult to mimic this functionality with instanceof checks and calling either of the createFrom* methods available in all PHP versions.

Libraries that extend DateTime and DateTimeImmutable can polyfill this functionality with a bit of trait-foo, using a trait that conditionally declares the createFromInterface method if it's not available in PHP version it runs on.

class MyCustomDateTime {
    use DateTimeCreateFromInterfacePolyfill;
    // ...

class MyCustomDateTimeImutable {
    use DateTimeImmutableCreateFromInterfacePolyfill;
    // ..

In the appropriately named files (for PSR-4 naming and autoloading), it is now possible to declare the two polyfill traits that will only contain the methods in PHP versions prior to 8.0

// path/to/DateTimeCreateFromInterfacePolyfill.php
if (PHP_VERSION_ID < 80000):
trait DateTimeCreateFromInterfacePolyfill {
    public function createFromInterface(\DateTimeInterface $object): \DateTime {
        if ($object instanceof \DateTimeImmutable) {
            return \DateTime::createFromImmutable($object);
        if ($object instanceof \DateTime) {
            return clone $object;
        throw new \InvalidArgumentException('Unexpected type');
trait DateTimeCreateFromInterfacePolyfill {}
// path/to/DateTimeImmutableCreateFromInterfacePolyfill.php
if (PHP_VERSION_ID < 80000):
trait DateTimeImmutableCreateFromInterfacePolyfill {
    public function createFromInterface(\DateTimeInterface $object): \DateTimeImmutable {
        if ($object instanceof \DateTime) {
            return \DateTimeImmutable::createFromMutable($object);
        if ($object instanceof \DateTimeImmutable) {
            return clone $object;
        throw new \InvalidArgumentException('Unexpected type');
trait DateTimeImmutableCreateFromInterfacePolyfill {}

Implementation Obsolete PR (2018)

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

Default error reporting is set to `E_ALL`


The PHP INI directive error_reporting controls which types of errors, warnings, and notices should be reported (either to screen or to logs).

In PHP 8.0, the default is changed to E_ALL.

- error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
+ error_reporting = E_ALL

The error_reporting value expects a bit-mask, and the default value prior to PHP 8.0 was the value recommended for production: E_ALL & ~E_DEPRECATED & ~E_STRICT. This value prevented deprecation notice and strict notices from making it to error logs or screen.

In a production system, the display_errors value must be set to Off, which will hide all errors from being printed on the screen, but allows error logs to record them.

Backwards Compatibility Impact

With the default error_reporting value set to E_ALL, PHP will trigger the error handler for all PHP errors, notices, and warnings. This is a good move because it can reveal deprecation notices and that were hidden due to the previous configuration.

To bring back the same functionality as PHP 7.4:

error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT

Related Changes


Inheritance rules are not applied to `private` class methods


PHP class methods declared private are not accessible by child classes, but child classes can declare a method with the same name without having to adhere to Liskov Substitution Principle. PHP 8.0 strictly enforces method signatures, but private class methods are not affected by this change.

Typed Properties introduced in PHP 7.4 already follow the same changes, and is not changed in PHP 8.0.

Prior to PHP 8.0, it was not allowed change to change the final or static flags of a class method. This is changed in PHP 8.0, that ensures private methods can be truly private without adhering to the inheritance rules enforced from parent class(es).

In all PHP versions, private class method parameters, private method return types, and private class property types are not validated against Liskov Substitution Principle. These changes in PHP 8.0 only applies to changes in abstract, static, and final flags in class methods.

final private methods

Declaring a class method final private does not make sense because all private class methods are inaccessible by child classes anyway.

Prior to PHP 8.0, it was allowed to declare a class method as final private, but it was not allowed to redeclare the method due to final clause enforcement.

class Foo {
    final private function testFoo(): void {}

class ChildFoo extends Foo{
    private function testFoo(): void {}

This was not allowed prior to PHP 8.0, and resulted in a fatal error:

Fatal error: Cannot override final method Foo::testFoo() in ... on line ...

In PHP 8.0 and later, this restriction is removed, and only a warning is emitted:

Warning: Private methods cannot be final as they are never overridden by other classes in ... on line ...

Note that this warning is emitted when a class declares a method as final private; not when a child class re-declares a private method.

final private class constructors

An exception is made for the warning mentioned above for class constructors. It is possible to declare a class constructor as final private:

class Foo {
    final private function __construct() {}

Attempting to re-declare the constructor results in the same fatal error in all PHP versions.

class Foo {
    final private function __construct() {}

class ChildFoo extends Foo{
    final private function __construct() {}

// Fatal error: Cannot override final method Foo::__construct() in ... on line ...

static methods

Changing the static flag in an inheritance was not allow prior to PHP 8.0. It is allowed in PHP 8.0.

class Foo {
    private function bar() {}
    static private function baz() {}

class ChildFoo extends Foo{
    static private function bar() {} // Adds `static` flag from Foo
    private function baz() {} // Removes `static` flag from Foo

Prior to PHP 8.0, this caused fatal errors:

Fatal error: Cannot make non static method Foo::bar() static in class ChildFoo in ... on line ...
Fatal error: Cannot make static method Foo::baz() non static in class ChildFoo in ... on line ...

abstract methods

PHP does not allow to declare a private abstract function in any PHP version. In PHP 8.0 and later, it is allowed to declare a method as abstract even if a method with same name exists in the parent class.

class Foo {
    private function test() {}

abstract class ChildFoo extends Foo{
    abstract protected function test();

Prior to PHP 8.0, this resulted in a fatal error:

Fatal error: Cannot make non abstract method Foo::test() abstract in class ChildFoo in ... on line ...

Magic Methods

In PHP 8.0, [magic method signatures are enforced](). Magic methods (e.g __get, __set, __callStatic, etc) must follow the signature even if they are declared private.

Backwards Compatibility Impact

All private method signature patterns were not allowed and resulted in a fatal error in PHP versions prior to 8.0. As PHP 8.0 now allows it, these changes should not cause further errors in PHP 8.0

Declaring a class method final private emits a PHP warning at the declaration time of the parent class. This behavior is different from the fatal error that is only triggered if a child class attempted to extend/re-declare the same method.

Related Changes

RFC Discussion Implementation

Calling non-static class methods statically result in a fatal error


PHP 8.0 no longer allows to call non-static class methods with the static call operator (::).

Calling non-static methods statically raised a PHP deprecation notice in all PHP 7 versions, and raised a Strict Standards notice in PHP 5 versions.

class Foo {
    public function bar() {}

// Deprecated: Non-static method Foo::bar() should not be called statically in ... on line ...

In PHP 8.0 and later, this results in a fatal error:

class Foo {
    public function bar() {}

// Fatal error: Uncaught Error: Call to undefined method Foo::bar() in ...:...

Note that this only affects calling non-static methods statically. Although discouraged, calling a static method non-statically ($this->staticMethod()) is allowed.

This change is implemented throughout the engine.

Variable Functions

class Foo {
    public function bar() {}

['Foo', 'bar']();
// Fatal error: Uncaught Error: Non-static method Foo::bar() cannot be called statically in ...:...


PHP no longer considers an array with class name and a method (['Foo', 'bar']) as a valid callable, and results in a fatal error. This includes PHP core functions that expect a callable. If such callable is passed to a function that expects a valid callable, a \TypeError will be thrown instead of a fatal error at call-time.

class Foo {
    public function bar() {}

call_user_func(['Foo', 'bar']);
call_user_func_array(['Foo', 'bar'], []);
// Fatal error: Uncaught TypeError: call_user_func(): Argument #1 ($function) must be a valid callback, non-static method Foo::bar() cannot be called statically in ...:...

This affects all functions ranging from call_user_func and call_user_func_array to register_shutdown_function, set_error_handler, set_error_handler.

register_shutdown_function function in PHP 8.0 versions until beta3 raised a PHP warning at the time register_shutdown_function function is called instead of the current behavior of throwing a \TypeError exception. This was corrected in PHP beta4.


is_callable function returns false on callable that calls non-static methods statically. It returned true prior to PHP 8.0.

class Foo {
    public function bar() {}

is_callable(['Foo', 'bar']); // false

static, self, and parent

static, self, and and parent pointers can continue to use the static call syntax inside a class.

class Test extends UpperTest{
    public function foo(): {}
    public function bar() {

The call above is still allowed because static, self, and parent are used inside the class scope.

static:: and self:: calls are identical to $this-> calls on non-static methods, and improves readability. In the example above, static::foo() and self::foo() calls can be safely replaced with $this->foo() to improve readability because foo is not a static method.

Backwards Compatibility Impact

For existing code that get fatal errors in PHP 8.0, the fix can as simple as using the correct syntax if there is a class instance in the same scope.

class Foo {
    public function bar() {}

$foo = new Foo();
- Foo::bar();
+ $foo->bar(); 

If there is no instantiated class object, and if the class can be instantiated without any parameters or side effects, it will be simple replacement as well.

class Foo {
    public function bar() {}

- Foo::bar();
+ (new Foo())->bar(); 

If the class constructor requires parameters, or tends to make any state changes, the fix can be more complicated. The instance needs to be injected to the scope the static call is made.

Note that functions that expect a callable parameter no longer accept callables with a non-static method as static method. A \TypeError exception will be thrown when the callable is passed, as opposed to when the callable is invoked.

class Foo {
    public function bar() {}
function takeCallable(callable $func) {}

- takeCallable(['Foo', 'bar']);
+ takeCallable([new Foo(), 'bar']);

is_callable function no longer returns true for such callables either.


Apache Handler: Module name and file path changes


The Apache 2 Handler SAPI in PHP 8.0 is renamed, and can affect when PHP is used as an Apache module.

It is recommended to use PHP-FPM with Apache mod_event MPM. However, PHP 8.0 still supports the Apache 2 Handler SAPI, which is integrated with Apache 2 web server as an Apache module.

Module identifier renamed to php_module

In PHP 8.0, the Apache module identifier is renamed to php_module.

In PHP 7.x versions, it was named php7_module, and in PHP 5.x, it was named php5_module.

This means that all references to the Apache module loading configuration needs to be updated:

- LoadModule php7_module "/usr/lib/apache2/modules/"
+ LoadModule php_module "/usr/lib/apache2/modules/"

Apache will throw an error similar to the one below if the module name php8_module is used:

Can't locate API module structure `php8_module` in file ...

A full guide on installing and upgrading PHP 8.0 on Ubuntu

Apache <IfModule> directives, and any other places the module name is used must be updated to their new name as well:

- <IfModule php7_module>
+ <IfModule php_module>
    DirectoryIndex index.html default.php index.php
    AddHandler application/x-httpd-php .php

Windows DLL file name changes

On Windows, the DLL file name is updated for PHP 8. Module identifier is php_module.

- LoadModule php7_module "W:/php/php-8.0.0-Win32-vc15-x64/php7apache2_4.dll"
+ LoadModule php_module "W:/php/php-8.0.0-Win32-vc15-x64/php8apache2_4.dll"

It might be necessary to manually update development environment bundles such as XAMPP, Wamp, or Laragon to reflect this.

Backwards Compatibility Impact

Due to the module identifier renaming, errors similar to Can't locate API module structure php8_module in file might occur, but most distributions, software collections, and PPAs automatically configure the Apache2Handler on behalf of the user, so the chances of end users running into this issue is uncommon.


Locale-independent `float` to `string` casting


PHP 8.0 changes the way PHP coerces float values to string.

Prior to PHP 8.0, float to string conversions depended on the current locale set with the setlocale function. setlocale function is not thread-safe, which means setting locale will affect all threads in a PHP process.

setlocale(LC_ALL, "fr_FR");
echo 1.618; // 1,618
            // ^^^ comma

PHP does not inherit the system locale by default, but calling setlocale without a specific locale makes it inherit the system locale. If setlocale() is called without an explicit locale set (e.g. setlocale(LC_ALL, null), PHP will take the system locale from LC_ALL environment variable. System locale can be obtained by running locale in terminal.

Both comma (,) and period (.) symbols are accepted as a valid decimal separator throughout the world, and there is no international standard that prefers one over another. However, PHP can only accept the period notation for floats:

$val = 1,618;
// PHP Parse error: syntax error, unexpected ',' in ... on line ...

PDO extension, and functions such as var_export, json_encode and serialize try to minimize the locale-dependence with special handling for float values.

In PHP 8, all string to float conversions will be locale-independent. This means even if PHP is running under a different locale that prefers periods for the decimal separator, PHP will not use that locale when float values are presented.

setlocale(LC_ALL, "de_DE");
echo 1.618; // 1.618
            // ^^^ period

printf functions and specifiers

printf-line of functions provide several modifiers to format and convert data formats.

*printf specifiers will continue to use the %f specifier in a locale-dependent way. It is already noted in the documentation that %f output is locale-aware.

For locale-independent float formatting, use %F specifier.

setlocale(LC_ALL, "fr_FR");

printf("%f", 1.618); // 1,618000
printf("%.3f", 1.618); // 1,618

printf("%F", 1.618); // 1.618000
printf("%.3F", 1.618); // 1.618

In PHP 8, using the string specifier (%s) will have the locale-independent float to string effect.

setlocale(LC_ALL, "fr_FR");

// PHP < 8.0
printf("%s", 1.618); // 1,618

// PHP >= 8.0
printf("%s", 1.618); // 1.618

Further, %g and %G specifiers are locale-aware, and continue to be so in PHP >= 8.0 as well. There are new %h and %H specifiers in PHP 8.0, that provide the same functionality as %g and %G specifiers, but in a locale-independent way.

setlocale(LC_ALL, "fr_FR");

// All PHP versions
printf("%g", 1.618); // 1,618
printf("%G", 1.618); // 1,618

// New in PHP 8.0
printf("%h", 1.618); // 1.618
printf("%H", 1.618); // 1.618
PHP < 8.0 PHP >= 8.0
%s (string) Locale-aware Locale-independent
%f (float) Locale-aware Locale-aware
%F (float) Locale-independent Locale-independent
%g (general) Locale-aware Locale-aware
%G (general) Locale-aware Locale-aware
%h (general) - New, Locale-independent
%H (general) - New, Locale-independent

Locale-aware float to string coercion

To make a locale-aware float to string coercion, it is still possible to use the sprintf function with %f or %g specifiers. These specifiers are in locale-aware in all PHP versions.

setlocale(LC_ALL, "fr_FR");

- echo (string) 1.618; // 1.618
+ echo sprintf("%g", 1.618); // 1,618

Note that setlocale function can be the root of side-effects due to the way it works; the locale is set per-process, and not per-thread, which opens up the possibility of one script/request setting the locale, and having that effect in other threads in the same process.

An ideal approach would be to use the intl extension and its format helpers which are arguably much more flexible, easy to test, and use with different locales simultaneously.

$formatter = new \NumberFormatter("fr_FR", \NumberFormatter::DECIMAL);  
echo $formatter->format(1.618); // 1,618

intl NumberFormatter performance
100K iterations of the NumberFormatter::format method took 0.012 sec, compared to 0.004 sec for sprintf. While it is a 3x performance difference, it is an absolutely minimal performance impact that wouldn't even register.

Backwards Compatibility Impact

This change can cause backwards-compatibility issues, which can be difficult to spot at once.

When strict types are not enabled, PHP silently converts float and string types back and forth when cast, or when used in parameter, property, or return types. Applications with strict types enabled would have already raised on errors when type silent coercion occur; For the rest this change will silently treat float-strings as locale-independent.

If you use setlocale function to set a locale, and relied on this PHP behavior to automatically format float values, it will now be necessary to explicitly format them to a locale-aware format.

Related Changes

RFC Discussion Implementation

Class magic method signatures are strictly enforced


Magic methods in PHP are special class method names that, if declared, brings special functionality to the class.

There are several magic methods in PHP. __constuct() magic method is called when a new class object is instantiated with new Foo() pattern, the __get() magic method is called when non-existing class property is requested, __toString() magic method is called when a class object is coerced to a string, and so on.

Although all these magic methods carried a semantic meaning, they were not programmatically enforced until PHP 8.0. The __toString() method, for example, semantically expects to return a string, but it was possible to return any data type, including ones that cannot be coerced to strings.

class Foo {
    public function __toString(): object {

In PHP 8.0 and later, if a magic method is declared, it must adhere to the function signature and return type if declared too.

A few signature checks are enforced for return types since their introduction in PHP 7.0. Namely, __construct, __destruct, and __clone are were not allowed to declare a return type at all, not even void (void was added in PHP 7.1).

Types are optional

Unlike a PHP class interface, that enforces the parameter and return types to be present in all implementations, magic method enforcement allows to not declare a type at all.

This allows existing classes that implement magic methods to continue to run without having to declare explicit types.

However, if a type is declared, it must follow the signature.

Fatal Errors on Signature Mismatches

PHP 8.0 throws fatal errors when interface implementations and class inheritance violates LSP. Magic methods follow the same pattern.

  • Return types are allowed to narrow the return type.
  • Method parameters are allowed to widen the parameter type.

Variance is allowed

Similar to standard class/interface inheritance, magic methods variance is allowed too.

For example, a __get() method which should return a mixed type can declare that its return type is bool because bool is already included in the mixed type.

Further, the __set() magic method can widen its method parameter from __set(string $name, $value) to __set(string|CacheEntry $name, $value) if the function can handle CacheEntry objects too.

Magic Method Signatures

class GoodFoo {

    public function __isset(string $name): bool {}
    public function __get(string $name): mixed {}
    public function __set(string $name, mixed $value): void {}
    public function __unset(string $name): void {}
    public function __set_state(array $properties): object {}

    public function __call(string $name, array $arguments): mixed {}
    public function __callStatic(string $name, array $arguments): mixed {}
    public function __invoke(mixed $arguments): mixed {}

    public function __clone(): void {}

    public function __serialize(): array {}
    public function __unserialize(array $data): void {}

    public function __sleep(): array {}
    public function __wakeup(): void {}

    public function __debugInfo(): ?array {}

Any magic method implementation must take all parameters in their signature, and if declares a return type, it must be of the same type of a sub type too.

Any declarations otherwise triggers a fatal error:

class BadFoo {
    public function __isset(string $name): array {}
    //                            mismatch ^^^^^ 

// Fatal error: BadFoo::__isset(): Return type must be bool when declared in ... on line ...
class BadBar {
    public function __call(array $name, array $arguments): mixed {}
    //           mismatch  ^^^^^

// Fatal error: BadBar::__call(): Parameter #1 ($name) must be of type string when declared in /in/JpKPd on line 4

Magic methods with no return types allowed

Magic methods that did not allow declaring any return type prior to PHP 8.0 continue to enforce this.

class BadBar {
    public function __construct(): void {}
    public function __destruct(): void {}

Neither of the declarations are allowed in PHP 8.0 and prior versions. They result in a fatal error:

Fatal error: Method BadBar::__construct() cannot declare a return type in ... on line ...
Fatal error: Method BadBar::__destruct() cannot declare a return type in ... on line ...

Stringable interface

PHP 8.0 introduces a new Stringable that is added automatically to any class that implements __toString magic methods. If the Stringable interface is explicitly declared (e.g Foo implements Stringable), the __toString() method signature is enforced with interface rules.

private Magic Methods

PHP 8.0 relaxes signature enforcement when a private method is re-declared in a child class. Magic methods must follow the signature even if they are declared private.

class Foo {
    private function __get(): mixed {}
// Fatal error: Method Foo::__get() must take exactly 1 argument in ... on line ...

Backwards Compatibility Impact

This is backwards-compatibility breaking change. However, the enforced signatures are semantically correct, and allows variance, mitigates potential compatibility issues.

  • If there are no types enforced, the their types will not be checked. However, the methods must accept same number of parameters.
  • mixed type is includes all types in PHP except void, which makes any return type fulfills return type signature.

Related Changes

RFC Discussion Implementation

`substr`, `iconv_substr`, `grapheme_substr` return empty string on out-of-bound offsets


substr, mb_substr, iconv_substr, and graphme_substr functions in PHP provides a way to retrieve a part of the provided string.

Prior to PHP 8, if the offset parameter is longer than the provided string itself, it returned a boolean false. This violated the documented function signature that mentioned string as the return type.

For example, prior to PHP 8, the following snippet returns false on these functions:

substr('FooBar', 42, 3); // false
mb_substr('FooBar', 42, 3); // ""
iconv_substr('FooBar', 42, 3); // false
grapheme_substr('FooBar', 42, 3); // false

In the snippet above, the string offset parameter is 42, although the string itself is only 6 characters. substr and its complement functions (except mb_string) from other extensions return false.

In PHP 8, the behavior of these functions have changed.


substr returns an empty string if the offset is larger than the length of the string. Prior to PHP 8, it returned false.

substr('FooBar', 42); // ""


mb_substr already returns an empty string in all PHP versions, and is not changed in PHP 8.0.


iconv_substr returned false if the offset is larger than the length of the provided string. This is changed in PHP 8 to return an empty string.

iconv_substr('FooBar', 42); // ""

On negative string offsets, iconv_substr function clamps the offset to the length of the string.

iconv_substr('FooBar', -42, 4); // "FooB"


grapheme_substr returns an empty string if the offset is larger than the length of the string. Prior to PHP 8, it returned false similar to substr.

grapheme_substr('FooBar', 42); // ""

This now applies to all the following functions:

substr('FooBar', 42, 3); // ""
mb_substr('FooBar', 42, 3); // ""
iconv_substr('FooBar', 42, 3); // ""
grapheme_substr('FooBar', 42, 3); // ""

The offset parameter can be a negative string, which makes substr return a portion of the string counted from the end of the provided string. If the negative string offset exceeds the length of the string, the cursor will not go beyond that. This functionality was not changed for substr and mb_substr functions.

iconv_substr and grapheme_substr functions now clamp negative offsets to the length of the string, following substr and iconv_substr functions.

PHP < 8.0 PHP >= 8.0
substr('FooBar', 42) false ""
substr('FooBar', -42, 4) "FooB" "FooB"
mb_substr('FooBar', 42) "" ""
mb_substr('FooBar', -42, 4) "FooB" "FooB"
iconv_substr('FooBar', 42) false ""
iconv_substr('FooBar', -42, 4) false "FooB"
grapheme_substr('FooBar', 42) false ""
grapheme_substr('FooBar', -42, 4) false "FooB"

Backwards Compatibility Impact

The return type of substr and iconv_substr functions will now be string, from the previous string|false, which brings them in line with mb_substr function.

If you relied on these functions return value to be false, it will now return an empty string ("") instead, which is arguably the more semantically correct behavior.

This change was made after PHP 8.0 beta 4 (final beta) was released. It will only be effective in PHP 8.0 versions RC1 and later, including GA (general availability) versions. In PHP 8.0 versions prior to RC1, grapheme_substr function was set to throw ValueError exceptions on invalid offsets.

Related Changes


PHP Startup Errors are displayed by default


PHP Startup Errors are emitted when PHP parses the INI files and starts up. These errors are different from standard application errors and their visibility is toggled with display_startup_errors INI directive. .

INI setting display_startup_errors controls whether the startup errors should be displayed.

Prior to PHP 8.0, the INI directive was set to Off, potentially hiding useful information. This value is set to On by default in PHP 8.0.

- display_startup_errors=Off
+ display_startup_errors=On = ""

The INI setting above is invalid, because the must be a non-empty non-numeric string. PHP emits a startup warning if it encounters such value:

# php -d"" test.php

Warning: PHP Startup: cannot be a numeric or empty '' in Unknown on line 0

A few such potential startup errors include:

  • Missing the module file ( Warning: PHP Startup: Unable to load dynamic library ''
  • Invalid value (""): Warning: PHP Startup: cannot be a numeric or empty '' in Unknown on line 0
  • Invalid session.upload_progress.freq value (session.upload_progress.freq=101%): Warning: PHP Startup: session.upload_progress.freq must be less than or equal to 100% in Unknown on line 0

Backwards Compatibility Impact

This is a mere default INI value change. It is possible to set this value back to Off in an INI file (more effective) or with a ini_set call (less effective).

ini_set('display_startup_errors', 'Off');

Related Changes


GD Extension: Windows DLL file name changed from `php_gd2.dll` to `php_gd.dll`


GD Extension: Windows DLL file name changed from php_gd2.dll to php_gd.dll

If you were to copy php.ini a PHP 7 php.ini file to PHP 8, it now needs to use the GD extension with php_gd.dll, or in its extension-less format:

- extension=gd2
+ extension=gd 

In Linux systems such as Ubuntu/Debian, the GD extension is loaded with name throughout all PHP versions, and no changes are necessary.

Further, GD extension now uses GdImage class objects instead of resources as its primary data object.

Backwards Compatibility Impact

Using gd2 as the extension name to load will result in a PHP startup warning (which are shown by default in PHP 8) because the DLL file is now renamed in to php_gd.dll:

Warning: PHP Startup: Unable to load dynamic library 'gd2' (tried: .../ext\gd2 (The specified module could not be found.), .../ext\php_gd2.dll (The specified module could not be found.)) in Unknown on line 0


`crypt()` function requires `$salt` parameter


PHP crypt() function requires its $saltparameter to be passed in PHP 8.0 and forward, changing from its prior behavior of raising a notice when the parameter was not passed.

crypt() function is largely replaced by password_hash() function and provides secure defaults. Not passing a password salt to the crypt() function makes PHP generate one, but due to historical reasons, this salt is insecure by today's security standards. Further, the salt had to adhere to certain patterns to switch certain hashing mechanisms.

Unless your application requires compatibility with other APIs that need to store crypt()-compatible hashes, consider using password_hash/verify/needs_rehash() functions.

Using crypt() function without a specific $salt parameter makes PHP pick a hashing algorithm based on the availability of the mechanisms in the system it runs on, and thus can lead to unexpected (and often insecure) results.

$hash = crypt('p4ssw0rd');

The above snippet results in a PHP notice since PHP >=5.6 and PHP < 8.0:

Notice: crypt(): No salt parameter was specified. You must use a randomly generated salt and a strong hash function to produce a secure hash. in ... on line ...

Since PHP 8.0, the second parameter is required, and thus will result in an ArgumentCountError exception.

ArgumentCountError: crypt() expects exactly 2 parameters, 1 given

Transition to PHP 8

It might be possible to detect the salt PHP automatically generated prior to PHP 8.0, and provide the very same to the crypt() function, so you can verify the password before moving to password_hash() function.

When the $salt parameter is not provided to the crypt() function, a random salt is generated, and returned along the the returned hash.

// $1$N1EHVYY9$qz.BE7oyWWkD85o5uCvO8/

In the example above, $1$N1EHVYY9$ (different in each invocation) is the random salt, and $1$ indicates that the hash is of CRYPT_MD5 type. Various mechanisms such as CRYPT_SHA512 orCRYPT_BLOWFISH will contain different identifiers, but they all will follow the same pattern, that can be captured with the Regular Expression below:


Until your application is using password_hash(), you will be able to temporarily authenticate users

$pattern = '/(^\$.+\$).+/';
$stored_hash = '$1$N1EHVYY9$qz.BE7oyWWkD85o5uCvO8/';
preg_match($pattern, $stored_hash, $matches);
if (empty($matches[1])) {
    return false;// Unknown salt pattern.

$hash = crypt('p4ssw0rd', $matches[1]);
if (!hash_equals($hash, $stored_hash)) {
    return false; // Invalid password.

$stored_hash = password_hash('p4ssw0rd', PASSWORD_DEFAULT);
// Now, store $stored_hash back.

Backwards compatibility impact

What was previously a PHP notice due to missing second parameter to crypt() will result in an ArgumentCountError exception in PHP 8.0.

You can continue to use the crypt() function until you migrate to use password_* functions, as long as the $salt parameter is passed, or extracted from the stored hash as mentioned in the example above.

password_* functions are not affected by this change.

Implementation RFC (2013)

PDO: Default error mode set to exceptions


PHP's PDO extension provides a consistent interface to access databases. it supports various database software, including MySQL, PostgreSQL and SQLite.

For each connection PDO makes to a database, it supports an attribute to specify how PDO should behave when an error occurs with the query, connection, or the database.

PDO::ATTR_ERRMODE attribute controls how PDO should behave on errors.

  • PDO::ERRMODE_SILENT (default): PDO will not raise warnings or throw exceptions in case of an error. It is up to the caller to inspect PDO::errorCode() and PDO::errorInfo() methods on both statement and PDO objects to determine errors.
  • PDO::ERRMODE_WARNING: Emits a warning (E_WARNING) in addition to setting the error code and information.
  • PDO::ERRMODE_EXCEPTION: Throws a PDOException in case of an error.

The error mode attribute can be set when instantiating the PDO object, or after the connection is established.

$pdo = new PDO($dsn, 'root', '', [
$pdo = new PDO($dsn, 'root', '');

Prior to PHP 8.0, the default error mode was to be silent (PDO::ERRMODE_SILENT); PDO did not emit a warning or throw an exception if the database returned an error. Most libraries that deals with database often explicitly set the error mode to PDO::ERRMODE_EXCEPTION because throwing an exception is desired as it minimizes the chances of overlooking database errors.

$pdo = new PDO('mysql:host=loc...', 'root', '');
$pdo->exec("DELETE FROM probably_missing_table WHERE id=42");

The snippet above, for example, tries to delete a record from a table that does not exist. It is desired that that the database error SQLSTATE[42S02]: Base table or view not found: 1146 Table 'phpwatch.myguests' doesn't exist is brought as a PDOException.

In PHP 8.0, the default PDO::ATTR_ERRMODE attribute is set to PDO::ERRMODE_EXCEPTION.

It is still allowed to explicitly set the attribute to PDO::ERRMODE_EXCEPTION. Applications that already used the PDO::ERRMODE_EXCEPTION error mode will not need to make any changes.

Backwards Compatibility Impact

Although not recommended, applications that relied on the PDO's silent behavior on errors can set PDO::ATTR_ERRMODE attribute to PDO::ERRMODE_SILENT. This will make PDO silently ignore all errors throughout the lifespan of that database connection.

$pdo = new PDO($dsn, 'root', '', [

To selectively ignore database errors, it is more appropriate to catch that error instead of setting the PDO::ATTR_ERRMODE attribute throughout the connection.

- $pdo->exec("DELETE FROM probably_missing_table WHERE id=42");
+ try {
+   $pdo->exec("DELETE FROM probably_missing_table WHERE id=42");
+ }
+ catch (PDOException) {}

This snippet uses the PHP 8.0 feature to catch exceptions only by type.

Both approaches above are not ideal, as they ignore errors returned from the database, and ideally should be corrected in the query or the database structure.

RFC Discussion Implementation

`@` Error Suppression operator does not silent fatal errors


PHP supports the @ error control operator (also called STFU operator with mixed feelings), that suppresses errors just for the expression that immediately follows.

For example, the unlink function emits a warning if the file does not exist, and calling it with the @ operator can suppress these errors.


This operator was historically used frequently to access array values (such as HTTP parameters), call certain functions that the outcome does not alter the control flow. While PHP is getting strict with better exception handling, and type checking for internal functions since PHP 8.0, the @ suppression operator is still useful in various areas, such as file system operations.

Related Articles

In PHP 8.0, the @ operator does not suppress certain types of errors that were silenced prior to PHP 8.0. This includes the following types of errors:

All of these errors, if raised, halts the rest of the application from being run. The difference in PHP 8.0 is that the error message is not silenced, which would have otherwise resulted in a silent error.

Related Changes in PHP 8.0

Note that the @ operator continue to silent warnings and notices.

Here is an example that this change in PHP 8.0 makes a difference.

function load_files() {
    require_once 'file-that-does-not-exist.oops';


Prior to PHP 8.0, this snippet would not have emitted any errors or notices (but stopped execution anyway). In PHP 8.0, this error (E_ERROR) is not suppressed, and the execution is stopped too.

Fatal error: Uncaught Error: Failed opening required 'file-that-does-not-exist.oops' (include_path='.:') in ...:...

Another example where this change is more pronounced is with the E_USER_ERROR error type.

function do_something() {
    trigger_error('Something went wrong', E_USER_ERROR);


While PHP versions prior to 8.0 suppresses this E_USER_ERROR, PHP 8.0 and later do not, and result in an error:

Fatal error: Something went wrong in ... on line ...

Error Type Error number @ effective
PHP < 8.0
@ effective
PHP >= 8.0
E_ERROR 1 Yes No
E_PARSE 4 Yes No
E_NOTICE 8 Yes Yes
E_USER_NOTICE 1024 Yes Yes
E_STRICT 2048 Yes Yes
E_ALL 32767 Yes No

The @ operator does not suppress any exceptions in any PHP version. Several PHP versions now throw \ValueError and \TypeError exceptions. See PHP 8.0: Internal function warnings now throw TypeError and ValueError exceptions.

Error handlers set with set_error_handler function will continue to receive errors they subscribe to.

Backwards Compatibility Impact

With this change in PHP 8.0, the @ operator might not suppress all the errors. This is a desired result, because all the error messages that are not silenced also cause the application to halt. Suppressing said errors would not allow the application to continue.

For recoverable errors that emit an E_USER_ERROR, it might be a good alternative to throw an exception instead, which makes it possible for the caller to catch it if possible.

If the pre-PHP 8.0 behavior is required:

  • Adjust the custom error handler to ignore the specific types of error. Error handlers receive the error number as its first argument.
  • Temporarily lower the error_reporting value to 0, before calling the potentially erroneous expression.

An example of a "safe" caller (shall be named "double shrug 🤷🏼‍♀️🤷🏼‍♂️ operator", but can take any name):

function 🤷🏼‍♀️🤷🏼‍♂️(callable $callable): mixed {
    $current_state = error_reporting();
    $return = $callable();
    return $return;

function load_files() {
    require_once 'file-that-does-not-exist.oops';

- @load_files();
+ 🤷🏼‍♀️🤷🏼‍♂️(function() {

To reiterate, both of these approaches will stop the execution of the application; only the error will be hidden.


Trailing commas are allowed in parameter lists and closure `use` lists


PHP 8.0 syntax allows to leave a trailing-comma in parameter lists and closure use lists.

function foo(string $foo, string $bar,) {}

function() use ($foo, $bar,) {


PHP 7.2 eased the syntax to allow trailing commas in grouped use statements for grouped use statements; PHP 7.3 eased the syntax to allow trailing commas in function and method calls, and this change in PHP 8.0 allows trialing commas in function/method declarations and use lists in closures.

class Request {
    public function __construct(  
        UriInterface $uri,  
        HeadersInterface $headers,  
        array $cookies,  
        array $serverParams,  
        StreamInterface $body,  
        array $uploadedFiles = [],  
    ) {
        // ...

Prior to PHP 8.0, this would have caused a parse error due to the disallowed trailing comma in the parameter list, at array $uploadedFiles = [],:

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

Similarly, trailing commas are allowed in PHP 8.0, but resulted in a parse error in earlier versions:

function() use (
    ) {

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

One of the major benefits of this is that diff outputs will be cleaner when new parameters or use variables are appended.

In the snippets above, adding a new parameter or a use variable to the end would produce simpler diff outputs, because the line with last parameter is not modified.

        StreamInterface $body,  
        array $uploadedFiles = [], 
+       array $extraOptions = [],

Without the trailing-commas allowed, the diff output would include the changes in the last line as well:

        StreamInterface $body,
-       array $uploadedFiles = []
+       array $uploadedFiles = [], 
+       array $extraOptions = []

Backwards Compatibility Impact

Due to this being a syntax change, this feature cannot be back-ported.

All code that contains trailing commas in parameter lists, or closure use lists will produce parse errors in versions prior to PHP 8.0:

Using trailing commas in parameter lists in PHP < 8.0

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

Using trailing commas in closure use lists in PHP < 8.0

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

Parameter lists

RFC Implementation

Closure use lists
RFC Implementation

Implicit negative array key increments do not skip negative numbers


PHP 8.0 changes how it increments negative numeric array keys.

Prior to PHP 8.0, if an array has its last key as a negative number (such as -42), an array increment operation skipped all negative numbers and selected the next key as 0 or the next positive number. In PHP 8.0, this behavior is dropped, and array keys will be incremented regardless of the sign of the last key number.

$array = [
    -42 => 'Foo',
$array[] = 'Bar';

Prior to PHP 8.0, the array is incremented to key 0, because the last key is a negative number.

array(2) {
    [-42]=> string(3) "Foo" 
    [0]=> string(3) "Bar" 

In PHP 8.0 and later, this special negative array key treatment is removed, and array keys are incremented by adding 1, regardless of the sign.

array(2) {
    [-42]=> string(3) "Foo" 
    [-41]=> string(3) "Bar" 

This change was planned since PHP 7.3, and was expected to change in PHP 8.0. However, PHP did not emit any deprecation notices or warnings when it encounters such array increments.

All array increment operations are affected. This includes the array_fill and array_push functions.


In PHP 8.0 and later, array_fill function behaves similar to the standard array incremental pattern; that it does not skip negative numbers.

$array = array_fill (-42, 3 , 'Value');

PHP 8.0 and later:

array(3) {
    [-42]=> string(5) "Value"
    [-41]=> string(5) "Value"
    [-40]=> string(5) "Value"

Prior to PHP 8.0, array_fill function skipped all negative numbers and started from 0:

array(3) {
    [-42]=> string(5) "Value"
    [0]=> string(5) "Value"
    [1]=> string(5) "Value"


Similar to array_fill, array_push function also determines the next array key without skipping negative numbers.

$array = [
    -42 => 'Foo',

array_push($array, "Bar");

PHP 8.0 and later:

array(2) {
    [-42]=> string(3) "Foo"
    [-41]=> string(3) "Bar"

Prior to PHP 8.0, the next key as determined by array_push skipped negative numbers to 0 or next positive integer:

array(2) {
    [-42]=> string(3) "Foo"
    [0]=> string(3) "Bar"

Backwards Compatibility Impact

This change breaks backwards-compatibility, and might result in subtle bugs that are not easily detected. However, this change will likely be a harmless change because all array values will be stored regardless of the key value.

Note that in all versions, PHP automatically coerces numeric strings (such as "42") to an int (such as 42) when used as an array key. This happens even if strict types is enabled (declare(strict_types=1)). It is possible that arrays are initiated with negative numbers without realizing it, and with PHP 8.0, direct access to said arrays (such echo $array[0]) can result in unexpected behavior.

Code that relied on PHP's behavior of skipping to 0 will not work as intended in PHP 8.0.

RFC Discussion Implementation

`XMLWriter` objects replace `xmlwriter` resources


As part of PHP's resource to object migration, XMLWriter extensions primitive resource objects are now converted to accept and return the existing XMLWriter objects.

XMLWriter extension provides both an object-oriented interface and a procedural function interface.

  • The XMLWriter class contains all methods to work with the functionality provided by the extension.

  • XMLWriter extensions procedural API, however, used xmlwriter resources and the xmlwriter resource must be passed as the first parameter to each function.

$writer = new XMLWriter();

The same functionality in procedural code:

$writer_resource = xmlwriter_open_memory();  
xmlwriter_start_document($writer_resource, '1.0');`

In PHP 8.0, the procedural-style functions accept and return XMLWriter instead of resource objects.

Object-oriented interface of XMLWriter extension has not changed in PHP 8.0. Furthermore, the procedural API functions are not deprecated.

Because the procedural-style functions accept XMLWriter objects, it is possible to mix the object-oriented and procedural patterns. Note that this can make the code lose its consistency at no benefit. However, this can ease the migration to object-oriented API as the previous resource objects are inter-changeable with the XMLWriter objects


Note that return values from the procedural API of XMLWriter extension will be standard PHP class objects, and will no longer return true for is_resource function calls.

It might be necessary to update existing code to accept XMLWriter objects as well.

- is_resource($xw)
+ is_resource($xw) || $xw instanceof XMLWriter

However, this is an anti-pattern, because failing to create an xmlwriter resource results in a false return value in all PHP versions. A check against false might be more appropriate:

- is_resource($xw) || $xw instanceof XMLWriter
+ !empty($xw)

Backwards Compatibility Impact

Unless there are is_resource checks, this resource to object migration is completely opaque.

The object-oriented API continue to work, and the procedural API is not deprecated either. It not handles XMLWriter objects that can be called interchangeably, as opposed to procedural-specific xmlwriter resources.


OpenSSL: `resource` to object migration


Along with PHP's resource to object transformation, PHP's OpenSSL extension changes its resource objects to standard PHP class objects in PHP 8.0.

The newly added class objects are not allowed to be instantiated with new OpenSSL...() constructs, and must be instantiated with the same functions that returned corresponding resource objects in prior PHP versions. Further, they are declared as final, which prevents them from being extended in a sub class.

OpenSSL key resources to OpenSSLAsymmetricKey objects

OpenSSL key resource type (produced by openssl_pkey_new function) is changed to OpenSSLAsymmetricKey class objects.

In PHP 8.0, openssl_pkey_new function returns an instance of OpenSSLAsymmetricKey class instead of OpenSSL key resources. All functions that previously accepted resources now accept the class objects as well.

OpenSSLAsymmetricKey class synopsis

final class OpenSSLAsymmetricKey {}

Instantiating a new object with new OpenSSLAsymmetricKey() construct will raise an error:

new OpenSSLAsymmetricKey();
PHP Error: Cannot directly construct OpenSSLAsymmetricKey, use openssl_pkey_new() instead in ... on line ...

OpenSSL X.509 resources to OpenSSLCertificate objects

In PHP 8.0 and later, openssl_x509_read function returns OpenSSLCertificate class objects instead of OpenSSL X.509 resources it did in PHP versions prior to PHP 8.0.

All functions that accepted/returned OpenSSL X.509 resources now accept/return OpenSSLCertificate objects instead.

OpenSSLCertificate class synopsis

final class OpenSSLCertificate {}

Instantiating a new object with new OpenSSLCertificate() construct will raise an error:

new OpenSSLCertificate();
PHP Error: Cannot directly construct OpenSSLCertificate, use openssl_x509_read() instead in ... on line ...

OpenSSL X.509 CSR resources to OpenSSLCertificateSigningRequest objects

openssl_csr_new returns OpenSSLCertificateSigningRequest objects in PHP 8.0 and later. This function returned OpenSSL X.509 CSR resources in prior PHP versions.

All functions that accepted OpenSSL X.509 CSR resource parameters now accept OpenSSLCertificateSigningRequest objects instead.

OpenSSLCertificateSigningRequest class synopsis

final class OpenSSLCertificateSigningRequest {}

Instantiating a new object with new OpenSSLCertificateSigningRequest() construct will raise an error:

new OpenSSLCertificateSigningRequest();
PHP Error: Cannot directly construct OpenSSLCertificateSigningRequest, use openssl_csr_new() instead in in ... on line ...

is_resource calls

is_resource function will no longer return true on return values of any of the OpenSSL extension functions.

Existing code that used is_resource function to check if the provided value is a valid OpenSSL resource will now need to check against the class names (OpenSSLAsymmetricKey, OpenSSLCertificate, and OpenSSLCertificateSigningRequest, depending on the use case) as well.

Using $value !== false can be used to check the validity as well, and works across all PHP versions.

Deprecation and resource freeing

In PHP 8.0, the following functions are deprecated.

Both of these functions are deprecated because the new objects automatically close the internal handlers when the objects fall out of scope, or when the request terminates.

Backwards Compatibility Impact

All OpenSSL functions return and accept class objects in lieu of the resource counter-parts in older PHP versions.

Unless there are is_resource calls on OpenSSL extensions resource types (pre-PHP 8.0), this update should not introduce any issues.

Note that openssl_pkey_free and openssl_x509_free functions a deprecated in PHP 8.0, and causes deprecation warnings in PHP 8.0. In code that must be backwards-compatible on PHP versions prior to 8.0, those functions needs to be called conditionally to avoid the deprecation notice.

- openssl_pkey_free($key);
+ if (\PHP_VERSION_ID < 80000) {
+   openssl_pkey_free($key);


`XMLParser` objects replace `xml` resources


As part of PHP's resource to object migration, XML Parser extensions primary resource types are migrated to XMLParser class objects in PHP 8.0.

Prior to PHP 8.0, xml_parser_create and xml_parser_create_ns functions returned a resource of type xml. In PHP 8.0, all XML Parser functions return and accept XMLParser objects.

XMLParser class synopsis

final XMLParser {}

xml_parser_create and xml_parser_create_ns functions must be used to instantiate XMLParser objects. Directly instantiating with new XMLParser construct is not allowed, and results in an error:

new XMLParser();
PHP Error: Cannot directly construct XmlParser, use xml_parser_create() or xml_parser_create_ns() instead in ... on line ...

is_resource calls

The return values of xml_parser_create and xml_parser_create_ns functions are standard PHP class objects, and are no longer resource objects.

is_resource function no longer returns true for the return values of said functions.

Destroying XMLParser objects

It is no longer necessary to call xml_parser_free that was required in previous PHP versions to avoid memory leaks.

xml_parser_free function is not deprecated, and calling it (for backwards-compatibility) will not cause any issues.

Note that it may be necessary to explicitly destroy XML parser objects (unset($parser)) in case the objects might not fall out of scope for the garbage collector to clean them. PHP 8's Weak Maps might help to associate additional data for XML Parser objects without objecting the garbage collector.

Backwards Compatibility Impact

Unless there are is_resource calls, there should be no backwards-compatibility issues. In PHP 8.0, all XML Parser functions accept XMLParser objects instead of resource objects.


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.

Related Changes

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.

Resource to Object Migration
PHP is gradually phasing out all resource types with class objects, and this migration is one step of the Resource to Object Migration plan.

Init functions 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 these newly added classes, CurlHandle, CurlMultiHandle, and CurlShareHandle follow resource-to-object semantics.

  • Directly instantiating objects is not allowed. For example, new CurlHandle will result in an error:

    new CurlHandle();
    new CurlMultiHandle();
    new CurlShareHandle();
    Cannot directly construct CurlHandle, use curl_init() instead in ... on line ...
    Cannot directly construct CurlMultiHandle, use curl_init() instead in ... on line ...
    Cannot directly construct CurlShareHandle, use curl_share_init() instead in ... on line ...
  • All classes are declared final. Extending them is not allowed:

    class Foo extends CurlHandle {}
    Class Foo may not inherit from final class (CurlHandle) in ... on line ...
  • No dynamic properties are allowed. Attempting to set a dynamic property will result in an error:

    $ch = curl_init();
    $ch->foo = 'Bar';
    Cannot create dynamic property CurlHandle::$foo in ... on line ...

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.


`GdImage` class objects replace GD image resources


From PHP 8.0 and forward, GD extension uses \GdImage class objects instead of resource s for its underlying data structures.

This is similar to Curl extension using \CurlHandle objects as opposed to resource in PHP 8.0.

\GdImage class

final class GdImage{
  • \GdImage is declared in global namespace.
  • Declared final.
  • Has no methods.
  • Cannot be instantiated with new \GdImage().
  • Cannot be cloned with clone $image.
  • Cannot be serialized.

GD functions return and accept \GdImage objects instead of resource

All GD functions that accepted resource values parameters prior to PHP 8.0 will now accept \GdImage objects. Similarly, their return values will be \GdImage objects instead of resource.

is_resource() calls

Note that all functions in GD extension now returns and expects \GdImage objects instead of resource objects. Unless you explicitly call is_resource() function on return values of GD functions (such as imagecreatefromjpeg() and imagecreate()), this change will be completely trouble-free.

If you are calling is_resource, you will now need to account for the \GdImage object return type too.

- if (is_resource($image)) {
+ if (is_resource($image) || $image instanceof \GdImage) {

A previous version of this post recommended to use is_object before instanceof calls on PHP versions prior to 7.2. It is not necessary to use is_object calls with instanceof operator as long as they are not literals, as shown in the example above.

imagedestroy() is no-op

imagedestroy() function is still available in PHP 8.0, but it does nothing. Memory and other resources will be freed when the \GdImage object is removed either with an explicit unset() call or when it falls out of scope.

Gd font

The remaining GD font resource is migrated to a class object in PHP 8.1: Font identifiers are \GdFont class objects.

Backwards Compatibility Impact

GD extension's resource to object migration is quite transparent. It will not be necessary to polyfill the new GdImageclass because the internal nature of this change.

Existing code that uses is_resource() will now need either change to check the class name of the object to support PHP 8.0.


Assertions throw exceptions by default


PHP supports Assertions with the assert() language construct since PHP 4. Until PHP 8.0, a failed assertion raised a warning by default, but not an exception.

assert(true === false);
// Warning: assert(): assert(true === false) failed in ... on line ...

To make the engine throw an exception, an INI option is needed:


In PHP 8.0, the default value for assert.exception INI value is changed from 0 to 1. This means failed assertions will throw an AssertionError exception by default.

assert(true === false);
// Fatal error: Uncaught AssertionError: assert(true === false) in ...:...

assert.warning INI value remains 1, but assert.exception=1 means an exception will be throw instead of a warning.

Assertion callback

There are no changes in the Assertion failure callback functionality.
Assertion callback will continue to receive the same information.

;INI configuration:
// assert_options() can be used instead of an INI configuration.
// assert_options(ASSERT_CALLBACK, 'assert_fail');
assert(true === false);

function assert_fail(string $file, int $line , string $assertion, string $description = ''): void {
    // Do something with the failed assertions.

Assertion failure callback will not receive the AssertionError exception. Standard exception handling can be used to catch AssertionError exceptions. This is the same behavior as previous versions.

It is also possible to use ASSERT_CALLBACK/assert.callback callback without any further warnings or exceptions with an INI setting:


Backwards Compatibility Impact

Assertions are not meant to be used in production code. Throwing an exception by default in PHP 8.0 means assertion failures will be front and center.

Restoring the original functionality, although not recommended, is possible with an INI setting.:


Implementation Discussion RFC (abandoned, 2019)

Sockets extension resources (`Socket` and `AddressInfo`) are class objects


In PHP 8.0 and later, all functions from Sockets extension return/accept objects of type \Socket and \AddressInfo class instead of resource of type Socket and AddressInfo.

This is similar to Curl extension using \CurlHandle objects as opposed to resource in PHP 8.0, and GD extension using \GdImage objects as opposed to gd resources.

Note that this only applies to socket_*() functions from ext/sockets. stream_socket_* functions (which return stream resources) are not affected.

\Socket class

final class Socket{

\Socket class objects replace return values of functions that formerly returned a resource of type Socket:

  • socket_accept()
  • socket_addrinfo_bind()
  • socket_addrinfo_connect()
  • socket_create()
  • socket_create_listen()
  • socket_import_stream()
  • socket_wsaprotocol_info_import()

Socket resources can be closed with the socket_close() function, which is still necessary in PHP 8.0 as well to promptly close an open socket.

\AddressInfo class

final class AddressInfo {

An array of \AddressInfo class objects will be returned by socket_addrinfo_lookup() function, which formerly returned an array of AddressInfo resources.

\Socket and \AddressInfo class semantics

  • Both classes are declared in the global namespace.
  • Declared final.
  • Has no methods.
  • Cannot be instantiated with new \Socket() and new AddressInfo().
  • Cannot be serialized.
  • Cannot be cloned.

Attempting to instantiate a \Socket object will throw a fatal error:

PHP Error:  Cannot directly construct Socket, use socket_create() instead in ... on line ...

socket_*() parameters and return values

All functions from Sockets extension will return/accept the new \Socket and \AddressInfo class objects.

is_resource() calls

All functions that create resources/class objects from the Sockets extension return false if the operation was unsuccessful.
Although not necessary, using is_resource() on the return value of such functions is quite common.

If you are calling is_resource, you will now need to account for the \Socket object return type too.

- if (is_resource($socket)) {
+ if (is_resource($socket) || $socket instanceof \Socket) {

It is also completely valid to check the return value of functions that return resources to check the return value for false.

- if (is_resource($socket)) {
+ if ($socket !== false) {

Backwards Compatibility Impact

Socket extensions resource to object transformation is quite seamless. Unless you have an is_resource() call that checks a passed/return value, it will not require any changes in PHP 8.0.


PostgreSQL: Several aliased functions are deprecated


PHP 8 deprecates several function aliases in PostgreSQL extension. These deprecated functions do not appear in the PHP documentation today, and all code that appear to use them are from the PHP 4 era.

However, there are several positive search results on GitHub, which hints of possible deprecation notices in PHP 8.0.

A total of 24 functions are deprecated, all of which were not documented. Last appearance of these functions in documentation appears to be from 2001.

Deprecated Function Replace with
pg_clientencoding pg_client_encoding
pg_cmdtuples pg_affected_rows
pg_errormessage pg_last_error
pg_fieldisnull pg_field_is_null
pg_fieldname pg_field_name
pg_fieldnum pg_field_num
pg_fieldprtlen pg_field_prtlen
pg_fieldsize pg_field_size
pg_fieldtype pg_field_type
pg_freeresult pg_free_result
pg_getlastoid pg_last_oid
pg_loclose pg_lo_close
pg_locreate pg_lo_create
pg_loexport pg_lo_export
pg_loimport pg_lo_import
pg_loopen pg_lo_open
pg_loread pg_lo_read
pg_loreadall pg_lo_read_all
pg_lowrite pg_lo_write
pg_numfields pg_num_fields
pg_numrows pg_num_rows
pg_result pg_fetch_result
pg_setclientencoding pg_set_client_encoding

Backwards Compatibility Impact

Deprecated functions above will raise a deprecation notice:

Deprecated: Function pg_numrows() is deprecated in ... on line ...

A simply function swap with canonical function will be the straight-forward fix. All the canonical functions are already available in PHP versions 4.2 through 8.0 and future.


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.

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.


`libxml_disable_entity_loader` function is deprecated


XML standard allows external entities, which can refer to other external resources, which often leads severe security vulnerabilities commonly categorized as XXE, or XML EXternal Entities.

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
  <!ELEMENT foo ANY >
  <!ENTITY xxe SYSTEM "file:///etc/passwd" >]>

In a text-book example above, the payload above will refer to the /etc/passwd file in the system the XML file is parsed.

Libxml, the library PHP uses to process XML in extensions such as DOM, XMLWriter and XMLReader was vulnerable to XXE attacks unless loading of external entities was disabled:


Calling libxml_disable_entity_loader() with a true/false value allowed to toggle disabling of this feature.

In PHP 8.0 and later, PHP uses libxml versions from 2.9.0, which disabled XXE by default. libxml_disable_entity_loader() is now deprecated.

Backwards Compatibility Impact

Attempting to call this function will now raise a warning:

Deprecated: Function libxml_disable_entity_loader() is deprecated in ... on line ...

On code that only runs on PHP 8.0 and later, it is now safe to remove all function calls.
For versions prior to PHP 8, a conditional call might be the best approach:

- libxml_disable_entity_loader(true);
+ if (\PHP_VERSION_ID < 80000) {
+      libxml_disable_entity_loader(true);
+ }

Alternately, LIBXML_VERSION < 20900 condition can be used for conditional entity loader toggle:

- libxml_disable_entity_loader(true);
+ if (\LIBXML_VERSION < 20900) {
+      libxml_disable_entity_loader(true);
+ }

Note that in libxml < 2.9.0, external entity loading is enabled by default. Removing libxml_disable_entity_loader(true) calls on environments that run PHP versions < 8.0 will open an XXE vulnerability.

Implementation & Discusion Discussion (2015 July)

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.