Что нового в PHP 8.1

['a' => 1, ...$array] — распаковка массива со строковыми ключами

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 появилась распаковка массива с помощью оператора .... Но это работало только с индексными массивами (массивами с целочисленными ключами). Теперь это также работает с ассоциативными массивами (массивы со строковыми ключами).

$arrayA = [ 'a' => 1 ];
$arrayB = [ 'b' => 2 ];

$result = [ 'a' => 0, ...$arrayA, ...$arrayB ]; // ['a' => 1, 'b' => 2]
Более поздние строковые ключи перезаписывают более ранние

Распаковка массива оператором ... соблюдает семантику функции array_merge(). То есть более поздние строковые ключи перезаписывают более ранние, а целочисленные ключи перенумеровываются:

// строковый ключ
$arr1 = ["a" => 1];
$arr2 = ["a" => 2];
$arr3 = ["a" => 0, ...$arr1, ...$arr2]; // ["a" => 2]

// целочисленный ключ
$arr4 = [1, 2, 3];
$arr5 = [4, 5, 6];
$arr6 = [...$arr4, ...$arr5]; // [1, 2, 3, 4, 5, 6]
// исходные целочисленные ключи не были сохранены.

readonly — свойства объекта

Wiki: https://wiki.php.net/rfc/readonly_properties_v2
Doc: readonly-properties

Модификатор readonly предотвращает изменение свойства объекта после инициализации.

Теперь можно не писать отдельный геттер под приватное свойство, а можно просто указать свойству public readonly:

class BlogData {
	public readonly Status $status;

	public function __construct( Status $status ){
		$this->status = $status;
	}
}

Раньше нужно было делать так:

class BlogData {
	private Status $status;

	public function __construct( Status $status ){
		$this->status = $status;
	}

	public function getStatus(): Status {
		return $this->status;
	}
}

readonly может применяться только к типизированным свойствам. Readonly-свойство без ограничений типа можно создать с помощью типа Mixed.

Статические readonly-свойства не поддерживаются.

Readonly-свойство можно инициализировать только один раз и только из области, в которой оно было объявлено. Любое другое присвоение или изменение свойства приведёт к исключению Error.

class Foo {
	public readonly string $prop;
}

$obj = new Foo();
$obj->prop = 'bar'; // Error: инициализация за пределами закрытой области.

Указание значения по умолчанию не допускается, потому что это, по сути, то же самое, что и константа.

class Test {
	// Error: не может быть значения по умолчанию
	public readonly int $prop = 42;
}

Readonly-свойства не могут быть уничтожены с помощью unset() после их инициализации. Но их можно уничтожить до инициализации из области, в которой было объявлено свойство.

Readonly-свойства допускают внутренние изменения. Объекты (или ресурсы), хранящиеся в readonly по-прежнему могут быть изменены внутри:

class Test {
	public function __construct( public readonly object $obj ){
	}
}

$test = new Test( new stdClass );
$test->obj->foo = 1; // Правильное внутреннее изменение.
$test->obj = new stdClass; // Неправильное переопределение.

never — тип возвращаемого значения функции

RFC: https://wiki.php.net/rfc/noreturn_type
Doc: https://www.php.net/manual/ru/language.types.never.php

Функция или метод, объявленные с типом never, указывают на то, что они не вернут значение и либо выбросят исключение, либо завершат выполнение скрипта с помощью вызова функции die(), exit(), trigger_error() или чем-то подобным.

function redirect( string $uri ): never {
	header( "Location: $uri" );
	exit();
}

function redirectToLoginPage(): never {
	redirect( '/login' );

	echo 'Hello'; // <- dead code detected by static analysis
}

enum — перечисления

Doc: https://www.php.net/manual/ru/language.enumerations.php
Wiki: https://wiki.php.net/rfc/enumerations

enum нужны для описания типов. Используйте перечисления вместо набора констант, чтобы валидировать их автоматически во время написания и выполнения кода.

Enum:
enum Color {
	case Red;
	case Black;
	case White;
}

Использование:

function foo( Color $color ){
	if( $color === Color::Red ){
		echo 'I`m red!';
	}
}

