BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles PHP 8 - Type System Improvements

PHP 8 - Type System Improvements

Key Takeaways

  • PHP 8 adds support for union types. A union type is  the union of multiple simple, scalar types. A value of a union type can belong to any of the simple types declared in the union type. 
  • PHP 8.1 introduces intersection types. An intersection type is the intersection of multiple class and interface types. A value of an intersection type must belong to all the class or interface types declared in the intersection type. 
  • PHP 8 introduces a mixed type that is equivalent to the union type object|resource|array|string|int|float|bool|null
  • PHP 8 introduces a static method return type , which requires the return value to be of the type of the enclosing class.   
  • PHP 8.1 introduces the never return type. A function returning never must not return a value, nor even implicitly return with a function call.    
  • PHP 8.2 adds support for true, null, and false as stand-alone types. 

This article is part of the article series "PHP 8.x". You can subscribe to receive notifications about new articles in this series via RSS.

PHP continues to be one of the most widely used scripting languages on  the web with 77.3% of all the websites whose server-side programming language is known using it according to w3tech. PHP 8 brings many new features and other improvements, which we shall explore in this article series.

In this article we will discuss extensions to the PHP type system introduced in PHP 8, 8.1, and 8.2. Those include union, intersection, and mixed types, as well as the static and never return types.

Additionally, PHP 8 also brings support for true, null, and false stand-alone types.

Some definitions

Type declarations in PHP are used with class properties, function parameters, and function return types. Various definitions are often used to describe a language in relation to its type system: strong/weak, dynamic/static.

PHP is a dynamically typed language. Dynamically typed implies that type checking is made at runtime, in contrast to static compile time type checking.  PHP is by default weakly typed, which implies fewer typing rules that support implicit conversion at runtime. Strict typing may however be enabled in PHP. 

PHP makes use of  types in different contexts :

  • Standalone type: A type that can be used in a type declaration, examples being int, string, array
  • Literal type: A type that also checks the value itself in addition to the type of a value. PHP supports two literal types - true and false
  • Unit type: A type that holds a single value, as an example null.

Besides simple types, PHP 8 introduces composite types such as union types, and intersection types. A union type  is the union of multiple simple types. A value has to match just one of the types in the union type. A union type may be used to specify the type of a class property, function parameter type, or function return type. The new type called mixed is a special type of union type. 

PHP 8.1 also adds intersection types to specify class types that are actually the intersection of multiple class types. Two new return types have been added. The return type never is used if a function does not return, which could happen if the function throws an exception or calls exit(), as an example. The return type static implies that the return value must be an instanceof the class in which the method is called.   

Union Types

If you are familiar with Venn diagrams you may remember set union and intersection. Union types are introduced in PHP 8 to support the union of simple types. The syntax to use for union type in a declaration is as follows:

Type1|Type2|....|TypeN

To start with an example, in the following script $var1 belongs to union type int|string|array. Its value is initialized to an integer value, and subsequently the value is set to each of the other types in the union type declaration. 

<?php

class A{

  public int|string|array $var1=1;
 
}

$a= new A();
echo $a->var1;
$a->var1="hello";
echo $a->var1;
$a->var1=array(
    "1" => "a",
    "2" => "b",
);
var_dump($a->var1);

The output from the script is as follows:

1
hello
array(2) { [1]=> string(1) "a" [2]=> string(1) "b" }

Being PHP a weakly typed language, if $var1’s value is set to float value 1.0, an implicit conversion is performed. The following script will give an output of 1

<?php

//declare(strict_types = 1);
class A{

public int|string|array $var1=1;
 
}

$a= new A();
 
$a->var1=1.0;
echo $a->var1;

However, if strict typing is enabled with the declaration declare(strict_types = 1) $var1’s value won’t get set to 1.0 and An error message is displayed:

Uncaught TypeError: Cannot assign float to property A::$var1 of type array|string|int

Weak typing can sometimes convert values to a closely related type, but type conversion cannot be always performed. For example, a variable of union type int|array cannot be assigned a string value, as in script:

<?php

class A{

public int|array $var1=2;
 
}

$a= new A();
 
$a->var1="hello";
echo $a->var1;

An error message is displayed:

Uncaught TypeError: Cannot assign string to property A::$var1 of type array|int

