Problem/Motivation

The Decorator pattern can be used to add additional functionality to a class at runtime.

It is currently used by the Views UI to add functionality to a View entity that is only relevant for use by the UI.

A large majority of the code is pass-through implementations of methods to satisfy the interface.

Due to EntityInterface being very large, this boilerplate makes the decorator very large.

Proposed resolution

Introduce a generic EntityDecorator to satisfy EntityInterface, allowing the actual needs of the class to be separate from the majority of the boilerplate.

This will also be used by the Layout Initiative.

Remaining tasks

User interface changes

API changes

Data model changes

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

tim.plunkett created an issue. See original summary.

tim.plunkett’s picture

Status: Active » Needs review
FileSize
21.61 KB

Status: Needs review » Needs work

The last submitted patch, 2: 2924796-decorator-2.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

tim.plunkett’s picture

tim.plunkett’s picture

Status: Needs work » Needs review
xjm’s picture

We probably want to run this by the entity subsystem maintainers.

I was at first taken aback by the sheer number of methods, but the reason for that is (of course) EntityInterface itself.

+++ b/core/lib/Drupal/Core/Entity/EntityDecorator.php
@@ -0,0 +1,403 @@
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(array $values = []) {
+    throw new \LogicException('Static methods cannot be called on the decorated entity');
+  }

I guess this is because the static methods are conceptually incompatible with decorating something?

tim.plunkett’s picture

ViewUi, which is the only decorator of an entity class in core, individually overrides the static methods to proxy it to View:

  public static function create(array $values = []) {
    return View::create($values);
  }

This can be done because it knows *exactly* which class it is supposed to be acting like.
Except, this will fail to work as expected if someone swapped out the entity class. But we choose to do it this way, hoping that no one would ever swap out the View entity class and override any of its static methods.

In the case of a more generic decorator, like the one being added in Layout Builder, we cannot know which class to proxy to.
We could have gone with proxying Entity:: in each case. But that would lead to unpredictable results in the case where it is overridden, like ::preCreate().

The safest course of action is the one taken here: throw exceptions in each one, and allow any subclass to fix their own static methods (if they so choose).

xjm’s picture

I wonder, would it make sense to document that more clearly about the static methods? Not sure how to without a ton of copypaste -- maybe a paragraph on the class docblock? (Which could also use more detail overall.)

xjm’s picture

I wonder also if we should have a followup to move the static methods to their own interface that EntityInterface can extend (for now), but that we'd decouple in the future (or deprecate entirely when a better solution for their usecase of "don't make it so hard to load an entity" comes around). Not in scope here of course.

xjm’s picture

Status: Needs review » Needs work

Yeah I think the class docblock needs to have some more extended information, including:

  • The purpose/usecase of an abstracted entity proxy.
  • The bit about why the static methods throw exceptions.
  • An @ingroup for the entity API group.

I always thought the Views UI thing was kind of a workaround we had for historical reasons, in order to not rewrite/refactor it a fifth time. (Or maybe only a fourth time for the UI.) So it'd be good to know why this pattern of wrapping all the things is useful and desirable. I can see how getting rid of the boilerplate is good, but I've not been clear on why we're doing it this way in the first place.

Thanks!

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
20.19 KB
2.38 KB

Normally decorators wouldn't need an entire helper class for this. An interface defines a single dedicated responsibility, and it wouldn't be a big deal to pass through each method in the actual class.
But EntityInterface is MASSIVE. And full of methods that should not be in there.
Not sure how to best explain that in docs without just being more grumpy at EntityInterface.

Added a link to https://en.wikipedia.org/wiki/Decorator_pattern, I think it's out of scope for Drupal core to explain software design patterns.

Added the @ingroup.

Also thought of an elegant way to sidestep the scary exception throwing in static methods: don't implement them
The class is already abstract, this will force the implementor to decide how they want to handle it.
ViewUI has an implementation for them anyway, and the Layout approach can re-add the exceptions itself.

Berdir’s picture

