PHP 8.1: What's New and Changed

Version StatusFuture Release

PHP 8.1 is currently in active development, and it will be released towards the end of year 2021.

Enums

TypeNew Feature

PHP 8.1 adds support for Enumerations. An Enumeration, or an Enum for short, is an enumerated type that has a fixed number of possible values.

RFC in progress.
This RFC is still being ironed out with some implementation details. This post will be actively updated, and this noticed removed once the Enum feature is merged to PHP.

A popular analogy for an Enum is suits in a deck of playing cards. A deck of playing cards has four suits, and they are fixed: Clubs, Diamonds, Hearts, and Spades.

In PHP, these suits can be enumerated with an Enum:

enum Suit {
    case Clubs;
    case Diamonds;
    case Hearts;
    case Spades;
}

With Suit Enum, it is now possible to enforce types when accepting or returning a suit value:

function pick_card(Suit $suit) {}
pick_card(Suit::Clubs);
pick_card(Suit::Diamonds);
pick_card(Suit::Hearts);
pick_card(Suit::Spades);

In contrast to using special strings or numbers internally (i.e. magic numbers) to store and work with parameters, Enums make the application code more readability, and avoids unexpected application state.

Enum Syntax

PHP 8.1 reserves and uses enum keyword to declare Enums. The syntax is similar to a trait/class/interface syntax:

enum Suit {
    case Clubs;
    case Diamonds;
    case Hearts;
    case Spades;
}

Enums are declared with the enum keyword, followed by the name of the Enum. An Enum can optionally declare string or int as backed values. Enums can also extend a class and/or implement interfaces.

Internally at the PHP parser level, there is a new token with named T_ENUM with value 369 assigned.

Enums can also hold a value for each case, which makes them Backed Enums.

enum HTTPMethods: string {
    case GET = 'get';
    case POST = 'post';
}

Following is an example of an Enum that declares backed value type, and implements an interface.

enum RolesClassLikeNamespacedEnum: string implements TestFor {  
  case Admin = 'Administrator';  
  case Guest = 'Guest';  
  case Moderator = 'Moderator';  
}

New enum_exists function

PHP 8.1 also adds a new enum_exists function to check if a given Enum exists.

function enum_exists(string $enum, bool $autoload = true): bool {}

Note that due to class-semantics of Enums, class_exists function also returns true for an Enum.

UnitEnum interface

Enums that are not backed with a value automatically implement UnitEnum interface.

interface UnitEnum {
    public static function cases(): array;
}   

Enums cannot explicitly implement this interface as it is internally done by the engine. This is only to assist in determining the type of a given Enum. The UnitEnum::cases method returns an array of all cases of a given Enum.

PHP classes are not allowed to implement this interface, and results in an error if attempted:

class FakeEnum implements UnitEnum {}
Fatal error: Non-enum class FakeEnum cannot implement interface UnitEnum in ... on line ...

Although Enums allow methods declared on them, declaring a method with name cases is not allowed:

enum Foo {
    public function cases(): array{}
}
Fatal error: Cannot redeclare Foo::cases() in ... on line ...

BackedEnum interface

If an Enum declares scalar backed values, that Enum automatically an interface called BackedEnum. Similar to UnitEnum interface, it is not possible to explicitly implement BackedEnum interface.

interface BackedEnum extends UnitEnum {
    public static function from(int|string $value): static;
    public static function tryFrom(int|string $value): ?static;
}

See BackedEnum::from and BackedEnum::tryFrom for their usage information.

Standard classes are not allowed to implement this interface.

class FakeEnum implements BackedEnum {}
Fatal error: Non-enum class FakeEnum cannot implement interface BackedEnum in ... on line ...

Similar to the restriction on not allowing a method with name cases is not allowed, any backed Enum must not declare a from or tryFrom method:

enum Foo: int {
    public function from() {}
    public function tryFrom() {}
}
Fatal error: Cannot redeclare Foo::from() in ... on line ...
Fatal error: Cannot redeclare Foo::tryFrom() in ... on line ...

Declaring Enums

Enums are internally implemented on top PHP classes, and they inherit most of the class semantics with additional restrictions imposed.

Enums support namespaces, autoloading, they can have methods (but not properties), implementing interfaces, and many other behaviors associated with PHP classes.

A basic enum is simply an enum structure, where each case is declared with the case keyword. With Enums supported in PHP 8.1, PHP now reserves "enum" as a reserved word, and prevents any functions, classes, interfaces, etc. from being created with enum. It can be part of a namespace due to changes in how PHP considers reserved keywords in namespaced values.

Enums can have zero or more cases

Within an enum structure, it may contain any number of "cases", from zero to unlimited. Both of these Enum declarations are valid:

enum ErrorStates {
}
enum HTTPMethods {
    case GET;
    case POST;
}

Enums may have optional values

It is possible to assign a string or int value to each case in an Enum. This can be useful when serializing data, storing them in a database, etc.

Enums that hold a value, i.e. a backed Enum, must:

  1. Declare the scalar type in the Enum declaration. Only string or int is allowed.
  2. Assign values for all cases.
  3. Hold values of same scalar type. It is not allowed to store string and int values mixed.
  4. Cases and assigned values are unique
enum Suit: string {
    case Clubs = '♣';
    case Diamonds = '♦';
    case Hearts = '♥';
    case Spades = '♠';
}

Backed Enums must declare the scalar type

In order for an Enum to be to associate values for each case (i.e. a Backed Enum), it must declare the scalar type at the Enum declaration.

Not doing so results in an error:

enum HTTPMethods {
    case GET = 'get';
    case POST = 'post';
}
Fatal error: Case GET of non-scalar enum HTTPMethods must not have a value in ... on line ...

Enums only support string and int scalar types. Any other type, including bool, null, ?string, ?int, or even string|int union types are not allowed.

enum HTTPMethods: object {
    case GET = 'get';
    case POST = 'post';
}
Fatal error: Enum scalar type must be int or string, object given in ... on line ...

Backed Enums must assign values for all cases

This section is work-in-progress

If an Enum declares the scalar type of values, it must set a value for all cases:

enum HTTPMethods: string {
    case GET;
    case POST;
}

In the snippet above, HTTPMethods Enum is declared to contain string, but the cases are not assigned a value. This is not allowed, and results in an error.

Backed Enums must hold values of same scalar type

With a Backed Enum, the values assigned to each case must be of the same type as declared in the type.

PHP strictly enforces this even with strict_types not enabled.

enum HTTPMethods: int {
    case GET = 1;
    case POST = '2';
}
Fatal error: Enum case type string does not match enum scalar type int in ... on line ...

