In PHP, asymmetric property visibility is a feature that makes it possible to declare class properties with separate access modifiers for reading and writing the property values.
For example, we can declare that a class property should be considered as public
when getting its value but be considered as private
when setting its value. It is a feature that was introdruced in PHP 8.4, with other PHP 8.4 features such as property hooks and DOM API update.
This post aims to demistify asymmetric property visibility in PHP. We will look at how to declare a class property asymmetrically, and also consider some important tips and rules that govern the declaration of class properties with asymmetric visibility.
Before getting started, it is assumed that you have at least PHP 8.4 installed. If you have not installed PHP 8.4, and you are using a Linux operating system such as Ubuntu, then you can install PHP 8.4 by following the discussion on how to install PHP 8.4 on Ubuntu system.
Table of Contents
Review of Property Visibility
The visibility of class properties, as well as methods, is controlled by access modifiers such as public
, protected
, and private
keywords. These access modifiers determine whether a class property or method can be accessed from code outside the class, or from derived classes.
In the following code listing, the $name
property is defined with the public
access modifier keyword.
class Person {
public string $name = 'Daniel';
}
If a class property is declared as public
, such as in listing 1, then any code outside the class can get and set the value of the property.
In the following code listing, the $name
property is defined with the public
access modifier. This means that we can get and set the value of the property in code outside the class.
class Person {
public string $name = 'Daniel';
}
// initialize object
$person = new Person();
// get and output the name
echo $animal->name; // output: Daniel
// set and output a new name
$animal = 'Joel';
echo $animal->name; // output: Joel
If a class property is declared as private
, then code outside the class cannot get or set the value of the property. We can only get and set the value of the property from code within the class only.
The following code listing adds an $email
property, defined with private
access modifier, to the Person
class:
class Person {
public string $name = 'Daniel';
private string $email = 'dan@mail.com';
}
Since the $email
property is defined as private
, we cannot get or set the value of the property in code outside the class. An attempt to set or get the value of the $email property, such as in the following example, will generate an error:
// intialize object
$person = new Person();
// Fatal error: Can't get $email as it is private
echo $person->$email;
// Fatal error: Can't set to $email as it is private
$person->$email = 'email@mail.com';
Lastly, if we declare a class property as protected
, we still cannot get or set its value from code outside the class. However, a class that inherits from the base class can get or set the value of the protected
class property.
The following code listing adds a $role
property to the Person
class, and defines a Developer
class that inherits from the Person
class.
class Person {
public string $name = 'Daniel';
private string $email = 'email@mail.com';
protected string $address = 'AK 46';
}
class Developer extends Person {
private string $stack = 'Full Stack';
public function __construct() {
// derived class can access protected
// protected property in base class
$this->address = 'PLT 12 B 4B';
}
}
// initialize object
$dev = new Developer();
// Fatal error: $address is protected
$dev->address = 'N5 67 B';
With protected
access modifier, we can only set the value of the $address
property in the Developer
class since it extends the Person
class. If we attempt to get or set a value to the $address
property outside the Developer
or Person
class, then an error will be generated.
Same Read and Write Visibility
If you have been following the discussions keenly, you will notice that we have been emphasizing on get and set operations on the class properties. This is to draw your attention to the fact that the access modifiers public
, private
, and protected
, which indicate the visibility of the class properties, apply to the get and set operations on the properties simultaneously.
For example, if we declare a class property as public
, it implies that the property is public
when getting its value and is also public
when setting its value.
If we declare a class property as private
, it implies that the property is private
when getting its value and is also private
when setting its value.
Similarly, if the property is declared as protected
, then that implies that the property is protected
when getting its value, and is also protected
when setting its value.
Can’t we have a way to indicate the visibility of class properties when getting their values and the visibility of the properties when setting their values?
For example, can’t we have a way to declare that a class property is, for instance, public
when getting its value, but private
when setting its value? Well, this is what the asymmetric property visibility, introduced in PHP 8.4, does. When declaring a class property, we can indicate the visibility of the property when accessing its value, and also indicate its visibility when setting its value.
The remaining discussions delve deeper into the asymmetric property visibility feature introduced in PHP 8.4.
Asymmetric Property Visibility
The asymmetric property visibility, introduced in PHP 8.4, is a feature that enables PHP developers to separately indicate the visibility of a class property when getting its value, and the visibility of the property when setting its value.
With the asymmetric property visibility feature, we can explicitly indicate the visibility for getting a property value and the visibility for setting a property value, all in a single property declaration. This creates distinct access visibility for getting and setting the property value.
Distinct Get and Set Visibility
In the default property visibility, as is in the case of PHP versions prior to version 8.4, the access modifier used in declaring a class property applies to the property’s visibility for getting and setting its value. If a class property is declared as public, then we can publicly get its value and publicly set its value.
However, with asymmetric property visibility, we can think of the get and set operations on the property as distinct, each with its own access modifier. This gives us the opportunity to use different access modifiers for getting and setting the property value.
Suppose, in listing 5, we want the $name
property to be accessible from outside the class but only allow code in the class to modify the property value. We can use the asymmetric property visibility feature to declare the property with two access modifiers: one for getting the property value and one for setting the property value using the following format:
<GET VISIBILITY> <SET VISIBILITY> <PROPERTY TYPE> <PROPERTY NAME>
As can be seen, we can declare a class property by indicating its get visibility as well as its set visibility. These are followed by the type of the property, then the property name. The get visibility can be different from the set visibility.
Declaring Class Properties Asymmetrically
To declare class properties asymmetrically, we may use the access modifiers (public
, protected
, or private
) twice to declare the property. The first access modifier, which appears on the left, indicates the visibility for getting the property. The second access modifier, which appears on the right, indicates the visibility for setting the value of the property.
When specifying the set visibility, we must explicitly indicate that it has a set visibility by enclosing the set
keyword in parentheses, and directly following the access modifier keyword for the set visibility. For example:
class Person {
public private(set) string $name;
}
In this code listing, we use two access modifier keywords to declare the $name
property. The first access modifier keyword, public
, specifies the visibility for getting the $name
property. The next access modifier keyword, private(set)
, specifies the visibility for setting the value of the property.
The declaration of the $name
property above implies that any code outside the class declaration can access the value of the property. However, any code outside the class cannot set the value of the property since the set visibility of the $name
property is private
.
Although we have said, and now know, that the second access modifier keyword specifies the visibility for setting the property value, we are mandated to explicitly indicate that it is a set visibility by directly following it with (set)
. That is, private(set)
, protected(set)
, or public(set)
.
There should be no spaces between the parentheses and the set
keyword. private(set)
is correct. However, private( set )
, private( set)
, and private(set )
are not permitted since they have spaces between the set
keyword and the parentheses.
Optional Get Visibility
In asymmetric property visibility, the get visibility is optional, and we can specify the set visibility without the get visibility. For such property declarations, the get visibility is by default public
, and we can access the value of the property from code outside the class.
Consider the following code listing:
class Person {
private(set) string $name = 'Daniel';
}
// initialize object
$person = new Persion();
echo $person->name; // output: Daniel
On line 2, we specified the set visibility without the get visibility. Since we did not explicitly indicate the get visibility of the property, it is implicitly public
. This follows the default behavior when we do not specify an access modifier for a class property.
The following code listing declares $name
and $role
with the same asymmetric visibility:
class Person {
// public get visibility, private set visibility
private(set) string $name = 'Daniel';
// public get visibility, private set visibility
public private(set) string $role = 'Developer';
}
// initialize object
$person = new Person();
// $name is implicitly public
echo "My name is {$person->name}\n";
// $role is explicitly public
echo "I am a {$person->role}\n";
Since the get visibility is optional, we defined the $name
property without explicit get visibility. However, it should be noted that if we fail to explicitly specify the get visibility, it implicitly defaults to public
, and any code outside the class can access the value of the property.
Rules of Asymmetric Visibility
Implementing asymmetric property visibility in PHP 8.4 is straightforward. However, there are certain rules that we need to consider to avoid any errors.
The following are some rules of asymmetric property visibility that we need to know to make our implementation a very smooth one.
1. Equal or Restrictive Set Visibility
In asymmetric property visibility, the visibility for writing (setting) must either be more restrictive or equal to the visibility for reading (getting) the property value.
For example, if the get visibility is public
, then the set visibility can be public
, protected
, or private
. If the get visibility is protected
, then the set visibility must be protected
or private
. And if the get visibility is private
, then the set visibility can only be private
. This implies that the set visibility must not be more permissive than the get visibility.
The following property definitions are all valid:
class Person {
// set visibility is more restrictive compared to get visibility
public protected(set) string $name = 'Daniel';
// set visibility is more restrictive compared to get visibiility
protected private(set) string $email = 'dan@mail.com';
// set visibility has equal visibility compared to get visibility
protected protected(set) string $role = 'Developer';
}
However, the following property definitions are not valid and will generate fatal error. This is because, in each case, the set visibility is weaker than the get visibility:
class Person {
// Fatal error: set visibility is weaker than get visibility
protected public(set) string $name = 'Daniel';
// Fatal error: set visibility is weaker than get visibility
private protected(set) string $email = 'dan@mail.com';
}
In the above class declaration, the set visibility for both $name
and $email
are more permissive, compared to their get visibility. This will general fatal error, such as seen in the following image:
Thus, a set visibility must impose a stricter access permission or have an equal access permission as the get visibility.
2. Applies to Typed Properties
In PHP 8.4, asymmetric property visibility can only be applied to properties that are declared with a data type such as int
, string
, float
, array
, etc.
The following code listing is valid since the data type of the $name
property is specified in the definition:
class Person {
// Data type of the property is required in
// asymmetric property declaration
public private(set) string $name = 'Daniel';
}
However, the following class declaration will generate a fatal error since no data type is specified in the definition of the $name
property.
class Person {
// Fata error: property has not data type
public private(set) $name = 'Daniel';
}
Thus, to declare a class property asymmetrically, we must also specify the data type of the property.
3. private(set)
Visibility is final
If a class declares a property with private(set)
visibility, then that property cannot be re-declared in a derived class. In other words, a property declared with private(set)
visibility is automatically final
and cannot be overridden in a derived class.
To understand this better, we should first remember that a derived class can re-declare a base class property that is not final
.
Let’s consider the following code listing:
class Person {
// property declaration
private string $name = 'Daniel';
}
class Student extends Person {
// re-declare the $name property from base class
private string $name = 'Joel';
public function getName(): string {
return $this->name;
}
}
// initialize object
$student = new Student();
// $student->getName() returns Joel
echo "My name is {$student->getName()}\n";
The Person
class declares a private $name
property. The Student
class extends the Person
class and re-declares the $name
property on line 8. Although the Person
class declares $name
as private
, the re-declaration of the $name
property in the Student
class is perfectly valid.
Executing listing 13 reveals that $student->getName()
returns the value of the $name
property in the Student
class. Thus, the $name
property in the Person
class gets overridden in the Student
class. Look at the output from a sample run below:
But, what if we want to prevent a derived class from overriding a base class property? The solution, without using asymmetric visibility, is to declare the property as final
in the base class.
In listing 13, we will have to explicitly declare the $name
property in the Person
class as final
to prevent the Student
class from overriding it.
Let’s update the declaration of the $name
property in the Person class as follows:
class Person {
// property declaration
final private string $name = 'Daniel';
}
After marking the $name
property in the Person
class as final
, we will get a fatal error when we execute the code:
Thus, we are able to prevent property override in derived classes by declaring the property as final
in the base class. But we can also prevent property override in derived classes by using the asymmetric property visibility feature.
If we declare a base class property as private(set)
, the property automatically becomes final
, and there will be no need to explicitly mark the property as final
. Derived classes cannot re-declare base class properties that are declared with private(set)
visibility.
Let’s update the declaration of the $name
property in the Person
class as follows:
class Person {
// property declaration
private(set) string $name = 'Daniel';
}
The declaration of $name
with private(set)
automatically makes the property final
, with no need to explicitly use the final
keyword. Classes that derive from the Person
class cannot re-declare the $name
property.
4. Property References Follow Set Visibility
When getting a reference to a class property, PHP uses the set visibility of the property to determine the validity of the property access statement. It examines the scope in which the property is being accessed, and also checks whether that scope can write to the property or not. If the scope of the code cannot set to the property, then the attempt to get the reference to the property will be flagged as an error.
Let’s consider the following code listing:
class Person {
public private(set) string $name = 'Daniel';
}
// initialize object
$person = new Person();
// get the name
$name = $person->name;
// output name
echo $name; // output: Daniel
Line 2 uses the asymmetric visibility feature to define the $name
property. As can be seen, the get visibility is public
. This means that we can access the $name
property outside the Person
class. Hence, on line 9, we access the value of $name
property into the $name
variable and output its value on line 12. That is perfectly fine.
Now suppose we want to get a reference to the $name
property. We will update the statement on line 9 to read as follows:
// get the name
$name = &$person->name;
If you cannot see the difference, then observe clearly and identify that we have preceded the $person
object with the character &
. Go ahead. Execute the code and see what happens. You should get a fatal error after executing the code:
The error message says that we cannot indirectly modify private(set)
property. But line 9 in listing 16 isn’t event modifying the $name
property. It is only getting a reference to the $name
property.
When getting a reference to a class property that is declared with asymmetric visibility, the PHP interpreter uses the set visibility of the property to validate the access statement. In this example, the set visibility is private(set)
. Since the scope of the code accessing the property cannot set to the property, it generates a fatal error.
But why is the error generated based on the set visibility when there is no line in the code that modifies the $name
property? Well, the reason is that if access to the reference is granted, we may use the reference to modify the value of the property. Therefore, the PHP interpreter thinks ahead.
Before granting a reference to a class property, the PHP interpreter checks the set visibility of the property, then checks again to see if the scope of the code can set to the property. If it can, then access to the property reference will be granted. If it cannot, then access to the reference will not be granted and a fatal error will be generated.
Let’s try another example in which access to the reference will be granted. In this example, we will define the $name
property to have a public(set)
visibility:
public public(set) string $name = 'Daniel';
Save and execute the code again. This time, it should work without any error. The reason why it works now is as was explained earlier. Since the set visibility is public
, the scope in which the property reference is being accessed is capable of setting to the property. Hence, access to the property’s reference is granted.
In conclusion, getting a reference to a class property follows the set visibility of the property but not its get visibility. This is because the PHP interpreter thinks ahead and checks if the current scope can set to the property or not.
Permitted Asymmetric Visibility
The following list contains access modifiers that can be used to define class properties asymmetrically:
public public(set)
public protected(set)
public private(set)
protected protected(set)
protected private(set)
private private(set)
private(set)
protected(set)
Summary
Asymmetric property visibility is a new feature introduced in PHP 8.4. It enables PHP developers to specify a separate access modifier for getting the value of a property and another access modifier for setting the value of the same property.
To declare a class property with asymmetric visibility, we specify two access modifiers in the property declaration statement. The first access modifier keyword (left) is the visibility for getting the property value, and the second access modifier keyword (right) is the visibility for setting the property value.
class Animal {
public private(set) string $name = '';
}
When declaring a class property with asymmetric visibility, the set visibility is specified with the access modifier keyword followed by the set
keyword, enclosed in parentheses. Thus, we can specify the set visibility as private(set)
, protected(set)
, or public(set)
. There should be no spaces between the set
keyword and the parentheses.
In asymmetric property visibility, we can specify the set visibility without an explicit get visibility. This implies that the get visibility is optional.
class Animal {
private(set) string $name = '';
}
If we do not indicate the get visibility explicitly, then the get visibility defaults to public
access, and any code outside the class can access the property.
Asymmetric property visibility is governed by some rules. Understanding these rules will help to avoid errors in PHP applications.
For instance, asymmetric visibility is permitted on class properties with explicit data types. We cannot use this feature on properties without a type.
If a class property is declared with private(set)
visibility, then it automatically becomes final
. In this case, a derived class cannot re-declare a base class property that has private(set)
visibility.
Getting a reference to a class property follows the set visibility of the property but not its get visibility. This implies that if a code attempts to get a reference to a class property, PHP uses the set visibility of the property to determine if the code getting the reference can modify the property or not. If it cannot, then an will be generated.