MMS • Deepak Vohra
Article originally posted on InfoQ. Visit InfoQ
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 typeobject|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 returningnever
must not return a value, nor even implicitly return with a function call. - PHP 8.2 adds support for
true
,null
, andfalse
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
andfalse
- 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.
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
.
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:
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
.
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.
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.
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.
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:
- int
- float
- string
- bool
As an example, a string value of “1” is converted to a float in the following script:
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”.
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
.
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:
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
.
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.
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:
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:
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:
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.
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:
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:
- Throw an exception
- Call
exit()
- 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:
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.
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:
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. |