+++ b/core/lib/Drupal/Core/Entity/EntityDecorator.php
@@ -0,0 +1,358 @@
+  public function __construct(EntityInterface $subject) {
+    $this->subject = $subject instanceof static ? $subject->getEntity() : $subject;

Is there a specific reason why we unwrap in the constructor?

Decorators are by design supposed to be added in multiple layers, this line means you can't re-decorate with the same class, but you can decorate a different class even if that class then again decorates your class.

https://3v4l.org/tNU9S

Just wondering if that's "just" an optimization or if it has a specific reason and if the above is really what you'd expect to happen.

And yes, there are a lot of methods on EntityInterface, but apart from the deprecated methods and the magic stuff, I'm not sure what we could do differently really? Caching adds a lot of methods, but that's a critical part of entities at this point, The config system also adds a few and so on. And having more of those on separate interfaces would in reality just mean that there are even more methods that entity-type-specific decorators like ViewsUI would still need to implement :)

Status: Needs review » Needs work

The last submitted patch, 11: 2924796-decorator-11.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
1.32 KB
19.94 KB

We're all a little guilty of overdoing it on EntityInterface, sorry for bringing it up :)

You're absolutely right on the __construct part being pointless. Not sure why I did that in the first place.

xjm’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Core/Entity/EntityDecorator.php
@@ -6,6 +6,10 @@
+ * @see https://en.wikipedia.org/wiki/Decorator_pattern

This is not documentation. ;) I didn't mean "explain to me what a decorator is in the class docblock"; I meant "document why this class exists and what it should be used for, possibly with an example".

xjm’s picture

Like, I know what a decorator is. (I learned that from @tim.plunkett I think like five years ago when we were working on the plugin system or something.) :) However, simply naming it that does not explain to the developer why it's useful or where it fits in the massive public API the entity system provides.

I think what it's being used for in both layouts and Views UI is to provide user interface representations of a thing that are decoupled from the thing, or etc. There could be other usecases.

Basically, a docblock for FooBar that simply contains Provides a Bar for Foo isn't usually sufficient.

xjm’s picture

Status: Needs work » Postponed (maintainer needs more info)

Okay, @tim.plunkett and I had a vigorous discussion yesterday about what the role of this is in the layout initiative and why we should provide a reusable API for it.

For now, for that initiative it's an internal implementation of layout section storage (that I am not on board with, but out of scope here). I think we should not add this as a generic API until we have a third usecase for it beyond the Views UI (which I'd always assumed we'd eventually change to remove the decoration, but also out of scope here) and the proposed layout entity section stuff.

So postponing on such a third usecase, because as it is I'm not quite convinced yet that decorating entities is a good pattern for core.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

drunken monkey’s picture

If you're also taking contrib use cases into account: In the Search API we have this pattern, too. Basically, we allow editing search indexes the same way as views, by making temporary changes and then letting the admin save (or discard) all of them at once.
So, this class would be quite useful to us.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

kriboogh’s picture

Whilst looking for a decorator solution for our project, I saw several other people are actually trying to do this kind of stuff, maybe a combined solution can be forged and put into core.

solomonrothman's https://www.drupal.org/sandbox/solomonrothman/2977603 (based on this thread)
Steve Jones' https://www.drupal.org/project/2992933/issues/2994042 (automatic solution)

Before I saw Steve's solution, I was also thinking into implementing something amongst those lines.

As for the comment by @Berdir in #12
"Decorators are by design supposed to be added in multiple layers, this line means you can't re-decorate with the same class, but you can decorate a different class even if that class then again decorates your class."

A nicer implementation to allow re-decoration can be found here:
http://jrgns.net/decorator-pattern-implemented-properly-in-php/index.html

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

mjpa’s picture

Came across this issue as I was looking to decorate an entity from a different module.

The patch in #14 needs a reroll for 8.7.x I believe which if I get time I will do.

Other than that, the constructor is incorrect as it just contains $this->subject; which does nothing, needs to be $this->subject = $subject; - again shall fix if I get time to do a reroll as there's no point submitting a patch that won't apply against the current version.

To use the entity decorator, you can either implement the static methods load/loadMultiple/etc in your decorator class, or extends the EntityDecorator class to add another abstract method to return the entity type class to proxy those methods to and implement then in a generic way in the EntityDecorator class.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

heddn’s picture

Status: Postponed (maintainer needs more info) » Active

Seems like a few more use case/examples have surfaced. Taking this out of postponed for now.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Ghost of Drupal Past’s picture

Here's how to decorate entities.

