-
Notifications
You must be signed in to change notification settings - Fork 7.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for "decorator" pattern #5168
Conversation
This assumes only the public interface is decorated / exposed to |
Yes, only the public (method) interface is decorated. It would be good if public properties were forwarded as well, but we would have to add support for 1st class "property accessors" first. |
Will methods returning |
Interesting case. As-is, if you decorate a fluent interface, the inner Not sure how that could be addressed best (and whether it needs to be addressed at all...) We could remap the inner |
That would be ideal, yes. Though in its absence I think making fluent interfaces suck less is worth the extra bit of magic here anyway. |
/* Forwards to the generated method. Note that unlike CALL_TRAMPOLINE, this is going to insert | ||
* a proper stack frame. */ | ||
ZEND_VM_HANDLER(195, ZEND_CALL_DECORATED, ANY, ANY) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ | |
{ | |
USE_OPLINE |
Personally I'd find the following syntax easier to understand in this case: class ComposedFoo decorates Foo {
private Foo $inner;
public function __construct(Foo $inner) { $this->inner = $inner; }
public function method4(): stdClass {
return $DO_SOMETHING_DIFFERENT_HERE;
}
} But I don't know if it's feasible or desired to do that. Thanks. |
@javiereguiluz Decoration must be tied to a property, because it has to forward to that property. |
Personally I find it uncommon to add property modifier which can be used only once. interface Foo {
public function method1(int $a): void;
public function method2(): int;
}
decorated class ComposedFoo implements Foo {
public function __construct(Foo $decorated) {
self::__decorate($decorated);
}
public function method2(): int {
return 5;
}
} Then a call to With above in mind we could introduce |
As with others, I think the syntax is a bit off. I think the reason is that adding the Similar to @brzuchal comment. Have we considered adding a new type of object? i.e. Alternatively, a magic class which can be extended might be another option. Could we consider creating an RFC? Seems like this functionality has interest. |
I think the delegation idea from Kotlin is a good alternative. |
@HallofFamer looks interesting and it can work with multiple interfaces where methods are derived from multiple constructor args.
|
I am also very much in support of this functionality, although I agree the syntax could use improvement. The single-object limitation seems possibly problematic. Is it that there's only one decorated object allowed, or only one per interface? Eg, would this be legal: Eg: class ComposedFooBar implements Foo, Bar {
private decorated Foo $aFoo;
private decorated Bar $aBar;
public function __construct(Foo $aFoo, Bar $aBar) {
$this->aFoo = $aFoo;
$this->aBar = $aBar;
}
public function method4(): stdClass {
return $DO_SOMETHING_DIFFERENT_HERE;
}
} Regarding methods that return class ComposedFooBar implements Foo, Bar {
// ...
public function method4(): stdClass {
$this->aFoo->method4();
return $this;
}
} So it's not fatal; and it's easier to work around than the other way around, I think. Nitpick: Is |
@brzuchal yup the fact that a class can implement multiple delegates is a brilliant solution that easily solves the problem with decorator which seem to be able to only 'decorate' one object. Also this feature has been battle-tested in Kotlin and is well loved by the community. |
@Crell Yeah, I came to the same conclusion regarding multiple decorated properties. Should be supported, and any conflicts should require that method to be explicitly declared in the class, in which case it can choose to forward to one or the other, or do something else entirely. |
This idea is excellent, and this is functionality I would love to see in PHP almost exactly as-is. However, IMHO, it really should use a keyword other than As some of the comments above allude to, I don't think the proposed syntax is directly related to the decorator pattern. Using the term delegate would be closer. My understanding is that the decorator pattern is adding functionality on top of existing functionality, thus decorating the existing functionality. In this implementation either the function call is delegated to the inner object, or the outer object overrides the inner's function call; Neither case is decoration. You have to explicitly call I.e. it's much closer to Kotlin's delegate pattern. This would be a slam dunk if the syntax were changed to: class ComposedFoo implements Foo {
private delegated Foo $inner; // or delegate
// The rest identical to the original proposal
} |
@nikic would it be possible in future to lazy load decorated property instance and compute it before first use? |
I wonder if we couldn't get a lot more bang for the buck by using a mystery magical method: class Foo decorates Bar {
private Bar $bar;
private function __getDecorator(string $class_id) {
if ($class_id === Bar::class) {
return $this->bar;
}
}
} Or for an insta lazy proxy for a public interface: class Foo decorates Bar {
private Bar $bar;
private BarFactory $bar_factory;
private function __construct(BarFactory $factory) {
$this->bar_factory = $factory;
}
private function __getDecorator(string $class_id) {
if ($class_id === Bar::class) {
// this return value would be cached
return $this->bar = $this->bar_factory->build();
}
}
} An alternative would be setting the decorators as part of the constructor but this doesn't have the benefit of the magic unless it allowed passing a callback in... class Foo decorates Bar {
public function __construct(Bar $bar) {
$this->__decorate(Bar::class, $bar);
}
} Just a few ideas anyway. |
|
@marandall your example with lazy load made me thinking about Speaking of multiple interfaces maybe sort of conflict resolution mechanism like in traits could help?! decorate class DecoratedFooBar implements Foo, Bar {
private decorate Foo $foo;
private decorate Bar $bar {
Foo::foo insteadof Bar;
Bar::foo as fooBar;
}
public function __construct(Foo $foo) {
$this->foo = $foo;
}
public function __init(string $name) {
if ($name == 'bar') {
$this->bar = new BarImpl();
}
}
} |
It will behave exactly as if you wrote The latter part doesn't work in the current implementation, but that's how it should work.
It can be narrowed by specifying the correct interface. You would write
It must be a single type, union types are forbidden in this position. In the future, it would be possible to support intersection types to forward multiple interfaces to the same object. |
I was initially interested in this feature, but quickly came to realise that it is only useful with classes/interfaces with many, many methods. If you have up to 4 methods, writing the forwarding logic yourself is trivial and a no-brainer, and doesn't need this kind of syntactic sugar. If you have a dozen methods, then the feature starts to become useful, but I'd also add: why do you have a dozen methods? What's going on? So the principal feature of this patch is very much unnecessary. What's interesting though is:
I'd probably rather declare the decoration at method level then: class Foo implements Bar
{
private Bar $bar;
// ...
function decorates $bar baz;
function decorates $bar taz;
} This would also solve the forwarding when multiple internal decorated references exist: class Foo implements Bar
{
private Bar $bar;
private Waz $waz;
// ...
function decorates $bar baz;
function decorates $waz taz;
} |
@Ocramius Even if there are only a small number of methods, the sugar would be useful if there are a large number of parameters, because the signature is automatically copied across. It's easy enough to write this: public function getFoo() {
return $this->delegated->getFoo();
} But rather annoying to write this: public function takePayment(Card $card, Money $amount, string $reference, bool $async=false) {
return $this->delegated->takePayment($card, $amount, $reference, $async);
} The decorator needs to copy across and maintain the full signature, including types and defaults. Taking your per-method syntax suggestion, this reduces to one line which automatically maintains that for you: public function decorates $delegated takePayment; As for whether to delegate each method individually or automatically based on an interface, I'll quote a comment you made on a previous thread:
I think that's a very good point, and there are probably use cases that favour both approaches. |
Done that a gazillion times: it's really trivial to do, and adding a language feature for that is not useful. I've adjusted the snippets above to remove any function signature, since it makes sense to inherit everything. Delegation becomes very dangerous with BC compliance and implicit method forwarding (where no implicit method forwarding is wished), so a per-method delegation is indeed preferable. |
You might think it's not useful enough to warrant an extra language feature, but I don't understand the absoluteness of saying it's not useful at all. It basically has all the same benefits as traits: the compiler does the copy-and-paste for you, so you don't have to. The usefulness of that is relative to the complexity of the thing you're copying and pasting, and what kind of changes might happen to it in future. For instance, imagine if the default value for
You are perfectly entitled to change your mind, but I just want to reiterate that this is basically the opposite of your response on the previous thread. |
Added language features come with:
Adding features should always be weighed with the advantages they bring. If a feature is marginally useful (Pareto 20%), then it's probably not worth adding.
Implicit delegation still leads to BC breaks: you probably didn't click through to the link @ Roave/BackwardCompatibilityCheck#111 In that example, you have two behavioral breakages:
|
Absolutely. I was just calling you out on the distinction between "not useful" (which is rather absolute, and a bit of a discussion-killer) and "not useful enough to outweigh the cost" (which acknowledges there is both value and cost, and expresses an opinion on their relative values).
I may have done at the time, but it was a long time ago :) It's an interesting example, although a bit confusing because it both extends and delegates the same class. If we change class LoggingCounter implements Counter
{
private Logger $logger;
private Counter $originalCounter;
public function __construct(Logger $logger, Counter $originalCounter)
{
$this->logger = $logger;
$this->originalCounter = $originalCounter;
}
public function increment() : void {
$this->logger->log('increment');
$this->originalCounter->increment();
}
public function count() : int {
return $this->originalCounter->count();
}
} Then the change would propagate as follows:
I suggested on the mailing list that the feature could be extended with an AOP-style syntax for decorating the delegated functions. If you allowed a default for that, you could implement class LoggingCounter implements Counter
{
private Logger $logger;
private delegate Counter $originalCounter;
public function __construct(Logger $logger, Counter $originalCounter)
{
$this->logger = $logger;
$this->originalCounter = $originalCounter;
}
// default decoration for methods from interface
after delegate from Counter {
$this->logger->log(__METHOD__);
}
// opt out for specific method
after delegate count {}
} On the other hand, you may be right that explicitly listing methods is safer, in which case it might look more like this: class LoggingCounter implements Counter
{
private Logger $logger;
private Counter $originalCounter;
public function __construct(Logger $logger, Counter $originalCounter)
{
$this->logger = $logger;
$this->originalCounter = $originalCounter;
}
// forward calls, then run block
delegate $originalCounter increment, increment2 {
$this->logger->log(__METHOD__);
}
// forward calls unchanged
delegate $originalCounter count;
} |
Inquiry: Would this just forward methods, or properties, too? (I'm thinking of the way Go does "inheritance", which is essentially an implicit version of this, for properties and for implicit interface fulfillment.) |
IMHO proposed |
@nikic — I am so excited about your work on PHP, and especially this PR. I started to write a comment but realized I was writing an epic so decided to write a blog post instead. You can read the summary as well as the rest of the post here. But to summarize here for those who don't click the link, if we look at how Go handles these challenges and look at how close PHP's To provide just one code example to illustrate what this might look like; nice, clean and simple:
My blog post also deals with:
Hopefully this parallel between traits and delegation makes sense to you and this might be a way forward for delegation in PHP? Thank you in advance for considering. |
This feature is super-useful when decorating code from /vendor folder; we can't control how that code is written. |
Worst kind of coupling, and a massive foot-gun. |
It is not under our control. One example would be FormConfigInterface . The one that I actually decorated (long time ago, can't remember what) is from gaufrette/filesystem. It is not coupling but dependency. And sometimes we need a simple way to change their behavior w/o writing tons of code just to fix one small thing. |
Closing as this seems inactive and there's no associated RFC. |
This is an incomplete prototype for better "decorator" support. The basic problem this solves is this: We generally encourage the use of composition over inheritance, but currently using inheritance is simply much more convenient...
A basic decoration pattern for such an interface would look something like this:
That is, we need to proxy through a large number of methods to the decorated object. Using
__call()
is not an alternative, because it loses the method signatures, and does not allow implementing the interface.This proposal reduces the above example to:
The property is marked as
decorated
, which means that we will automatically add forwarding methods for all public methods ofFoo
, unless they are explicitly overridden in the class. These methods will have full signatures, so they still satisfy the interface.Some notes:
decorated
property is allowed, at least for now. I could be convinced that multiple make sense, but that comes with issues on how conflicts are handled.public
method interface is forwarded. Forwarding public properties would in principle also make sense, but would only be possible once we have a first-class "property accessor" feature.