In a slightly more complex example, the following script uses union types in a class property declaration, function parameters,  and function return type.

<?php

class A{

public string|int|bool $var1=true;

function fn1(string|int|array $a, object|string $b): 

string|bool|int {
    return $a;
}

}
$a=new A();
echo $a->var1;
echo $a->fn1("hello","php"); 

The output is:

1
hello

Null in union types

A union type can be nullable, in which case null is one of the types in the union type declaration. In the following script, a class property, function parameters, and function return type are all declared with a nullable union type.

<?php

class A{

public string|null $var1=null;

function fn1(string|int|null $a=null, object|false|null $b=null): 

string|bool|null {
    return null;
}

}
$a=new A();
echo $a->var1;
echo $a->fn1(); 

The false type in union types

The false pseudo-type may be used in a union type. In the following example, the false type is used in class property declaration, function parameters, and function return type, all of which are union type declarations.

<?php

class A{

public string|int|false $var1=1;

function fn1(string|int|false $a, false|string $b): 

string|false|int {
    return $a;
}

}
$a=new A();
echo $a->var1;
echo $a->fn1("hello",false);

The output is:

1
hello

If bool is used in a union type, false cannot be used, as it is considered a duplicate declaration. Consider the following script in which a function declares a function parameter with a union type that includes both false and bool.

<?php
function fn1(string $a, bool|string|false  $b): object {
    return $b;
} 

An error message is displayed:

Duplicate type false is redundant

Class types in union types

A class type may be used in a union type. The class type A is used in a union type in the following example:

<?php

class A{}
 
function fn1(string|int|A $a, array|A $b): A|string  {
    return $a;
}
$a=new A();
var_dump(fn1($a,$a));

The output is:

object(A)#1 (0) { }

However, a class type cannot be used in a union type if the type object is also used. The following script uses both a class type and object in a union type.

<?php

class A{}
 
function fn1(object|A $a,  A $b): A   {
    return $a;
}

An error message is displayed:

Type A|object contains both object and a class type, which is redundant

If iterable is used in a union type, array and Traversable cannot be used additionally. The following script uses iterable with array in a union type:

<?php

function fn1(object $a, iterable|array $b):  iterable {
     
}

An error message is displayed:

Type iterable|array contains both iterable and array, which is redundant

Union types and class inheritance

If one class extends another, a union type may declare both classes individually, or just declare the superset class. As an example, in the following script, class C extends class B, which extends class A. Classes A, B, and C are then included in union type declarations of function parameters. 

<?php

class A  {
function fn1(){
  return "Class A object";
}
}

class B extends A {
function fn1(){
  return "Class B object";
}
}

class C extends B {
function fn1(){
  return "Class C object";
  }
}

$c = new C();
 

function fn1(A|B|C $a, A|B|C $b): string {
     
    return $a->fn1();
}

echo fn1($c,$c);

Output is:

Class C object

Alternatively, fn1 could declare only class A as the function parameters’ type, with the same output:

  function fn1(A $a, A $b): string {
     
    return $a->fn1();
}

Void in union types

The void return type cannot be used in union type. To demonstrate, run the following script:

<?php

function fn1(int|string $a, int|string $b): void|string {
     
    return $a;
}

An error message is displayed:

Void can only be used as a standalone type

Implicit type conversion with union types

Earlier we mentioned that, if strict typing is not enabled, a value that does not match any of the types in a union type gets converted to a closely related type. But, which of the closely related types? The following order of preference is used for implicit conversion:

  1. int
  2. float
  3. string
  4. bool

As an example, a string value of "1" is converted to a float in the following script:

<?php

 
class A{

public  float|bool  $var1=true;
 
}

$a= new A();
 
$a->var1="1";
var_dump($a->var1);
?>

The output is

float(1)

However, if int is included in the union type, output is int(1). In the following script a variable of union type int|float is assigned a string value of "1.0". 

<?php

class A{

public  int|float    $var1=1;
 
}

$a= new A();
 
$a->var1="1.0";
var_dump($a->var1);
?>

The output is:

float(1)

In the following script the string value "true" is interpreted as a string value because the union type includes string.

<?php

class A{

public  float|bool|string   $var1=1;
 
}

$a= new A();
 
$a->var1="true";
var_dump($a->var1);
?>

