PHP прошел долгий путь в работе с типами. Теперь у нас есть скалярные типы, возвращаемые типы, nullable-типы и даже типы свойств в PHP 7.4!

PHP 8.0 поставляется с поддержкой  объединений типов (Union Types)!

В версиях до PHP 8.0 вы могли объявлять только один тип для свойств, параметров и возвращаемых значений. В PHP 7.1 и более новых версиях появились nullable-типы, что означает возможность объявить тип как  null  с помощью объявления, похожего на  ?string . 

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

class Example {
    private int|float $foo;
    public function squareAndAdd(float|int $bar): int|float {
        return $bar ** 2 + $foo;
    }
} 

Аналогично типам, применявшимся в предыдущих версиях PHP, теперь PHP будет следить за тем, чтобы параметры функций, возвращаемые типы и свойства классов соответствовали одному из типов, объявленных в определении. 

Объединения типов до PHP 8.0

Nullable-типы

В PHP уже есть поддержка nullable-типов; вы можете объявить тип как nullable, добавив префикс  ?  к типу. Например, если определенное свойство может быть  string  или  null , вы можете объявить его как  ?string . С появлением объединений типов в PHP 8.0 запись  string|null  будет функционально эквивалентна  ?string . 

Тип  iterable 

Также существует псевдотип  iterable , который функционально эквивалентен  array|Traversable . 

Комментарии PHPDoc

Стандарт PHPDoc поддерживал объединения типов. Вы могли использовать комментарий PHPDoc для объявления типов, разрешенных для определенного блока кода:

class Example {  

 /**  
  * @var int|float  
  */  
  private $foo;  

  /**  
   * @param int|float $bar  
   * @return int|float  
   */  
  public function squareAndAdd(int $bar): int {  
    return $bar ** 2 + $this->foo;  
  }
 } 

Объединения типов в PHP 8.0

Начиная с PHP 8.0, вы можете объявлять любое количество произвольных типов для свойств, аргументов и возвращаемых типов. Есть несколько исключений, которые не имеют смысла для совместного использования, например  string|void . 

Теперь вы можете полностью избавиться от комментариев PHPDoc  @var ,  @param  и  @return  в пользу типов, контролируемых непосредственно в самом коде. Это определенно поможет очистить шаблонные комментарии, которые присутствовали исключительно потому, что их нельзя было объявить в коде ранее. 

Существующие nullable-типы не удаляются и не объявляются устаревшими. Вы можете продолжать использовать  ?string  как сокращение для  string|null .

Тип  iterable  также не будет удален и будет функционально равен  array|Traversable . 

В RFC подробно объясняются некоторые ограничения и другие улучшения объединений типов:

Тип  void  не разрешен в объединении

В PHP уже есть поддержка типа  void . Он разрешен только в качестве возвращаемого типа, так как использование  void  в любом другом месте не имело бы смысла. Функция либо не должна возвращать никакого значения, либо должна вызывать  return;  без указания значения. В объединениях типов PHP 8.0 тип  void  не разрешается комбинировать с любым другим типом. 

Например, следующее не допускается:

function foo(): void|null {} 

Специальный тип  false  

Ядро PHP и многие устаревшие библиотеки возвращают  return false  для указания отрицательного результата. 

Например, если у вас есть функция, которая загружает учетную запись пользователя по ее ID, она может возвращать  return false , чтобы указать, что пользователь не существует. 

Чтобы упростить внедрение объединений типов, разрешено использовать  false  как часть объединения типов. 

function user_load(int $id): User|false {
} 

Приведенный выше фрагмент имитирует функцию  user_load()  в Drupal, где она возвращает объект  User , если учетная запись пользователя найдена, или  false  в противном случае. Этот специальный тип  false  может быть очень полезен в такой ситуации. 

Существует также несколько функций ядра PHP, которые следуют этому же шаблону. Например, функция  strpos()  возвращает  int  с позицией, где байты искомой строки были найдены в строке, или  false , если они не найдены. Специальный тип  false  здесь тоже может пригодиться. 

  •   false  нельзя использовать как отдельный тип. Это означает, что объявление типа вроде  public false $foo  не допускается. Для этого вы все еще можете использовать тип  bool .  Начиная с PHP 8.2, использование  false  в качестве отдельного типа разрешено.
  •  До PHP 8.2 не существует псевдотипа  true . Цель псевдотипа  false  — приспособиться к (в основном устаревшему) коду, который возвращает  false  в случае ошибки. Используйте там тип  bool . В  PHP 8.2 вводится  true  в качестве типа, но его нельзя использовать в объединении с  false , то есть  false|true  не допускается, и вместо этого нужно использовать  bool .
  •  Псевдотип  false  разрешен везде, где разрешены типы: свойства классов, аргументы функций и возвращаемые типы поддерживают тип  false . 
  • *Если используется  bool , то  false  не может быть использован в том же объявлении типа.
 Изменения в PHP 8.2 В семантику, описанную выше, внесены некоторые изменения:

Nullable-типы ( ?TYPE ) и  null  нельзя смешивать.

