I am proposing a straightforward refactoring.

Currently, theme hook suggestions are collected within ThemeManager::render().
I propose to split this out into a separate function.

Let's start with a private function within the same class. Later we can decide to move this elsewhere, make it protected, or whatever else.
Let's really keep it private for now to allow future refactoring.

Comments

donquixote created an issue. See original summary.

donquixote’s picture

Status: Active » Needs review
StatusFileSize
new4.1 KB
borisson_’s picture

Title: Factor out theme hook suggestion building from ThemeManager::render() into a separate function. » Refactor out theme hook suggestion building from ThemeManager::render() into a separate function.

Yeah, the render function is a 250+ lines behemoth of a function. I really like making this simpler. However, this doesn't really make it that much simpler.

I mean, this part isn't the most difficult to understand of that method. I'm not sure we should just do this without a bigger plan of how to improve the surrounding code.

On the other hand, this is an improvement, so if anyone else feels this is sufficient as a start - I can certainly understand that as well.

donquixote’s picture

I am in favor of piecemeal refactoring and picking low-hanging fruits first.
I actually did start to do more refactoring locally, but this can easily be a follow-up.

The suggestion picking is self-contained and not interdependent on the rest of the function. Or rather, the dependency can be expressed as parameters and return value.

With this code in the main function, a reader does not immediately see that those variables are only used in this section of code, and not further down. Splitting it out makes this obvious, and hence reduces (gradually) the mental burden of reading the beast function. It also shows what the suggestions depend on.

donquixote’s picture

I actually did start to do more refactoring locally, but this can easily be a follow-up.

If you are curious:
For a follow-up, I plan to create an interface ThemeRenderInterface (name to be discussed) as parent of ThemeManagerInterface, which only has the ::render() function.
One implementation of ThemeRender with the logic from ThemeManager::render(), but where all the lazy-initialized object properties are already set in the constructor. So it would be immutable.

The first call to ThemeManager::render() would create this new immutable object, and then call the ::render() function of this object.
ThemeManager itself would still have all the mutable and resettable lazy initialization from global state mess, but the main logic would live elsewhere, in an immutable and thus predictable and more easily testable class (which is not by itself registered as a service!)

Once we are there, we could split parts of the ::render() function into decorator layers.

We could also split out the suggestion building into a separate object, possibly with an interface.
This would reduce the dependency of the ThemeRender object, because the module handler and theme manager are only needed for the suggestion building.
I personally don't mind to create such one-off interfaces. But in general, people seem not too happy about it.

I also started working on an AltererInterface, with implementations for theme-based altering and module-based altering. Again, these objects can be lazily created in ModuleHandler and ThemeManager, and can be mostly immutable (except for the cache/buffer of collected alter functions).

I then noticed that splitting out AltererInterface is only worthwhile if we also split out getImplementations() and invokeAll() from module handler into a separate interface. Then the suggestion-building would no longer depend on module handler and theme manager, but on AltererInterface and ModuleHookInterface (if this is what we would call it).

---------

All of this I expect to be highly controversial, whereas this patch is not.
Also, such bigger changes need to be proposed as a PR, where commits are separated, not as a huge patch. I tried to do this in the past, posted links to github with every patch, and posted the patches with format-patch for multiple commits. But most people at the time wanted to see one big diff instead.

Therefore I prefer to do things piecemeal, and hope that smaller issues have a chance to be committed faster.

borisson_’s picture

I am in favor of piecemeal refactoring and picking low-hanging fruits first.
I actually did start to do more refactoring locally, but this can easily be a follow-up.

Good point. Are there followups created we can link to this issue?
I agree, this does make it better than it was.

donquixote’s picture

Are there followups created we can link to this issue?

I like to "test the waters" before I put too much effort :)
I could push a POC branch to github if you are interested.

donquixote’s picture

For a follow-up, I plan to create an interface ThemeRenderInterface (name to be discussed) as parent of ThemeManagerInterface, which only has the ::render() function.
One implementation of ThemeRender with the logic from ThemeManager::render(), but where all the lazy-initialized object properties are already set in the constructor. So it would be immutable.

The first call to ThemeManager::render() would create this new immutable object, and then call the ::render() function of this object.

If the object is created locally by ThemeManager, and not passed around elsewhere, we don't need an interface for it.
We can still add an interface later, but we can start with just a class. Maybe mark it as @internal so we can change it later?

But either way, this should be a follow-up.

borisson_’s picture

Status: Needs review » Reviewed & tested by the community

Sure, that all sounds like a big refactor that would be hard to get in, while this doesn't seem very controversial at all. Let's get this in as a start.

larowlan’s picture

I'm plus one for this change as it is a step in the right direction.