Output is:

string(4) "true"

But, in the following script the same "true" string is converted to a bool value because string is not in the union type:

<?php

class A{

public  float|bool    $var1=1;
 
}

$a= new A();
 
$a->var1="true";
var_dump($a->var1);
?>

Output is;

bool(true)

In another example, with rather unpredictable output, consider the script that assigns a string value to a variable of union type int|bool|float

<?php

class A{

public  int|bool|float    $var1=1;
 
}

$a= new A();
 
$a->var1="hello";
var_dump($a->var1);
?>

Output is:

bool(true)

The string is converted to a bool because conversion to an int or float cannot be made.

The new mixed type

PHP 8 introduces a new type called mixed which is equivalent to the union type object|resource|array|string|int|float|bool|null. As an example, in the following script, mixed is used as a class property type, function parameter type, and function return type. Strict typing is enabled to demonstrate that mixed is not affected by strict typing.

<?php
declare(strict_types = 1);

class A{

public  mixed    $var1=1;
 
function fn1(mixed $a):mixed{ return $a;}
}

$a= new A();
  
var_dump($a->fn1(true));
var_dump($a->var1);
$a->var1="hello";
var_dump($a->var1);

The flexibility of mixed is apparent with the different types in output:

bool(true) 
int(1) 
string(5) "hello"

It is redundant to use other scalar types in a union type along with mixed as mixed is a union type of all other scalar types. To demonstrate this, consider the script that uses mixed in a union type with int.

<?php
 
class A{

function fn1(int|mixed $a):mixed{ return $a;}
}

An error message is displayed:

Type mixed can only be used as a standalone type

Likewise, mixed cannot be used with any class types. The following script generates the same error message as before:

<?php
 
class A{}
class B{

function fn1(A|mixed $a):mixed{ return $a;}
}

The mixed return type can be narrowed in a subclass method’s return type. As an example, the fn1 function in an extending class narrows a mixed return type to array.

<?php
 
class A{
public function fn1(mixed $a):mixed{ return $a;}
}
class B extends A{

public function fn1(mixed $a):array{ return $a;

}

New standalone types null, false and true

Previous to PHP 8.2, the null type was PHP’s unit type, i.e. the type that holds a single value: null. Similarly, the false type was a literal type of type bool. However, the null and false types could only be used in union type and not as stand-alone types. To demonstrate this, run a script such as the following in PHP 8.1 and earlier: 

<?php

class A{

public  null $var1=null;

}
$a=new A();
echo $a->var1;

The script outputs error message in PHP 8.1:

Null can not be used as a standalone type

Similarly, to demonstrate that the false type could not be used as a stand-alone type in PHP 8.1 or earlier, run the following script:

<?php

class A{

public false $var1=false;

}

The script generates error message with PHP 8.1:

False can not be used as a standalone type 

PHP 8.2 has added support for null and false as stand-alone types. The following script makes use of null as a method parameter type and method return type.

<?php

class NullExample {
  public null $nil = null;
 
  public function fn1(null $v): null { return null;  }
}

null cannot be explicitly marked nullable with ?null. To demonstrate, run the following script:

<?php
 
class NullExample {
    public null $nil = null;
 
    public function fn1(?null $v): null { return null;  }
}

An error message is generated:

null cannot be marked as nullable

The following script makes use of false as a stand-alone type.

<?php
 
class FalseExample {
    public false $false = false;
 
    public function fn1(false $f): false { return false;}
}

null and false may be used in a union type, as in the script:

<?php
 
class NullUnionExample {
    public null $nil = null;
 
    public function fn1(null $v): null|false { return null;  }
}

In addition, PHP 8.2 added true as a new type that may be used as a standalone-type. The following script uses true as a class property type, a method parameter type and a method return type.

<?php

class TrueExample {
    public true $true = true;
 
    public function f1(true $v): true { return true;}
}

The true type cannot be used in a union type with false, as in the script:

<?php
 
class TrueExample {
 
