rfc:propertygetsetsyntax-v1.2

Request for Comments: Property Accessors Syntax

  • Version: 1.2
  • Created: 2012-12-29
  • Updated: 2013-01-17
  • Author: Clint Priest <cpriest at php dot net>
  • Contributors: Nikita Popov
  • Status: Declined, Vote Failed

Fork

Introduction

This document describes the property accessor syntax. The RFC which this was crafted from is located here: https://wiki.php.net/rfc/propertygetsetsyntax

Previous Revision: https://wiki.php.net/rfc/propertygetsetsyntax-v1.1

What Are Property Accessors?

Property accessors provide a clean, easy to understand and unified syntax for get/set accessors. They allow read and write requests (gets and sets) from a class property 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.

Terminology / Glossary

To provide clarity of documentation, the following glossary and example are provided:

  • Traditional Property - A property which has no accessors, a class “variable,” the only type of property presently available
  • Guarded Property - A property with accessors which guard read, write, isset and unset access through code
  • Accessor - A method defined for a property which implements code that is executed when a property is accessed
    • getter - A type of accessor which is used to retrieve the value of the property
    • setter - A type of accessor which is used to change the value of a property
    • issetter - A type of accessor which is used to determine whether the property isset()
    • unsetter - A type of accessor which is used to unset() the property
// Code sample indicating the terminology
class TimePeriod {
    private $Seconds;  // <-- Traditional Property
 
    public $Hours {    // <-- Guarded Property
        get() { return $this->Seconds / 3600; }      // <-- Accessor, more specifically a getter
        set($x) { $this->Seconds = $x* 3600; }       // <-- Accessor, more specifically a setter
        isset() { return isset($this->Seconds); }    // <-- Accessor, more specifically an issetter
        unset() { unset($this->Seconds); }           // <-- Accessor, more specifically an unsetter
    }
}

A fully implemented, symmetrically defined property with accessors should be functionally identical to a traditional property

Syntax

Basic Syntax

This is the basic property accessor syntax as implemented.

class TimePeriod {
    private $Seconds;
 
    // Properties accessors are implemented just like you would define an actual property
    public $Hours {
        get() { return $this->Seconds / 3600; }
 
        // In the set accessor, the variable $x holds the incoming value to be "set" 
        set($x) { $this->Seconds = $x * 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

Typehints The setter also allows for optional type hinting, such as

    set(callable $x) { ... }

Version 1.1 did not have parenthesized syntax, v1.2 adds optional parenthesis format. If no parenthesis are provided, then $value is automatically provided in the case of a setter, the following is equivalent to the previous block:

class TimePeriod {
    private $Seconds;
 
    // Property accessors 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; } 
    }
}

Overloading Properties

Properties can be overloaded in extending classes. An overloaded property can replace an existing accessor declaration without touching the other, replace both the get and set declarations, or add an omitted accessor 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 as there is no setter
    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 will be inherited */
    }
 
    public $Minutes {
        // A setter is added, turning this property into a read/write property instead of read only
        set { $this->Seconds = $value * 60; }
    }
 
    public $Milliseconds {
        // An accessor can have its visibility increased in a child class, just like regular PHP methods
        // This accessor is now public instead of protected
        public set {
            // Due to technical limitations accessing the parent property is only possible through Reflection
            (new ReflectionPropertyAccessor(get_parent_class(), 'Milliseconds'))->setValue($this, $value);
        }
    }
}

Removal of Accessor

Subclasses may also eliminate the accessor aspect of a property by re-declaring the property without the accessor syntax. This prunes the accessor functions from the sub-class that would ordinarily be inherited by the sub-class. Take this example:

class A {
     public $Foo {
         get { return 1; }
     }
}
 
class B extends A {
     public $Foo = 5;
}
$o1 = new A()
echo $o1->Foo;   // Echos 1 via A::get:$Foo
 
$o2 = new B();
echo $o2->Foo;   // Echos 5 from direct property access (no accessors present in class B)

Note that the re-defined property must still be public (may not be redefined as private or protected).

