Что нового в PHP 8.4

public $foo { set () } — Хуки свойств

Doc: https://www.php.net/manual/ru/migration84.new-features.php
Wiki: https://wiki.php.net/rfc/property-hooks

Хуки свойства позволяют добавить логику при чтении/записи (get/set) свойства — без создания отдельных методов getFoo() и setFoo() (геттеров и сеттеров).

Было:

class User {
	private string $name;

	public function setName( string $name ): void {
		if ( $name === '' ) throw new ValueError('Empty');
		$this->name = $name;
	}

	public function getName(): string {
		return ucfirst( $this->name );
	}
}

Стало:

class User {
	public string $name {
		set {
			if ( $value === '' ) throw new ValueError('Empty');
			$this->name = $value;
		}
		get => ucfirst( $this->name );
	}

	public function __construct(string $name) {
		$this->name = $name;
	}
}

Теперь можно писать $user->name = 'Tim'; и при установки значения будет вызвана set функция:

$user = new User();
$user->name = 'tim'; // проходит через set-хук
echo $user->name; // Tim
Поддерживается короткая запись get => expr;:
public string $fullName { get => $this->first . ' ' . $this->last; }
Свойства в интерфесах с get и set:

Раньше интерфейсы могли требовать только методы, а теперь могут требовать свойства — читаемые (get) и/или записываемые (set).

То есть теперь можно сказать:

“Любой класс, реализующий этот интерфейс, обязан иметь $name, которое можно читать (или писать)”

Пример:

interface Named {
	public string $name { get; }  // обязательное читаемое свойство
}

class User implements Named {
	public string $name = 'Tim';  // подходит, get есть по умолчанию
}

class Person implements Named {
	public string $name {
		get => strtoupper( $this->realName ); // подходит, есть гет хук
	}
}

Это делает интерфейсы ближе к «контрактам данных», а не только к наборам методов.

Делает свойства «умными», без лишней магии __get/__set:
public string $slug {
	set {
		$this->slug = strtolower( trim( $value ) );
	}
}
// Без магии и лишнего кода

Итого: Хуки свойств обеспечивают поддержку вычисляемых свойств, которые могут быть понятны IDE и инструментам статического анализа, без необходимости писать DocBlock-комментарии, которые могут не совпадать. Кроме того, они позволяют выполнять надёжную предварительную или последующую обработку значений, без необходимости проверять, существует ли в классе соответствующий геттер или сеттер.

public private(set) string $ver — Разная область видимости свойств

Doc: https://www.php.net/manual/ru/language.oop5.visibility.php#language.oop5.visibility-members-aviz
Wiki: https://wiki.php.net/rfc/asymmetric-visibility-v2

В PHP 8.4 можно задавать асимметричную область видимости свойств — отдельно для чтения и записи.

set-видимость указывается как private(set) или protected(set) сразу после модификатора видимости.

class Book {
	public function __construct(
		public    private(set)   string $title,
		public    protected(set) string $author,
		protected private(set)   int    $year,
	) {}
}

class SpecialBook extends Book {
	public function update( string $author, int $year ): void {
		$this->author = $author; // Всё хорошо
		$this->year = $year; // Критическая ошибка
	}
}

$b = new Book( 'PHP', 'Peter', 2024 );

echo $b->title;   // OK
echo $b->author;  // OK
echo $b->pubYear; // Fatal

$b->title   = 'How not to PHP';    // Fatal
$b->author  = 'Pedro H. Peterson'; // Fatal
$b->pubYear = 2023;                // Fatal

Особенности:

  • Get всегда имеет ту же или более широкую область, чем set.
    Другими словами ограничение для set должно быть равно или сильнее чем у get:

    public protected(set)  // OK
    protected public(set)  // error
  • private(set) делает свойство финальным - его нельзя переопределить или повторно объявить в дочернем классе.

  • Работает только с типизированными свойствами - можно ставить только у свойств с типом - например string, int, array и т.д.:

    public private(set) string $title; // OK
    public private(set) $title;        // fatal
  • Получение ссылки на свойство подчиняется видимости set, а не get. Это связано с тем, что ссылка разрешает изменять значение свойства.

    Аналогично, попытка записи в массив, который содержится в свойстве, включает в себя как внутреннюю операцию чтения - get, так и операцию записи - set, и поэтому подчинится области видимости set, поскольку ограничение области видимости для записи сильнее.

  • Нельзя исползовать пробел: private( set ) - пробел вызовет ошибку.
Наследование

При наследовании можно ослабить доступ для чтения или записи, но private(set) — финальное, переопределить нельзя:

class Book {
	protected                string $title;
	public    protected(set) string $author;
	protected private(set)   int    $year;   // final
}

class SpecialBook extends Book {
	// OK - ограничение на чтение слабее, а на запись – такое же
	public protected(set) string $title;

	// OK - ограничение на чтение такое же, а на запись – слабее
	public string $author;

	// Fatal Error - cвойства с private(set) — окончательны!
	public protected(set) int $year;
}

new MyClass()->method() — без скобок

Doc: https://www.php.net/manual/ru/migration84.new-features.php#migration84.new-features.core.new-chaining
Wiki: https://wiki.php.net/rfc/new_without_parentheses

К свойствам и методам только что инициализированного объекта теперь можно обращаться, не оборачивая выражение new в круглые скобки.

// Было:
$ver = ( new PhpVersion() )->getVersion();

// Стало:
$ver = new PhpVersion()->getVersion();

#[\Deprecated] — Атрибут

Doc: https://www.php.net/manual/ru/class.deprecated.php
Wiki: https://wiki.php.net/rfc/deprecated_attribute

Атрибут #[\Deprecated], позволяет объявлять функции, методы, константы и enum-кейсы как устаревшие.

При вызове таких (устаревших) элементов будет выдаваться предупреждение E_USER_DEPRECATED.

Можно указать сообщение $message и версию $since, которые попадут в текст предупреждения. Например:

#[\Deprecated('Use newMethod() instead', since: '2.4')]
function oldMethod() {}

oldMethod(); // Deprecated: Function oldMethod() is deprecated since 2.4, Use newMethod() instead

Еще примеры использования:

#[\Deprecated]
function test() {
}

#[\Deprecated("use test() instead")]
function test2() {
}

#[\Deprecated("use test() instead", since: "2.4")]
function test3() {
}

#[\Deprecated(since: "2024-05-07")]
function test4() {
}

class Clazz {
	#[\Deprecated]
	public const OLD_WAY = 'foo';

	#[\Deprecated]
	function test() {
	}

	#[\Deprecated("use test() instead")]
	function test2() {
	}
}

enum MyEnum {
	#[\Deprecated]
	case OldCase;
}
Рефлексия

Метод isDeprecated() классов ReflectionFunctionAbstract и ReflectionClassConstant будет возвращать true:

#[\Deprecated]
function test() {}

$r = new ReflectionFunction('test');

var_dump( $r->isDeprecated() ); // bool(true)
class Clazz {
	#[\Deprecated]
	public const OLD_WAY = 'foo';
}

$r = new ReflectionClassConstant( Clazz::class, 'OLD_WAY' );

var_dump( $r->isDeprecated() ); // bool(true)

Атрибут #[\Deprecated] в stub-файлах будет работать аналогично док-комментарию /** @deprecated */.

  • Старые функции с таким комментарием не получат атрибут автоматически.
  • Авторам расширений рекомендуется добавить #[\Deprecated] вручную для единообразия.

В глобальном пространстве нельзя объявлять класс с именем Deprecated.

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