core has a generate-proxy-class script which gets very close to writing the decorator for us. Example:

php ./core/scripts/generate-proxy-class.php 'Drupal\somemodule\Entity\SomeEntityClass' ./
modules/contrib/mymodule/src

. After generating do the following:

  1. Move the file to ./modules/contrib/mymodule/src -- it lands in ./modules/contrib/mymodule/src/ProxyClass/Entity which we hardly need.
  2. Fix namespace to be just Drupal\mymodule.
  3. change namespace {} notation to the usual namespace ;
  4. Reformat the file to adhere to Drupal code style.
  5. Delete everything in the class until and including the lazyLoadItself() method.
  6. search-replace lazyLoadItself() with subject.
  7. Add a ENTITY_TYPE constant
  8. Add the code from this gist.
  9. Finally, replace the static methods calling OriginalEntityClass:: and ancestors up to and including EntityBase:: using the load method in the gist as a pattern. It's really simple.

To hook it in, one can implement hook_entity_type_alter:

function mymodule_entity_type_alter(array &$entity_types) {
  $entity_type = $entity_types['my_entity_type'];
  $entity_type->set(MyEntityDecorator::class, $entity_type->getClass());
  $entity_type->setClass(MyEntityDecorator::class);
}

While $property for EntityType::set() is a completely arbitrary string, a namespaced class name is pretty much guaranteed not to be used by anything else. The getDecoratedClass method uses the class saved here.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Anybody’s picture

Nice one, I think this is still relevant. To add a use-case:

In a contrib module I'd like to extend entities with further API methods, for example for a user hasSubscription(): bool or getSubscriptions(): array for developer convenience.

geek-merlin’s picture

@Anybody: No, that's a different party. In simple words: If you add methods to a decorating class, the next decorator can wrap and hide your methods.

I guess you want BundleClasses: https://www.drupal.org/node/3191609

;-)

Anybody’s picture

Thanks @geek-merlin! Great reminder, I had already forgotten that I read about it once!
[Oh man, the Drupal('s great features) documentation. Probably the most important area left behind (by us all as community).] BTT!

Berdir’s picture

> In a contrib module

No, in a *contrib* module you don't want to use bundle classes for user or any other entity type that's not your own IMHO, exactly because these are not decorators. You can only have exactly one bundle class active for a given bundle. So if you have two contrib modules doing that, one of them would win and the other would not be used, breaking code that depends on it.

Bundle classes are for custom modules and I guess possibly your own entity types/bundles in contrib modules if you have predefined, specific bundles.

If we want to allow contrib modules to add some methods to other entity type classes then we would indeed need such a decorator system, but that IMHO comes at a price of complexity, decorators need to be correctly implemented and add extra method calls for every single decorator that is there, as each method call is passed through all decorated classes, which also makes things even harder to debug around entities.

A better approach for that might be an Adapter pattern IMHO, so if you have subscriber related logic for a user, you can do new SubscriberUserAdapter($user), and then use your methods and just those on a user when you need them. It's an extra line in your code and then only exposes the specific methods you defined, but it's also not overhead to pass through every single time anyone does something with a user.

Anybody’s picture

@Berdir: Thanks for the additional explanation! Like you wrote, there's no silver bullet for such cases and all solutions come with pro's and con's.

What I currently use is kind of Proxy pattern. And I think my case isn't that far away from what the issue summary described. But the DX downside is that you have to implement all methods and delegate them to the proxied object, which causes lots of boilerplate and is problematic in evolving systems like Drupal, where the Interface may be subject to change and requires to proxy to adapt these changes.

I was also thinking about, if Traits might help, but it then seems to end up similar to extending the base class... so I think let's come back to the original issue here and keep the cases like described in #34 in mind. Being able to "extend" entity classes is surely helpful, and already possible in some ways. Perhaps we'll come to better out of the box solutions and patterns for Drupal here in the future.
Thanks a lot for your feedback and support, much appreciated!

geek-merlin’s picture

@Anybody: There is a package for that too: https://packagist.org/packages/geeks4change/composable-inheritance
(Feel free to contact me on another channel if it's useful for you.)

Anybody’s picture

Thank you very much @geek-merlin that looks super interesting! :)
I'm done with my project where I ran into this, but will definitely consider it in the future!