# RFC: Asymmetric visibility

- Status: Declined
- Authors: Ilija Tovilo (tovilo.ilija@gmail.com), Larry Garfield (larry@garfieldtech.com)
- Date: 2022-07-21
- Version: 1.0
- Implementation: https://github.com/php/php-src/pull/9257
- Original: https://wiki.php.net/rfc/asymmetric-visibility
- Page: https://externals.io/rfc/asymmetric-visibility
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

## Votes

### Include asymmetric visibility? (closed)

Yes: 14 · No: 12

Voters: alec (No), asgrim (No), ashnazg (No), bmajdak (Yes), bwoebi (Yes), crell (Yes), derick (Yes), galvao (Yes), geekcom (No), girgias (Yes), ilutov (Yes), kalle (No), kelunik (No), nicolasgrekas (Yes), ocramius (No), patrickallaert (No), pierrick (Yes), ramsey (Yes), sebastian (No), seld (Yes), sergey (Yes), stas (No), svpernova09 (Yes), thekid (No), theodorejb (No), timwolla (Yes)

### If voting no, why? (closed)

Dislike feature itself: 6 · Syntax details: 4 · Other (specify below): 2

Voters: alec (Syntax details), asgrim (Other (specify below)), ashnazg (Dislike feature itself), geekcom (Dislike feature itself), kalle (Syntax details), kelunik (Syntax details), ocramius (Dislike feature itself), patrickallaert (Syntax details), sebastian (Dislike feature itself), stas (Dislike feature itself), thekid (Dislike feature itself), theodorejb (Other (specify below))

## Discussions on the mailing list

- [\[RFC\] Asymmetric visibility](https://externals.io/message/118353.md) — started on 2022-08-05, 45 messages
- [\[RFC\]\[Poll\] Asymmetric Visibility and Accessor syntax](https://externals.io/message/118557.md) — started on 2022-09-03, 2 messages
- [\[RFC\]\[Vote\] Asymmetric Visibility](https://externals.io/message/119240.md) — started on 2023-01-06, 9 messages

---

## 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:

```php
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:

```php
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:

```php
public protected(set) string $foo;
protected private(set) string $bar;
```

but the following is NOT legal:

```php
// 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:

```php
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:

```php
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:

```php
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.

```php
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.

```php
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](https://externals.io/rfc/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](https://externals.io/message/118557#118628). 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](https://externals.io/rfc/property-hooks)) 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.

**Vote: Include asymmetric visibility?** — Yes: 14 · No: 12 (closed)

If you vote no, the authors would appreciate feedback as to why (if you vote Yes, ignore this).

**Vote: If voting no, why?** — Dislike feature itself: 6 · Syntax details: 4 · Other (specify below): 2 (closed)

- 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](https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html).

Syntax decisions in this RFC are supported by a poll conducted in September 2022. The results were [posted to the Internals list](https://externals.io/message/118557#118628).