The accessors are only removed when using the public $Foo; syntax. public $Foo { } on the other hand will inherit the parent accessors without modifying them.

Asymmetric Accessor Accessibility

Property accessors can have different levels of visibility for the getter and setter. This is achieved by setting either the get or set accessor 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;    // Fatal error: Cannot set protected property TimePeriod::$Hours from context '' 

In the above example the getter inherits the public access level of the property definition while the setter becomes protected.

isset / unset

To facilitate complete functionality with properties 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 property 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); }
    }
}

Guarding

Accessors guard properties access such that if a property has accessors defined for it, the accessors will be called whenever the property is accessed. Only the appropriate accessors themselves may directly access the underlying property, even from within the same class.

class TimePeriod {
    public $Hours {
        get { return $this->Hours ?: "not specified"; }
        set { $this->Hours = $value; }
    }
}
 
$o = new TimePeriod();
 
echo $o->Hours;   // echos not specified
 
$o->Hours = 1;
echo $o->Hours;   // echos 1

For Additional Clarity

All interaction with a guarded property is proxied through the appropriate accessor except when within the same accessor:

  • Within a get scope, reads are not proxied; the property is read directly.
  • Within a set scope, writes are not proxied; the property is written to directly.
  • Within an isset scope, calls to isset() are not proxied; isset() will function normally and read the underlying property normally which will be through the getter.
  • Within an unset scope, calls to unset() are not proxied; unset() will function normally and remove the underlying property. This will not remove the accessors, only the value that the property inherently had.

Accessors are free to interact with other properties which are visible to them, but access to other guarded properties is always proxied.

Accessors are not required to use the property value, but it always exists.

Interaction Between Accessors

Accessors are independent, in the below example the get accessor Foo::get:$b attempting to access $this->a goes through the getter for a.

class Foo {
    public $a {
        get { $this->a = 1; return 2; }
        set;
    }
    public $b {
        get { return $this->a; }
    }
}
 
$foo = new Foo();
echo $foo->a; // Echos 2 but underlying property is set to 1
echo $foo->b; // Echos 2 via getter for $a which again sets its underlying property $a to 1
 
/* Note, that without the set; a warning would be produced by an attempt to set $this->a from the 
   getter for $a, this is because a setter is required to write to the property, even the getter 
   cannot write to it's own property without the setter, only the setter may do that. */

Automatic Implementations

You may also use automatic implementations of accessors by not defining a body to the accessor. Doing so causes an automatic implementation to occur.

Because property accessors shadow traditional properties, the property data storage is accessible only from within the accessor.

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 {
    // Accessors are implemented just like you would define an actual accessor, but without a body
    public $Hours {
        get;
        set;
        isset;
        unset;
    }
}

Translates to this:

class TimePeriod {
    public $Hours {
        get { return $this->Hours; }
        set { $this->Hours = $value; } 
        isset { return $this->Hours !== NULL; }
        unset { $this->Hours = NULL; }
    }
}
Note: isset & unset implementations are always provided with default implementations unless the author explicitly defines their own.

Invalid Usage of isset()/unset()

There are two cases where isset and unset do not logically make sense as detailed below:

  • If there is no getter defined, then an isset() call is technically invalid since the value cannot be obtained to see if it was set. In this case, isset() will silently ignore the invalid state, return false and continue execution.
  • If there is no setter defined, then an unset() call is technically invalid since the value cannot be changed as there is no setter. In this case, unset() will emit a warning and continue execution but otherwise have no effect.

Recursion

Recursion with property accessors works identically to their __get() and __set() cousins in that recursion is guarded.

When the accessor that was called attempts to access the property it will directly access the underlying property, this is the only context in which the recursion guard is bypassed.

1  class A {
2      public $Foo {
3         get {
4             if($this->Foo)
5                 return $this->Foo;
6             return $this->Foo = 5;
7         }
8         set;
9     }
10 }
 
11 $o = new A();
12 echo $o->Foo;
13 echo $o->Foo;
 
