This is an old revision of the document!
PHP RFC: Asymmetric Visibility
- Version: 0.9
- Date: 2022-07-21
- Author: Ilija Tovilo (tovilo.ilija@gmail.com), Larry Garfield (larry@garfieldtech.com)
- Status: Under discussion
- First Published at: http://wiki.php.net/rfc/asymmetric-visibility
- Implementation: https://github.com/php/php-src/pull/9257
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, ) {} }
Abbreviated form
In earlier versions of PHP, the visibility modifier was optional and if not specified defaulted to public
. That is still technically the case if the var
keyword is used instead, although in practice that is almost never done.
In the interest of compactness, this RFC allows the same default assumption if only a set
visibility is specified. If a set
visibility is specified but no get
visibility, the get
visibility defaults to public
. That is, the following two declarations are semantically identical:
public protected(set) string $foo; protected(set) string $foo;
A non-public get
visibility must still be specified explicitly.
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 and __unset
In PHP 8.2, the behavior of __set
has some subtleties to it due to readonly
introducing asymmetric visibility through the back door. Specifically, when trying to assign to a property that has been defined, and the class has a __set
method defined:
- If the property is read-accessible
- And the property has been
unset()
explicitly, then__set
is called. - And the property has NOT been
unset()
explicitly- And the property is write-accessible, then the property will be set directly.
- And the property is NOT write-accessible, then an error is thrown.
- If the property is NOT read-accessible, then
__set
is called.
This RFC continues the rules above without change. A property with asymmetric visibility that is written to from a scope where it is readable but not writeable, __set
will be called if and only if the property had previously been unset()
. Otherwise, an error will be thrown.
If __set()
is not defined, the write will fail with an error regardless.
class Example { public private(set) string $pubpriv; public function __set($name, $value): void { echo "__set($name, $value)\n"; $this->$name = $value; } public function unsetPub(): void { unset($this->pubpriv); } } $ex = new Example(); $ex->pubpriv = 'a'; // Error, because the property is readable but not unset $ex->unsetPub(); $ex->pubpriv = 'a'; // Calls __set, because pubpriv has been unset
The logic for calling unset()
externally (and thus triggering __unset()
) is the same.
Relationship with readonly
The readonly
flag, introduced in PHP 8.1, has an implicit private(set)
behavior. While there are ways to recast the meaning of readonly
such that it will combine with asymmetric visibility cleanly, there's no clear consensus on which of those ways is best, nor some of the edge cases they introduce. Therefore, for the time being, mixing readonly
with explicit asymmetric visibility is not allowed.
This restriction can and should be relaxed in a later, dedicated RFC where those details can be hashed out more explicitly.
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.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.
readonly compatibility
As noted above, at this time asymmetric visibility cannot be combined with readonly
properties. While the implementation of interlacing the two features is not difficult, there are some edge cases that need to be sorted out. For instance, it may require relaxing the “permitted visibility” rules in some cases. We felt that was best pushed to a separate RFC to minimize controversy on this RFC. The authors believe such interlacing can and should be done, just in a separate RFC.
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. The proposed syntax was slightly favored in a poll. It also 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 property accessors in the future.
Proposed Voting Choices
This is a simple yes-or-no vote to include this feature. 2/3 majority required to pass.
References
This syntax is borrowed directly from Swift's access control system.
Syntax decisions in this RFC are supported by a poll conducted in September 2022. The results were posted to the Internals list.