rfc:friend-classes

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
rfc:friend-classes [2016/01/07 17:42] mdwheelerfc:friend-classes [2018/07/14 12:52] (current) – vote end mdwheele
Line 1: Line 1:
 ====== PHP RFC: Class Friendship ====== ====== PHP RFC: Class Friendship ======
-  * Version: 0.1 +  * Version: 1.0.0 
-  * Date: 2015-12-10+  * Date: 2017-09-21
   * Author: Dustin Wheeler, mdwheele@ncsu.edu   * Author: Dustin Wheeler, mdwheele@ncsu.edu
-  * Status: Draft+  * Status: Declined
   * First Published at: http://wiki.php.net/rfc/friend-classes   * First Published at: http://wiki.php.net/rfc/friend-classes
- 
- 
  
 ===== Introduction ===== ===== Introduction =====
-Class Friendship allows a class to be better encapsulated by granting per-class access to protected properties. Class Friendship is an explicit and concise expression of tight-coupling between collaborators that separates concerns delegated to a friended class for purposes of better encapsulation of behaviour. This affords developers an opportunity to better-model objects as behavioural "tell don't ask" units while explicating concerns like presentation through friendship. Class Friendship is a valuable expression for object modeling when used properly. It also has value in white-box testing as a tactical refactoring tool when approaching legacy applications. +Class Friendship allows a class to be better encapsulated by granting per-class access to protected members. Class Friendship is an explicit and concise expression of tight-coupling between collaborators that separates concerns delegated to a friended class for purposes of better encapsulation of behaviour. This affords developers an opportunity to better-model objects as behavioural "tell don't ask" units while explicating concerns like presentation through friendship. Class Friendship is a valuable expression for object modeling when used properly. It also has value in white-box testing as a tactical refactoring tool when approaching legacy applications. 
  