Line 12: Calls A::get:$Foo;
Line  4: Directly reads the underlying property (getter is guarded from a 2nd call)
Line  6: Calls A::set:$Foo(5) and finally returns the value 5
Line 13: Calls A::get:$Foo;
Line  4: Directly reads the underlying property (getter is guarded from a 2nd call)
Line  5: Directly reads the underlying property (getter is guarded from a 2nd call) and returns the value

Illegal Context Access (Recursion Guarding)

If an accessor is called while it is already being guarded (from recursion) from an illegal context (anything that isn't the same accessor) then the following occurs:

  • get called from illegal context: Recursion warning is emitted, NULL is returned.
  • set called from illegal context: Recursion warning is emitted, set is ignored.
  • isset called from illegal context: Recursion warning is emitted, false is returned.
  • unset called from illegal context: Recursion warning is emitted, unset is ignored.

Abstract Accessors

Individual accessors may be defined abstract which will cause the class to be abstract and require any extending classes to define a body for the abstract accessors.

class Foo {
    public $bar {
        abstract get;
    }
}
 
class SubFoo extends Foo {
}
 
/* Fatal error: Class Foo contains 2 abstract accessors and must be declared
      abstract or implement the remaining accessors (Foo::$bar->get, Foo::$bar->isset) in ... */

Just like abstract functions, it is illegal to declare an accessor abstract and to provide a body:

class Foo {
    public $bar {
        abstract get { return 'test'; }
    }
}
 
/* Fatal error: Abstract function Foo::$bar->get() cannot contain body in ... */

You may also declare an entire property as abstract such as:

class Foo {
    abstract public $bar {
        get;
    }
}
 
/* This marks all declared accessors as abstract, as well as the class.  An extending class 
   would need to provide a body for any declared accessors */

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 Accessors

The get or set accessor 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 accessor is declared final
        set { $this->Seconds = $value * 3600; }
    }
}
 
class HalfTimePeriod extends TimePeriod {
    private $Seconds;
 
    public $Hours {
        // This attempt to overload the getter of the "Hours" property 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 Property Accessors

Static property accessors will not be in the first release of accessors, there are too many engine changes needed to enable this functionality.

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; }
        set(&$value) { $this->_dataArray = $value; }
    }
}
 
$o = new SampleClass();
sort($o->dataArray);
/* $o->dataArray == array(1,2,3,5); */

All of the following work and have test cases written for them

  • $foo->bar[] = 1
  • $foo->bar[123] = 4
  • $foobar = &$foo->bar
  • $foobar->baz = $x with $bar being object or empty or non-object value
  • $foo->bar{$x} = “foo” (string offsets)

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 properties 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.

Developers wishing to prevent a setter from being defined by sub-classes will need to use the final keyword with something along these lines:

class TimePeriod {
    private $Seconds;
 
    public $Hours {
        get() { return $this->Hours; }
        private final set($value) { throw new Exception("Setting of TimePeriod::$Hours is not allowed."); }
    }
}

Interface Property Accessors

Interfaces may define property accessor declarations without a body. The purpose of this is to define property accessors that must exist in an implementing class.

When a class implements an interface that defines a getter, it can add in a setter 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;
    }
}

Furthermore, a traditional property satisfies any requirements that an property accessor declaration within an interface declares, therefore, the following is a valid implementation of the above interface:

class A implements iSampleInterface {
    public $MyProperty;
}

Traits

Property 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, isset, unset, etc.

trait SampleTrait {
    private $Seconds = 3600;
 
    public $Hours {
        get { return $this->Seconds * 3600; }
        set { $this->Seconds = $value / 3600; }
    }
}

Miscellaneous Q/A

__FUNCTION__ and __METHOD__ will resolve to the internally used function name:

class Bar {
    public $foo {
        get { var_dump(__FUNCTION__, __METHOD__); }
    }
}
(new Bar)->foo;
 
string(9) "$foo->get"       // __FUNCTION__
string(14) "Bar::$foo->get" // __METHOD__

These names are also used in backtraces and error messages, for example:

Fatal error: Call to protected accessor Test::$foo->set() from context ''

