PHP RFC: Asymmetric Visibility
- Version: 1.0
- Date: 2022-07-21
- Author: Ilija Tovilo (tovilo.ilija@gmail.com), Larry Garfield (larry@garfieldtech.com)
- Status: Declined
- 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 syntax is 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, ) {} }
In the interest of explicitness, if a set
visibility is specified, a normal/get
visibility must also be specified. Doing otherwise results in a compile error. That is, the following are legal:
public protected(set) string $foo; protected private(set) string $bar;
but the following is NOT legal:
// Compile error protected(set) string $foo;
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, obtaining a reference to a property follows the set
visibility, not the get
visibility. Trying to obtain 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 specified explicitly, MUST be strictly lesser than the main (get
) 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.
Inheritance
PHP already allows child classes to redeclare parent class properties, if and only if they have the same type and their visibility is the same or wider. That is, a protected string $foo
can be overridden with public string $foo
but not private string $foo
. This RFC continues that rule, but independently for get
and set
operations.
That means, for instance, the following is legal:
class A { private string $foo; } class B extends A { protected private(set) string $foo; } class C extends B { public protected(set) string $foo; } class D extends C { public string $foo; }
As in each child class, the get
visibility is the same or wider than the parent, and the set
visibility is the same or wider than the parent. Narrowing the visibility is not allowed, however.
class A { public string $foo; } class B extends A { // This is an error. public protected(set) string $foo; }
Interaction with __set and __unset
In PHP 8.2, the behavior of __set
has some subtleties to it due to readonly
. In addition, readonly
is actually two different modifiers in one: a write-once marker and an implicit private(set)
. That introduces a question as to which aspect of it is associated with its special behavior.
The following is (to the best of our ability to determine) the existing logic in PHP 8.2 for how __set
behaves:
// When writing to a property on an object with __set: if (property is NOT read-visible) { call __set } else { // It is read-visible if (property is set-visible) { assign property } else { // The property is NOT set-visible if (property is unset) { call __set } else { if (property is readonly) { error } else { call __set } } } }
In particular, readonly
has an extra requirement that __set
will only be called if the property has been explicitly unset()
. By associating that conceptually with the write-once-ish part of readonly
, we're able to avoid the need for that on properties that have only asymmetric visibility.
The net result is that properties with an explicit asymmetric visibility will trigger __set
(if defined) if written to from a scope where they are not visible, regardless of whether they have been unset()
or not. (Whether or not that requirement for readonly
properties is reasonable is out of scope for this RFC.)
If __set()
is not defined, the write will fail with an error regardless.
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.
Static properties
This functionality applies only to object properties. It does not apply to static properties. For various implementation reasons that is far harder, and also far less useful. It has therefore been omitted from this RFC.
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.
Modifying asymmetric properties via ReflectionProperty::setValue()
is allowed, just as it is for protected
or private
properties, even outside of the classes scope.
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.
Abbreviated form
It would be possible in the future to allow the get
visibility to default to public
if only a set
visibility is specified. That may help avoid long property declarations if combined with readonly
or other features. It has been omitted for now in the interest of explicitness, but could easily be reintroduced in the future.
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 intend 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.
If you vote no, the authors would appreciate feedback as to why (if you vote Yes, ignore this).
- Name: Reason
- Theodore Brown: Proposal feels unfinished since it can't be used in conjunction with readonly properties/classes. In my opinion the issues with this need to be resolved first, to avoid the language moving towards a messy hodgepodge of features that don't work well together.
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.