DEV Community

Rasmus Schultz
Rasmus Schultz

Posted on

PHP Typed Properties: Think Twice.

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

And you may be looking forward to porting it to something short and sweet, like this:

class Product
{
    public int $price;

    public string $description;
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 😎✌

Top comments (15)

Collapse
 
blackcat27 profile image
Josh Kitchens • Edited

Both of these examples are flawed.

First, you shouldn't be using public fields. Full stop. It breaks encapsulation, leaks implementation details of the class, and means your class can find itself in an unknown or inconsistent state at any time due to outside influence.

Php doesn't offer properties like C# and other languages, which are really just shorthand for getters and setters. Which is the preferred pattern for exposing private fields.

And to address your point about "double type checking". That's EXACTLY what you're getting. Php only checks types at the compilation stage for typed arguments and returns. Type hints offer no such safety. And even if they did, should you forget the type hint, php will happily let you assign an argument typed as a string into a field you meant to be an int. With the typed field, that's no longer possible.

And your argument for not being able to add additional constraints is also flawed. It has nothing to do with interfaces themselves, just bad interface design. Again, it's the breaking encapsulation and exposing direct fields which causes the problems you bring up. It has nothing to do with typed fields.

There's no "trade off" to be had with typed fields. There's just good code and bad code. And this is bad code.

Collapse
 
mindplay profile image
Rasmus Schultz • Edited

Both of these examples are flawed.

It's all one example, going through iterations with changing requirements.

First, you shouldn't be using public fields. Full stop. It breaks encapsulation, leaks implementation details of the class, and means your class can find itself in an unknown or inconsistent state at any time due to outside influence.

By design. (Outside influence is what's desired with a mutable model.)

The initial requirement is that price be an integer and description be a string, both being mutable.

The first and second code sample equally satisfy those requirements.

Php doesn't offer properties like C# and other languages, which are really just shorthand for getters and setters. Which is the preferred pattern for exposing private fields.

In C#.

But as you pointed out, PHP doesn't offer accessors.

The preferred (only) pattern in PHP at the moment is getter/setter methods - and we're now getting an alternative (public, type-hinted properties) which can satisfy the requirements of the first example, but can't satisfy the new requirements (abstraction, validation) introduced in the following examples.

And to address your point about "double type checking". That's EXACTLY what you're getting.

The point was, if the input to setPrice() has already been type-checked, you don't need the extra type-check when setting the property - your class is already safe from incorrect mutation.

(If you want an extra layer of safety against bugs in your own implementation, sure, a type-hinted private field provides that extra safety. Personally, I'm getting that guarantee from static analysis already with a doc-block.)

Php only checks types at the compilation stage for typed arguments and returns. Type hints offer no such safety.

Did you mean doc-blocks?

PHP checks all static type-hints at run-time, including property type-hints introduced by the RFC. (It's an interpreted language - there's no compilation stage during which assignments could be checked for correctness.)

And your argument for not being able to add additional constraints is also flawed. It has nothing to do with interfaces themselves, just bad interface design. Again, it's the breaking encapsulation and exposing direct fields which causes the problems you bring up. It has nothing to do with typed fields.

There's no "trade off" to be had with typed fields.

There is in the example I used.

It's very likely a lot of PHP developers were excited about this feature so they could simplify models with trivial constraints in this manner.

There are other articles out there already presenting exactly this opportunity with very similar examples.

The point of this article was to make sure these people understand the trade-offs they're making in terms of being able to iterate to meet new requirements.

There's just good code and bad code. And this is bad code.

It seems you read the article making your own assumptions about the requirements for each example. The code in these examples satisfy the requirements for each of those examples at the time - the point was to illustrate how simple requirements often change towards more complex requirements.

You seem to be angry or offended about something, I'm not exactly sure what?

I think for the most part you've just completely misunderstood what I'm trying to say with this article. As mentioned, there are already other articles proposing the kind of change in the first example - in fact, it's similar to the example shown in the RFC itself.

I'm just trying to help people understand the consequences of making such a change.

Collapse
 
blackcat_dev profile image
Sasha Blagojevic

Isn't the first example breaking the Encapsulation principle of OOP? I rarely ever use public properties, they are almost always protected or in some cases private.

I'd put these interfaced properties that you propose in Abstract classes rather than in Interfaces. Makes more sense in my opinion, Interfaces should stay stateless and remain the messaging contract. :)

Collapse
 
kip13 profile image
kip
Collapse
 
blackcat_dev profile image
Sasha Blagojevic

You can call it however you want but if another object can directly (on properties) change the state of your object, that object is not encapsulated :)

Collapse
 
nicolus profile image
Nicolas Bailly

Typed property doesn't mean you should suddenly expose your properties as public and forego getters and setters ! These are two unrelated issues IMHO. What typed properties will allow you to do is replace

/**
 * @var int
 */
private $price;

with

private int $price;

With the added bonus that it won't be only interpreted by your IDE but also trigger an error if you try and use something that's not a integer when using $price inside your class.

Now if you want to avoid using getters and setters for every single property but still maintain the ability to add validation/mutators/accessors later on, there's an old RFC that would solve this issue by using C# style getters and setters : wiki.php.net/rfc/propertygetsetsyntax

Collapse
 
okdewit profile image
Orian de Wit • Edited

Exactly! Docblocks have always been an abomination in my opinion, abusing PHP's metaprogramming flexibilities to create incomplete static analysis tooling for something that needs to be in the language itself.

A comment should be used to communicate to fellow humans, not to tools — that's what the rest of the syntax is for.

First we got parameter & return type hints, which made docblocks much less necessary. I see typed properties as the next major step to liberate us from ugly syntax.

But indeed, whether to use protected /get/set or public is completely unrelated to typed properties.

In many cases, protected fields are advisable.

The difference is that IF you use a public property somewhere (I use them commonly on simple DTO classes), or if a setter method does some (slightly too complicated) mutation, you now get some extra safety...

Collapse
 
mcfedr profile image
Fred Cox

no, you've gained the option of doing anything else at the point of get and set - you can change your implementation - you can refactor your field into something different and maintain your existing interface. a public field is just that, once its there you can never remove it.

Collapse
 
na2axl profile image
Nana Axel

If we read this article carefully, we can see that the first example does not really break the principle of encapsulation. The first example shows two non typed properties with getters and setters, but theses ones doesn't provide any validation on the value defined by the user, they only provide type checking. The same type checking provided with public typed properties. Or the principle of encapsulation is to hide class implementation and control the state of the object, since the user can do anything stupid. When I see this it's just like C# auto-properties (a property with an empty getter and setter), it's a language concept.

Now for my opinion, this feature will make more senses if we can use embedded accessors like in the last example, so, interfaces declare properties like:

interface ProductInterface
{
    public int $price { get; }
    public string $description { get; set; }
} 

So it will be the property itself which will provide encapsulation (again, like C# does).

But for now it remains a dream, but it will surely be a great feature, and have a ton of use cases, like for my ORM for example, which will allow the user to give an explicit type for properties mapped to database table columns.

Collapse
 
alexanderschnitzler profile image
Alexander Schnitzler

I second that!

Context is key here. There are objects in our daily life that are just a way to organise data. DTO's, entities and such. The integrity of those objects should be made sure from the outside anyway.

And then there are objects whose inner state is critical. It's objects where you might only want to expose properties via constructor and getters to control the state.

However, both types don't really justify hiding protected or private properties behind getters and setter that don't do anything else but to expose said properties as if they were public in the first place.

Collapse
 
adrienh profile image
Adrien H • Edited

Doing anything else than getting a property in its getter, or setting it in its setter, breaks the Separation of Concerns. If validations are to be performed on the data of an instance, maybe you should prefer a real validation model; let's say, for the most simple example, a validate() method which would check each potential flaw in the values, like a wrong file name, a negative value in an age, etc. You would execute this validate() callback before the database inserts, for example.

That's exactly what OOP frameworks like Symfony are doing, and that's a good way to go. One should never introduce unpredictable behavior in a getter or a setter. Getter gets, setter sets. Separation of Concerns.

From this point, you could decide to simply stop using getters and setters. And let's admit that having a typed public property feels exactly the same that having a private properties with getters/setters, except with less code and slightly more perf'. You could find it hypocritical. After all, encapsulation is about hiding private implementations; a private property exposed by a public getter is not encapsulated.

I thought like that back then in Java, but I evolved and I use getters/setters today. That's a question of consistency:

First, the lack of native typed collections PHP is a problem and makes PHP type system still very incomplete (without even speaking of generics). Since we don't have types like string[], we still need adders on collections. Second, sometimes you would want to make a property read-only, and then you will need a getter with no setter. These specific needs can impose getters or adders in some cases, which leads to mixing pure public access with indirect method accessors. This mix feels kinda... I don't know... asymetric? Inconsistent. If there is only one case forcing me to use a getter or a setter, I will always use them, that's my way today.

To me, accessors are more about consistency than encapsulation.

Collapse
 
aleksikauppila profile image
Aleksi Kauppila

You can still get the benefits of type-checking internally though, right?

This is where it will shine. I don´t write setters, so changes internally will be written directly to the property.

It´s so strange that the RFC also has example with public properties. What an awful thing to recommend.

Collapse
 
mindplay profile image
Rasmus Schultz

It´s so strange that the RFC also has example with public properties. What an awful thing to recommend.

I don't think it's meant as a recommendation or "good practice" - I'm pretty sure the folks behind this RFC know what they're doing. This was probably just the first, most simple example that came to mind, in order to illustrate how the feature would work.

But yeah, clearly the community seem to have jumped to that conclusion - that was my motivation for writing this article 🙂

Collapse
 
goranjviv profile image
goranjviv

You gain debugging power. Goodluck debugging whoever changed that one property in an app that's got 5000 objects changing each other's properties on user input, such as mouse movement.

Thread Thread
 
dakujem profile image
Andrej Rypo

I'd like to know how to get mouse movement to PHP. Just curious...