Backed Enum cases and values must be unique

This section is work-in-progress

A valid Enum must not contain duplicate cases, or duplicate values. For example, both of these declarations are invalid:

enum Test {
    case FOO;
    case FOO;
}
enum Test: string {
    case FOO = 'baz';
    case BAR = 'baz';
}

Enum values must be unique as well, because BackedEnum::from supports retrieving an Enum object from a given scalar value.

Case-Sensitivity

The name of the Enum itself is case-insensitive, and it follows how PHP treats classes and functions in a case-insensitive manner.

Individual cases in an Enum are case-insensitive.

enum CaseInSenSitive {
    case bAR;
    case Bar;
}

Class Semantics in Enums

Namespaces

Enums support namespaces. They follow the standard namespace syntax that is otherwise used in classes, traits, interfaces, functions, etc.

namespace Foo\Bar;

enum HTTPMethods {}

Autoloading

Just like PHP supports autoloading for classes, traits, and interfaces, Enums support autoloading as well.

Note that this might require updates to class-map generators that scan files for autoloadable items.

Magic constants

Enums fully support all magic constants that PHP supports for classes.

  • ::class constant that refers to the name of the Enum itself.
  • __CLASS__ magic constant that refers to the name of the Enum from within the Enum.
  • __FUNCTION__ in Enum method context.
  • __METHOD__ in Enum method context.

Class/object functions and instanceof

Enums behave similar to classes when they are used with functions that support inspecting classes and objects.

For example, gettype, is_a, is_object, get_class and get_debug_type (new in PHP 8.0) functions behave as if an Enum is a standard PHP object.

enum Suit {
    case Clubs;
    case Diamonds;
    case Hearts;
    case Spades;
}

gettype(Suit::Clubs); // "object"

is_object(Suit::Spades); // true
is_a(Suit::Clubs, Suit::class); // true

get_class(Suit::Clubs); // "Suit"
get_debug_type(Suit::Clubs); // "Suit"

Suit::Clubs instanceof Suit; // true
Suit::Clubs instanceof UnitEnum; // true
Suit::Clubs instanceof object; // false

Enums with Properties, Methods, and Traits

Enums are made in a way that it can compare one Enum case with another. Enums must be stateless, as in they do not allow storing properties in them.

Enums allow methods

Enums can contain methods. They also support standard method visibility modifiers as well as static methods.

This can be quite useful use cases such as declaring a label(): string method that returns a user-friendly label.

enum HTTPStatus: int {
    case OK = 200;
    case ACCESS_DENIED = 403;
    case NOT_FOUND = 404;

    public function label(): string {
        return static::getLabel($this);
    }

    public static function getLabel(self $value): string {
        return match ($value) {
            HTTPStatus::OK => 'OK',
            HTTPStatus::ACCESS_DENIED => 'Access Denied',
            HTTPStatus::NOT_FOUND => 'Page Not Found',
        };
    }
}

echo HTTPStatus::ACCESS_DENIED->label(); // "Access Denied"
echo HTTPStatus::getLabel(HTTPStatus::ACCESS_DENIED); // "Access Denied"

The snippet above uses match expressions added in PHP 8.0

Enums can implement interfaces

Enums can implement interfaces. Enums must fulfill the contracts of the interfaces just like standard class must.

interface Foo {}
enum FooEnum implements Foo {}

Enums must not contain properties

One of the most important differences between an Enum and a class is that Enums are not allowed to have any state. Declaring properties, or setting properties is not allowed. static properties are not allowed either.

enum Foo {
    private string $test;
    private static string $test2;
}
Fatal error: Enums may not include member variables in ... on line ...

Further, dynamically setting properties is not allowed either:

enum Foo {
    case Bar;
}
$bar = Foo::Bar;
$bar->test = 42;
Error: Enum properties are immutable in ...:...

Instantiating with new is not allowed

Although Enum cases themselves are objects, it is not allowed to instantiate them with the new construct.

Both of the following new constructs are not allowed:

enum Foo {
    case Bar;
}

new Foo(); // Fatal error: Uncaught Error: Cannot instantiate enum Foo
new Foo::Bar(); Parse error: syntax error, unexpected identifier "Bar", expecting variable or "$"

Enums cannot be extended, and must not inherit

This section is work-in-progress

Enums are internally declared as final, and Enum may not inherit from another Enum or a class.

enum Foo extends Bar {}
Parse error: syntax error, unexpected token "extends", expecting "{" in ... on line ...

If a class attempts to extend an Enum, that will result in an error as well because all Enums are declared final.

enum Foo {}
class Bar extends Foo {}
Fatal error: Class Bar may not inherit from final class (Foo) in ... on line ...

Enums support property-less traits

Enums can use traits, as long as the trait does not declare any properties.

trait NamedDocumentStatus {
    public function getStatusName(): string {}
}

enum DocumentStats {
    use NamedDocumentStatus;

    case DRAFT;
    case PUBLISHED;
}

If the traits used contains any properties (static or otherwise), using that trait results in a fatal error:

trait NotGood {
    public string $value;
}
enum Foo {
    use NotGood;
}
Fatal error: Enum "Foo" may not include properties in ... on line ...

Disallowed magic methods

To prevent Enum objects from having any state, and to ensure two Enums are comparable, Enums disallow implementing several magic methods:

  • __get(): To prevent maintaining state in Enum objects.
  • __set(): To prevent dynamic property assignment and maintaining state.
  • __construct(): Enums do not support new Foo() construct all.
  • __destruct(): Enums must not maintain state.
  • __clone(): Enums are uncloneable objects.
  • __sleep(): Enums do not support life-cycle methods.
  • __wakeup(): Enums do not support life-cycle methods.
  • __set_state(): To prevent coercing state to Enum objects.

All of the following magic method declarations are not allowed.

enum Foo {
    public function __get() {}
    public function __set() {}
    public function __construct() {}
    public function __destruct() {}
    public function __clone() {}
    public function __sleep() {}
    public function __wakeup() {}
    public function __set_state() {}
}

If they are declared, it will trigger a fatal error with a message similar to this:

Fatal error: Enum may not include __get in ... on line ...

Classes vs Enums

Enums store fixed values, with optional backed values, and they do not allow state. They serve distinctly different purposes, but Enums share some semantics with classes.

Classes Enums
Syntax class Foo {} enum Foo {}
Properties
Static Properties
Methods
Static Methods
Autoloading
Instantiating: new Foo()
Implement interfaces
Inheritance: Foo extends Bar
Magic Constants: ::class, __CLASS__, etc.
Object Comparison Foo === Foo Not equal Equals
Traits Supports Supports without properties

