Built-in decorator attribute?

php.internals

David Gebler

5 years ago
With the introduction of attributes in PHP 8, this new behaviour is still quite sparsely documented. Some of the articles I've seen out there, though, liken PHP's attributes to similar constructs in other languages including decorators in Python. Attributes are not the same thing as (Python's concept of) decorators and they shouldn't be confused; a decorator is a function which wraps another function and is automatically called in place of the wrapped function. This isn't currently possible in PHP. Using frameworks like Symfony, we can start to build things like this: class UserProfileController { #[LoginRequired] public function editProfile(...) { } } but the logic of enforcing our "require the user to be logged in" decorator relies on the surrounding framework controlling the flow of execution, reading the attribute and deciding whether to call the decorated method editProfile() at all. What we *can't* do is something like this: class Foo { private function timer(callable $wrapped) { $start = microtime(true); $wrapped(); $end = microtime(true); $total = $end - $start; echo "Executed function in $total second(s)\n"; } #[timer] public function bar($a, $b) { ... } #[timer] public function baz($a, $b) { ... } } What I'm wondering is whether there's a desire / interest for a built-in attribute to provide this kind of behaviour modification. I'm thinking something like class Foo { private function timer(callable $wrapped) { ... } #[Decorator([self::class, 'timer'])] public function bar() { echo "Bar"; } } Where this would result in any call to $foo->bar() being equivalent to as if the above were defined as: class Foo { private function timer(callable $wrapped) { ... } public function __bar() { echo "Bar"; } public function bar() { return $this->timer([$this, '__bar']); } } I'm not saying I have the skills to implement this attribute (though I'd happily try), I'm not even in a position to propose a draft RFC at this stage, just throwing the idea out there to get a feel for what people think of the concept? Regards, Dave

Peter Stalman

5 years ago
Hi David, This sounds a lot like Asect Oriented Programming. Have you looked into that? PHP framework: https://github.com/goaop/framework PECL extension: https://aop-php.github.io/ Thanks, Peter On Sat., Mar. 13, 2021, 08:51 David Gebler, <davidgebler@gmail.com> wrote:

David Gebler

5 years ago
Decorators are a way of bringing aspect oriented programming into PHP core, yes, among other uses. Go AOP is a fairly bulky framework which could be easily replaced by a Decorator attribute for the purposes of cross-cutting changes to function behaviour. Regards, David On Sat, Mar 13, 2021 at 10:51 PM Peter Stalman <sarkedev@gmail.com> wrote:

Peter Stalman

5 years ago
Hi again David, I've been thinking about this some more, it could be quite useful. I'd like to propose a bit more of a Attributes-y syntax: ``` <?php #[Decorator(Decorator::TARGET_METHOD)] // or #[Attribute(Attribute::DECORATOR_METHOD)] class Timer { private $start; public function __construct( private string $name = 'Tim', ) { $this->start = microtime(true); // these could be made available $foo = $this->getObject(); $args = $this->getArguments(); // [$a, $b] $ref = $this->getReflection(); } public function __destruct() { $end = microtime(true); $total = $end - $start; echo "Executed {$this->name} in $total second(s)\n"; } } class Foo { #[Timer] public function bar($a, $b) { ... } #[Timer('Bob')] public function baz($a, $b) { ... } } ``` Thanks, Peter On Sat., Mar. 13, 2021, 15:04 David Gebler, <davidgebler@gmail.com> wrote:

Benjamin Eberlei

5 years ago
On Sat, Mar 13, 2021 at 5:51 PM David Gebler <davidgebler@gmail.com> wrote:
> With the introduction of attributes in PHP 8, this new behaviour is still > quite sparsely documented. Some of the articles I've seen out there, > though, liken PHP's attributes to similar constructs in other languages > including decorators in Python. > > Attributes are not the same thing as (Python's concept of) decorators and > they shouldn't be confused; a decorator is a function which wraps another > function and is automatically called in place of the wrapped function. > > This isn't currently possible in PHP. Using frameworks like Symfony, we can > start to build things like this: > > class UserProfileController { > #[LoginRequired] > public function editProfile(...) { } > } > > but the logic of enforcing our "require the user to be logged in" decorator > relies on the surrounding framework controlling the flow of execution, > reading the attribute and deciding whether to call the decorated method > editProfile() at all. > > What we *can't* do is something like this: > > class Foo { > private function timer(callable $wrapped) > { > $start = microtime(true); > $wrapped(); > $end = microtime(true); > $total = $end - $start; > echo "Executed function in $total second(s)\n"; > } > > #[timer] > public function bar($a, $b) { ... } > > #[timer] > public function baz($a, $b) { ... } > } > > What I'm wondering is whether there's a desire / interest for a built-in > attribute to provide this kind of behaviour modification. > > I'm thinking something like > > class Foo { > private function timer(callable $wrapped) { ... } > > #[Decorator([self::class, 'timer'])] > public function bar() { > echo "Bar"; > } > } > > Where this would result in any call to $foo->bar() being equivalent to as > if the above were defined as: > > class Foo { > private function timer(callable $wrapped) { ... } > > public function __bar() { > echo "Bar"; > } > > public function bar() { > return $this->timer([$this, '__bar']); > } > } > > I'm not saying I have the skills to implement this attribute (though I'd > happily try), I'm not even in a position to propose a draft RFC at this > stage, just throwing the idea out there to get a feel for what people think > of the concept? >
In my opinion it would be a fantastic addition to Core to be used by application frameworks with "hook philosophies" that hack this functionality on top of PHP with code generation or event dispatchers at the moment (Magento 2, Drupal, Neos, Wordpress and so on) makes this a potential future with wide adoption. If you'd get 2/3 votes for it is another topic. However, as functionality it could be provided as an extension first for a proof of concept. The ingredients are all there, it doesn't need to be in core: 1. Register an internal attribute, see my #[Deprecated] PR as an example https://github.com/php/php-src/pull/6521 2. Register a zend_observer as a first step, that detects functions/methods with a new #[Intercept] or whatever attribute you want and registers observer callbacks. See ext/zend_test https://github.com/php/php-src/blob/master/ext/zend_test/test.c or tideways/php-xhprof-extension: https://github.com/tideways/php-xhprof-extension/blob/master/tideways_xhprof.c#L30-L57 3. Use C API zend_call_function in the observer to call your interceptor. Cobbling this together as a Frankestein monster from existing code should be achievable even if you haven't worked with PHP core yet imho. This could replace the php-aop extension that isn't maintained anymore. Using a zend_obserer would only allow you to register a before and after hook, the alternative with a "$wrapped()" would be significantly more complex with the existing building blocks. Hooking into zend_exeute_ex would allow you to implement around handling, but at this point is not recommended anymore, because its incompatible with JIT and might be removed in the future.

David Gebler

5 years ago
Hi Ben, I have been looking at your #[Deprecated] PR to get an idea of where to start with implementing this, I think Peter's suggestion of how it might look syntactically is also interesting - it's exactly that sort of question of how would you imagine devs implementing and using decorators in PHP that I wanted to get a feel for before I started trying to build it or draft an RFC. Your other tips regarding zend_observer and zend_call_function are much appreciated, saves me a lot of digging through the source figuring out the appropriate APIs for myself. Before and after hooks are a similar but slightly different implementation, semantically, to what I was originally suggesting but may be the preferred option. I think to be useful, you probably still need some way to choose not to call the original, decorated function - but I guess you could just throw an exception in your before hook and that might be sufficient control in practice. Regards, David On Sun, Mar 14, 2021 at 11:52 AM Benjamin Eberlei <kontakt@beberlei.de> wrote:

Larry Garfield

5 years ago
On Sun, Mar 14, 2021, at 7:33 AM, David Gebler wrote:
> Hi Ben, > I have been looking at your #[Deprecated] PR to get an idea of where to > start with implementing this, I think Peter's suggestion of how it might > look syntactically is also interesting - it's exactly that sort of question > of how would you imagine devs implementing and using decorators in PHP that > I wanted to get a feel for before I started trying to build it or draft an > RFC. Your other tips regarding zend_observer and zend_call_function are > much appreciated, saves me a lot of digging through the source figuring out > the appropriate APIs for myself. > > Before and after hooks are a similar but slightly different implementation, > semantically, to what I was originally suggesting but may be the preferred > option. I think to be useful, you probably still need some way to choose > not to call the original, decorated function - but I guess you could just > throw an exception in your before hook and that might be sufficient control > in practice. > > Regards, > David > > On Sun, Mar 14, 2021 at 11:52 AM Benjamin Eberlei <kontakt@beberlei.de> > wrote: > > > > > > > On Sat, Mar 13, 2021 at 5:51 PM David Gebler <davidgebler@gmail.com> > > wrote: > > > >> With the introduction of attributes in PHP 8, this new behaviour is still > >> quite sparsely documented. Some of the articles I've seen out there, > >> though, liken PHP's attributes to similar constructs in other languages > >> including decorators in Python. > >> > >> Attributes are not the same thing as (Python's concept of) decorators and > >> they shouldn't be confused; a decorator is a function which wraps another > >> function and is automatically called in place of the wrapped function. > >> > >> This isn't currently possible in PHP. Using frameworks like Symfony, we > >> can > >> start to build things like this: > >> > >> class UserProfileController { > >> #[LoginRequired] > >> public function editProfile(...) { } > >> } > >> > >> but the logic of enforcing our "require the user to be logged in" > >> decorator > >> relies on the surrounding framework controlling the flow of execution, > >> reading the attribute and deciding whether to call the decorated method > >> editProfile() at all. > >> > >> What we *can't* do is something like this: > >> > >> class Foo { > >> private function timer(callable $wrapped) > >> { > >> $start = microtime(true); > >> $wrapped(); > >> $end = microtime(true); > >> $total = $end - $start; > >> echo "Executed function in $total second(s)\n"; > >> } > >> > >> #[timer] > >> public function bar($a, $b) { ... } > >> > >> #[timer] > >> public function baz($a, $b) { ... } > >> } > >> > >> What I'm wondering is whether there's a desire / interest for a built-in > >> attribute to provide this kind of behaviour modification. > >> > >> I'm thinking something like > >> > >> class Foo { > >> private function timer(callable $wrapped) { ... } > >> > >> #[Decorator([self::class, 'timer'])] > >> public function bar() { > >> echo "Bar"; > >> } > >> } > >> > >> Where this would result in any call to $foo->bar() being equivalent to as > >> if the above were defined as: > >> > >> class Foo { > >> private function timer(callable $wrapped) { ... } > >> > >> public function __bar() { > >> echo "Bar"; > >> } > >> > >> public function bar() { > >> return $this->timer([$this, '__bar']); > >> } > >> } > >> > >> I'm not saying I have the skills to implement this attribute (though I'd > >> happily try), I'm not even in a position to propose a draft RFC at this > >> stage, just throwing the idea out there to get a feel for what people > >> think > >> of the concept? > >> > > > > In my opinion it would be a fantastic addition to Core to be used by > > application frameworks with "hook philosophies" that hack this > > functionality on top of PHP with code generation or event dispatchers at > > the moment (Magento 2, Drupal, Neos, Wordpress and so on) makes this a > > potential future with wide adoption. If you'd get 2/3 votes for it is > > another topic. > > > > However, as functionality it could be provided as an extension first for a > > proof of concept. The ingredients are all there, it doesn't need to be in > > core: > > > > 1. Register an internal attribute, see my #[Deprecated] PR as an example > > https://github.com/php/php-src/pull/6521 > > > > 2. Register a zend_observer as a first step, that detects > > functions/methods with a new #[Intercept] or whatever attribute you want > > and registers observer callbacks. See ext/zend_test > > https://github.com/php/php-src/blob/master/ext/zend_test/test.c or > > tideways/php-xhprof-extension: > > https://github.com/tideways/php-xhprof-extension/blob/master/tideways_xhprof.c#L30-L57 > > > > 3. Use C API zend_call_function in the observer to call your interceptor. > > > > Cobbling this together as a Frankestein monster from existing code should > > be achievable even if you haven't worked with PHP core yet imho. This could > > replace the php-aop extension that isn't maintained anymore. > > > > Using a zend_obserer would only allow you to register a before and after > > hook, the alternative with a "$wrapped()" would be significantly more > > complex with the existing building blocks. > > > > Hooking into zend_exeute_ex would allow you to implement around handling, > > but at this point is not recommended anymore, because its incompatible with > > JIT and might be removed in the future. > > > > > >> > >> Regards, > >> Dave
I've been toying (mostly mentally) with the idea of using attributes for guard rules on object properties and function parameters. Benjamin gave me basically the same answer. :-) It sounds like both that and AOP-attributes reduce to the same core problem: Allow an attribute to specify a caller that intercepts an action being taken (call method, write to property, hell maybe even read property although that seems less useful). So, what's the minimal surface area needed in C (either core or an extension) to allow the rest of that to be done in user space? --Larry Garfield

David Gebler

5 years ago
Python's implementation of decorators is great for these kinds of reasons. You get direct access to the function being called, the arguments which were passed to it and any return value after (and if) you execute it. Python's magic here, of course, is that functions and class methods are first class objects we can reassign at will, so decorators are just syntactical convenience. An attributes-based PHP implementation will I think necessarily look and behave a bit differently, but we should be able to achieve much the same purposes with before and after hooks. I'm willing to forgo being able to manipulate the arguments passed to the decorated function before it's called, but any before hook would at least need to be able to access them to be useful. Similarly I think you need to be able to get the return value of the decorated function in an after hook. The Timer is a trivial example which doesn't need these, but most useful hooks would. A simple real-world use case example: I have a library which abstracts a database layer and the Database class is full of public methods which take a table name parameter. In every single method which does this, right at the top I have a condition if ($this->isValidTableName($name)) I'd love to be able to replace that with a #[ValidTableName] decorator, which obviously would need to access the parameter passed to the attributed function. If I could throw an exception from that decorator, to effectively prevent the decorated method being called if the table name is not valid (and this should require no additional implementation in C beyond what Ben's suggested), overall I think we've got a good basis of a useful feature. Equally though, I think this is potentially a really, really useful and powerful enough feature that it deserves more attention and forethought before I try to cobble something together as a Frankenstein monster. I'm starting to familiarize myself with the attributes implementation in the meantime to try and build up an idea of how I might build this. Regards, David On Sun, Mar 14, 2021 at 4:31 PM Larry Garfield <larry@garfieldtech.com> wrote:

Peter Stalman

5 years ago
On Sun, Mar 14, 2021 at 5:34 AM David Gebler <davidgebler@gmail.com> wrote:
> Before and after hooks are a similar but slightly different implementation, > semantically, to what I was originally suggesting but may be the preferred > option. I think to be useful, you probably still need some way to choose > not to call the original, decorated function - but I guess you could just > throw an exception in your before hook and that might be sufficient control > in practice. >
I could be as simple as `return false;` as well, since https://wiki.php.net/rfc/make_ctor_ret_void was declined. As you mentioned one issue is getting the return value of the decorated function, which would be easier for a wrapper type of implementation. Before/after works well for timing and profiling, but wrapping works better for things like caching. Would it make sense to have both options? Some things to consider: 1) Wrapper and/or before/after pattern? As discussed. 2) Can Decorators take arguments? I would think this should be included, i.e. `#[Timer(1)]`. How would it be conveniently available and separate from the wrapped function arguments? 3) Can Decorators be used on Decorator functions? ``` #[Decorator] class MyDecorator { #[MyDecorator] public function wrapper(callable $callable, $args = []) { /* */ } } ``` 4) Can Decorators be used on Generator functions? ``` #[Decorator] class MyDecorator { public function wrapper(callable $callable, $args = []) { yield 1; yield 2; return call_user_func_array($callable, $args); } } ``` 5) Can Decorators be used recursively? ``` #[Decorator] class MyDecorator { public function wrapper(callable $callable, $args = []) { if (true or false) return $this->wrapper($callable, $args); return call_user_func_array($callable, $args); } } ``` Thanks, Peter

Peter Stalman

5 years ago
On Sun, Mar 14, 2021 at 6:34 PM Peter Stalman <sarkedev@gmail.com> wrote:
> Would it make sense to have both options? >
6) Multiple decorators on the same function? One problem with the wrapper pattern is that if you have multiple decorators then wouldn't that end up calling the function multiple times? Not so with the before/after. It really depends on the intended behavior. How does Python and others handle this? Thanks, Peter

David Gebler

5 years ago
> Would it make sense to have both options?
If you have the wrapper pattern, you already have both options. This would be my preferred way of doing it but harder to implement, particularly in any way which significantly mimics Python's way of doing it.
> One problem with the wrapper pattern is that if you have multiple
decorators then wouldn't that end up calling the function multiple times? Not so with the before/after. It really depends on the intended behavior. How does Python and others handle this? Python the @decorator syntax is just sugar for re-assigning a method so they are easily stacked. Say we have a trivial decorator: def timer(func): def wrap(): start = time.perf_counter() func(*args) end = time.perf_counter() print("Did thing in this many seconds...") return wrap Then @timer def foo(a, b, c): # do some stuff is equivalent to foo = timer(foo) and @timer @somethingElse def foo(a, b, c): # do some stuff is equivalent to foo = timer(somethingElse(foo)) Regards, David On Mon, Mar 15, 2021 at 1:46 AM Peter Stalman <sarkedev@gmail.com> wrote:

Marc

5 years ago
On 14.03.21 12:52, Benjamin Eberlei wrote:
> On Sat, Mar 13, 2021 at 5:51 PM David Gebler <davidgebler@gmail.com> wrote: > >> With the introduction of attributes in PHP 8, this new behaviour is still >> quite sparsely documented. Some of the articles I've seen out there, >> though, liken PHP's attributes to similar constructs in other languages >> including decorators in Python. >> >> Attributes are not the same thing as (Python's concept of) decorators and >> they shouldn't be confused; a decorator is a function which wraps another >> function and is automatically called in place of the wrapped function. >> >> This isn't currently possible in PHP. Using frameworks like Symfony, we can >> start to build things like this: >> >> class UserProfileController { >> #[LoginRequired] >> public function editProfile(...) { } >> } >> >> but the logic of enforcing our "require the user to be logged in" decorator >> relies on the surrounding framework controlling the flow of execution, >> reading the attribute and deciding whether to call the decorated method >> editProfile() at all. >> >> What we *can't* do is something like this: >> >> class Foo { >> private function timer(callable $wrapped) >> { >> $start = microtime(true); >> $wrapped(); >> $end = microtime(true); >> $total = $end - $start; >> echo "Executed function in $total second(s)\n"; >> } >> >> #[timer] >> public function bar($a, $b) { ... } >> >> #[timer] >> public function baz($a, $b) { ... } >> } >> >> What I'm wondering is whether there's a desire / interest for a built-in >> attribute to provide this kind of behaviour modification. >> >> I'm thinking something like >> >> class Foo { >> private function timer(callable $wrapped) { ... } >> >> #[Decorator([self::class, 'timer'])] >> public function bar() { >> echo "Bar"; >> } >> } >> >> Where this would result in any call to $foo->bar() being equivalent to as >> if the above were defined as: >> >> class Foo { >> private function timer(callable $wrapped) { ... } >> >> public function __bar() { >> echo "Bar"; >> } >> >> public function bar() { >> return $this->timer([$this, '__bar']); >> } >> } >> >> I'm not saying I have the skills to implement this attribute (though I'd >> happily try), I'm not even in a position to propose a draft RFC at this >> stage, just throwing the idea out there to get a feel for what people think >> of the concept? >> > In my opinion it would be a fantastic addition to Core to be used by > application frameworks with "hook philosophies" that hack this > functionality on top of PHP with code generation or event dispatchers at > the moment (Magento 2, Drupal, Neos, Wordpress and so on) makes this a > potential future with wide adoption. If you'd get 2/3 votes for it is > another topic.
I can fully agree here - this would be very useful and I also thought in trying out something similar to wrap expensive functions by a cache but I don't have enough C skills and even less time for that. Thanks, Marc

Ryan

5 years ago
I was able to get a proof of concept of the following at https://github.com/iggyvolz/hooks: On 3/14/2021 7:52 AM, Benjamin Eberlei wrote:
> However, as functionality it could be provided as an extension first for a > proof of concept. The ingredients are all there, it doesn't need to be in > core: > > 1. Register an internal attribute, see my #[Deprecated] PR as an example > https://github.com/php/php-src/pull/6521 > > 2. Register a zend_observer as a first step, that detects functions/methods > with a new #[Intercept] or whatever attribute you want and registers > observer callbacks. See ext/zend_test > https://github.com/php/php-src/blob/master/ext/zend_test/test.c or > tideways/php-xhprof-extension: > https://github.com/tideways/php-xhprof-extension/blob/master/tideways_xhprof.c#L30-L57 > > 3. Use C API zend_call_function in the observer to call your interceptor. > > Cobbling this together as a Frankestein monster from existing code should > be achievable even if you haven't worked with PHP core yet imho. This could > replace the php-aop extension that isn't maintained anymore. > > Using a zend_obserer would only allow you to register a before and after > hook, the alternative with a "$wrapped()" would be significantly more > complex with the existing building blocks. > > Hooking into zend_exeute_ex would allow you to implement around handling, > but at this point is not recommended anymore, because its incompatible with > JIT and might be removed in the future.
I would certainly describe it as a Frankenstein monster (due to my lack of core experience), but it proves it is possible in an extension. I wasn't able to figure out overriding the method - it looks like that might be possible by messing with the zend_function object but that would certainly run into issues with jit.

Dik Takken

5 years ago
On 16-03-2021 02:37, Katie Volz wrote:
> I was able to get a proof of concept of the following at > https://github.com/iggyvolz/hooks:
That looks like a nice starting point to explore how this could work. I use decorators a lot in Python code and would love to see something similar in PHP. One thing I am wondering though: How does this affect the debugging experience of PHP code? Suppose that I am step debugging and I step into a function call. When that function happens to be decorated, I can imagine two possible scenarios: 1. The debugger enters the decorator function, not the called function. 2. The debugger jumps to the entry point of the called function first, then to the decorator function. When exiting the decorator function, it jumps back to the entry point of the called function again. I guess that using an observer results in scenario 1. This would yield a confusing debugging experience. At the call site, you cannot see that the function is decorated. You expect to enter the called function but the debugger will jump some place else. Even worse when multiple decorators are stacked. Note that Python decorators also make the debugger jump straight into the decorator. While this is not nice, it is expected because a Python decorator actually replaces the original function at run time. A PHP implementation would probably not do that, so maybe it could actually work like in scenario 2? Regards, Dik Takken

Rowan Collins

5 years ago
On 16/03/2021 09:42, Dik Takken wrote:
> I guess that using an observer results in scenario 1. This would yield a > confusing debugging experience. At the call site, you cannot see that > the function is decorated. You expect to enter the called function but > the debugger will jump some place else. Even worse when multiple > decorators are stacked.
One thing to note is that this kind of implicit jump already happens a lot with class autoloaders. It is confusing, though. Regards,
-- Rowan Tommins [IMSoP]