-A common use-case for friendship here is separating presentation concerns from what would otherwise be a "tell don't ask" behavioral unit or write model. This can be applied in a variety of scenarios. Without this feature, developers are left with a trade-off of marking internal properties public to make them available and sacrificing design opportunities torwards the future. Another common occurrence is the addition of getters that simply proxy internal state and grow the public API of a unit by necessity. To be sure, this feature is not merely about pulling off "access to protected class members", but about modeling a semantic relationship between two collaborators. In fact, access to private and protected class properties is already possible through closure scope "juggling" or nasty use of `debug_backtrace`. Both of these are included in references below. While it is technically possible to execute friend-like features in user-land, it would be preferable to concisely represent the relationship with a known and well-documented concept supported by the language.+A common use-case for friendship here is separating presentation concerns from what would otherwise be a "tell don't ask" behavioral unit or write model. This can be applied in a variety of scenarios. Without this feature, developers are left with a trade-off of marking internal members public to make them available and sacrificing design opportunities towards the future. Another common occurrence is the addition of getters that simply proxy internal state and grow the public API of a unit by necessity. To be sure, this feature is not merely about pulling off "access to protected class members", but about modeling a semantic relationship between two collaborators. In fact, access to private and protected class members is already possible through closure scope "juggling" or use of ''[[http://php.net/manual/en/function.debug-backtrace.php|debug_backtrace]]'', which can be difficult to reason-about. Both of these are included in references below. While it is technically possible to execute friend-like features in user-land, it would be preferable to concisely represent the relationship with a known and well-documented concept supported by the language.
  
-The purpose of the feature should not be conflated confused with the goals of something like package-private classes or namespace visibility, in general. I feel those features apply more closely to the types of behaviors user-land sees in Symfony (and other framework) packages that mark properties as ''@internal'' but are forced to make them public to share access between internal data structures. I don't necessarily feel that class friendship is the "Right Answer TM" in this case, but I think that the dance package developers currently have to do to express "don't use this property, we use this internally to help you" is worth improving.+The purpose of the feature should not be conflated or confused with the goals of something like package-private classes or namespace visibility, in general. I feel those features apply more closely to the types of behaviors user-land sees in Symfony (and other framework) packages that mark members as ''@internal'' but are forced to make them public to share access between internal data structures. I don't necessarily feel that class friendship is the "Right Answer TM" in this case, but I think that the dance package developers currently have to do to express "don't use this property, we use this internally to help you" is worth improving.
  
 ===== Proposal ===== ===== Proposal =====
-Support for class friendship is added through a new keyword, ''friend''. It allows per-class access to protected properties as follows:+Support for class friendship is added through a new keyword, ''friend''. It allows per-class access to protected members as follows:
  
 ==== Basic Usage ==== ==== Basic Usage ====
-A subject class may declare another class a friend through the use of a new ''friend'' keyword similar to how Traits are implemented. This enables the named friend class access to protected properties of the subject. There are other properties of Class Friendship, as implemented in C++. These properties are described below. +A subject class may declare another class a friend through the use of a new ''friend'' keyword similar to how Trait syntax works. This enables the named friend class access to protected members of the subject. There are other properties of Class Friendship, as implemented in C++. These properties are described below. 
  
-C++ implements Class Friendship such that friends have access to both private and protected properties. [[https://marc.info/?l=php-internals&m=144972388219786&w=2|In discussing this implementation detail]], there was concern that allowing unfettered access to all properties risked exposing intentionally hidden implementation details local to a given unit whereby a developer absolutely did not want the given property accessible by any means other than the subject class. This RFC suggests that friend classes in PHP only have access to protected and higher properties+C++ implements Class Friendship such that friends have access to both private and protected members. [[https://marc.info/?l=php-internals&m=144972388219786&w=2|In discussing this implementation detail]], there was concern that allowing unfettered access to all members risks exposing intentionally hidden implementation details local to a given unit whereby a developer absolutely did not want the given property accessible by any means other than the subject class. This RFC suggests that friend classes in PHP only have access to protected and higher members
  
 Below, a class ''Person'' declares ''HumanResourceReport'' as a friend for the purposes of separating presentation concerns: Below, a class ''Person'' declares ''HumanResourceReport'' as a friend for the purposes of separating presentation concerns:
Line 60: Line 58:
     public function getFullName()     public function getFullName()
     {     {
-        // PersonFormatter would not have access to protected properties +        // HumanResourceReport would not have access to protected  
-        // of Person if not explicitly listed as a friend.+        // members of Person if not explicitly listed as a friend.
         return $this->person->firstName . ' ' . $this->person->lastName;         return $this->person->firstName . ' ' . $this->person->lastName;
     }     }
Line 114: Line 112:
  
 <code php> <code php>
-class FibonacciTest+class FibonacciTest extends PHPUnit_Framework_TestSuite
 { {
     public function testAssignmentAlgoForStateIsCorrect()     public function testAssignmentAlgoForStateIsCorrect()
Line 134: Line 132:
 </code> </code>
  
-Characterization Tests are a form of white-box test useful for describing the **current actual** behaviour of a unit given knowledge of that unit's internals. They are usually a tactical measure used to verify that modifications made to a system to not have unintended or undesirable changes in **how** the system works. These tests are useful to initiate a refactoring loop. Friend designations are also a good marker for follow-up work to improve units. This may possibly eliminate the need for white-box tests after improving the behavioural API of the system under test.+Characterization Tests are a form of white-box test useful for characterizing the **current actual** behaviour of a unit given knowledge of that unit's internals. They are usually a tactical measure used to verify that modifications made to a system to not have unintended or undesirable changes in **how** the system currently works. These tests are useful to initiate a refactoring loop. Friend designations are also a good marker for follow-up work to improve units. This may possibly eliminate the need for white-box tests after improving the behavioural API of the system under test.
  
 Currently, in many examples, we have to either change visibility of properties that only exist for implementation, provide meaningless getters to these properties (thus polluting the public API of the object and risking abuse by other objects) or navigate the Reflection API or Proxy implementations. We really want to declare a limited set of collaborators privileged access to these properties for a single purpose in the use-case for these types of tests. Currently, in many examples, we have to either change visibility of properties that only exist for implementation, provide meaningless getters to these properties (thus polluting the public API of the object and risking abuse by other objects) or navigate the Reflection API or Proxy implementations. We really want to declare a limited set of collaborators privileged access to these properties for a single purpose in the use-case for these types of tests.
Line 143: Line 141:
 === Friendships are not symmetric === === Friendships are not symmetric ===
 If class ''A'' is a friend of class ''B'', class ''B'' is **NOT** automatically a friend of class ''A''. If class ''A'' is a friend of class ''B'', class ''B'' is **NOT** automatically a friend of class ''A''.
 +
 +<code php>
 +class A
 +{   
 +    protected $property = 'foo';
 +    
 +    public function touch(B $instance)
 +    {
 +        echo $instance->property;
 +    }
 +}
 +
 +class B
 +{
 +    friend A;
 +    
 +    protected $property = 'bar';
 +    
 +    public function touch(A $instance)
 +    {
 +        echo $instance->property;
 +    }
 +}
 +
 +$a = new A();
 +$b = new B();
 +
 +$b->touch($a); // Fatal error: Uncaught Error: Cannot access protected property A::$property
 +$a->touch($b); // string(3) "bar"
 +</code>
  
 === Friendships are not transitive === === Friendships are not transitive ===
-If class ''A'' is a friend of class ''B'', and class ''B'' is a friend of class ''C'', class ''A'' is not automatically a friend of class ''C''.+If class ''A'' is a friend of class ''B'', and class ''B'' is a friend of class ''C'', class ''A'' is not automatically a friend of class ''C'' and vice-versa. 
 + 
 +<code php> 
 +class A 
 +{        
 +    public function touch(C $instance) 
 +    { 
 +        echo $instance->property; 
 +    } 
 +
 + 
 +class B 
 +{    
 +    friend A; 
 +
 + 
 +class C 
 +
 +    friend B; 
 +     
 +    protected $property = 'foo'; 
 +
 + 
 +$a = new A(); 
 +$c = new C(); 
 + 
 +$a->touch($c); // Fatal error: Uncaught Error: Cannot access protected property C::$property 
 +</code>
  
 === Friendships are not inherited === === Friendships are not inherited ===
 A friend of class ''Base'' is not automatically a friend of class ''Derived'' and vice versa; equally if ''Base'' is a friend of another class, ''Derived'' is not automatically a friend and vice versa. A friend of class ''Base'' is not automatically a friend of class ''Derived'' and vice versa; equally if ''Base'' is a friend of another class, ''Derived'' is not automatically a friend and vice versa.
 +
 +<code php>
 +class Base
 +{
 +    friend Friendly;
 +}
 +</code>
 +
 +<code php>
 +class Derived extends Base
 +{
 +    protected $property = 'foo';
 +}
 +</code>
 +
 +<code php>
 +class Friendly
 +{
 +    public function touch(Derived $instance)
 +    {
 +        echo $instance->property;
 +    }
 +}
 +</code>
 +
 +<code php>
 +$derived = new Derived();
 +$friendly = new Friendly();
 +
 +$friendly->touch($derived); // Fatal error: Uncaught Error: Cannot access protected property Derived::$property
 +</code>
  
 === Access due to friendship is inherited === === Access due to friendship is inherited ===
-A friend of ''Derived'' can access the protected members of ''Derived'' that were inherited from ''Base''. Note, however, that a friend of ''Derived'' only has access to members inherited from ''Base'' to which ''Derived'' has access, itself, (e.g. if ''Derived'' inherits from ''Base'', ''Derived'' only has access to the protected (and public) members inherited from ''Base'', not private members, so neither does a friend.)+A friend of ''Derived'' can access the protected members of ''Derived'' that were inherited from ''Base''. Note, however, that a friend of ''Derived'' only has access to members inherited from ''Base'' to which ''Derived'' has access, itself, (e.g. if ''Derived'' inherits from ''Base'', ''Derived'' only has access to the protected members inherited from ''Base'', not private members, so neither does a friend.)
  
 <code php> <code php>
Line 159: Line 245:
     protected $accessible = 'to child classes of Base';     protected $accessible = 'to child classes of Base';
          
-    public function touch()+    protected function touch()
     {     {
         echo $this->secret . PHP_EOL;         echo $this->secret . PHP_EOL;
Line 178: Line 264:
 class Friendly class Friendly
 { {
-    public function touch(Derived $derived)+    public function touch(Derived $instance)
     {     {
-        var_dump($derived->someProperty); // string(%d) "that will be accessed ... functionality"+        var_dump($instance->someProperty); // string(%d) "that will be accessed ... functionality"
                  
-        var_dump($derived->accessible);   // string(%d) "to child classes of Base"+        var_dump($instance->accessible);   // string(%d) "to child classes of Base"
                                           // While Friendly is not a friend of Base, it can still access this                                            // While Friendly is not a friend of Base, it can still access this 
-                                          // property because it is accessible to Derived, for which it *is*  +                                          // property because it is accessible to Derived through protected 
-                                          // a friend.+                                          // property                                      
                                                                                                                                                
-        var_dump($derived->secret);       // Notice: Undefined property: Derived::$secret ...+        var_dump($instance->secret);       // Notice: Undefined property: Derived::$secret ...
     }     }
 } }
Line 196: Line 282:
 $friendly = new Friendly $friendly = new Friendly
  
-$derived->touch(); // Output: "to everyone but Base" 
 $friendly->touch($derived); $friendly->touch($derived);
 </code> </code>
  
 ==== Errors ==== ==== Errors ====
-In all cases above, the error message received is no different than if you attempted to access private or protected properties of a class through traditional means. That is to say, the error message will not hint ''"Whoops! You don't have access to this property because friendship isn't symmetric."''+In all cases above, error messages received are no different than if an object attempted to read or write private or protected members of a class it did not have access to. That is to say, the error message will not hint members / rules of class friendship (e.g. "You don't have access to this property because friendship isn't symmetric.")
  
 ==== Additional Thoughts ==== ==== Additional Thoughts ====
-I have purposely kept this RFC fairly slim for a number of reasons. First and foremost, I want to make it clear that I do **not** see this feature in competition with any other RFC or suggestion for limited-visibility collaborators. Rather, I see it working in concert with something like namespace visibility or package-privacy. I feel that class friendship is about object modeling and making explicit privileged relationships between two or more classes. It is a form of tighter coupling to achieve better encapsulation of behaviour.+I have purposely kept this RFC fairly slim for a number of reasons. First and foremost, I want to make it clear that I do **not** see this feature in competition with any other RFC or suggestion for limited-visibility collaborators. Rather, I see it as a feature used in concert with something like namespace visibility or package-privacy. I feel that class friendship is about object modeling and making explicit privileged relationships between two or more classes. It is a form of tighter coupling to achieve better encapsulation of behaviour.
  
-Secondly, the RFC is purposefully (yet still usefully) slim to "test the waters" on such a feature for inclusion in PHP. This may not be the most popular feature in the world, but I believe that it does scratch a considerable itch for the testing and object modeling communities overlapping the PHP community. This RFC fulfills an 80% use-case (80/20) for the spirit of class friendship and paves the way for further implementation of:+Secondly, the RFC is purposefully (yet usefully) slim to "test the waters" on such a feature for inclusion in PHP. I believe class friendship scratches a considerable itch for the testing and object modeling communities within PHP. This RFC fulfills an 80% use-case (80/20) for the spirit of class friendship and paves the way for further implementation of:
  
-  * Friendship to global function +  * Friendship to global functions 
-  * Friendship to a single class method +  * Friendship to class methods 
-  * Friendship to namespace (caveat)+  * Friendship to namespace(s), possibly
  
-While namespace friendship might seem like a good idea, it is actually more "in the vein" of "package-privacyor "namespace visibilityand begins to leave what many consider the spirit of class friendship. There are uses where this is not the case, which is why I have included it. +While namespace friendship might seem like a good idea, it is probably more in the domain of package-privacy or namespace visibility and begins to leave what many consider the spirit or intent of class friendship. There are uses where this is not the case, which is why I have included it. 
  
 ===== Proposed PHP Version(s) ===== ===== Proposed PHP Version(s) =====
-This proposal targets the next minor version of PHP, which at the time of this writing is PHP 7.1.+I had intended to target the next minor version of PHP with this change. Since 7.3 is closing in on feature freeze and whether or not 7.4 will happen before 8.0 is currently under discussion, this RFC's target depends on outcome of discussion. If there is to be a PHP 7.4, it will go there. Otherwise, it will wait until PHP 8.
  
 ===== RFC Impact ===== ===== RFC Impact =====
 ==== To Opcache ==== ==== To Opcache ====
 This is an open issue pending code review. I am unfamiliar-enough with Opcache implementation to be able to appropriately assess impact. This is an open issue pending code review. I am unfamiliar-enough with Opcache implementation to be able to appropriately assess impact.
- 
-==== To Reflection API ==== 
-New methods are added to ''ReflectionClass'': 
- 
-  - ''public array ReflectionClass::getTraitNames(void)'' - Returns an array of friend names on current class. Returns ''NULL'' in case of error. 
-  - ''public array ReflectionClass::getTraits(void)'' - Returns associative array of friend names in keys and instances of friend's ''ReflectionClass'' in values. Returns ''NULL'' in case of error. 
- 
-===== Open Issues ===== 
-==== Policy ==== 
-  * Implementation requires code review to advise on improvements as well as inform that opcache was appropriately considered 
-  * Verify current PHP functionality around class property visibility is undisturbed 
-  * Add example for access inheritance 
- 
-==== Reflection API ==== 
-  * Implement addition of a ''getFriends'' method to ''ReflectionClass'' 
  
 ===== Future Scope ===== ===== Future Scope =====
-While this RFC specifies friendship between classes, there is opportunity to extend this implementation and syntax:+While this RFC specifies friendship between classes, there is opportunity to extend this implementation and syntax to include progressive enhancements. Snippets below are hypothetical implementations, but have not been discussed and are not tied to this RFC. I on include them as examples of further possible work.
  
-  * Friendship to global functions  +  * **Friendship to global functions**\\ A class may declare a global function as friend. This might be used if someone wanted to expose a procedural interface to an existing object model or begin to refactor a procedural model to become a façade over a new object model. Modeling best-practice aside, it functions much like standard class friendship. 
-  * Friendship to class methods +  * **Friendship to class methods**\\ Friendship to class methods is a narrower expression of standard class friendship. Instead of declaring the entire class a friend, we declare that a method from a friended class can access the subject's protected properties. 
-  * Friendship to namespace(s)+  * **Friendship to namespace(s)**\\ A class might declare an entire namespace as friend. In this way, any class that is part of that namespace would be friended.
  
 ===== Proposed Voting Choices ===== ===== Proposed Voting Choices =====
  
-This RFC proposes two voting choices:+As this RFC adds syntax to the language, a 2/3 majority is required. (see [[voting]])
  
-  Should friend classes be added to PHP (yes / no)? +Voting starts on 2018-07-06 21:00 UTC and closes on 2018-07-13 21:00 UTC. 
-  Should friends have access to private members (yes / no)?+
  
-As this is a language change, a 2/3 majority is required for whether to add Friend Classes to PHP. (see [[voting]])+<doodle title="Support Class Friendship?" auth="mdwheele" voteType="single" closed="true"> 
 +   * Yes 
 +   * No 
 +</doodle>
  
 ===== Patches and Tests ===== ===== Patches and Tests =====
-I have implemented the RFC as described with tests to verify all usage examples above. I will link this as soon as I am able and will open a pull request against ''master'' to monitor TravisCI build status as I make changes.+I have implemented the RFC as described with tests to verify all usage examples above.  
 + 
 +https://github.com/php/php-src/pull/3347
  
 As this is my first contribution to PHP, it is my opinion that my request should be placed under higher scrutiny and I am completely ready and willing to accept all feedback to improve implementation. As this is my first contribution to PHP, it is my opinion that my request should be placed under higher scrutiny and I am completely ready and willing to accept all feedback to improve implementation.
Line 268: Line 342:
 ===== Changelog ===== ===== Changelog =====
   * v0.1 - Created   * v0.1 - Created
 +  * v0.2 - Copy-editing. Clarifications. Add more code examples.
 +  * v0.2.1 - Fix copy+paste error from ReflectionClass documentation regarding trait methods.
 +  * v0.2.2 - Remove voting choice on implementation detail. Remove example of combined future scope syntax. Correct lacking clarity that friendship applies to all protected members, not just properties.
 +  * v1.0.0 - Final draft of RFC before re-opening discussion
rfc/friend-classes.1452188576.txt.gz · Last modified: 2017/09/22 13:28 (external edit)