PHP7.4 introduced this cool new feature called “typed properties” which add the existing PHP type system to class properties. This means that you can now enforce which types a property has without having to encapsulate it in an object.
An example class containing typed properties looks like this:
class ChangeName
{
public UserId $userId;
public ?string $name = 'Wouter';
}
There are some limitations to Typed Properties, though.
Visibility declaration
The most simple one is that it can only be added to properties that have a visibility declaration. You thus require the public, protected, or private keyword before being able to add a type declaration. The var keyword is also supported. Let’s not use that, though, because this keyword has been deprecated for quite a while already.
class Invalid
{
// this is invalid code giving Parse error: syntax error, unexpected 'string'
string $invalidProperty;
}
Unsupported types
The callable and void types are not supported. Void is not allowed because there isn’t really a valid use case to have void properties. Callables are not supported because adding a callable to a property could make it dependent on a private method within that class. This would introduce invalid code because you should not be able to call the callable from outside of the class. The proposed workaround for this issue is using a Closure type instead.
This is an example of how the callable type could be misused (from the RFC to introduce typed properties)
class Invalid
{
public callable $callable;
public function __construct()
{
// $this->$callable is callable here
$this->$callable = [$this, 'method'];
}
private function method() {}
}
$obj = new Test;
// $obj->$callable is NOT callable here
($obj->$callable)();
Uninitialized fields
The last potential problem is a “Typed property must not be accessed before initialization” error. This is happening because typed properties without a default value have a new state: uninitialized
. This error is introduced because, if a constructor does not set a certain value in a typed property, it is in an invalid state. Consider this example:
class User
{
private int $id;
private string $username;
public function __construct(int $id)
{
$this->id = $id;
}
public function getUsername()
{
$this->username;
}
}
$user = new User(1);
Here, we have a user where the value of username does not respect the type, and it is thus in an invalid state. That’s why typed properties without a default value now have an uninitialized
state. When calling a method that accesses such an uninitialized state, you’ll get the “Typed property User::$username
must not be accessed before initialization” error.
Note that nullable typed properties also have this uninitialized state instead of having null as default value. You’ll thus also get this error in the following piece of code:
class Address
{
private string $firstLine;
private ?string $secondLine;
public function __construct(string $firstLine)
{
$this->firstLine = $firstLine;
}
public function setSecondLine(?string $secondLine)
{
$this->secondLine = $secondLine;
}
public function getSecondLine()
{
$this->secondLine;
}
}
// invalid usage
$address = new Address('221B Baker Street');
// throws Typed property Address::$secondLine must not be accessed before initialization
$address->getSecondLine();
// valid usage, the setSecondLine initializes the secondLine field
$address = new Address('221B Baker Street');
$address->setSecondLine(null);
$address->getSecondLine();
It is thus important to make sure that – before accessing a property – it is given a value. The best ways to enforce this are making sure the property is set in the constructor or giving them default values. It is also possible to use the isset
keyword to check if a property is already initialized.
With that in mind, you should be able to safely convert your PHP7.4 code to use typed properties. Enjoy this added type safety!
Member discussion