Using Enums

The main use case of Enums is type safety. The PHP engine will ensure that a passed or returned value is belongs to one of the allowed values. Without having to validate passed values belongs to one of the allowed types, the PHP engine enforces it, and allows IDEs and static analyzers to highlight potential bugs as well.

With Backed Enums, it is possible to store the enumerated values in a database or other storage, and restore a programmatically identical object.

Comparing Enum Values

At any given time, two Enums holding the same value is considered identical, just as how PHP considers two strings to be identical. This is a major difference between two objects, because two objects from an instantiated class are not considered identical even though they hold exact same values.

new stdClass() === new stdClass(); // false
Suit::Hearts === Suit::Hearts; // true

Enums as Parameter, Property and Return Types

Enums can be used as parameter, return, and property types. When the Enum name is used, the passed/set/return values must be one of the Enumerated values.

enum Suit {
    case Clubs;
    case Diamonds;
    case Hearts;
    case Spades;
}
function play(Suit $suit, string $value) {}

The play function accepts an enumerated value of Suit, and passing any other value will result in a \TypeError. This can greatly improve the code because PHP validates the passed values without having to write additional validations inside the play function.

play(Suit::Clubs, 'J');
play(Suit::Hearts, 'Q');

If any value other than a Suit Enum value is passed, it will cause a type error:

play('clubs', 'J');
TypeError: play(): Argument #1 ($suit) must be of type Suit, string given

It does not allow to use values from another Enum either:

play(UnoColors::Blue, 'J');
TypeError: play(): Argument #1 ($suit) must be of type Suit, UnoColors given

Further, if an undefined Enum value is used, it will cause an undefined class constant because Enum values and class constants share the same syntax.

Enum name and value properties

Each Enum contains a name property that refers to the name of the property. This value is read-only.

enum Suit {
    case Clubs;
    case Diamonds;
    case Hearts;
    case Spades;
}

echo Suit::Clubs->name; // "Clubs"

In a Backed Enum, there is also value property for the backed value (either string or int).

enum Suit: string {
    case Clubs = '♣';
    case Diamonds = '♦';
    case Hearts = '♥';
    case Spades = '♠';
}

echo Suit::Clubs->name; // "Clubs"
echo Suit::Clubs->value; // "♣"

Attempting to change name or value properties will result in an error:

Suit::Clubs->name = 'Hearts';
Error: Enum properties are immutable in ...:...

Further, Enums without a backed value (UnitEnum) do not have a value property. Only Backed Enums (BackedEnum) have value property. Attempting to use an undefined property (including value) currently raises a warning in PHP 8.1.

enum Suit {
    case Clubs;
    case Diamonds;
    case Hearts;
    case Spades;
}

echo Suit::Clubs->value;
Warning: Undefined property: Suit::$value in ... on line ...

Getting All Enum Values

Both UnitEnum and BackedEnum Enums support a ::cases method, that returns all values of the Enum.

enum Suit {
    case Clubs;
    case Diamonds;
    case Hearts;
    case Spades;
}

Suit::cases();
// [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spaces]

The Suit::cases() method returns an array of all possible Enums. The return values are Enum objects themselves, and not the name or value properties.

Getting Enum by a backed value

All Backed Enums (BackedEnum) support from and tryFrom methods that allow retrieving an instance from the backed value.

enum Suit: string {
    case Clubs = '♣';
    case Diamonds = '♦';
    case Hearts = '♥';
    case Spades = '♠';
}

$clubs = Suit::from('♥');

var_dump($clubs); // enum(Suit::Hearts)
echo $clubs->name; // "Hearts";
echo $clubs->value; // "♥"

If there is no Enum by that value, PHP will throw a ValueError exception.


There is also a tryFrom method, that returns null if an Enum does not exist by that value.

enum Suit: string {
    case Clubs = '♣';
    case Diamonds = '♦';
    case Hearts = '♥';
    case Spades = '♠';
}

$clubs = Suit::tryFrom('not-existing');

var_dump($clubs); // null

Backwards Compatibility Impact

Enums use a new syntax, and it is not possible to use Enums in PHP versions prior to 8.1.

Native PHP approaches such as myclabs/php-enum are not compatible with PHP 8.1 Enums.

The Enum syntax will result in a ParserError in PHP < 8.1.


RFC Discussion Implementation

Curl: File uploads from strings with `CURLStringFile`

TypeNew Feature

PHP Curl extension supports making HTTP(s) requests with file uploads. Since PHP 5.5, PHP Curl extension provides a CURLFile class that accepts a URI or a path to a file, a mime type, and a file name to upload it as.

With CURLFile class, it accepts a path or a URI, and not the contents itself. For files already stored on disk or any other stream, using CURLFile class is quite straight-forward:

$txt_curlfile = new \CURLFile('test.txt', 'text/plain', 'test.txt');

