Типизированные свойства в PHP

Типизированные свойства в PHP

Улучшение системы типов

Типизированные свойства классов были добавлены в PHP 7.4 и обеспечивают значительное улучшение системы типов PHP. Эти изменения полностью приняты и обратно совместимы

В этом посте мы подробно рассмотрим эту новую фичу, но сначала давайте кратко пробежимся по наиболее важным моментам:

  • Типизированные свойства доступны с PHP 7.4
  • Они доступны только в классах и требуют модификатора доступа: public, protected, private; или же var
  • Допускаются все типы, за исключением void и callable

Вот как это выглядит в действии:


class Foo
{
public int $a;

public ?string $b = 1;

private Foo $prop;

protected static string $static = 'default';
}

Неинициализированный тип состояния

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

Несмотря на то, что следующий код, на первый взгляд, вполне рабочий:


class Foo
{
public int $bar;
}

$foo = new Foo;

Даже если значение $bar не является целым числом после создания объекта Foo, PHP будет выдавать ошибку только при обращении к $bar:


var_dump($foo->bar);


Fatal error: Uncaught Error: Typed property Foo::$bar must not be accessed before initialization

Как можно заметить из сообщения об ошибке, существует новый тип «состояния переменной»: неинициализированный.

Если бы у $bar не было типа, его значение было бы просто null. Однако типы могут быть обнуляемыми, поэтому невозможно определить, было ли установлено типизированное свойство обнуляемого типа или просто забыто. Вот почему  было добавлено "неинициализированное" состояние.

Есть четыре важных момента, которые нужно помнить о неинициализированных состояниях:

  • Вы не можете считать значение из неинициализированных свойств, это приведет к фатальной ошибке.
  • Поскольку при обращении к свойству проверяется неинициализированное состояние, вы можете создать объект с неинициализированным свойством, даже если его тип не имеет значения NULL.
  • Вы можете написать неинициализированное свойство перед чтением из него.
  • Использование unset в типизированном свойстве сделает его неинициализированным, в то время как отключение нетипизированного свойства сделает его null.

Особенно обратите внимание, что следующий код, где неинициализируемое, ненулевое свойство устанавливается после создания объекта, является рабочим:


class Foo
{
public int $a;
}

$foo = new Foo;

$foo->a = 1;

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

Значения по умолчанию и конструкторы

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


class Foo
{
public int $bar = 4;

public ?string $baz = null;

public array $list = [1, 2, 3];
}

Вы можете использовать null по умолчанию, только если тип на самом деле обнуляем. Это может показаться очевидным, но есть некоторое устаревшее поведение с параметрами по умолчанию, где допускается следующее:


function passNull(int $i = null)
{
    /* … */
}

passNull(null);

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

Также обратите внимание, что невозможно иметь значения по умолчанию с object типами или классами. Вы должны использовать конструктор, чтобы установить их значения по умолчанию.

Очевидным местом для инициализации типизированных значений, конечно же, будет конструктор:


class Foo
{
private int $a;

public function __construct(int $a)
{
$this->a = $a;
}
}

Но также помните, что я упоминал ранее: допустимо писать в неинициализированное свойство вне конструктора. Пока из свойства ничего не читается, неинициализированная проверка не выполняется.

Типы типов

Я уже упоминал, что типизированные свойства будут работать только в классах (пока), и что им нужен модификатор доступа или ключевое слово var перед ними.

Что касается доступных типов, то могут использоваться почти все типы, кроме void и callable.

Поскольку void означает отсутствие значения, имеет смысл, что его нельзя использовать для ввода значения. А вот с callable есть небольшой нюанс.

Callable в PHP может быть написан так:


$callable = [$this, 'method'];

Скажем, у вас будет следующий (неработающий) код:


class Foo
{
public callable $callable;

public function __construct(callable $callable)
{ /* … */ }
}

class Bar
{
public Foo $foo;

public function __construct()
{
$this->foo = new Foo([$this, 'method'])
}

private function method()
{ /* … */ }
}

$bar = new Bar;

($bar->foo->callable)();

В этом примере $callable относится к частному Bar::method, но вызывается в контексте Foo. Из-за этой проблемы и было решено не добавлять callable в список поддерживаемых типов.

Это не имеет большого значения, потому что замыкания (Closure) это допустимый тип, который будет помнить контекст $this, в котором он был создан.

С учетом всего написанного, вот список всех доступных типов:

  • bool (логический)
  • int (целый)
  • float (вещественный)
  • string (строковый)
  • array (массив)
  • iterable (псевдотип)
  • object (объект)
  • ? (nullable)
  • self (объект того же тип)
  • classes (классы)

Принуждаемые и строгие типы

PHP, будучи динамическим языком, за что его любят и ненавидят, будет стараться по возможности приводить или преобразовывать типы. Допустим, вы передаете строку, где ожидаете целое число, PHP попытается автоматически преобразовать эту строку:


function coerce(int $i)
{ /* … */ }

coerce('1'); // 1

Те же принципы применяются к типизированным свойствам. Следующий код валиден и преобразует '1'в 1:


class Bar
{
public int $i;
}

$bar = new Bar;

$bar->i = '1'; // 1

Если вам не нравится это поведение, вы можете отключить его, объявив строгие типы:


declare(strict_types=1);

$bar = new Bar;

$bar->i = '1'; // 1

Fatal error: Uncaught TypeError: Typed property Bar::$i must be int, string used


Типовая дисперсия и наследование

Несмотря на то, что в PHP 7.4 появилась улучшенная дисперсия типов, типизированные свойства все еще остаются неизменными. Это означает, что следующий код будет не валиден:


class A {}
class B extends A {}

class Foo
{
public A $prop;
}

class Bar extends Foo
{
public B $prop;
}

Fatal error: Type of Bar::$prop must be A (as in class Foo)

Если приведенный выше пример не убедил вас, то взгляните на следующий пример:


class Foo
{
public self $prop;
}

class Bar extends Foo
{
public self $prop;
}

PHP за кадром заменит self на конкретный класс, на который он ссылается, перед запуском кода. Это означает, что в этом примере будет сгенерирована та же ошибка. Единственный способ справиться с этим - сделать следующее:


class Foo
{
public Foo $prop;
}

class Bar extends Foo
{
public Foo $prop;
}

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

Хотя я согласен с этим мнением, стоит отметить, что можно изменить тип унаследованного свойства, но только если модификатор доступа также изменится с private на protected или public:


class Foo
{
private int $prop;
}

class Bar extends Foo
{
public string $prop;
}

Однако изменение типа с обнуляемого на ненулевое или обратное не допускается.


class Foo
{
public int $a;
public ?int $b;
}

class Bar extends Foo
{
public ?int $a;
public int $b;
}

Fatal error: Type of Bar::$a must be int (as in class Foo)


Еще кое-что

Как было сказано в начале этого поста, типизированные свойства являются основным дополнением к PHP. Можно еще много чего сказать о них. Я бы посоветовал вам прочитать RFC, чтобы узнать больше деталей.

Если вы еще не читали о полном списке внесенных изменений и добавленных функций в PHP 7.4, то советую незамедлительно это сделать. Честно говоря, это один из лучших релизов за последнее время, и оно того стоит!

Сергей Мухин

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

Есть вопросы?

Я почти всегда в режиме онлайн

Связаться со мной