Skip to content

Instantly share code, notes, and snippets.

@CHH
Last active March 20, 2023 13:48
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CHH/4520200 to your computer and use it in GitHub Desktop.
Save CHH/4520200 to your computer and use it in GitHub Desktop.
PHP Decorators RFC

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.
<?php
class Injector
{
static protected $services;
static function setServices(\Pimple $services)
{
static::$services = $services;
}
/**
* Property decorator maker
*/
static function inject($service)
{
$services = static::$services;
$decorator = function($object, $property) use ($services, $service) {
return $services[$service];
};
return $decorator;
}
}
$services = new \Pimple;
$services['foo'] = function() {
return "bar";
};
Injector::setServices($services);
class Foo {
{% Injector::inject('foo') %}
protected $bar;
function getBar()
{
return $this->bar;
}
}
$f = new Foo;
var_dump($f->getBar());
// Output:
// string("bar")
<?php
function expects(/* $type,... */)
{
$types = func_get_args();
$decorator = function(callable $fn) use ($types) {
$wrapper = function() use ($fn, $types) {
foreach (func_get_args() as $i => $arg) {
if (($type = gettype($arg)) !== $types[$i] and isset($types[$i]) and $types[$i] !== 'mixed') {
throw new \InvalidArgumentException("Expected type of argument $i to be {$types[$i]}, got $type");
}
}
return call_user_func_array($fn, func_get_args());
};
return $wrapper;
};
return $decorator;
}
function returns($type)
{
$decorator = function($fn) use ($type) {
$wrapper = function() use ($fn, $type) {
$returnValue = call_user_func_array($fn, func_get_args());
if (($returnType = gettype($returnValue)) !== $type) {
throw new \UnexpectedValueException("Expected return type $type, got $returnType");
}
return $returnValue;
};
return $wrapper;
};
return $decorator;
}
{% expects('string', 'int') %}
{% returns('string') %}
function foo($bar, $baz)
{
return "Hello World";
}
{% returns('string') %}
function bar()
{
return array('Hello World');
}
// Works
foo('bar', 0);
// Throws exception: "Expected return type string, got array"
bar();
// Throws exception "Expected type of argument 1 to be int, got array"
foo('bar', array());
<?php
function decorator(callable $decorator)
{
// This function replaces 'trace'
return function($wrapped) use ($decorator) {
// This function replaces the function which 'trace' is used on.
return function() use ($wrapped, $decorator) {
$arguments = func_get_args();
return call_user_func($decorator, $wrapped, $arguments);
};
};
}
{% 'decorator' %}
function trace(callable $fn, array $arguments)
{
printf("Calling %s with arguments %s\n", print_r($fn, true), print_r($arguments, true));
return call_user_func_array($fn, $arguments);
}
{% 'trace' %}
function foo()
{
}
<?php
require "vendor/autoload.php";
class SilexApplicationDecorator
{
protected $app;
function __construct($app)
{
$this->app = $app;
}
function get($route)
{
$app = $this->app;
$decorator = function(callable $fn) use ($route, $app) {
$app->get($route, $fn);
return $fn;
};
return $decorator;
}
}
$app = new SilexApplicationDecorator(new \Silex\Application);
{% $app->get('/') %}
function index()
{
return "this is the index!";
}
@sagittaracc
Copy link

You can also see the examples with all current possible features - https://github.com/sagittaracc/php-python-decorator/tree/main/tests/examples

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment