Что нового в PHP 8.1
- Release: https://www.php.net/releases/8.1/ru.php
- Wiki: PHP 8.1
- github: Список изменений PHP 8.1
- https://habr.com/ru/news/591739/
['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
—