These functions are not directly callable by the user, e.g. doing something like $this->{'$foo->get'} will not work.

Reflection

ReflectionProperty changes

The class has the following functions added:

  • getGet(): Returns a ReflectionMethod object for the getter or false if no getter is defined.
  • getSet(): Returns a ReflectionMethod object for the setter or false if no setter is defined.
  • getIsset(): Returns a ReflectionMethod object for the isset accessor.
  • getUnset(): Returns a ReflectionMethod object for the unset accessor.
  • hasAccessors(): Returns true if the property has accessors, false otherwise.

A fairly extensive test-suite has been created to test the functionality as well.

Backward Compatibility

There are no known backward compatibility issues.

Internal Implementation

Impact on APC and other Zend extensions

In addition to the “implementation details document” linked in the previous section this section outlines the impact the accessors implementation has on APC and other Zend extensions.

Most Zend extensions should not be affected by this change. Accessors are normal zend_op_arrays, they are called as any other function and have a meaningful name. As such extensions like XDebug should not need any adjustments to support accessors.

One extension that will require minor changes is APC. APC has to copy all zend_op_arrays and zend_property_infos because they may be modified at runtime. Due to this proposal additional op_arrays may be located in property_info->accs and need to be copied too. Here are the code snippets that need to be inserted in APC to do this: https://gist.github.com/4615156 (full code with the changes: https://gist.github.com/4597660). Other extensions that do something similar will require updates along the same lines.

Thus the impact of the change on Zend exts is rather small.

Tests

  • 2012-12-30: 67 tests at this time
  • 2013-01-17: 83 tests at this time

Voting

Voting ends not before Wednesday, January 23rd 2013. The PHP language is expanded, so a 2/3 majority is required.

Accept PHP Accessors for 5.5?
Real name Yes No
ab (ab)  
aeoris (aeoris)  
aharvey (aharvey)  
andi (andi)  
arpad (arpad)  
bjori (bjori)  
cataphract (cataphract)  
colder (colder)  
cpriest (cpriest)  
datibbaw (datibbaw)  
davidc (davidc)  
derick (derick)  
dmitry (dmitry)  
fa (fa)  
googleguy (googleguy)  
gooh (gooh)  
guilhermeblanco (guilhermeblanco)  
hradtke (hradtke)  
ircmaxell (ircmaxell)  
jguerin (jguerin)  
joris (joris)  
jpauli (jpauli)  
juliens (juliens)  
jwage (jwage)  
krakjoe (krakjoe)  
kriscraig (kriscraig)  
laruence (laruence)  
lbarnaud (lbarnaud)  
levim (levim)  
lstrojny (lstrojny)  
mattficken (mattficken)  
mike (mike)  
nikic (nikic)  
olemarkus (olemarkus)  
padraic (padraic)  
pajoye (pajoye)  
patrickallaert (patrickallaert)  
pierrick (pierrick)  
ralphschindler (ralphschindler)  
rasmus (rasmus)  
rdohms (rdohms)  
reeze (reeze)  
remi (remi)  
rquadling (rquadling)  
salathe (salathe)  
sebastian (sebastian)  
seld (seld)  
szarkos (szarkos)  
thorstenr (thorstenr)  
toby (toby)  
tyrael (tyrael)  
weierophinney (weierophinney)  
willfitch (willfitch)  
yohgaki (yohgaki)  
zeev (zeev)  
zhangzhenyu (zhangzhenyu)  
Final result: 34 22
This poll has been closed.

Change Log

  • 2013-01-05: Changed getGetter() and ilk to getGet()
  • 2013-01-05: Noted that ReflectionPropertyAccessor will be a sub-class of ReflectionProperty
  • 2013-01-05: Added Other Notes / Case Insensitivity note
  • 2013-01-09: Note that public $Foo {} will inherit
  • 2013-01-09: Update error messages and __FUNCTION__ info
  • 2013-01-09: Remove note on case-sensitivity. We properly support case-sensitivity now.
rfc/propertygetsetsyntax-v1.2.txt · Last modified: 2017/09/22 13:28 by 127.0.0.1