    public function f1(true $v): true|false { return true;}
}

The script generates an error message:

Type contains both true and false, bool should be used instead 

Similarly, true cannot be used in a union type with bool, as in the script:

class TrueExample {
        public function f1(true $v): true|bool { return true;}
}

The script generates an error message:

Duplicate type true is redundant

Intersection types

PHP 8.1 introduces intersection type  as a composite type. Intersection types can be used with class and interface types. An intersection type is used for a type that represents multiple class and interface types rather than a single class or interface type. The syntax for an intersection type is as follows:

Type1&Type2...TypeN

When to use an intersection type, and when a union type? If a type is to represent one of multiple types, we use a union type. If a type is to represent multiple types at once, we use an intersection type. The next example best illustrates the difference. Consider classes A, B, and C that have no relation. If a type is to represent any of these types use a union type, as in the script:

<?php

class A
{
  function fn1(){
    return "Class A object";
  }
}

class B  
{
  function fn1(){
    return "Class B object";
  }
}

class C  
{
  function fn1(){
    return "Class C object";
  }
}

$c = new C();
 

function fn1(A|B|C $a, A|B|C $b): string {
     
    return $a->fn1();
}

echo fn1($c,$c); 
?>

The output is:

Class C object

If we had used an intersection type in the script, an error message would result. Modify the function to use intersection types:

function fn1(A&B&C $a, A&B&C $b): string {
     
    return $a->fn1();
}

An error message is displayed:

Uncaught TypeError: fn1(): Argument #1 ($a) must be of type A&B&C, C given

The intersection would be suitable if class C extends class B extends class A, as in the script:

<?php

class A
{
  function fn1(){
  return "Class A object";
}
}

class B  extends A
{
  function fn1(){
  return "Class B object";
}
}

class C  extends B
{
  function fn1(){
  return "Class C object";
  }
}

$c = new C();
 

function fn1(A&B&C $a, A&B&C $b): string {
     
    return $a->fn1();
}

echo fn1($c,$c); 
?>

The output is:

Class C object

Scalar types and intersection types

Intersection types can only be used with class and interface types, but cannot be used with scalar types. To demonstrate this, modify the fn1 function in the preceding script scalar type as follows:

function fn1(A&B&C&string $a, A&B&C $b): string {
     
}

An error message is displayed:

Type string cannot be part of an intersection type

Intersection types and union types

Intersection types cannot be combined with union types. Specifically, intersection type notation cannot be combined with the union type notation in the same type declaration. To demonstrate, modify the fn1 function as follows:

function fn1(A&B|C $a, A&B|C $b): string {
    
}

A parse error message is output:

Parse error: syntax error, unexpected token "|", expecting variable

An intersection type can be used with a union type in the same function declaration, as demonstrated by function:

function fn1(A&B&C $a, A|B|C $b): string {
       
}

Static and never return types

PHP 8.0 introduces static as a new return type, and PHP 8.1 introduces never as a new return type.   

Static return type

If a return type is specified as static, the return value must be of the same type as the class in which the method is defined. As an example, the fn1 method in class A declares a return type static, and therefore must return a value of type A, which is the class in which the function is declared.

<?php

class A 
{
    public int $var1=1;
    public function fn1(): static
    {    
      return new A();
    }
}

 
$a=new A();
echo $a->fn1()->var1;

Output is:

1

The function declaring a static return type must belong to a class. To demonstrate this, declare never as return type in a global function:

<?php

  function fn1(): static
    {    
       
    }

An error message is displayed:

Cannot use "static" when no class scope is active

The class object returned must be the enclosing class. The following script would generate an error because the return value is of class type B, while the static return type requires it to be of type A.

<?php

 
class B{}
class A 
{
    public int $var1=1;
    public function fn1(): static
    {    
      return new B();
    }
}

The following error message is generated:

Uncaught TypeError: A::fn1(): Return value must be of type A, B returned

If class B extends class A, the preceding script would run ok and output 1.

class B extends A{}

The static return type can be used in a union type. If static is used in a union type the return value doesn’t necessarily have to be the class type. As an example, static is used in a union type in script:

<?php

class A 
{
    public int $var1=1;
    public function fn1(): static|int
    {    
      return 1;
    }
}

 
$a=new A(); 
echo $a->fn1();

Output is

 1

The type static cannot be used in an intersection type. To demonstrate this, consider the following script.

<?php

class B extends A{}
class A 
{
    public function fn1(): static&B
    {    
      return new B();
    }
}

