Recently Nikita Popov posted a pull request for PHP named “Add support for “decorator” pattern.” I started to comment on the PR but soon realized I was writing a full-on blog post, so here ya go.
I actually have been thinking about the need for delegation for months now, so I have had plenty of time to develop my thoughts about how delegation could be implemented in PHP from a userland perspective.
Note that I am referring to this feature as “Delegation” vs. “Decoration” because I believe it is a better description of the feature, and because several other people said the same on the PR.
My concept of delegation[1] is informed both by my Go experience, and my experience in PHP specifically with respect to using PHP traits.
Delegation in Go (aka Type Embedding)
First about delegation/type delegation in Go (you can play with this code in the Go playground to see how it works for yourself):
Basically Go simply allows you to specify the struct
[3] name in the declaration of another struct and that embeds an instance of the struct to delegate to into the delegating struct. This example embeds the struct Bar
in the struct Foo
and automatically instantiates an instance of Bar.[4]
The automatic instantiation is possible because Go does not have nor require constructors[2].
Delegating to multiple classes with conflicting methods names
One of the big questions that always comes up is “How can we deal with naming conflicts?” In Go, they just punt. Go does not allow it, as you can see here (also in playground):
Of note, with Go we can use “object initializers” as you see on lines 16-19
to package struct instantiation and initialization into one expression and retain type safety. The example above uses Go’s object initializers to instantiate Greeting
in one expression.
“My kingdom for object initializers in PHP! But I digress…”
me
Resolving method naming conflicts in Go
Go allows you to call same-named methods by naming their classes explicitly:
g := Greeting{}
g.English.SayHello()
g.French.SayHello()
You can also do this inside the struct itself to solve the method naming conflicts. Create a same-named method in the embedding struct and then by explicit disambiguating the embedded structs in code, for example (which is also in the playground:
Using Go’s approach in PHP
If we were to model object delegation/type embedding in PHP it might look like this:
<?php
class English {
function say_hello(): string {
printf( '%s\n', "hello" );
}
}
class French {
function say_hello(): string {
printf( '%s\n', "bonjour" );
}
}
class Greeting {
public $language string;
public English; //allows Greeting to delegate to an instance of English
public French; //allows Greeting to delegate to an instance of French
function say_hello(): string {
switch ( $this->language ) {
case "english":
$g->English->say_hello();
break;
case "french":
$g->French->say_hello();
break;
}
echo "unknown language: {$this->language}";
}
}
$greet = new Greeting();
$greet->language = 'english';
$greet->English = new English();
$greet->French = new French();
$greet->say_hello() ); // prints "hello"
$greet->language = "french";
$greet->say_hello(); // prints "bonjour"
$greet->language = "esperanto"
$greet->say_hello(); // prints "unknown language: esperanto"
I think this could work well, but something tells me the developers with a vote on PHP internals won’t go for public <embeddedClassName>
inside an embedding class as a syntax to support automatic delegation of methods to embedded classes in PHP.
No worries though, I have another, better proposal that follows and I discuss delegations similarity to PHP’s Traits.
Traits in PHP vs. Delegation?
Looking at the proposed PR I am struck by how similar the PHP trait
is to the concept of delegating method calls to an instance of one class to the methods of an instance of another class.
I am not speaking of internal implementation of traits — I do not actually know how they are implemented — just the concept. After all, a trait is a collection of methods and properties with a symbol name that can be use
d by a class and have its properties treated as part of that class. That is eerily similar to the proposed delegation solution in the PR.
Use the syntax for traits, with modifications
Taking what we know from PHP traits, I propose that we extend the use
statement with a class
modifier, like so:
<?php
class Greeting {
use class English;
use class French;
function __construct() {
$this->English = new English();
$this->French = new French();
}
}
The above example would tell PHP that any methods called on Greeting
that do not exist in Greeting
but do exist in either English
or French
will be delegated to the applicable instance automatically, assuming a valid instance has been instantiated to embed in the instance of Greeting
.
One thing this does is provide a default name for delegated classes so we do not have to declare them separately as Nikita’s proposal requires. You will see how this benefits us below.
Automatic delegated instance instantiation
Further, if the constructor for the delegated classes have no parameters PHP could automatically instantiate any delegated classes. This is the first benefit of the default property names.
So automated delegated instance creation makes the above example as simple as:
<?php
class Greeting {
use class English;
use class French;
}
When the constructor for any delegated class does have parameters then the developer would need to assign them a valid properly-typed instance before first use, such as in __construct()
, in __get()
methods, or in get_<property>
methods.
A nice optimization would be to assign a closure to the delegate property that would be executed on first access[5] thus instantiating and capturing the instance of the delegated-to class.
Disambiguating conflicting methods names
The nice part of using the Trait syntax is that traits already handle disambiguation. This would give us the following syntax:
<?php
class Greeting {
public $language string;
use class English, French {
English::say_hello as say_english_hello;
French::say_hello as say_french_hello;
}
function say_hello() {
switch ( $this->Language ) {
case "english":
$this->say_english_hello()
break;
case "french":
$this->say_hello_in_french()
break;
default:
echo "unknown language: {$this->language}";
}
}
}
And I would assume that the code to parse this type of syntax has already been implemented?
Renaming delegated properties
Another concern is that using the class names as properties won’t work well if the classes are in different namespaces. Or maybe developers just don’t want to use class naming syntax for properties. That’s an easy fix by using as
:
<?php
<?php
class Greeting {
use class English as $english;
use class French as $french;
function __construct() {
$this->english = new English();
$this->french = new French();
}
}
Including or excluding methods
Yet another concern might be that you want to delegate to some of the methods but not all. If we add include
and exclude
options, then a developer could use one or the other:
<?php
class Greeting {
use class English {
include cater_to_customers;
};
use class French {
exclude close_on_sundays;
}
}
class English {
function cater_to_customers() {}
function shopping_in_sweatpants() {}
function order_french_fries() {}
}
class French {
function close_on_sundays() {}
function order_nutella() {}
function order_baguette() {}
}
Including or excluding methods
Some developers might prefer to name each method individually instead of using would want to delegate only specific methods from a large class, and would prefer to detail them individually instead of using include
and/or exclude
.
For this we could again extend the use
statement with a method
modifier.
<?php
class Greeting {
use method English::cater_to_customers;
use method French::order_nutella;
use method French::order_baguette;
}
Summary
In summary I am proposing the PHP leverage the use
statement like the PHP trait
does, but with a class
modifier to allow for automatic delegation of method calls to contained instances.
Also I am proposing we leverage the disambiguation syntax that is already available for traits, and extend it in a few ways that traits do not address such as:
- Use
as
to allow for renaming from the property names for delegate classes, - Add
include
orexclude
to allow including or excluding specific methods, and - Add
use method
to allow for including specific methods.
Footnotes
[1] This PR feels more like delegation — vs. decoration — as several others have mentioned. [2] In Go structs are its closest equivalent to classes.
[3] Many Go developers creates functions to construct structs that need initialization, For example a developer might create a function named NewFoo() that requires a Bar
parameter and returns an initialized Foo
.
[4] Since Go supports pointers you can also embed a pointer to another struct and that won’t automatically initialize the pointer, but is out of scope of this comment to explain further.
[5] This is something we cannot currently do in PHP, but would be a useful performance-enhancing feature given typed properties in PHP. For example, if a property is typed to contain an instance of a class it could be assigned a closure that returns an instance of the required class, and PHP would execute the closure first access of the property to create the required instance. But this is probably better in another RFC.