rfc:propertygetsetsyntax-v1.1

Request for Comments: Property accessors syntax - As Implemented

Fork

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 { $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.

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 { $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 { $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 { $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 {
            // 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:

  1. Check for parent non-static accessor, call that if defined.
  2. Check for parent static accessor, call that if defined.
  3. 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 { $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 { $this->Seconds = $value; }
        isset { return isset($this->Seconds); }
        unset { unset($this->Seconds); }
    }
}

Automatic Implementations

You may also use automatic implementations of property accessors by not defining a body to the accessor. Doing so causes an automatic implementation to occur. Automatic implementations create a protected backing field automatically, named the same as the property with a double underscore (__) preceding it. The get and set implementations directly get and set from this automatic backing field.

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;
        set;
        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 $this->__Hours; }
        set { $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 { $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 { $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 ( $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 { 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 { 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

Read-Only And Write-Only Properties

Defining accessors with only a getter or only a setter will make them read only and write only respectively but this does not enforce anything with subclasses.

You could declare a get-only accessor final in order to enforce rules upon subclasses but this would also prevent any changes to that getter by a subclass, which may not be the desired outcome.

What may be needed instead is to allow a subclass to alter the getter but still enforce that the property must remain read-only. That is the use case the following new keywords, read-only and write-only, address.

read-only keyword

new keyword

You can use the read-only keyword in the definition of the property to enforce the property as read only. No setter may be defined and an attempt to set the property results in a fatal error.

class TimePeriod {
    private $Seconds;
 
    // This property has specified the read-only keyword and therefore is read-only
    public read-only $Hours {
        get { return $this->Seconds / 3600; }
        // Setter may not be defined 
    }
}
 
// Results in Fatal Error 
 
$o = new TimePeriod();
$o->Hours = 4;

write-only keyword

new keyword

You can use the write-only keyword in the definition of the property to enforce the property as write only. No getter may be defined and an attempt to get the property results in a fatal error.

class TimePeriod {
    private $Seconds;
 
    // This property has specified the write-only keyword and therefore is write only 
    public write-only $Hours {
        // Getter may not be defined 
        set { $this->Seconds = $value * 3600; } 
    }
}
 
// Results in Fatal Error 
 
$o = new TimePeriod();
echo $o->Hours;

Overriding read-only & write-only

The read-only and write-only keywords are inherited and must be declared by child classes.

class TimePeriod {
    private $Seconds;
 
    // This property has specified the read-only keyword and therefore is read-only
    public read-only $Hours {
        get { return $this->Seconds / 3600; }
        // Setter may not be defined 
    }
}
 
class TimePeriod2 extends TimePeriod {
    /* Results in fatal error, $Hours must be declared as read-only, 
     * as in parent which would further disallow the set {} from be declared. */
    public $Hours {
        get { return $this->Seconds / 3600; }
        set { $this->Seconds = $value; }
    }
}

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, and may indicate if they are read-write, read-only, or write-only.

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;
        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 { $this->Seconds = $value / 3600; }
    }
}

Reflection

Changes

  • ReflectionClass::getProperties() will now return an array of ReflectionProperty and ReflectionPropertyAccessor classes.
  • ReflectionClass::getMethods() will not return accessor functions (hides implementation detail).
  • 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
  • isReadOnly(): Returns true if the accessor is defined as read-only.
  • isWriteOnly(): Returns true if the accessor is defined as write-only.

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
  • isAutoImplemented(): Returns true if the method is an accessor and was automatically implemented 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 implemented as ordinary functions (with appropriate access levels) with specialized names. get/set/isset/unset for a property named $Hours would be __getHours(), __setHours($value), __issetHours() and __unsetHours() respectively.

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.

Safety Checks

read-only and write-only keywords

These keywords may not be specified multiple times, nor may they be used with regular properties (non-accessor) or methods.

Tests

  • 2011-12-21 : 21 Test Cases Created
  • 2012-10-06 : Numerous additional test cases created, 79 in total now.

References

Changelog

  1. 2011-12-21 Clint Priest: “As Implemented” document created based in large part by original RFC
  2. 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.
  3. 2012-03-28 Clint Priest: Auto-backing fields are now protected rather than public
  4. 2012-03-30 Clint Priest: git fork at https://github.com/cpriest/php-src
  5. 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.
  6. 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.
  7. 2012-10-07 Clint Priest: Reorganized document sections for easier, cumulative concept conception.
rfc/propertygetsetsyntax-v1.1.txt · Last modified: 2017/09/22 13:28 by 127.0.0.1