PHP Decorators (alternative proposal to Annotations)
Inspiration: Python Decorators
Todo
- Further refine how class decorators should work. Should they work on the instance level? Or should they just receive the class name, and can only be used for providing metadata? Instance level is probably the only one that makes sense in PHP. Problem is though, that then the decorator is not called when used in the declaration.
- Error conditions
- Specify ordering of decorators like in the PEP
What is a Decorator
A decorator is a callable which receives the decorated value as argument, and is able to produce a replacement for the decorated value which is then bound to the name of the definition.
Decorator Syntax
Decorators are expressions above the declaration of a function, method, class or class property. Decorators are expressed
within {% %}
and can only be used when followed by one of the beforementioned declarations. Everything inside {% %}
are PHP expressions.
The following expressions are valid within decorators:
- Constants
- Function calls
- Strings
- Arrays
<?php
function baz()
{
return "foo";
}
{% 'bar' %}
{% baz() %}
function foo() {}
Decorator expressions are evaluated at define time.
When the expression within {% %}
is a callable, then it gets called. This callable is then able to produce the decorated value which is then bound to the name of the definition. When the callable is a function name then the function name is resolved by namespace rules for functions.
This means that if a function exists which is named bar
in the current namespace, then this function gets called
with the "foo"
as argument, and is able to return a replacement for foo
, or "foo"
as is.
<?php
// No-op decorator
function bar(callable $fn) {
return $fn;
}
Advantages over Annotations
- Does not introduce a "sub language" with its own rules. All decorator expressions are plain PHP expressions, like String Literals, Array Literals or Function calls.
- Works also when only working with functions.
- Comes with ready to use runtime features, does not require any frameworks or special proxy objects which use the Metadata to be useful.
- Decorators are just callables, so it's easy for PHP extensions or PHP core to provide decorators optimized for efficency.
- Is able to directly provide AOP and some Metaprogramming capabilities:
- Implement function parameter checks and return type checks
- Run code around a function invocation
- Easy to grasp. All decorators are just callables.
Differences over Python
- Different syntax. The
@
character would be ambigous for the PHP parser, i.e. can't be distinguised from any error suppressed expression. - In PHP you've to use the callback notation, this means that you've to usually quote decorator names.
Function Decorators
A simple Proof of Concept implementation using PHP (without syntax) is available here: http://3v4l.org/4fMIL
A function decorator receives the original function as first argument and must return a wrapper callable which is used instead of the wrapped function.
If multiple decorators are listed for a function, then they get composed in the given order, from the outside in.
Example:
<?php
{% 'catch_all' %}
{% 'foo' %}
function do_something() {
throw new Exception("Error!");
}
// Equal to defining "do_something" as:
// function do_something() {
// $fn = catch_all(foo(function() {
// throw new Exception("Error!");
// };
// return $fn();
// }));
do_something();
Because the expression within the {% %}
is regular PHP code, you
can pass parameters to the decorator by using a function call to produce a parameterized decorator callable.
<?php
function cache($key)
{
global $cache;
// Return a parameterized decorator function
$decorator = function(callable $fn) use ($cache, $key) {
$wrapper = function() use ($fn, $cache, $key) {
if ($cache->contains($key)) {
return $cache->fetch($key);
} else {
$data = call_user_func_array($fn, func_get_args());
$cache->save($key, $data);
return $data;
}
};
return $wrapper;
};
return $decorator;
}
{% cache("foo") %}
function do_something_expensive()
{
sleep(4);
return "foo";
}
# First call will take 4 seconds, second call will return right away
echo time();
echo do_something_expensive();
echo time();
echo do_something_expensive();
echo time();
Function decorators don't necessarily need to return a new callable. They can as well return the callable unmodified:
<?php
function at_exit(callable $fn) {
register_shutdown_function($fn);
return $fn;
}
{% 'at_exit' %}
function foo() {
echo "Shutdown!";
}
Function decorators can also be used to decorate methods. In this case
the receiver ($this
) is made implicitly available as the $this
variable inside the decorator function.
Example:
<?php
function catch_all(callable $fn) {
$wrapper = function() use ($fn) {
if (isset($this)) {
echo "I'm a method decorator!";
} else {
echo "I'm a function decorator!";
}
return call_user_func_array($fn, func_get_args());
};
return $wrapper;
}
class Foo {
{% 'catch_all' %}
function bar()
{
}
}
{% 'catch_all' %}
function bar()
{}
$f = new Foo;
$f->bar()
// Output:
// I'm a method decorator!
bar();
// Output:
// I'm a function decorator!
Decorating classes
This is not yet possible because in PHP classes are not values, and you can't do anything interesting with them. The use case for class decorators in Python was to make methods static, add methods etc., which you can do with Metaclasses (which PHP lacks).
This RFC suggests to leave this feature out, and implement it when there is anything interesting to do with class values.
Syntax Considerations
The syntax is open for discussion. The only requirement for a syntax is, that it's unambigous for the Parser and easy to type.
Here are some possible candidates:
{% decorator %}
%decorator
%{decorator}
<decorator>
[decorator]
— used by C#, but ambigous with short syntax for array literals.|decorator
@decorator
and looking if there's no semicolon at the end of the line (not preferred)
The main goal for this RFC is, that the expression within the decoration are pure PHP expressions like Strings, Array or Function Calls. Everything else around is open for discussion.
Errors
- If a function decorator doesn't return a callable, then an error of level
E_NOTICE
should be raised.
@ is the convention used by TypeScript, it's more elegant to prepend a symbol than have to insert tags I feel.