rfc:asymmetric-visibility

PHP RFC: Asymmetric Visibility

Introduction

PHP has long had the ability to control the visibility of object properties -- public, private, or protected. However, that control is always the same for both get and set operations. That is, they are “symmetric.” This RFC proposes to allow properties to have separate (“asymmetric”) visibility, with separate visibility for read and write operations. The specifics are mostly borrowed from Swift.

Proposal

This RFC provides a new syntax for declaring the “set” operation visibility of an object property. Specifically:

class Foo
{
    public private(set) string $bar;
}

This code declares a property $bar that may be read from public scope but may only be modified from private scope. It may also be declared protected(set) to allow the property to be set from any protected scope (that is, child classes).

The behavior of the property is otherwise unchanged, aside from the restricted visibility.

Asymmetric visibility properties may also be used with constructor property promotion:

class Foo
{
    public function __construct(
        public private(set) string $bar,
    ) {}
}

References

While a reference to a property with restricted set visibility may still be obtained, it may only be obtained from a scope that would allow writing. Put another way, getting a reference to a property follows the set visibility, not the get visibility. Trying to get a reference to a property with a more restrictive scope will result in an error.

For example:

class Foo
{
    public protected(set) int $bar = 0;
 
    public function test() {
        // This is allowed, because it's private scope.
        $bar = &$this->bar;
        $bar++;
    }
}
 
class Baz extends Foo
{
    public function stuff() {
        // This is allowed, because it's protected scope.
        $bar = &$this->bar;
        $bar++;
    }
}
 
$foo = new Foo();
 
// This is fine, because the update via reference is 
// inside the method, thus private scope.
$foo->test();
 
// This is also fine.
$baz = new Baz();
$baz->stuff();
 
// Getting this reference is not allowed here, because this is public
// scope but the property is only settable from protected scope.
$bar = &$foo->bar;

Object properties

If the property is an object, the restricted visibility applies only to changing the object referenced by the property. It does not impact the object itself. That is consistent with the behavior of the readonly property.

Example:

class Bar
{
    public string $name = 'beep';
}
 
class Foo
{
    public private(set) Bar $bar;
}
 
$f = new Foo();
 
// This is allowed
$f->bar->name = 'boop';
 
// This is NOT allowed
$f->bar = new Bar();

Permitted visibility

The set visibility, if it differs from the main (get) visibility, MUST be strictly lesser than the main visibility. That is, the set visibility may only be protected or private. If the main visibility is protected, set visibility may only be private. Any violation of this rule will result in a compile time error.

Interaction with __set

Currently, if a symmetrically defined property is accessed from an illegal scope and there is a __set method defined on the class, __set will be called with the var/val pair of the assignment. The same happens with readonly properties but only when the property is inaccessible (e.g. a private property is accessed from global scope). If the readonly property is in scope (and already initialized) an error is thrown instead. This behavior is intended to avoid surprising behavior when classes implement __set for unrelated purposes. We propose the same behavior for asymmetric visibility.

If the read visibility is unmatched __set is called. If it is matched but the write visibility is unmatched an error is thrown.

class Foo {
    protected private(set) string $prot;
    public private(set) string $pub;
 
    public function __set($name, $value) {
        var_dump($name, $value);
    }
}
 
$foo = new Foo();
 
// Calls __set
$foo->prot = 'Foo';
 
// Throws an error
$foo->pub = 'Foo';

There are a two main reasons for this decision:

  1. As noted, readonly is a form of asymmetric visibility, and thus consistency with that behavior is important.
  2. If at some point in the future it is determined that falling back to __set does make sense, it is much easier to enable at that time than to remove it if we find it causes too many problems.

Relationship with readonly

While at first glance public private(set) may seem like it is redundant with readonly, they are in fact different. readonly allows only private set, but only allows it once. public private(set) allows a property to be set an unlimited number of times, but only from private scope.

However, it may be possible to redefine readonly in terms of asymmetric visibility in the future. (See the “Future Scope” section below.)

Using both readonly and asymmetric visibility on the same property is not allowed and will result in an Error.

Typed properties

Asymmetric visibility is only compatible with properties that have an explicit type specified. This is mainly due to implementation complexity. However, as any property may now be typed mixed and defaulted to null, that is not a significant limitation.

Reflection

The ReflectionProperty object is given two new methods: isProtectedSet(): bool and isPrivateSet(): bool. Their meaning should be self-evident.

class Test
{
    public string $open;
    public protected(set) string $restricted;
}
 
$rClass = new ReflectionClass(Test::class);
 
$rOpen = $rClass->getProperty('open');
print $rOpen->isProtectedSet() ? 'Yep' : 'Nope'; // prints Nope
 
$rRestricted = $rClass->getProperty('open');
print $rRestricted->isProtectedSet() ? 'Yep' : 'Nope'; // prints Yep

Additionally, the two constants ReflectionProperty::IS_PROTECTED_SET and ReflectionProperty::IS_PRIVATE_SET are added. They are returned from ReflectionProperty::getModifiers(), analogous to the other visibility modifiers.

Backward Incompatible Changes

None. This syntax would have been a parse error before.

Proposed PHP Version(s)

PHP 8.3

RFC Impact

Future Scope

This RFC is kept very simple. However, it does allow for future expansion.

Alternate operations

At this time, there are only two possible operations to scope: read and write. In concept, additional operations could be added with their own visibility controls. Possible examples include:

  • init - Allows a property to be set only from initialization operations, such as __construct, __clone, __unserialize, etc.
  • once - Allows a property to be set only once, and then frozen thereafter. In this case, public private(once) would be exactly equivalent to readonly, whereas public protected(once) would be similar but also allow the property to be set from child classes.
  • unset - Allows a property to be unset from a different scope than it can be set to a real value.

This RFC does NOT include any of the above examples; they are listed only to show that this syntax supports future expansion should a use be found.

Additional visibility

Should PHP ever adopt packages and package-level visibility, this syntax would be fully compatible with it. For example, public package(set) would be a natural syntax to use.

This RFC does NOT include any discussion of such expanded visibility definition, just notes that it in no way precludes such future developments.

Property accessors

Asymmetric visibility has been proposed before as a component of the Property Accessors RFC. That RFC models directly on C# syntax, which puts limited-operation visibility on the right of the property as part of the accessor definition.

This RFC uses a syntax borrowed from Swift, which provides similar functionality but with the visibility modifiers together on the left side of the property. That has two benefits over the C# style syntax:

  1. It is less confusing, as all visibility is in one place together.
  2. It allows asymmetric visibility to be proposed and discussed independently of the larger property accessor question.

While the authors do support (and intent to work on) property accessors generally, we feel it is better addressed separately from asymmetric visibility as the two are orthogonal considerations. Notably, this RFC does NOT preclude or limit the development of asymmetric visibility in the future.

Proposed Voting Choices

This is a simple yes-or-no vote to include this feature.

References

This syntax is borrowed directly from Swift's access control system.

rfc/asymmetric-visibility.txt · Last modified: 2022/08/05 21:39 by ilutov