foo( Color::Red ); //> I`m red!

Каждый case: Color::Red, Color::Black является отдельным объектом enum(Color::Red), enum(Color::Black). Каждый этот объект наследуется от типа (объекта) Color:

var_dump( Color::Red ); // enum(Color::Red)
var_dump( Color::Red instanceof Color ); // bool(true)

Т.е. под капотом это не числа 0,1,2 как в некоторых языках, а именно объекты. У каждого такого объекта есть встроенное свойство $name:

echo Color::Red->name; //> Red
Enum со значениями:
enum Color: string {
	case Red = 'R';
	case Black = 'B';
	case White = 'W';
}

Использование:

echo Color::Red->name; //> Red
echo Color::Red->value; //> R
var_dump( Color::from( 'R' ) ); // enum(Color::Red)
Зачем это нужно?

Чтобы ответить на этот вопрос, рассмотрим пример без енумов и с ними.

Допустим, у нас продаются машины трех цветов: красные, черные и белые. Как описать цвет, какой тип выбрать?

Если мы опишем цвет машины как string:

class Car {
	private string $color;

	function setColor( string $color ): void {
		$this->color = $color;
	}
}

То при вызове $myCar->setColor(...) непонятно, какую именно строку туда писать: "red" или "RED" или "#ff0000". А также, легко ошибиться, написав не то что нужно (rad или Red, например). Ну и, IDE не подскажет возможные значения, и статический анализатор не сможет проанализировать этот момент.

Это приводит к тому, что программисты группируют константы в класс, чтобы явно видеть все варианты.

class Color {
	public const RED   = "red";
	public const BLACK = "black";
	public const WHITE = "white";
}

А задавая цвет, пишут:

$myCar->setColor( Color::RED );

Это казалось бы то что нужно. Но если с кодом работает новый разработчик и он впервые видит метод $myCar->setColor(...), он может и не знать, что где-то есть константы для цветов. И все так же может сунуть туда любую строку без какого-либо сообщения об ошибке.

Поэтому здесь нужен не класс с константами, а отдельный тип. И вот тут на помощь приходит enum:

enum Color {
	case Red;
	case Black;
	case White;
}

Теперь мы можем использовать тип Color везде где необходимо:

class Car {
	private Color $color;

	function setColor( Color $color ): void {
		$this->color = $color;
	}
}

Из сигнатуры метода любому новичку сразу видно какие варианты есть (IDE их подскажет). При вызове метода $myCar->setColor() в него нельзя передать ничего кроме: $myCar->setColor( Color::White ). Читаемость и поддерживаемость кода на высоте.

Иетрация по enum

Чтобы пройтись по всем значениям enum, можно сгенерировать список с помощью метода ::cases() и передать его в "foreach":

enum Shapes {
	case RECTANGLE;
	case SQUARE;
	case CIRCLE;
	case OVAL;
}

foreach( Shapes::cases() as $shape ){
	echo $shape->name . "\n";
}

Получим:

RECTANGLE
SQUARE
CIRCLE
OVAL
enum как класс

Помимо полей "case" в enum может быть еще много всего. По сути это разновидность класса. Он может содержать методы, может реализовывать интерфейсы и использовать трейты.

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 будет тот конкретный объект case, для которого мы вызываем метод.

callable(...) — Callback-функции как объект первого класса

Doc: https://www.php.net/manual/ru/functions.first_class_callable_syntax.php
Wiki: https://wiki.php.net/rfc/first_class_callable_syntax

callable(...) — это способ создания анонимных функций из callable-объектов — это альтернатива синтаксису вызываемых объектов в виде строк Class::method или массивов [ 'Class', 'method' ].

Пример:

$fn = Closure::fromCallable('strlen'); // раньше
$fn = strlen(...);                     // теперь

$fn = Closure::fromCallable( [ $this, 'method' ] ); // раньше
$fn = $this->method(...)                            // теперь

$fn = Closure::fromCallable( [ Foo::class, 'method' ] ); // раньше
$fn = Foo::method(...);                                  // теперь

Синтаксис callable(...) создаёт объект Closure из callable-объекта. Часть callable принимает любое выражение, которое можно вызвать в PHP.

