UPDATE: Property Hooks resolves the problems described in this article, and will ship with PHP 8.4. 🎉
The Typed Properties RFC for PHP has been merged to master, and will be available in PHP 7.4. 😍
But, before you pounce on this feature and start porting all your existing model classes to use this feature, you should definitely read this.
Today you might have something rather verbose and ceremonious like this:
class Product
{
/**
* @var int
*/
private $price;
/**
* @var string
*/
private $description;
public function getPrice(): int
{
return $this->price;
}
public function setPrice(int $price)
{
$this->price = $price;
}
public function getDescription(): string
{
return $this->description;
}
public function setDescription(string $description)
{
$this->description = $description;
}
}
And you may be looking forward to porting it to something short and sweet, like this:
class Product
{
public int $price;
public string $description;
}
These are in deed functionally equivalent on the surface.
But hold up.
Adopting this new feature comes with two major trade-offs.
What if you want to abstract behind an interface?
You can't.
Once you start using public, type-hinted properties, you can no longer introduce an abstraction, so you'll have to back-port to getters and setters.
In other words, if you choose public, type-hinted properties, you've chosen to forego any current or future abstraction!
What if you want to add validation?
Like, say, a maximum price or maximum string-length for the description?
You can't.
Once you've chosen type-hinted properties, you can no longer add in any new constraints beyond the simple type-checks supported by type-hinted properties.
Also note that making any of these two very typical changes will now be a breaking change going forward, so there is definitely that to consider.
Shame.
Refactoring back to type-hinted properties then, I suppose?
You can still get the benefits of type-checking internally though, right?
interface ProductInterface
{
public function getPrice(): int;
public function getDescription(): string;
}
class Product implements ProductInterface
{
private int $price;
private string $description;
public function getPrice(): int
{
return $this->price;
}
public function setPrice(int $price)
{
$this->price = $price;
}
public function getDescription(): string
{
return $this->description;
}
public function setDescription(string $description)
{
$this->description = $description;
}
}
Hm.
Well, sure, but you already had type-safety from the type-hints on the setter-methods.
Now you have two type-checks. Doesn't exactly make things "twice as type-safe", does it?
In terms of static analysis and IDE support, there's no real difference.
The only thing that's really won by using type-hinted properties here, is slightly shorter syntax compared with the php-doc blocks. (And maybe an extra layer of protection against internal bugs.)
Were you planning on adding some inline documentation with doc-blocks? Then you'll need the doc-blocks anyhow - and the type-hints in doc-blocks aren't optional, so you'll be repeating all your types an extra time.
At this point, you can just remove those property type-hints again. What you had was already fine.
Now...
I'm not just posting to ruin the excitement and spoil your day.
Just wanted to make you aware of the serious trade-offs you're making if you choose type-hinted properties for the sake of brevity and convenience.
Type-hinted properties will certainly have their uses for convenient internal type-checks in classes that aren't simply models with public getters and setters, so there is something to be thankful for. Just that this isn't going to immediately bring you the kind of convenience and brevity you may have experienced in other languages.
It's an important step on the way!
But two more features are required to address the bring about the brevity and convenience you were hoping for.
Properties in interfaces
To address the first trade-off, we need to add support for type-hinted properties in interfaces.
Something like:
interface ProductInterface
{
public int $price;
public string $description;
}
class Product implements ProductInterface
{
public int $price;
public string $description;
}
Other languages where you enjoyed type-hinted properties? They have this.
And this may sound simple enough, but it raises a lot of interesting and difficult questions, which I won't even get into here.
And this of course only addresses the first trade-off - remember, you also traded away the ability to add new constraints beyond the simple type-checks supported by type-hinted properties.
Accessors
To address the second trade-off, we need to add support for accessors - in classes, and in interfaces.
At this point we're venturing into fantasy-land, but bear with me.
Adding a length constraint to $description
, we might get something like:
interface ProductInterface
{
public int $price;
public string $description;
}
class Product implements ProductInterface
{
public int $price;
private string $_description;
public string $description {
get {
return $this->_description;
}
set {
if (strlen($description) > 100) {
throw new RangeException();
}
$this->_description = $description;
}
}
}
Again, other languages where type-hinted properties gave you so much feels? Yeah, they had this feature.
Anyhow, the point of this example is to show how you could mix and match accessors and public properties, and (most importantly) the fact that you'd be able to refactor between public properties and accessors without breaking the interface.
It's actually a feature that has been proposed before. In 2009 without making it to a vote, and in 2012, which was rejected, and a variation on that in 2013, also rejected.
So this definitely isn't "just so" - it's another feature that will need very careful and deliberate design, and it's probably a long way in the future.
The End.
I hope this post helped you understand why this new feature is both an exciting step in the right direction, and at the same time, something you should use in full awareness of the trade-offs you're making.
Cheers 😎✌