This is an old revision of the document!
Request for Comments: Property accessors syntax - As Implemented
- Version: 1.2
- Date: 2011-12-21
- Updated: 2012-10-12
- Author: Clint Priest <phpdev at zerocue dot com>
- Status: Ready for Review & Discussion, Code Mostly Complete
Fork
- The changes for this RFC are available here: https://github.com/cpriest/php-src
Introduction
This document describes the accessor syntax As Implemented. The RFC which the implementation was crafted from is located here: https://wiki.php.net/rfc/propertygetsetsyntax
What Are Properties?
Properties provide a clean, easy to understand and unified syntax for get/set accessors. They allow incoming and outgoing requests (gets and sets) from a class member to be run through a method first. This method can perform validation or a transformation, or update other areas of the class. Properties do not even have to be associated with a class member, and can generate their own data on-the-fly.
Syntax
Basic Syntax
This is the property syntax for accessors as implemented.
class TimePeriod { private $Seconds; // Accessor properties are implemented just like you would define an actual property public $Hours { get() { return $this->Seconds / 3600; } // In the set accessor, the variable $value holds the incoming value to be "set" set($value) { $this->Seconds = $value * 3600; } } }
// Accessing the property is the same as accessing a class member $time = new TimePeriod(); $time->Hours = 12; // Stored as 43200 echo $time->Hours; // Outputs 12
Note that “get” and “set” were not made to be new keywords, they are processed as strings by the parser.
Type Hinting
Type hinting for the set() accessor will be allowed, for example:
class TimePeriod { private $Seconds; // Accessor properties are implemented just like you would define an actual property public $Hours { get() { return $this->Seconds / 3600; } // In the set accessor, the variable $value holds the incoming value to be "set" set(TestClass $value) { $this->Seconds = $value->toSeconds(); } } }
Overloading Properties
Properties can be overloaded in extending classes. An overloaded property can replace an existing get or set declaration without touching the other, replace both the get and set declarations, or add an omitted get or set declaration turning the property into a read-write property. Additionally, a property may have its visibility increased through overloading. Get or set declarations cannot be removed or hidden by the child class in any way.
class TimePeriod { protected $Seconds = 3600; public $Hours { get() { return $this->Seconds / 3600; } set($value) { $this->Seconds = $value * 3600; } } // This property is read-only public $Minutes { get() { return $this->Seconds / 60; } } /* public getter, protected setter */ public $Milliseconds { get() { return $this->Seconds * 1000; } protected set($value) { $this->Seconds = $value / 1000; } } } class HalfTimePeriod extends TimePeriod { /* Overload getter, inherit setter */ public $Hours { get() { return ($this->Seconds / 3600) / 2; } /* The base setter method will be inherited */ } public $Minutes { // A set method is added, turning this property into a read-write property instead of read-only set($value) { $this->Seconds = $value * 60; } } public $Milliseconds { // A property method can have its visibility increased in a child class, just like regular PHP methods // This method is now public instead of protected public set($value) { // You can access a base class property explicitly, just like accessing a base class member or method parent::$Milliseconds = $value; } } }
Note that if parent:: scope access would be overloaded, resolution priorities follow:
- Check for parent non-static accessor, call that if defined.
- Check for parent static accessor, call that if defined.
- Check for parent static property, use that value if defined.
This does not conflict with existing functionality which is presently only #3.
Asymmetric Accessor Accessibility
Properties can have different levels of visibility for the get and set methods. This is achieved by setting either the get or set method to a lower visibility than the property is set to.
class TimePeriod { private $Seconds = 3600; public $Hours { get() { return $this->Seconds / 3600; } protected set($value) { $this->Seconds = $value * 3600; } } }
$o = new TimePeriod(); echo $o->Hours; // Prints 1 $o->Hours = 12; // Error, unable to set protected property
In the above example the getter inherits the public access level of the property definition.
isset / unset
To facilitate complete functionality with accessors it is necessary to provide accessor functions to act on isset() and unset() calls. These operate just like their magic __isset() and __unset() functions but are definable within the accessor block.
class TimePeriod { private $Seconds = 3600; public $Hours { get() { return $this->Seconds / 3600; } set($value) { $this->Seconds = $value; } isset() { return isset($this->Seconds); } unset() { unset($this->Seconds); } } }
Automatic Implementations
isset and unset automatic implementations
The isset automatic implementation tests for the property to be non-null. (See php equivalent below) The unset automatic implementation sets the property to be null. (See php equivalent below)
class TimePeriod { // Accessor properties are implemented just like you would define an actual property public $Hours { get() { return 45; } set($value) { $this->_Hours = $value; } isset(); unset(); } }
Translates to this:
class TimePeriod { // Accessor properties are implemented just like you would define an actual property protected $__Hours; public $Hours { get() { return 45; } set($value) { $this->_Hours = $value; } isset() { return $this->Hours != NULL; } unset() { $this->Hours = NULL; } } }
Note that isset/unset implementations will always be provided (where appropriate) if they are not defined or if they are explicitly auto-defined (as above).
“Where appropriate” means that if you have only defined a getter then unset is not available, likewise if you have only defined a setter, then isset is not available.
Lastly, isset/unset may be explicitly defined as indicated in prior sections, the “will always be provided” only applies to cases where they are not explicitly defined.
Final Properties
Properties declared final are not allowed to be overloaded in a child class, just like final methods.
class TimePeriod { private $Seconds; public final $Hours { get() { return $this->Seconds / 3600; } set($value) { $this->Seconds = $value * 3600; } } } class HalfTimePeriod extends TimePeriod { private $Seconds; // This attempt to overload the property "Hours" will throw an error because it was declared final in the base class public $Hours { get() { return ($this->Seconds / 3600) / 2; } } }
Final property methods
The get or set method of a property can be declared “final” independently of each other. This would allow for one of them to be overloaded, but not the other.
class TimePeriod { private $Seconds; // Notice there is no "final" keyword on the property declaration public $Hours { final get() { return $this->Seconds / 3600; }// Only the get method is declared final set($value) { $this->Seconds = $value * 3600; } } } class HalfTimePeriod extends TimePeriod { private $Seconds; public $Hours { // This attempt to overload the get method of the "Hours" will throw an error // because it was declared final in the base class get() { return ($this->Seconds / 3600) / 2; } // This would be accepted set($value) ( $this->Seconds = ($value * 3600) * 2; ) } }
Static Properties
Static properties act identically to regular properties, except in a static context.
class TimePeriod { private static $Seconds; public static $Hours { get() { return self::$Seconds / 3600; } set($value) { self::$Seconds = $value * 3600; } } }
Accessing a static property is the same as accessing a static class member
TimePeriod::$Hours = 12; // Stored as 43200 echo TimePeriod::$Hours; // Outputs 12
Parent accessors may also be used
class TimePeriod2 extends TimePeriod { public static $Hours { get() { return parent::$Seconds / 3600; } set($value) { parent::$Seconds = $value * 3600; } } }
References
Functions such as sort() require a reference to the underlying data storage value in order to modify them, in these cases you can place the & before the get to indicate the returning of a reference variable.
class SampleClass { private $_dataArray = array(1,2,5,3); public $dataArray { &get() { return $this->_dataArray; } } } $o = new SampleClass(); sort($o->dataArray); /* $o->dataArray == array(1,2,3,5); */
Operators
The following operators have tests written for them and work as though it were any other variable. If the operator attempts to make a change to a property for which no setter is defined, it will produce an error such as “Cannot set property xxx, no setter defined.” If a setter is defined, then the assignment operator works as expected.
The following operators have code tests written already: Pre/Post Increment/Decrement, Negation, String Concatenation (.), +=, -=, *=, /=, &=, |=, +, -, *, /, %, &, |, &&, ||, xor, ~, ==, ===, !=, !==, >, <, >=, <=, .=, <<, >>, Array Union (array + array), instanceof
Controlling access to subclasses
Rather than the originally proposed read-only and write-only new keywords, people were more in favor of defining a final property, for example:
class TimePeriod { private $Seconds; // This property is read-only to those outside the class public $Hours { get() { return $this->Seconds / 3600; } private final set($value) { $this->Seconds = $value * 3600; } } } // A subclass which attempts to re-define a final setter (as above) would be met with a compile error. // Results in scope permission violation $o = new TimePeriod(); $o->Hours = 4;
Interface Properties
Interfaces may define property declarations, without a body. The purpose of this is to define properties that must exist in an implementing class.
When a class implements an interface that defines a getter, it can add in a set method to turn the property into a read-write property. The inverse is also true for implementing an interface with a setter only. This is because interfaces are designed to enforce what should be in a class, and not what should not be in a class.
interface iSampleInterface { public $MyProperty { get(); set($value); isset(); unset(); } }
Traits
Accessors work as expected with traits including automatic accessor properties. You can use any feature with traits that you could with classes including asymmetrical access levels, read-only/write-only, isset, unset, etc.
trait SampleTrait { private $Seconds = 3600; public $Hours { get() { return $this->Seconds * 3600; } set($value) { $this->Seconds = $value / 3600; } } }
Error Messaging
All messaging that would refer to a function call will be translated to show access to a property. This is already implemented most everywhere, but notably an exception thrown from an accessor doesn't look right (reveals implementation details) and it's likely that debug_backtrace() at present would do the same.
Reflection
Changes
- ReflectionClass::getProperties() will now return an array of ReflectionProperty and ReflectionPropertyAccessor classes.
- ReflectionClass::getProperty() will also return an appropriate class based on being a property or an accessor. planned
Additions
ReflectionPropertyAccessor
The new class has all of the same functions as the ReflectionProperty as well as:
- getGetter(): Returns a ReflectionMethod object for the getter or false if no getter is defined.
- getSetter(): Returns a ReflectionMethod object for the setter or false if no setter is defined.
- getIssetter(): Returns a ReflectionMethod object for the isset accessor. planned
- getUnsetter(): Returns a ReflectionMethod object for the unset accessor. planned
All other functions have been updated to return appropriate values and/or provide appropriate actions for an accessor.
ReflectionMethod class
- isAccessor(): Returns true if the method is an accessor planned
A fairly extensive test-suite has been created to test the functionality as well.
Backward Compatibility
There are no known backward compatibility issues.
Implementation
Accessor information is stored in a new zend_accessor_info struct. These structures are stored in a new HashTable property in a zend_class_entry structure named accessors. They are indexed by the hash_value of the property name and are thus quickly accessed during property resolution.
typedef struct _zend_accessor_info { zend_uint flags; const char *doc_comment; int doc_comment_len; zend_function *getter; zend_function *setter; zend_function *isset; zend_function *unset; } zend_accessor_info;
Internally the accessors are still ordinary functions with appropriate access levels but are not accessible by directly calling their internal names, they are only accessible by way of property syntax (echo $o->Hours or $o->Hours = 5)
Two new function flags have been defined:
#define ZEND_ACC_READONLY 0x20000000 #define ZEND_ACC_WRITEONLY 0x40000000
An additional byte was added to zend_internal_function:
zend_uchar purpose;
This was in lieu of using 4 additional flag values for which there was not room. There are presently five states purpose can be in, they are:
#define ZEND_FNP_UNDEFINED 0 /* No special purpose function */ #define ZEND_FNP_PROP_GETTER 1 /* Special purpose accessor: getter */ #define ZEND_FNP_PROP_SETTER 2 /* Special purpose accessor: setter */ #define ZEND_FNP_PROP_ISSETTER 3 /* Special purpose accessor: issetter */ #define ZEND_FNP_PROP_UNSETTER 4 /* Special purpose accessor: unsetter */
* __get(), __set(), __isset() and __unset() guards were used and the functionality is the same with the new accessors.
* Error producing lines have been modified to check the function for ZEND_ACC_IS_ACCESSOR mask with more appropriate error report occurring. For example: Cannot override final property getter TimePeriod::$Hours
Static Accessors
There was no built-in mechanism to handle custom get/set/isset/unset for static properties, these were handled by catching references to static properties, checking for the existence of a static accessor and converting the compilation into a function call. When a static setter is being used, the compiled code first becomes a static getter call and the zend_do_assign backpatches the op_array to become a call to the setter, as appropriate.
This yielded the possibility that a getter call was being made while it should not be allowed (if there was no getter defined) and so pass_two() was changed to look for these non-backpatched illegal static getter calls and a compile time error is produced.
Tests
- 2011-12-21 : 21 Test Cases Created
- 2012-10-06 : Numerous additional test cases created, 79 in total now.
References
Changelog
- 2011-12-21 Clint Priest: “As Implemented” document created based in large part by original RFC
- 2012-03-28 Clint Priest: Accessors being called where no get/set would be handled by the original get()/set() functions, this is no more. If there is an accessor defined, the buck stops there for that variable name.
- 2012-03-28 Clint Priest: Auto-backing fields are now protected rather than public
- 2012-03-30 Clint Priest: git fork at https://github.com/cpriest/php-src
- 2012-03-31 Clint Priest: Cleaned up read-only, write-only and over-riding to be in line with original RFC, they now are inherited and immutable, child accessors must define read-only/write-only as parent has done.
- 2012-10-06 Clint Priest: Added isset/unset accessors, static and object based. Added several dozen new tests. Refactored the auto-generation of code to use php within such that an automatic implementation is compiled php from within the interpreter.
- 2012-10-07 Clint Priest: Reorganized document sections for easier, cumulative concept conception.
- 2012-10-11 Clint Priest: After much discussion on the internals list, the following changes were desired:
- Eliminate the ability for an accessor to be called via $o->__getHours(), the accessor functions will be completely unavailable for use except as property references ($o->Hours)
- Change syntax to use public set($value) {}, public get(), etc. Along with that means no “magic” $value in the setter
- If possible, allow type hinting in the definition
- Eliminate automatically implemented get; set;, no automatic backing field creation will occur.
- read-only / write-only keywords will be eliminated, the use of the word final on a setter/getter will take their place.
- Exceptions thrown from an accessor reveal underlying implementation details, this is to be avoided, checking debug_backtrace() information as well