What’s new in PHP 8.1
- Release: https://www.php.net/releases/8.1/ru.php
- Wiki: PHP 8.1
- github: Changelog PHP 8.1
- https://habr.com/ru/news/591739/
['a' => 1, ...$array] — unpacking an array with string keys
DOC: https://www.php.net/manual/ru/language.types.array.php#language.types.array.unpacking
RFC: https://wiki.php.net/rfc/array_unpacking_string_keys
C PHP 7.4 there was array unpacking using the ... operator. But it worked only with indexed arrays (arrays with integer keys). Now it also works with associative arrays (arrays with string keys).
$arrayA = [ 'a' => 1 ]; $arrayB = [ 'b' => 2 ]; $result = [ 'a' => 0, ...$arrayA, ...$arrayB ]; // ['a' => 1, 'b' => 2]
Later string keys overwrite earlier ones
Array unpacking with the ... operator follows the semantics of array_merge(). That is, later string keys overwrite earlier ones, and integer keys are reindexed:
// string key $arr1 = ["a" => 1]; $arr2 = ["a" => 2]; $arr3 = ["a" => 0, ...$arr1, ...$arr2]; // ["a" => 2] // integer key $arr4 = [1, 2, 3]; $arr5 = [4, 5, 6]; $arr6 = [...$arr4, ...$arr5]; // [1, 2, 3, 4, 5, 6] // original integer keys are not preserved.
readonly — object properties
Wiki: https://wiki.php.net/rfc/readonly_properties_v2
Doc: readonly-properties
The readonly modifier prevents modification of a property after initialization.
Now you don't need to write a separate getter for a private property; you can simply declare the property as public readonly:
class BlogData {
public readonly Status $status;
public function __construct( Status $status ){
$this->status = $status;
}
}
Earlier you would have to do:
class BlogData {
private Status $status;
public function __construct( Status $status ){
$this->status = $status;
}
public function getStatus(): Status {
return $this->status;
}
}
readonly can be applied only to typed properties. AReadonly property without a type constraint can be created using the Mixed type.
Static readonly properties are not supported.
A Readonly property can be initialized only once and only from the scope in which it was declared. Any other assignment or modification of the property will throw an Error.
class Foo {
public readonly string $prop;
}
$obj = new Foo();
$obj->prop = 'bar'; // Error: initialization outside of the restricted scope.Specifying a default value is not allowed, because that is effectively a constant.
class Test {
// Error: cannot have a default value
public readonly int $prop = 42;
}Readonly properties cannot be destroyed with unset() after initialization. But they can be destroyed before initialization from the scope in which the property was declared.
Readonly properties allow internal mutation. Objects (or resources) stored in readonly can still be mutated internally:
class Test {
public function __construct( public readonly object $obj ){
}
}
$test = new Test( new stdClass );
$test->obj->foo = 1; // Valid internal mutation.
$test->obj = new stdClass; // Invalid reassignment.never — return type of a function
RFC: https://wiki.php.net/rfc/noreturn_type
Doc: https://www.php.net/manual/ru/language.types.never.php
A function or method declared with the never type indicates that it does not return a value and will either throw an exception or terminate the script by calling die(), exit(), trigger_error(), or something similar.
function redirect( string $uri ): never {
header( "Location: $uri" );
exit();
}
function redirectToLoginPage(): never {
redirect( '/login' );
echo 'Hello'; // <- dead code detected by static analysis
}
enum — Enumerations
Doc: https://www.php.net/manual/ru/language.enumerations.php
Wiki: https://wiki.php.net/rfc/enumerations
Enums are needed to describe types. Use enumerations instead of a set of constants to validate them automatically during writing and executing code.
Enum:
enum Color {
case Red;
case Black;
case White;
}
Usage:
function foo( Color $color ){
if( $color === Color::Red ){
echo 'I`m red!';
}
}
foo( Color::Red ); //> I`m red!
Each case: Color::Red, Color::Black is a separate object enum(Color::Red), enum(Color::Black). Each such object inherits from the type (object) Color:
var_dump( Color::Red ); // enum(Color::Red) var_dump( Color::Red instanceof Color ); // bool(true)
That is, under the hood these are not numbers 0,1,2 as in some languages, but objects. Each such object has a built-in property $name:
echo Color::Red->name; //> Red
Enum with values:
enum Color: string {
case Red = 'R';
case Black = 'B';
case White = 'W';
}
Usage:
echo Color::Red->name; //> Red echo Color::Red->value; //> R var_dump( Color::from( 'R' ) ); // enum(Color::Red)
Why is this needed?
To answer this question, consider an example without enums and with them.
Suppose we sell cars of three colors: red, black, and white. How to describe the color, which type to choose?
If we describe the car color as a string:
class Car {
private string $color;
function setColor( string $color ): void {
$this->color = $color;
}
}
Then when calling $myCar->setColor(...) it is unclear which exact string to write: "red" or "RED" or "#ff0000". Also, it is easy to make mistakes by writing something unnecessary (rad or Red, for example). Moreover, IDE won't suggest possible values, and a static analyzer won't be able to analyze this moment.
This leads developers to group constants in a class to clearly see all variants.
class Color {
public const RED = "red";
public const BLACK = "black";
public const WHITE = "white";
}
And when setting the color, they write:
$myCar->setColor( Color::RED );
That seems to be exactly what is needed. But if a new developer works with the code and first encounters the method $myCar->setColor(...), they may not know that there are color constants somewhere. They can still pass any string without any error message.
Therefore, here you need not a class with constants, but a separate type. And this is where enums come to the rescue:
enum Color {
case Red;
case Black;
case White;
}
Now we can use the type Color wherever necessary:
class Car {
private Color $color;
function setColor( Color $color ): void {
$this->color = $color;
}
}
From the method signature, any newcomer immediately sees which options exist (IDE will suggest them). When calling the method $myCar->setColor() you cannot pass anything except: $myCar->setColor( Color::White ). Readability and maintainability of the code are on point.
Iteration over an enum
To iterate over all values of an enum, you can generate a list using the method ::cases() and pass it to "foreach":
enum Shapes {
case RECTANGLE;
case SQUARE;
case CIRCLE;
case OVAL;
}
foreach( Shapes::cases() as $shape ){
echo $shape->name . "\n";
}
We get:
RECTANGLE SQUARE CIRCLE OVAL
enum as a class
In addition to the field "case", an enum can contain a lot of other things. Essentially it is a kind of class. It can contain methods, implement interfaces, and use traits.
interface Colorful {
public function color(): string;
}
trait Rectangle {
public function shape(): string {
return 'Rectangle';
}
}
enum Suit implements Colorful {
use Rectangle;
case Hearts;
case Diamonds;
case Clubs;
case Spades;
public function color(): string {
return match( $this ){
self::Hearts, self::Diamonds => 'Red',
self::Clubs, self::Spades => 'Black',
};
}
}
echo Suit::Spades->color(); //> Black
echo Suit::Hearts->color(); //> Red
echo Suit::Hearts->shape(); //> Rectangle
$this will be the specific object of the case for which we call the method.
callable(...) — Callback functions as a first-class object
Doc: https://www.php.net/manual/ru/functions.first_class_callable_syntax.php
Wiki: https://wiki.php.net/rfc/first_class_callable_syntax
callable(...) is a way to create anonymous functions from callable objects — it is an alternative to the syntax of callable objects in the form of strings Class::method or arrays [ 'Class', 'method' ].
Example:
$fn = Closure::fromCallable('strlen'); // before
$fn = strlen(...); // now
$fn = Closure::fromCallable( [ $this, 'method' ] ); // before
$fn = $this->method(...) // now
$fn = Closure::fromCallable( [ Foo::class, 'method' ] ); // before
$fn = Foo::method(...); // now
The syntax callable(...) creates a Closure object from a callable object. The part callable accepts any expression that can be called in PHP.
A partial list of possible syntaxes:
class Foo
{
public function method() {}
public static function staticmethod() {}
public function __invoke() {}
}
$obj = new Foo();
$classStr = 'Foo';
$methodStr = 'method';
$staticmethodStr = 'staticmethod';
$f1 = strlen(...);
$f2 = $obj(...); // Callable object
$f3 = $obj->method(...);
$f4 = $obj->$methodStr(...);
$f5 = Foo::staticmethod(...);
$f6 = $classStr::$staticmethodStr(...);
// Traditional callable syntax with strings and arrays
$f7 = 'strlen'(...);
$f8 = [ $obj, 'method' ](...);
$f9 = [ Foo::class, 'staticmethod' ](...);
The advantage of the syntax is that it is easily analyzable statically and uses the scope where the object is created.
// Scope example
Scope is defined where the callable object is created, not where it is invoked. For example, if you use an array callable [ $this, 'privateMethod' ], the scope corresponds to the method call site, while using the new syntax $this->privateMethod(...) — to the place where the callable object is created.
Consider:
class Test {
public function getPrivateMethod() {
return [ $this, 'privateMethod' ]; // Fatal error: Call to private method Foo::privateMethod() from global scope
return Closure::fromCallable( [ $this, 'privateMethod' ] ); // works, but ugly
return $this->privateMethod(...); // works
}
private function privateMethod() {
echo __METHOD__, "\n";
}
}
$test = new Test;
$privateMethod = $test->getPrivateMethod();
$privateMethod();
Note: You cannot create an object with this syntax (for example, new Foo(...)), because the syntax new Foo() is not a call.
Note: you cannot combine with the Nullsafe operator. Both of the following results lead to a compilation error:
$obj?->method(...); $obj?->prop->method(...);
A&X $var — intersection types
Doc: https://www.php.net/manual/en/language.types.declarations.php
Wiki: https://wiki.php.net/rfc/pure-intersection-types
Full support for intersection types, to create new types whose values must conform to multiple types simultaneously.
function count_and_iterate( Iterator&Countable $value ) {
foreach( $value as $val ){
echo $val;
}
count( $value );
}
// Previously you had to write this:
function count_and_iterate( Iterator $value ) {
if( ! ( $value instanceof Countable ) ){
throw new TypeError( 'value must be Countable' );
}
foreach( $value as $val ){
echo $val;
}
count( $value );
}
NOTE: You cannot combine Intersection types and Union types. For example: A&B|C.
= new Class() — extended initialization of objects
RFC: https://wiki.php.net/rfc/new_in_initializers
Objects can now be used as values:
- as default parameters.
- as static variables.
- as global constants.
- in attribute arguments.
- when creating nested attributes (annotations).
Default parameters
function test(
$foo = new A,
$bar = new B( 1 ),
$baz = new C( x: 2 ),
) {
}
Another example:
// Now you can write like this:
class MyController {
public function __construct(
private Logger $logger = new NullLogger(),
) {}
}
// Before you would write like this:
class Test {
private Logger $logger;
public function __construct(
?Logger $logger = null,
) {
$this->logger = $logger ?? new NullLogger();
}
}
Static variables
class MyClass {
public static function getInstance() {
static $instance = new self();
return $instance;
}
}
Global constants
class Config {
public $setting = 'default';
}
const GLOBAL_CONFIG = new Config();
echo GLOBAL_CONFIG->setting; // 'default'
In nested attributes (annotations)
class MyAttribute {
public function __construct( public $value ) {}
}
#[MyAttribute( new DateTime('now') )]
class TestClass {
// Class with an attribute
}
Another example:
class User {
#[\Assert\All(
new \Assert\NotNull,
new \Assert\Length( min: 5 )
)]
public string $name = '';
}
In PHP attributes (annotations) can be useful in various contexts, but without reflection their use becomes limited, since reflection is the main mechanism for dynamic access to metadata (attributes) at runtime.
Example of using custom attributes and accessing them via reflection:
#[Attribute(Attribute::TARGET_PROPERTY)]
class Validate {
public function __construct(
public bool $notNull = false,
public int $minLength = 0
) {}
}
class User {
#[Validate( notNull: true, minLength: 5 )]
public string $name = '';
public function validate() {
$reflectionClass = new ReflectionClass( $this );
$properties = $reflectionClass->getProperties();
foreach( $properties as $property ){
$attributes = $property->getAttributes( Validate::class );
if( ! empty( $attributes ) ){
$value = $this->{$property->getName()};
$validate = $attributes[0]->newInstance();
// NotNull validation
if( $validate->notNull && is_null( $value ) ){
throw new Exception( "The {$property->getName()} cannot be null." );
}
// Length validation
if( strlen( $value ) < $validate->minLength ){
throw new Exception( "The {$property->getName()} must be at least {$validate->minLength} characters long." );
}
}
}
}
}
Now we use:
try{
$user = new User();
$user->name = 'John'; // This name is too short (less than 5 characters)
$user->validate();
}
catch( Exception $e ){
echo $e->getMessage(); // Output: "The name must be at least 5 characters long."
}array_is_list() — new function
DOC: https://www.php.net/manual/en/function.array-is-list.php
A new function array_is_list() has been added.
Determines whether the passed array is a list. An array is considered a list if its keys consist of consecutive integers from 0 to count($array)-1.
Example:
array_is_list( [] ); // true array_is_list( [ 'apple', 2, 3 ] ); // true array_is_list( [ 0 => 'apple', 'orange' ] ); // true // The array does not start at 0 array_is_list( [ 1 => 'apple', 'orange' ] ); // false // The keys are not in the correct order array_is_list( [ 1 => 'apple', 0 => 'orange' ] ); // false // Non-integer keys array_is_list( [ 0 => 'apple', 'foo' => 'bar' ] ); // false // Non-consecutive keys array_is_list( [ 0 => 'apple', 2 => 'bar' ] ); // false
—