Неполный список возможных синтаксисов:

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(...);  // Вызываемый объект
$f3 = $obj->method(...);
$f4 = $obj->$methodStr(...);
$f5 = Foo::staticmethod(...);
$f6 = $classStr::$staticmethodStr(...);

// Традиционный callable-синтаксис со строками и массивами
$f7 = 'strlen'(...);
$f8 = [ $obj, 'method' ](...);
$f9 = [ Foo::class, 'staticmethod' ](...);

Преимущество синтаксиса в том, что он легко доступен для статического анализа и использует область видимости скоупа, где создается объект.

Пример скоупа

Область видимости определяется там, где создается callable-объект, а не там, где происходит его вызов. Например, если использовать массив callable [ $this, 'privateMethod' ], то область видимости соответствует месту вызова метода, а при использовании нового синтаксиса $this->privateMethod(...) — месту создания callable-объекта.

Рассмотрим на примере:

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();

Заметка: Создать объект этим синтаксисом (например, new Foo(...)) нельзя, поскольку синтаксис new Foo() не является вызовом.

Заметка: нельзя комбинировать с Nullsafe оператором. Оба следующих результата приводят к ошибке компиляции:

$obj?->method(...);
$obj?->prop->method(...);

A&X $var — пересечение типов

Doc: https://www.php.net/manual/en/language.types.declarations.php
Wiki: https://wiki.php.net/rfc/pure-intersection-types

Полноценная поддержка пересечений типов (intersection types), чтобы создавать новые типы, значения которых должны подпадать одновременно под несколько типов.

function count_and_iterate( Iterator&Countable $value ) {
	foreach( $value as $val ){
		echo $val;
	}

	count( $value );
}

// Раньше надо было писать так:

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 );
}

ЗАМЕТКА: Нельзя совмещать Пересечение типов и Объединение типов. Например: A&B|C.

= new Class() — расширенная инициализация объектов

RFC: https://wiki.php.net/rfc/new_in_initializers

Объекты теперь можно использовать в качестве значений:

  • параметров по умолчанию.
  • статических переменных.
  • глобальных констант.
  • в аргументах атрибутов.
  • при создании вложенных атрибутов (аннотаций).
Параметры по умолчанию
function test(
	$foo = new A,
	$bar = new B( 1 ),
	$baz = new C( x: 2 ),
) {
}

Еще пример:

// Сейчас можно писать так:

class MyController {

	public function __construct(
		private Logger $logger = new NullLogger(),
	) {}

}

// Раньше нужно было писать так:
class Test {

	private Logger $logger;

	public function __construct(
		?Logger $logger = null,
	) {
		$this->logger = $logger ?? new NullLogger();
	}

}
Статические переменные
class MyClass {
	public static function getInstance() {
		static $instance = new self();
		return $instance;
	}
}
Глобальные константы
class Config {
	public $setting = 'default';
}

const GLOBAL_CONFIG = new Config();

echo GLOBAL_CONFIG->setting; // 'default'
Во вложенных атрибутах (аннотациях)
class MyAttribute {
	public function __construct( public $value ) {}
}

#[MyAttribute( new DateTime('now') )]
class TestClass {
	// Класс с атрибутом
}

Еще пример:

class User {
	#[\Assert\All(
		new \Assert\NotNull,
		new \Assert\Length( min: 5 )
	)]
	public string $name = '';
}

В PHP атрибуты (аннотации) могут быть полезны в различных контекстах, но без рефлексии их использование становится ограниченным, поскольку рефлексия является основным механизмом для динамического доступа к метаданным (атрибутам) во время выполнения кода.

Пример использования кастомных атрибутов и доступ к ним через рефлексию:

#[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." );
				}
			}
		}
	}
}

Теперь используем:

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() — новая функция

DOC: https://www.php.net/manual/en/function.array-is-list.php

Добавлена новая функция array_is_list().

Определяет, является ли переданный массив списком. Массив считается списком, если его ключи состоят из последовательных чисел от 0 до count($array)-1.

Пример работы:

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

Заметка встроена в: PHP 5.3 - 8.5 — Синтаксис, Новинки