Хотя  ?string  является сокращением для  string|null , эти две нотации нельзя смешивать. 

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

TYPE_1|TYPE_2|null 

Оно не должно объявляться как  ?TYPE_1|TYPE_2 , так как это было бы неоднозначным объявлением. 

Проверка ошибок на этапе компиляции

Объединения типов не допускают дублирующихся или избыточных типов в объявлении. Эта проверка будет происходить на этапе компиляции  без автозагрузки классов/интерфейсов.

Дублирующиеся типы не допускаются.

Вы не можете объявить  int|int  или  int|INT , так как по сути это одно и то же. Более того,  int|?int  также не допускается. Последнее приведет к синтаксической ошибке, а первое вызовет следующую ошибку:

Fatal error: Duplicate type ... is redundant in ... on line ... 

Избыточные типы не допускаются.

Объединения типов не допускают избыточных типов классов. Однако важно отметить, что это относится только к нескольким специальным типам. 

  •   bool|false  не допускается, потому что  false  является типом  bool .
  •  `object` нельзя использовать с именем класса, потому что все объекты классов также относятся к типу  object .
  •   iterable  нельзя использовать с  array  или  Traversable , потому что  iterable  уже является объединением типов  array|Traversable . 
  •  Имена классов можно использовать, даже если один наследует другой.

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

Вот несколько примеров:

function foo(): bool|false {} // Fatal error: Duplicate type false is redundant in ... on line ...
function foo(): DateTime|object {} // Fatal error: Type DateTime|object contains both object and a class type, which is redundant in ... on line ...
function foo(): iterable|array {} // Fatal error: Type iterable|array contains both iterable and array, which is redundant in ... on line ... 

Поскольку объявление типа проверяется только на этапе компиляции, следующее является допустимым:

class A{}
class B extends A{}
function foo(): A|B {} 

Вариантность типов

Вариантность объединений типов соответствует принципу LSP. Это гарантирует, что все подклассы и реализации интерфейса не должны изменять поведение программы и по-прежнему соответствовать контракту. Это соблюдается в PHP даже без объединений типов.

  •  Параметры контрвариантны: типы могут быть расширены супертипом. 
  •  Возвращаемые типы ковариантны: типы могут быть сужены до подтипа.
  •  Типы свойств инвариантны: типы не могут быть изменены на подтип или супертип.

Добавление или удаление типов в объединение

class A{
    public function foo(string|int $foo): string|int {}
}
class B extends A{
    public function foo(string|int|float $foo): string {}
} 

В приведенном выше фрагменте типы параметров расширены (с добавлением типа  float ). Это разрешено, поскольку все программы, использующие класс  B , ожидают, что они будут принимать все типы, которые принимает класс  A . Класс  B  по-прежнему выполняет этот контракт.

Возвращаемый тип в  B::foo  сужен еще сильнее, но это все равно соответствует LSP, поскольку класс  B  соответствует возвращаемому типу, объявленному в  A::foo . 

Если бы вы изменили тип таким образом, что класс  B  перестал бы выполнять контракт, это вызвало бы фатальную ошибку:

class A{
    public function foo(string|int $foo): string|int {}
}
class B extends A{
    public function foo(string|float $foo): string|int {}
} Fatal error: Declaration of B::foo(string|float $foo): string|int must be compatible with A::foo(string|int $foo): string|int in ... on line ... 

Вариантность отдельных типов в объединении

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

 Подклассы

class ParentClass {}
class ChildClass extends ParentClass{}

class FooParent {
    public function foo(string|ChildClass $a): string {}
    public function bar(string $b): string:ParentClass {}
}

class FooChild extends FooParent{
    public function foo(string|ParentClass $a): string|ChildClass {}    
    public function bar(string $b): string:ChildClass {}
} 

Это абсолютно корректно, так как в функциях  FooChild::foo  параметры расширяются. Весь существующий код, работающий с классом  FooParent , будет работать, потому что  ChildClass  является подтипом  ParentClass  с той же функциональностью. 

В функции  FooChild::bar  возвращаемый тип сужен еще сильнее. Это также допустимо, поскольку  FooChild::bar  по-прежнему выполняет свой контракт, возвращая подтип  string  или  ChildClass , который наследует  ParentClass .

 Nullable

class FooParent {
    public function foo(string $a): int|null {}
}

class FooChild extends FooParent{
    public function foo(string|null $b): int|null {}    
} 

Возвращаемым типам разрешено терять тип  nullable , а параметрам разрешено делать так, чтобы они принимали  null .

  bool  и  false 

В объединении типов PHP считает  false  подтипом для  bool . Это позволяет использовать такую вариантность:

class FooParent {
    public function foo(int|false $a): int|bool {}
}

class FooChild extends FooParent{
    public function foo(int|bool $b): int|false {}  
} 

Влияние на обратную совместимость

Объединения типов — это новая функция в PHP, и ее новый синтаксис не нарушает существующую функциональность. Поскольку это синтаксическое изменение в движке, эту функциональность нельзя перенести в более старые версии с помощью полифила.