$ch = curl_init('http://example.com/upload.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, ['file' => $txt_curlfile]);
curl_exec($ch);

Using CURLFile was difficult if the file being uploaded is already stored in the memory, or not written to disk. It was possible to use data:// URIs with Base64 encoding, but it was not straight forward.

$txt = 'test content';
$txt_file = 'data://application/octet-stream;base64,' . base64_encode($txt);
$txt_curlfile = new \CURLFile($txt_file, 'text/plain', 'test.txt');

$ch = curl_init('http://example.com/upload.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, ['file' => $txt_curlfile]);
curl_exec($ch);

From PHP 8.1 and later, Curl extension has a new CURLStringFile class, that works similar to CURLFile class, but accepts the contents of the file instead of a path or a URI.

$txt = 'test content';
$txt_curlfile = new \CURLStringFile($txt, 'text/plain', 'test.txt');

$ch = curl_init('http://example.com/upload.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, ['file' => $txt_curlfile]);
curl_exec($ch);

The new CURLStringFile class makes it easy to make a Curl file upload request using data that is already stored in memory. Some example use cases include uploading an image that PHP processes and is stored in a PHP variable, a processed XML file, or a PDF file generated from a PHP library.

CURLStringFile class synopsis

class CURLStringFile {
    public string $data;
    public string $postname;
    public string $mime;

    public function __construct(string $data, string $postname, string $mime = "application/octet-stream") {}
}
  • Unlike CURLFile class, CURLStringFile class objects allow serializing and unserializing.
  • CURLStringFile objects can be clone. This is true for CURLFile instances as well.
  • CURLStringFile class enforces typed properties. It is not allowed to set values other than string values.

CURLStringFile Polyfill

Curl extension treats CURLFile class objects as a file upload, and it treats the same way for all classes that extend CURLFile.

Taking advantage of that, it is possible to create a polyfill for CURLStringFile.

class CURLStringFile extends CURLFile {
    public function __construct(string $data, string $postname, string $mime = "application/octet-stream") {
        $this->name     = 'data://'. $mime .';base64,' . base64_encode($data);
        $this->mime     = $mime;
        $this->postname = $postname;
    }
}

Internally, the polyfilled CURLStringFile class behaves exactly same as a CURLFile class with a data:// URI, but it is not handled inside CURLStringFile::__construct hiding the complexity, and working as a polyfill.

The polyfill above should work in all PHP versions >= 7.0.

Backwards Compatibility Impact

CURLStringFile is a new class from the Curl extension, and it will not be possible to use it in PHP versions older than PHP 8.1. However, it is possible to polyfill this class to older PHP versions.


Implementation

Array unpacking support for string-keyed arrays

TypeNew Feature

Since PHP 7.4, PHP supports array spread operator ( ...) for array unpacking. An array can be unpacked with another array if the array expression is prefixed with the spread operator. This effectively results in an array_merge of all arrays in the expression.

$array_1 = ['foo', 'bar'];
$array_2 = ['baz', 'qux'];

$array_unpacked = [...$array_1, ...$array_2, ...['quux']];
$array_merged   = array_merge($array_1, $array_2, ['quux']);

var_dump($array_unpacked);
// ['foo', 'bar', 'baz', 'qux', 'quux'];

var_dump($array_merged);
// ['foo', 'bar', 'baz', 'qux', 'quux'];

array_merge function merges all gives arrays in the order they are passed. Arrays with numeric keys will appended and renumbered, and duplicate string keys will be overwritten with later values.

Prior to PHP 8.1, the spread operator only supported arrays with numeric keys. Using an array with string key resulted in an error prior to PHP 8.1:

$array = [...['a' => 'foo'], ...['b' => 'bar']];
Fatal error: Cannot unpack array with string keys in ... on line ...

From PHP 8.1, array unpacking supports arrays with string keys as well.

$array = [...['a' => 'foo'], ...['b' => 'bar']];
// ['a' => 'foo', 'b' => 'bar'];

Array Unpacking vs array_merge

Array unpacking and array_merge results in identical values.

$array_unpack = [
    ...['a' => 'foo'], 
    ...['b' => 'bar'],
    ...['c' => 'baz']
];
$array_merge = array_merge(
    ['a' => 'foo'],
    ['b' => 'bar'],
    ['c' => 'baz'],
);

$array_unpack === $array_merge; // true

Array Unpacking vs + operator

Array unpacking overwrites duplicate string keys, while the + operator ignores duplicate string keys.

var_dump(
     ['a' => 'foo']
    +['a' => 'bar']
);
// ['a' => 'foo'];

var_dump([
     ...['a' => 'foo'],
     ...['a' => 'bar']
]);
// ['a' => 'bar'];

Backwards Compatibility Impact

The array unpacking operator did not support string-keys prior to PHP 8.1, and resulted in a fatal error. Existing code that runs successfully on older versions should not cause any issues in PHP 8.1 or later.

array_merge results in identical results in all PHP versions. array_merge , and should be used on code that requires compatibility with PHP versions prior to 8.1.


RFC Discussion Implementation

Explicit Octal numeral notation

TypeNew Feature

PHP supports various numeral systems, including the default decimal (base-10), binary (base-2), octal (base-8), and hex (base-16).

Numeral systems apart from decimal are prefixed with their own prefix:

  • Hex with 0x prefix: e.g. 0x11 = 17
  • Binary with 0b prefix: e.g. 0b11 = 3
  • Octal with 0 prefix: e.g. 011 = 9

In PHP 8.1 and later, Octal numerals also support the 0o (zero, followed by o as in Oscar) prefix, which means octal numeric literals can be made more obvious and readable that they are indeed octal numerals. 0O (zero, followed by upper case O as in Oscar) is also supported.

Many programming languages use 0 prefix for octal numeric literals, just like PHP. However, many languages also support the explicit 0o notation and have been recommending the 0o.

echo 0xff; // 255
echo 0b111; // 7
echo 77; // 77
echo 077; // 63
echo 0o77; // 63, In PHP 8.1
echo 0O77; // 63, In PHP 8.1
077 === 63; // true
077 === 0o77; // true
077 === 0O77; // true

Further, the underscore numeric separators are supported in 0o/0O prefixed numeric literals as well:

echo 0o7716_1412; // 16573194

0 Prefix Is Not Changed

The existing 0 prefix is not deprecated, and continue function exactly the same.

The 0o/0O prefixes are supported in addition to the existing 0 prefix.

No Changes in Numeric Strings

Numeric strings, i.e. strings that contain a number, are not impacted by this change. "0o16" (as a string) will not be interpreted as a number, and does not return true for is_numeric function either.

echo "016"; // "016"
is_numeric("0o77"); // false
is_numeric("0b11"); // false
is_numeric("0x16"); // false

Note that "016" returns true for is_numeric, and it is not changed:

is_numeric("016"); // true

Backwards Compatibility Impact

The 0o and 0O prefixes are new prefixes, that were not supported in older PHP versions. Attempting to use them in older PHP versions result in a syntax error:

$value = 0o77;
Parse error: syntax error, unexpected identifier "o77" in ... on line ...

RFC Discussion Implementation

New `array_is_list` function

TypeNew Feature

PHP 8.1 brings a new function array_is_list, that returns whether a given array is an array with all sequential integers starting from 0.

In other words, this function returns true if the given array is semantic list of values; an array with all keys are integers, keys start from 0, with no gaps in between.

array_is_list function returns true on empty arrays as well.

array_is_list([]); // true
array_is_list([1, 2, 3]); // true
array_is_list(['apple', 2, 3]); // true
array_is_list(['apple', 'orange']); // true
array_is_list([0 => 'apple', 'orange']); // true
array_is_list([0 => 'apple', 1 => 'orange']); // true

Any array with keys not starting from zero, or any array with not all keys are integers in sequential order evaluates to false:

// Key does not start with 0
array_is_list([1 => 'apple', 'orange']); // false

// Keys are not in order
array_is_list([1 => 'apple', 0 => 'orange']); // false

// Non-integer keys
array_is_list([0 => 'apple', 'foo' => 'bar']); false

// Non-sequential keys
array_is_list([0 => 'apple', 2 => 'bar']); false

array_is_list Function Synopsis

function array_is_list(array $array): bool {
}
  • array_is_list only accepts array parameters. Passing any other type will throw a TypeError exception.
  • array_is_list function does not accept iterable or other array-like class objects such as ArrayAccess, SPLFixedArray, etc.
  • array_is_list is declared in the global namespace.

Polyfill

array_is_list function can be trivially polyfilled with user-land PHP code:

function array_is_list(array $array): bool {
    if (empty($array)) {
        return true;
    }

    $current_key = 0;
    foreach ($array as $key => $noop) {
        if ($key !== $current_key) {
            return false;
        }
        ++$current_key;
    }

    return true;
}

Backwards Compatibility Impact

array_is_list is a new function added in PHP 8.1. Unless there is an existing function with the same name in global namespace, there should be no backwards-compatibility issues.

Further, array_is_list function can be back-ported to almost any PHP version.


RFC Discussion Implementation

Hash functions accept algorithm-specific `$options`

TypeNew Feature

Hash functions in PHP accept a new optional $options parameter in PHP 8.1, that can further specify custom algorithm-specific options.

The following functions accept the new $options parameter as their last parameter:

Function synopses

function hash(string $algo, string $data, bool $binary = false, array $options = []): string|false
function hash_file(string $algo, string $filename, bool $binary = false, array $options = []): string|false
function hash_init(string $algo, int $flags = 0, string $key = '', array $options = []): HashContext

Supported Custom Options

MurmurHash3 and xxHash algorithms support specifying custom options with the new $options parameter.

hash('murmur3f', 'php.watch'); // "ba0189037daec822d973d304602d44f0"
hash("murmur3f", "php.watch", options: ["seed" => 42]); // 0cafc26e49efe230cdbd109458fef893
hash("xxh3", "php.watch"); // "f406cee14d73b176"
hash("xxh3", "php.watch", options: ["seed" => 44]); // acb8c705fdcd579c

These snippet use named parameters introduced in PHP 8.0.

In PHP 8.1, the following new hash $option values are accepted:

Backwards Compatibility Impact

The new $options parameter is a new parameter with a default value of an empty array, and will not cause any backwards-compatibility issues if the hash* functions are called with the additional $options parameter. PHP will silently ignore the additional parameters, but note that the output will be different across PHP versions without $options parameter support.

As of now, the only hashing algorithms that support custom options are MurmurHash3 and xxHash algorithms added in PHP 8.1.

Related Changes


Implementation

xxHash hash algorithms support

TypeNew Feature

xxHash is an extremely fast hashing algorithm that is not designed for cryptographic purposes, but provides excellent randomness and dispersion of output, and uniqueness of to minimize collisions. Some xxHash variants in fact, are faster than the RAM throughput provided CPU cache is sufficient and fits.

PHP 8.1 adds support for xxHash algorithm in following variants:

  • xxh32: 32-bit hash output
  • xxh64: 64-bit hash output
  • xxh3: 64-bit hash output
  • xxh128: 128-bit hash output
hash('xxh32',  'php.watch'); // cb729dd1
hash('xxh32',  'php.watch'); // 9c823891d744a55e
hash('xxh3',   'php.watch'); // f406cee14d73b176
hash('xxh128', 'php.watch'); // 16c27099bd855aff3b3efe27980515ad

xxHash is a streaming hash, and can be used as such:

$context = hash_init('xxh3');
hash_update($context, 'php.');
hash_update($context, 'watch');
hash_final($context); // f406cee14d73b176

xxHash is not a cryptographic hashing algorithm. For password hashing, use password_hash and its friends. Furthermore, none of the xxHash variants are allowed in hash_hmac function.

In PHP 8.1, xxHash is the fastest hashing algorithm supported in PHP.

Algorithm PHP implementation speed (GB/s)
xxh3 15.19
xxh128 14.78
crc32c 14.12
xxh64 13.32
murmur3f 8.87
xxh32 7.47
sha2-256 0.25
sha1-160 0.70
md5-128 0.77

The results above are an excerpt from PHP Hash Algorithm Benchmarks.

xxHash Options

PHP 8.1 supports specifying algorithm-specific options with the new $options parameter. Along with this, xxh* hashing algorithms accept additional options:

Only one option is accepted at a time. For example, if a seed option is specified, it cannot use a secret option in the same hashing operation.

If both secret and seed options are passed, an Error will be thrown.

hash("xxh3", "php.watch", false, ["secret" => $secret, 'seed' => 43]);
xxh3: Only one of seed or secret is to be passed for initialization in ... on line ...

seed option

All xxHash variants accept a seed option through the third parameter ($options) of the hash function.

hash('xxh3', 'php.watch'); // "f406cee14d73b176"

hash("xxh3", "php.watch", false, ["seed" => 42]); // de9956536d1e9543
hash("xxh3", "php.watch", options: ["seed" => 42]); // de9956536d1e9543

The third example uses named parameters introduced in PHP 8.0.

secret option

xxh3 and xxh128 variants of xxHash accept a secret value in the third parameter ($options array).

The required key must be larger than 135 bytes, and a key longer than 256 bytes will discarded with a warning emitted.

$secret = random_bytes(256);

hash("xxh3", "php.watch", false, ["secret" => $secret]);
// "a0dad59ce6341fb0"

It is important that the secret value has a sufficient entropy. PHP built-in random_bytes function is a cryptographically secure random-number generator.

xxh32 and xxh64 variants do not recognize a secret option, and if passed, it will be silently ignored.

  • Passing a secret value smaller than 136 bytes causes an error:

    hash("xxh3", "php.watch", false, ["secret" => 'ab'])
    xxh3: Secret length must be >= 136 bytes, ... bytes passed in ... code on line ...
  • Passing a secret value larger than 256 bytes makes PHP trim the value to 256 bytes with a warning:

    hash("xxh3", "php.watch", false, ["secret" => str_repeat('a', 257)]);
    hash(): xxh3: Secret content exceeding 256 bytes discarded in ... code on line ...

Backwards Compatibility Impact

xxHash is newly added to PHP 8.1, and older PHP versions will not be able to use xxHash algorithms via hash method.

Attempting to use xxHash algorithms in hash function results in a ValueError exception:

Fatal error: Uncaught ValueError: hash(): Argument #1 ($algo) must be a valid hashing algorithm in ...:...

On PHP versions prior to 8.0, a warning will be raised:

Warning: hash(): Unknown hashing algorithm: xxh3 in ... on line ...

Alternatives implementations include:

  • Megasaxon/php-xxhash - A PHP extension to add xxhash32 and xxhash64 for xxHash variants.
  • exussum12/xxhash - A pure-PHP implementation with FFI support. The pure-PHP implementation is ~600x slower than the native code, and the FFI implementation is only ~0.4x slower.

Related Changes


Implementation

MurmurHash3 hash algorithm support

TypeNew Feature

PHP 8.1 adds support for MurmurHash hashing algorithm.

MurmurHash is a non-cryptographic hashing algorithm, and PHP 8.1 supports MurmurHash3 version of it. Following variants are supported:

  • murmur3a: 32-bit hash
  • murmur3c: 128-bit hash on x86 architecture
  • murmur3f: 128-bit hash on x64 architecture
hash('murmur3a', 'php.watch'); // "ac96fab7"
hash('murmur3c', 'php.watch'); // "6d0fe9c3f960dc75cf42632f3e78ffda"
hash('murmur3f', 'php.watch'); // "ba0189037daec822d973d304602d44f0"

MurmurHash is a streaming hash, which means the values can be updated in sequence without having to hash the input string as a whole.

$context = hash_init('murmur3f');
hash_update($context, 'php');
hash_update($context, '.');
hash_update($context, 'watch');
$hash = hash_final($context); // "ba0189037daec822d973d304602d44f0"

MurmurHash is not designed as a cryptographic hashing algorithm. For password hashing, use password_hash and its friends. Furthermore, none of the MurmurHash variants are allowed in hash_hmac function.

MurmurHash algorithm is faster than most of the hashing algorithms, and is one of the fastest non-cryptographic algorithms available in PHP, along with the new xxHash algorithms.

Algorithm PHP implementation speed (GB/s)
murmur3a 3.98
murmur3c 6.20
murmur3f 8.87
sha2-256 0.25
sha1-160 0.70
md5-128 0.77

The results above are an excerpt from PHP Hash Algorithm Benchmarks.

Custom Seed Options

PHP 8.1 supports specifying algorithm-specific options with the new $options parameter. Along with this, murmur* hashing algorithms accept additional options. The only supported option is seed value.

hash('murmur3f', 'php.watch'); // "ba0189037daec822d973d304602d44f0"
hash("murmur3f", "php.watch", false, ["seed" => 42]); // 0cafc26e49efe230cdbd109458fef893
hash("murmur3f", "php.watch", options: ["seed" => 42]); // 0cafc26e49efe230cdbd109458fef893

The third example uses named parameters introduced in PHP 8.0.

Backwards Compatibility Impact

MurmurHash is newly added to PHP 8.1, and due to current lack of a hash registry that supports adding custom hashing algorithms, producing MurmurHash hashes using the hash() function is not possible on older PHP versions.

On PHP 8.0, a ValueError exception will be thrown if attempted to hash('murmur...', '...'):

Fatal error: Uncaught ValueError: hash(): Argument #1 ($algo) must be a valid hashing algorithm in ...:...

On PHP versions prior to 8.0, a warning will be raised:

Warning: hash(): Unknown hashing algorithm: murmur3a in ... on line ...

Alternatives implementations include:

Related Changes


Implementation

Curl: DNS-over-HTTPS support

TypeNew Feature

In PHP 8.1, the Curl extension supports specifying a server for DNS-Over-HTTPS. It requires PHP to be compiled with libcurl version 7.62 or later.

Most of the current operating systems and distributions already support it, as they often include Curl 7.68 or later in Ubuntu 20.04, and other Linux/Windows/Mac OS operating systems.

A list of public DoH servers are available at Curl documentation.

CURLOPT_DOH_URL

The DNS-Over-HTTPS server URL is configured by setting the CURLOPT_DOH_URL option. This constant will only be available if the underlying libcurl version is >= 7.62.

$ch = curl_init('https://example.com');
curl_setopt($ch, CURLOPT_DOH_URL, 'https://dns.google/dns-query');
curl_exec($ch);

The behavior of the CURLOPT_DOH_URL is exactly same as the libcurl behavior; see CURLOPT_DOH_URL explained.

DoH Server URL Validation

The entered server URL is not validated at the time it is set. It is validated when the Curl request is executed. It must be an HTTPS URL.

If the provided DNS server is not a valid URL, or does not return a valid response, the request will fail. There is no fall-back to system DNS resolver, or a default DoH server configured in Curl.

Unset DoH Server URL

To unset a previously configured DoH server URL, set its value to null.

curl_setopt($ch, CURLOPT_DOH_URL, null);

Note that the Curl handle's DNS cache is independent of DoH servers. Setting a different DoH server URL, or unsetting it will not clear the DNS cache, and Curl will reuse values previously returned by system DNS resolver or any of the DoH servers configured.

Backwards Compatibility Impact

This feature is only available on PHP 8.1 and later, and it is not possible to back-port this feature.

It is possible to explicitly set an IP address to a host with CURLOPT_RESOLVE option, if the caller obtains the DNS information by using a DoH server with its own DNS querying.

Cross-version compatibility can be achieved by checking the CURLOPT_DOH_URL constant:

$ch = curl_init();
if (defined('CURLOPT_DOH_URL')) {
    curl_setopt($ch, CURLOPT_DOH_URL, 'https://dns.google/dns-query');
}
curl_exec($ch);

Implementation

GD Extension: Font identifiers are `\GdFont` class objects

TypeChange

Prior to PHP 8.1, the imageloadfont function from GD extension returned a font-identifier resource ID as an integer. In PHP 8.1 and later, the resource ID is migrated to a GdFont class instance.

All functions that previously accepted a resource ID from imageloadfont function now accept the new GdFont class objects, making this an opaque change.


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.


imageloadfont function return value is changed from int to GdImage. This is different from other resource to object migrations that replaced a resource with a class instance.

gd resource for primitive GD images was migrated to GdImage class instances in PHP 8.0: GdImage class objects replace GD image resources

GdFont class synopsis

final class GdFont {}

Directly instantiating a GdFont instance is not allowed, and it is still required to use the imageloadfont:

$font = new GdFont();
PHP Error:  You cannot initialize a GdFont object except through helper functions in ... on line ...

GdFont class is also declared final, to prevent it from being extended with child classes, to reduce potential backwards-compatibility issues if the GdFont class structure changes in the future.

Backwards Compatibility Impact

Prior to this change in PHP 8.1, the GD Font resources were int values. All code with parameter/return/property type with this assumption now needs to accommodate GdFont class instances.

In PHP 8.0, Union Types can enforce this requirement.

- function addFont(int $gdfont){}
+ function addFont(int|GdFont $gdfont) {}

imageloadfont function continues to return false in case the font file could not be loaded, and a check against false value might be more readable and appropriate:

$font = imageloadfont($file);
- if (is_int($font)) {
+ if ($font !== false) {
 // 
}

Implementation

FTP Extension: Connection resources are `\FTPConnection` class objects

TypeChange

FTP connection resources from the ftp extension are migrated to class objects in PHP 8.1.

Prior to PHP 8.1, FTP connections created with ftp_connect(), and ftp_ssl_connect() functions returned a resource of type ftp. In PHP 8.1 and later, those functions return a FTPConnection class instance instead.

All functions that previously accepted resources accept the new type as well.

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.

FTPConnection Class Synopsis

final class FTPConnection {}

Similar to other resource to object migrations, the new FTPConnection class is declared final to prevent them from extended (and reduce potential backwards-compatibility issues if PHP core changes the methods of the FTPConnection class).

Instantiating a new instance of FTPConnection is not allowed, and results in an \Error Exception.

new FTPConnection();
Cannot directly construct FTPConnection, use ftp_connect() or ftp_ssl_connect() instead

is_resource checks

User-land PHP code that work with ftp resources sometimes check for the validity of the resource object with is_resource function.

This was not necessary because ftp_connect and ftp_ssl_connect functions return false on error. This pattern is maintained in PHP 8.1 too.

To check if creation of an FTP resource was successful, a simple check for false is cross-version compatible, and is just as accurate as a is_resource check.

Existing code that relied on is_resource function now needs to account for the new FTPConnection class objects in PHP 8.1.

- is_resource($ftp_connection)
+ is_resource($ftp_connection) || $ftp_connection instanceof \FTPConnection

Alternately, a check against false might be more appropriate and more readable.

- is_resource($ftp_connection)
+ $ftp_connection !== false

Destroying FTPConnection objects

Destroying an FTPConnection object, or leaving it for the garbage collector to clean up will automatically close the FTP connection.

It is still possible to explicitly close the FTP connection with ftp_close function. This function is not deprecated and is still functional.

Attempting to use a closed FTP connection object will throw a ValueError exception.

ValueError: FTPConnection is already closed

Backwards Compatibility Impact

Similar to other resource to object migrations, FTP extension seamless upgrades return and accepted parameter types to the new object-based resources.

is_resource function now returns false instead of the previous true return value for FTP connection objects. This might be a backwards-compatibility issue if the FTP connection resources are checked with that function.


Implementation

IMAP: `imap` resources are `\IMAPConnection` class objects

TypeChange

In PHP 8.1, IMAP connection resources from the imap extension are migrated to class objects.

Prior to PHP 8.1, imap_open() function returned a resource object of type imap. From PHP 8.1 and later, it returns an \IMAPConnection class instance instead.

All functions that previously accepted resources accept the new type as well.

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.

IMAPConnection Class Synopsis

final class IMAPConnection {}

In line with other resource to object migration semantics, the new IMAPConnection class is declared final as well, to minimize the potential backwards-compatibility breaks when PHP modifies the implementation details of the class.

imap_open function is still used to create IMAPConnection class instances, and instantiating with new IMAPConnection() construct is not allowed:

new IMAPConnection();
Cannot directly construct IMAPConnection, use imap_open() instead

is_resource checks

All resource to object migrated class objects no longer return true for is_resource function, and any code that used is_resource function to check the validity of an IMAP connection resource will cause problems in PHP 8.1 and later.

In all PHP versions, imap_open function returns false in case it failed to create an IMAP resource. This is still the case for PHP 8.1 and later as well.

Existing code that needs to check the validity of a given variable for an IMAP connection needs to account for IMAPConnection class object return values in PHP 8.1 and later.

- is_resource($imap_connection)
+ is_resource($imap_connection) || $imap_connection instanceof \IMAPConnection

Alternately, a check against false might be more appropriate and more readable, and works across all PHP versions too.

- is_resource($imap_connection)
+ $imap_connection !== false

Closing IMAPConnection resources

IMAP connections are automatically closed when the IMAPConnection is no longer referenced (i.e fall out of scope), or when explicitly destroyed with unset($connection).

imap_close function, that was used to close imap resources is still available and functional. Attempting to use a closed IMAPConnection object results in a \ValueError exception.

ValueError: IMAPConnection is already closed

Backwards Compatibility Impact

Following other resource to object migrations, IMAP extension seamless upgrades return and accepted parameter types to the new object-based resources.

is_resource function now returns false instead of the previous true return value for IMAP connection objects. This might be a backwards-compatibility issue if the IMAP connection resources are checked with that function. See is_resource checks section for possible workarounds


Implementation

finfo Extension: `file_info` resource are migrated to existing `finfo` objects

TypeChange

finfo extension has gone through the Resource to Object Migration, that the procedural API in the finfo extension transparently returns and accepts finfo class instances.


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.


In PHP 8.1, the following functions from finfo extension return and accept finfo class instances instead of resource objects with type file_info.

finfo class

finfo class already exists in all PHP versions (PHP >= 5.3), but prior to PHP 8.1, the procedural-style finfo_* functions returned/accepted resources instead. From PHP 8.1, it is even possible to interchange the procedural return values with Object-Oriented style objects.

The finfo class continues to function with same semantics as before, and there are no changes in finfo class.

Destructing finfo class instances

The finfo_close function is still available, and is not deprecated.

Calling finfo_close no longer has any effect. It is possible to continue to use a closed resource, which was not possible prior to PHP 8.0.

$f = finfo_open();
finfo_close($f);
finfo_file($f, 'test.txt'); // Allowed in PHP 8.1, but not prior.

Prior to PHP 8.1, using a closed resource was not allowed, and the snippet above would have resulted in an error:

TypeError: finfo_file(): supplied resource is not a valid file_info resource

In PHP 8.1 and later, because the finfo_close function has no effect, this pattern does not result in an error.

finfo objects will be automatically destroyed when they fall out of scope. It is possible to destroy them explicitly as well:

unset($finfo);

Backwards Compatibility Impact

Similar to other Resource to Object Migration changes, the use of is_resource function on return value of finfo_open no longer returns true. It returns false instead.

To accept external finfo instances, it is now necessary to account for finfo class instances as well for compatibility with older PHP versions:

- is_resource($finfo)
+ is_resource($finfo) || $finfo instanceof finfo

Upon resource/object instantiation, a check against !== false is sufficient as well:

$finfo = finfo_open();
- is_resource($finfo);
+ $finfo !== false 

Alternately, a full-on migration to the OOP-style finfo class might be an easier upgrade, as it has no effective changes at all.

Apart from the is_resource function, the rest of the finfo API remains opaque, because all functions that previously accepted a file_info resource now accept finfo class objects.


Implementation

Pspell Extension: `pspell`, `pspell config` resources are `\PSpell`, `\PSpellConfig` class objects

TypeChange

All resource types from Pspell extension are migrated to opaque class objects in PHP 8.1.

Prior to PHP 8.1, Pspell extension used resource object types pspell and pspell config with an integer identifier. These two identifier values are replaced with class objects named PSpell and PSpellConfig.

All functions that previously returned and accepted resource integer identifiers now accept their corresponding class objects.

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.

PSpell Class

PSpell class replaces the resource objects of type pspell. PHP returned an int identifier for this prior to PHP 8.1, and returns a PSpell class object in PHP 8.1 and later instead.

final class PSpell {}

All functions that returned or accepted pspell object identifiers are changed to return/accept new PSpell class objects.

The following functions are used to instantiate PSpell objects. They previously returned pspell resource identifiers as integers.

The following functions accepted pspell resource object identifiers, and are changed to accept PSpell class objects instead:

Instantiating a PSpell object with new PSpell construct is not allowed:

new PSpell();
PHP Error:  You cannot initialize a PSpell object except through helper functions in ... code on line ...

It is not allowed to serialize a PSpell object either:

serialize(pspell_new("en", "", "", ""));
Serialization of 'PSpell' is not allowed

PSpellConfig Class

The new PSpellConfig class replaces pspell config resource object identifiers (int values).

final class PSpellConfig {}

Similar to all other resource to class object migrations, all functions that returned/accepted pspell config resource object identifiers now accept PSpellConfig class instances.

  • pspell_config_create() function previously returned and int identifier for pspell config resource objects, but return PSpellConfig class instances instead in PHP 8.1 and later.

  • pspell_new_config() function previously accepted pspell config resource objects identifiers as int, and are changed to accept PSpellConfig class objects from PHP 8.1 and later.

It is not allowed to use new PSpellConfig construct to instantiate a PSpellConfig instance.

new PSpellConfig();
PHP Error:  You cannot initialize a PSpellConfig object except through helper functions in ... code on line ...

Further, it is not allowed to serialize a PSpell object either.

serialize(pspell_config_create('en'));
Serialization of 'PSpellConfig' is not allowed

Destroying PSpell resources

Prior to PHP 8.1, there were no specific functions to forcefully close a pspell/pspell config resource. PHP automatically cleans them with garbage cleaning.

From PHP 8.1 and later, the returned PSpell/PSpellConfig objects follow general object garbage-cleaning mechanisms, and they will be cleared when the objects fall out of scope. It is possible to explicitly destroy an object with unset() construct too.

Backwards Compatibility Impact

Prior to PHP 8.1, all pspell_* functions returned/accepted resource objects as integer identifiers. They are now changed in PHP 8.1 and later to return class objects instead.

Failure to instantiate any object return false, and this pattern is not changed in PHP 8.1.

It was possible to serialize the return values from pspell_* functions, erroneously so, prior to PHP 8.1, but the new class objects are not allowed to be serialized.


Implementation

`version_compare` operator restrictions

TypeChange

In PHP 8.1, the version_compare() function is made more strict with the operator parameter.

In PHP 8.0, this was made to throw a ValueError exception if an unknown parameter was passed, as part of the internal function exception handling changes.

In PHP 8.1, this is further restricted to disallow partial values for operator parameter.

version_compare(1, 2, '');
version_compare(1, 2, '!');
version_compare(1, 2, 'g');
version_compare(1, 2, 'l');
version_compare(1, 2, 'n');

All of the statements above are were allowed in PHP 8.0, and are no longer allowed in PHP 8.1. These values are undocumented values from the documentation.

Note that PHP 8.0 started to throw TypeError for all invalid values. The operators disallowed in PHP 8.1 are due to internal partial matching changes.

In PHP 8.1, only the following operators are allowed:

  • ==, =, and eq
  • !=, <>, and ne
  • > and gt
  • >= and ge
  • < and lt
  • <= and le

Backwards Compatibility Impact

PHP 8.0 started to disallow a vast majority of disallowed values, and this change in PHP 8.1 is a continuation of it. The disallowed values were not documented.

Using the correct values fixes the issue for all PHP versions.


Implementation

Passing `null` to non-nullable internal function parameters is deprecated

TypeDeprecation

In PHP, passing null to a function/method parameter that is not declared nullable is not allowed. Prior to PHP 8.1, this restriction was only applied to user-land functions, and not internal functions.

From PHP 8.1, passing null to a function/method parameter not declared nullable emits a deprecation notice. This is in preparation to remove this functionality to throw a \TypeError exception in a future PHP version as it does already for user-land functions/methods.

For example, strlen function expects the first parameter to be of type string. By definition, it does not accept null values (i.e. not declared as ?string nor string|null union type). For historical reasons, PHP allowed passing null, for these parameters. This behavior is inconsistent from user-land functions/methods because they would have thrown a \TypeError exception if null is passed to a parameter type not declared as nullable.

strlen(null); // 0
Deprecated: strlen(): Passing null to parameter #1 ($string) of type string is deprecated in ... on line ...

This behavior was allowed in the first place for historical reasons. PHP 7.0's introduction of scalar types did not allow null for parameters unless the parameter was explicitly declared nullable. However, internal functions were not changed to strictly enforce scalar types to ease upgrading to PHP 7.0.

In PHP 8.0, internal functions were updated to throw \TypeError and \ValueError exceptions instead of warnings.

This deprecation notice will likely occur often in legacy code bases that relied on PHP to coerce the default value for a parameter from null.

Upgrading null parameters to default values

Code that uses null in a function/method call in place of the default value will need to replace the null value with the default value from the function declaration.

For example, a strlen(null) call emits a deprecation, and the sensible upgrade to this call would be strlen('').

To bring another example, preg_split function has a third parameter int $limit = -1. Passing null to this parameter emits a deprecation notice in PHP 8.1, and the sensible upgrade to this call would be setting $limit parameter to -1.

Backwards Compatibility Impact

This is a backwards-compatibility breaking change. Updating the null parameters to adhere to the expected type should fix the deprecation notice across all PHP versions.

Further, it may be possible for automated code fixing tools to update said null parameters to the default values from the function declaration.


RFC Discussion Implementation