 An error message is generated:

Type static cannot be part of an intersection type

Return type never

If the return type is never, the function must not return a value, or return at all, i.e, the function does not terminate. The never return type is a subtype of every other return type. This implies that never can replace any other return type in overridden methods  when extending a class. A never-returning function must do one of the following:

  1. Throw an exception
  2. Call exit()
  3. Start an infinite loop

If a never-returning function is never to be called, the function could be empty, as  an example:

<?php

class A
{
function fn1(): never {
     
} 
}

The fn1() function in class A cannot be called as it would imply the function returns implicit NULL. To demonstrate, modify the preceding script to:

<?php

class A {
function fn1(): never {
   } 
}

$a=new A();
$a->fn1();

An error message is generated when the script is run:

Uncaught TypeError: A::fn1(): never-returning function must not implicitly return

The following script would generate the same error message as the if condition is never fulfilled and the function implicitly returns NULL:

<?php

function fn1(): never
{
    if (false) {
        exit();
    }
}
fn1();

Unlike the static return type, never may be used as a return type of a function not belonging to the scope of a class, for example:

<?php

class A
{
  
}
 
function fn1(): never {
     
}

A function with return type as never must not return a value. To demonstrate this, the following script declares a function that attempts to return value although its return value is never.

<?php

  function fn1(): never
    {    
     return 1;
    }

An error message is generated:

A never-returning function must not return

If the return type is never, the function must not return even implicitly. For example, the fn1 function in the following script does not return a value, but returns implicitly when its scope terminates.

<?php

  function fn1(): never
    {    
    }

fn1();

An error message is displayed:

Uncaught TypeError: fn1(): never-returning function must not implicitly return

What is the use of a function that declares return type never, and does not terminate? The never return type could be used during development, testing, and debugging. A function that returns the never type could exit with a call to exit(). Such a function may even be called, as in the following script:

<?php

  function fn1(): never
    {    
      exit(); 
    }

fn1();

A never returning function could throw an exception, for example:

<?php

  function fn1(): never {
      
    throw new Exception('Exception thrown');
     
}

A function including an infinite loop could declare a never return type, as in the example:

<?php

  function fn1(): never {
     while (1){}
}

The never return type can override any other type in derived classes, as in the example:

<?php
class A
{
   
  function fn1(): int {
      
  }
}
class B extends A{
function fn1(): never {
      
}
}

The never return type cannot be used in a union type. To demonstrate this, the following script declares never in a union type.

<?php
 
class A{  
function fn1(): never|int {
      
}
}

An error message is displayed:

never can only be used as a standalone type

The never type cannot be used in an intersection type. To demonstrate this, the following script uses never with class type B.

<?php
 
class B{}
class A{  
function fn1(): never&B {
      
}
}

An error message is generated:

Type never cannot be part of an intersection type

Scalar types do not support aliases

As of PHP 8, a warning message is generated if a scalar type alias is used. For example, if boolean is used instead of bool a message indicates that boolean would be interpreted as a class name. To demonstrate this, consider the following script in which integer is used as a parameter type in a function declaration.

<?php
    function fn1(integer $param) {}
    fn1(1);
?>

The output from the script includes a warning message is as follows:

Warning: "integer" will be interpreted as a class name. Did you mean "int"? Write "\integer" to suppress this warning 

Fatal error: Uncaught TypeError: fn1(): Argument #1 ($param) must be of type integer, int given 

Returning by reference from a void function is deprecated

As of PHP 8.1, returning by reference from a void function is deprecated, because only variable references can be returned by reference whereas a void return type does not return a value. To demonstrate this, run the following script:

<?php
function &fn1(): void {}
?>

The output is a deprecation message:

Deprecated: Returning by reference from a void function is deprecated

Summary

In this article we discussed the new types related features introduced in PHP 8, including  union, intersection, and mixed types, and the static and never return types. In the next article, we will describe new features relating to PHP’s  arrays, variables, operators, and exception handling.  

 

This article is part of the article series "PHP 8.x". You can subscribe to receive notifications about new articles in this series via RSS.

PHP continues to be one of the most widely used scripting languages on  the web with 77.3% of all the websites whose server-side programming language is known using it according to w3tech. PHP 8 brings many new features and other improvements, which we shall explore in this article series.

About the Author

Rate this Article

Adoption
Style

BT