Typed properties in PHP 7.4
Typed class properties have been added in PHP 7.4 and provide a major improvement to PHP ‘s type system. These changes are fully opt-in and non breaking to previous versions.
In this post we’ll look at the feature in-depth, but first let’s start by summarising the most important points:
- They are available as of PHP 7.4 , which is scheduled to be released in November of 2019
- They are only available in classes and require an access modifier: public , protected or private ; or var
- All types are allowed, except void and callable
This is what they look like in action:
class Foo < public int $a; public ?string $b = 'foo'; private Foo $prop; protected static string $static = 'default'; >
If you’re unsure about the added benefit of types, I’d recommend you reading this post first.
# Uninitialized
Before looking at the fun stuff, there’s an important aspect about typed properties that’s essential to talk about first.
Despite what you might think on first sight, the following code is valid:
class Foo < public int $bar; > $foo = new Foo;
Even though the value of $bar isn’t an integer after making an object of Foo , PHP will only throw an error when $bar is accessed:
var_dump($foo->bar); Fatal error: Uncaught Error: Typed property Foo::$bar must not be accessed before initialization
As you can read from the error message, there’s a new kind of «variable state»: uninitialized.
If $bar didn’t have a type, its value would simply be null . Types can be nullable though, so it’s not possible to determine whether a typed nullable property was set, or simply forgotten. That’s why «uninitialized» was added.
There are four important things to remember about uninitialized:
- You cannot read from uninitialized properties, doing so will result in a fatal error.
- Because uninitialized state is checked when accessing a property, you’re able to create an object with an uninitialized property, even though its type is non-nullable.
- You can write to an uninitialized property before reading from it.
- Using unset on a typed property will make it uninitialized, while unsetting an untyped property will make it null .
Especially note that the following code, where an uninitialised, non-nullable property is set after constructing the object, is valid
class Foo < public int $a; > $foo = new Foo; $foo->a = 1;
While uninitialized state is only checked when reading the value of a property, type validation is done when writing to it. This means that you can be sure that no invalid type will ever end up as a property’s value.
Interface Default Methods
# Defaults and constructors
Let’s take a closer look at how typed values can be initialized. In case of scalar types, it’s possible to provide a default value:
class Foo < public int $bar = 4; public ?string $baz = null; public array $list = [1, 2, 3]; >
Note that you can only use null as a default if the type is actually nullable. This might seem obvious, but there’s some legacy behaviour with parameter defaults where the following is allowed:
function passNull(int $i = null) < /* … */ > passNull(null);
Luckily this confusing behaviour is not allowed with typed properties.
Also note that it’s impossible to have default values with object or class types. You should use the constructor to set their defaults.
The obvious place to initialize typed values would of course be the constructor:
class Foo < private int $a; public function __construct(int $a) < $this->a = $a; > >
But also remember what I mentioned before: it’s valid to write to an uninitialized property, outside of the constructor. As long as there are nothing is reading from a property, the uninitialized check is not performed.
# Types of types
So what exactly can be typed and how? I already mentioned that typed properties will only work in classes (for now), and that they need an access modifier or the var key word in front of them.
As of available types, almost all types can be used, except void and callable .
Because void means the absence of a value, it makes sense that it cannot be used to type a value. callable however is a little more nuanced.
See, a «callable» in PHP can be written like so:
Say you’d have the following (broken) code:
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)();
In this example, $callable refers to the private Bar::method , but is called within the context of Foo . Because of this problem, it was decided not to add callable support.
It’s no big deal though, because Closure is a valid type, which will remember the $this context where it was constructed.
With that out of the way, here’s a list of all available types:
- bool
- int
- float
- string
- array
- iterable
- object
- ? (nullable)
- self & parent
- Classes & interfaces
# Coercion and strict types
PHP , being the dynamic language we love and hate, will try to coerce or convert types whenever possible. Say you pass a string where you expect an integer, PHP will try and convert that string automatically:
function coerce(int $i) < /* … */ > coerce('1'); // 1
The same principles apply to typed properties. The following code is valid and will convert ‘1’ to 1 .
class Bar < public int $i; > $bar = new Bar; $bar->i = '1'; // 1
If you don’t like this behaviour you can disabled it by declaring strict types:
declare(strict_types=1); $bar = new Bar; $bar->i = '1'; // 1 Fatal error: Uncaught TypeError: Typed property Bar::$i must be int, string used
# Type variance and inheritance
Even though PHP 7.4 introduced improved type variance, typed properties are still invariant. This means that the following is not valid:
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)
If the above example doesn’t seem significant, you should take a look at the following:
class Foo < public self $prop; > class Bar extends Foo < public self $prop; >
PHP will replace self behind the scenes with the concrete class it refers to, before running the code. This means that the same error will be thrown in this example. The only way to handle it, is by doing the following:
class Foo < public Foo $prop; > class Bar extends Foo < public Foo $prop; >
Speaking of inheritance, you might find it hard to come up with any good use cases to overwrite the types of inherited properties.
While I agree with that sentiment, it’s worth noting that it is possible to change the type of an inherited property, but only if the access modifier also changes from private to protected or public .
The following code is valid:
class Foo < private int $prop; > class Bar extends Foo < public string $prop; >
However, changing a type from nullable to non-nullable or reverse, is not allowed.
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)
Noticed a tpyo? You can submit a PR to fix it. If you want to stay up to date about what’s happening on this blog, you can follow me on Twitter or subscribe to my newsletter:
# There’s more!
Like I said at the start of this post, typed properties are a major addition to PHP . There’s lots more to say about them. I’d suggest you reading through the RFC to know all the neat little details.
If you’re new to PHP 7.4 , you probably want to read the full list of changes made and features added. To be honest, it’s one of the best releases in a long time, and worth your time!
Finally, if you have any thoughts you want to share on the topic, I’d love to hear from you! You can reach me via Twitter or e-mail.
Type classes in php
While waiting for native support for typed arrays, here are a couple of alternative ways to ensure strong typing of arrays by abusing variadic functions. The performance of these methods is a mystery to the writer and so the responsibility of benchmarking them falls unto the reader.
PHP 5.6 added the splat operator (. ) which is used to unpack arrays to be used as function arguments. PHP 7.0 added scalar type hints. Latter versions of PHP have further improved the type system. With these additions and improvements, it is possible to have a decent support for typed arrays.
function typeArrayNullInt (? int . $arg ): void >
function doSomething (array $ints ): void (function (? int . $arg ) <>)(. $ints );
// Alternatively,
( fn (? int . $arg ) => $arg )(. $ints );
// Or to avoid cluttering memory with too many closures
typeArrayNullInt (. $ints );
function doSomethingElse (? int . $ints ): void /* . */
>
$ints = [ 1 , 2 , 3 , 4 , null ];
doSomething ( $ints );
doSomethingElse (. $ints );
?>
Both methods work with all type declarations. The key idea here is to have the functions throw a runtime error if they encounter a typing violation. The typing method used in doSomethingElse is cleaner of the two but it disallows having any other parameters after the variadic parameter. It also requires the call site to be aware of this typing implementation and unpack the array. The method used in doSomething is messier but it does not require the call site to be aware of the typing method as the unpacking is performed within the function. It is also less ambiguous as the doSomethingElse would also accept n individual parameters where as doSomething only accepts an array. doSomething’s method is also easier to strip away if native typed array support is ever added to PHP. Both of these methods only work for input parameters. An array return value type check would need to take place at the call site.
If strict_types is not enabled, it may be desirable to return the coerced scalar values from the type check function (e.g. floats and strings become integers) to ensure proper typing.
same data type and same value but first function declare as a argument type declaration and return int(7)
and second fucntion declare as a return type declaration but return int(8).
function argument_type_declaration(int $a, int $b) return $a+$b;
>
function return_type_declaration($a,$b) :int return $a+$b;
>
Type classes in php
PHP uses a nominal type system with a strong behavioral subtyping relation. The subtyping relation is checked at compile time whereas the verification of types is dynamically checked at run time.
PHP’s type system supports various base types that can be composed together to create more complex types. Some of these types can be written as type declarations.
Base types
Some base types are built-in types which are tightly integrated with the language and cannot be reproduced with user defined types.
- Built-in types
- null type
- Scalar types:
- bool type
- int type
- float type
- string type
- false
- true
- Interfaces
- Classes
- Enumerations
Composite types
It is possible to combine simple types into composite types. PHP allows types to be combined in the following ways:
Intersection types
An intersection type accepts values which satisfies multiple class-type declarations, rather than a single one. Individual types which form the intersection type are joined by the & symbol. Therefore, an intersection type comprised of the types T , U , and V will be written as T&U&V .
Union types
A union type accepts values of multiple different types, rather than a single one. Individual types which form the union type are joined by the | symbol. Therefore, a union type comprised of the types T , U , and V will be written as T|U|V . If one of the types is an intersection type, it needs to be bracketed with parenthesis for it to written in DNF : T|(X&Y) .
Type aliases
PHP supports two type aliases: mixed and iterable which corresponds to the union type of object|resource|array|string|float|int|bool|null and Traversable|array respectively.
Note: PHP does not support user-defined type aliases.