Tagging for subsystem maintainer review, I'll ping @effulgentsia about this, as its his wheelhouse.

tim.plunkett’s picture

Not directly related, and not conflicting at all, but thought you might be interested:
#2863819: Convert theme hooks (defined by hook_theme()) to be objects

donquixote’s picture

@tim.plunkett Thanks for the link.

When I first saw your comment, I thought that the new class would do the actual rendering.
Somewhat like this: $registry->getThemeHook('page')->render($variables).
I thought about this before, but then decided it would not be such a good idea, because of all the suggestion and fallback logic. Such an object would not be self-contained, but still need the full registry to resolve suggestions.

Your proposal solves a different problem. I am not sure yet if I like it, it introduces a big (in lines of code) class.
Also, serializing objects is more costly and fragile than serializing / unserializing arrays.
More feedback over there.

effulgentsia’s picture

Wearing my theme subsystem maintainer hat, +1, so removing the "Needs subsystem maintainer review" tag.

Wearing my committer hat:

+++ b/core/lib/Drupal/Core/Theme/ThemeManager.php
@@ -388,6 +361,56 @@ public function render($hook, array $variables) {
+  private function buildThemeHookSuggestions($hook, array $info, array $variables) {

I think this should be protected instead of private. We don't yet have resolution on #2727011: [policy, no patch] Private vs protected, and the role of inheritance, and until we do, I think we should stick with Drupal core's existing convention of virtually never using private methods. I don't see any reason for this one to justify a deviation from the current convention.

Status: Reviewed & tested by the community » Needs work
Mixologic’s picture

Status: Needs work » Reviewed & tested by the community

Temporary testbot hiccup.

donquixote’s picture

Hi,
sorry, I was going to reply earlier, but somehow got held up.

@effulgentsia (#14):

I think this should be protected instead of private. We don't yet have resolution on #2727011: [policy, no patch] Private vs protected, and the role of inheritance, and until we do, I think we should stick with Drupal core's existing convention of virtually never using private methods. I don't see any reason for this one to justify a deviation from the current convention.

I could upload a different version where the method is protected instead of private, but I am skeptical about this.

Having a private method would allow very easy follow-up refactoring.
Making it protected would let us wonder if anything else is already using the method. So this would no longer be a nice preparation for a follow-up, but rather make such a step more difficult.

I suppose we could mark the thing as @internal, but it just feels silly to do this, if it would be so much easier (technically, not politically) to just make it private.

donquixote’s picture

Status: Reviewed & tested by the community » Needs work
+  private function buildThemeHookSuggestions($hook, array $info, array $variables) {

It is unnecessary to pass the entire $info array to buildThemeHookSuggestions().
We only need to pass the value of $info['base hook'].
At first I thought we can use the variable $base_theme_hook for this, but this won't cover all cases correctly.
Instead, we would need a separate variable like e.g. $info_base_hook.

This is one reason why I prefer to keep this private: The signature of the method is tied to implementation details.

markhalliwell’s picture

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.

effulgentsia’s picture

This is one reason why I prefer to keep this private: The signature of the method is tied to implementation details.

In that case, can we open a follow-up issue for refactoring the method signature and add a @todo to the method's docblock explaining that it's private until that (and potentially other refactoring) is done?

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.

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.

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.

andypost’s picture

Status: Needs work » Needs review
StatusFileSize
new1.5 KB
new3.96 KB

Set method protected and internal, also addressed #19

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

Seems like a straight forward refactor.

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.

  • longwave committed d21fd5d2 on 11.x
    Issue #2953921 by andypost, donquixote, borisson_, effulgentsia:...
longwave’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed d21fd5d28e to 11.x (10.2.x). Thanks!

As a straight refactor I don't think this needs a CR, I suspect people will never override it but it does make the parent method slightly shorter.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

claudiu.cristea’s picture

This change broke our site when switching to Drupal 10.2. Indeed, we've implemented hook_theme_suggestions_HOOK_alter() in a way that is not following the docs. We did:

function mytheme_suggestions_page_title_alter(array &$suggestions, array &$variables): void {
  ...
  $variables['foo'] = $bar;
}

Note that $variables is passed by reference which doesn't follow the signature of hook_theme_suggestions_HOOK_alter(). But that is still possible with hook implementation as, with hooks, there's no strong, enforcement on signature (like with interfaces/classes).

As, buildThemeHookSuggestions() doesn't pass $variables by reference, the change is lost.

I wonder if this could be considered a regression. What if there are more such unorthodox hook implementations out in the wild?

EDIT: Actually, we set that variable here to make it available to all suggestions. I'm more and more convince that this is a regression

claudiu.cristea’s picture