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
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
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
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
Hi David,
This sounds a lot like Asect Oriented Programming. Have you looked into
that?PHP framework:
https://github.com/goaop/frameworkPECL extension:
https://aop-php.github.io/Thanks,
PeterWith 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
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
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,
DavidHi David,
This sounds a lot like Asect Oriented Programming. Have you looked into
that?PHP framework:
https://github.com/goaop/frameworkPECL extension:
https://aop-php.github.io/Thanks,
PeterOn Sat., Mar. 13, 2021, 08:51 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?Regards,
Dave
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:
-
Register an internal attribute, see my #[Deprecated] PR as an example
https://github.com/php/php-src/pull/6521 -
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 -
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
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:
Register an internal attribute, see my #[Deprecated] PR as an example
https://github.com/php/php-src/pull/6521Register 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-L57Use 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
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,
DavidOn 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:
Register an internal attribute, see my #[Deprecated] PR as an example
https://github.com/php/php-src/pull/6521Register 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-L57Use 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
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:
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,
DavidOn 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:
Register an internal attribute, see my #[Deprecated] PR as an
example
https://github.com/php/php-src/pull/6521Register 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
- 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,
DaveI'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
--
To unsubscribe, visit: https://www.php.net/unsub.php
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:
-
Wrapper and/or before/after pattern?
As discussed. -
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? -
Can Decorators be used on Decorator functions?
#[Decorator]
class MyDecorator
{
#[MyDecorator]
public function wrapper(callable $callable, $args = []) { /* */ }
}
- 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);
}
}
- 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
Would it make sense to have both options?
- 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
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
Would it make sense to have both options?
- 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
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
I was able to get a proof of concept of the following at
https://github.com/iggyvolz/hooks:
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:
Register an internal attribute, see my #[Deprecated] PR as an example
https://github.com/php/php-src/pull/6521Register 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-L57Use 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.
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:
- The debugger enters the decorator function, not the called function.
- 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
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]