Problem/Motivation

#2238217: Introduce a RouteMatch class removed the getActiveTheme() method. Granted, not much was using it, but that is changing (two patches in the queue were using it and broke).

Now instead of calling that method, a properly injected class also has to inject a RouteMatch and pass it along. All I want is the current theme, I should be able to ask the theme negotiator for that.

In addition to all of the other classes implementing ThemeNegotiatorInterface, ThemeNegotiator is the one acting as the service, and has the extra methods to gather and delegate. This is similar to BreadcrumbManager, which implements BreadcrumbBuilderInterface.

Proposed resolution

Remove the RouteMatch parameter from determineActiveTheme(), so it serves as a replacement for getActiveTheme().
Inject RouteMatch into any theme negotiators that need it via their service definition.

Additionally, add a new interface for the addNegotiator() method that only ThemeNegotiator (the service class) implements.

Remaining tasks

N/A

User interface changes

N/A

API changes

ThemeNegotiatorInterface::determineActiveTheme() no longer takes any parameters.
Theme negotiators that need a route match should inject it via their service definition.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

tim.plunkett’s picture

Status: Active » Needs review
FileSize
10.16 KB
Crell’s picture

+++ b/core/lib/Drupal/Core/Theme/ChainThemeNegotiatorInterface.php
@@ -0,0 +1,35 @@
+/**
+ * Defines an interface for a chained service which determines the active theme.
+ */
+interface ChainThemeNegotiatorInterface extends ThemeNegotiatorInterface {

The naming is consistent with both ChainRouter and ChainBreadcrumbBuilder, so +1 from me.

Having the extra getActiveTheme() method worries me a little, though. I can see why it's there and what the advantage is, but one of the nice things about reusing the interface directly is that I can wire a single router or a single breadcrumb builder up directly, without any negotiation, and everything still works. With this additional method that's not the case here.

tim.plunkett’s picture

Here's a completely different approach. This way we don't need another new method.

Also, quite a few of the negotiators don't need the route match anyway, it seems weird to pass it to both methods.

effulgentsia’s picture

Back in #2134513: [policy, no patch] Trait strategy, some people expressed concerns about both trait proliferation and setter injection. I'm not really all that bothered by either, but this probably needs a review from someone who is.

Crell’s picture

If we're going to remove RouteMatch from the passed parameters, just make it a constructor parameter for the negotiators that care. #3 makes the negotiators non-stateless, even though they're registered as services. We can simplify it by just passing the RouteMatch to each negotiator in its constructor, no trait, no muss, no fuss. And then we have a unified interface between the negotiators and the aggregating service, which we want; we probably should still add a chain interface anyway as with breadcrumbs and the patch from #1, it just won't need anything but an addX() method.

tim.plunkett’s picture

Assigned: Unassigned » tim.plunkett
Status: Needs review » Needs work

Might as well. Thanks!

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
25.42 KB

Status: Needs review » Needs work

The last submitted patch, 7: theme-negotiator-2292217-7.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
1.16 KB
26.05 KB

Missed a spot.

Crell’s picture

No chain interface?

Other than that, #9 looks good to me.

tim.plunkett’s picture

FileSize
30.97 KB
5.75 KB

Added, and rerolled after the kernel and current theme condition issues.

tim.plunkett’s picture

FileSize
31.01 KB
698 bytes

Missing the interface.

Crell’s picture

Status: Needs review » Reviewed & tested by the community

Assuming the bot is still happy I think we're good here. Thanks, Tim!

Dries’s picture

The issue summary and issue title seems wrong. The patch itself looks good to me though.

tim.plunkett’s picture

Title: Bring back ThemeNegotiator::getActiveTheme() » ThemeNegotiator::determineActiveTheme() should not require a RouteMatch to be passed in
Issue summary: View changes

Good point, that was about 3 approaches ago :)
Thanks, updated!

xjm’s picture

Updated a relevant change record for this and the original RouteMatch issue: https://www.drupal.org/node/2158619/revisions/view/6783809/7398343

dawehner’s picture

In addition to all of the other classes implementing ThemeNegotiatorInterface, ThemeNegotiator is the one acting as the service, and has the extra methods to gather and delegate. This is similar to BreadcrumbManager, which implements BreadcrumbBuilderInterface.

Doesn't that mean that there should be two separate interfaces? As a developer of a theme negotiator I cannot imagine that you would ever NOT want to have the route match passed along.

effulgentsia’s picture

Status: Reviewed & tested by the community » Needs review

As a developer of a theme negotiator I cannot imagine that you would ever NOT want to have the route match passed along.

@dawehner: What's the benefit of having it passed-in vs. constructor injected. I don't see a down side to #12's approach of using CI, but I'd like to understand your concern more.

sun’s picture

The way I see it, it's about explicit configuration/state of a service vs. call-time/context-specific arguments.

When moving RouteMatch + Request into the constructor of theme negotiators, the same change must include a new architectural facility to "reset" all theme negotiators, so that a new context can be negotiated.

Thus far, I interpreted theme negotiators as simple I/O processing plugins. You get some context in, you return a result based on that context.

I should be able to trigger theme negotiation for an arbitrary/ad-hoc request/context and get a proper result back. This may happen and be necessary for rendering e.g. theme-specific blocks (HTML fragments) as part of a master request. Sub-requests may or may not be another use-case.

I was confused by the previous RequestRouteMatch injection change already. (Even more so by losing access to the current Request that corresponds to the matched Route - which doesn't make sense.)

Already in HEAD, the most trivial theme negotiator implementation possible (?theme= GET query parameter matching) suddenly had to (1) introduce state and (2) became considerably more complex:

+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
...
+ protected $requestStack;
+
+ public function __construct(RequestStack $request_stack) {
+   $this->requestStack = $request_stack;
+ }
...
- public function applies(Request $request) {
-   return (bool) $request->query->has('theme');
+ public function applies(RouteMatchInterface $route_match) {
+   return (bool) $this->requestStack->getCurrentRequest()->query->has('theme');
  }
...
- public function determineActiveTheme(Request $request) {
-   return $request->query->get('theme');
+ public function determineActiveTheme(RouteMatchInterface $route_match) {
+   return $this->requestStack->getCurrentRequest()->query->get('theme');

IMHO, theme negotiators should be atomic I/O plugins; i.e.:

  1. The theme negotiator gets a RouteMatch context in, it returns the negotiation result (if any).
  2. The RouteMatch context supplies access to all ancestors + related data, including the Request.
  3. A theme negotiator service MAY be stateful, but its persistent state may only hold persistent configuration.
tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned

There was a major regression for anyone trying to find out what the current active theme is.
If my (working) approach is going to be held up, it'd be nice if someone else proposed an alternative.

dawehner’s picture

There was a major regression for anyone trying to find out what the current active theme is.

Noone disagrees with that and the route match issue was one of these "let's push it through because this is one less beta blocker" issues.

If my (working) approach is going to be held up, it'd be nice if someone else proposed an alternative.

I hope it is okay to criticize other people.

Well, my approach would be: Have one interface for the main theme negotiator and one for the sub-thingies. Here is an excerpt of HEAD april 23th (last checkout on this machine):

class ThemeNegotiator ... {
  public function getActiveTheme() {
    $request = $this->requestStack->getCurrentRequest();
    if (!$request->attributes->has('_theme_active')) {
      $this->determineActiveTheme($request);
    }
    return $request->attributes->get('_theme_active');
  }

  /**
   * {@inheritdoc}
   */
  public function applies(Request $request) {
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function determineActiveTheme(Request $request) {
  }
}
tim.plunkett’s picture

So you mean the patch I had in #1?

dawehner’s picture

I think we should certainly change internal details of our code if it helps both the DX of end-developers and if needed add complexity, but yeah I am so out of things that I would not trust me.

effulgentsia’s picture

I continue to think the patch in #12 is correct, and I disagree with dawehner and sun.

We have multiple independent services for getting data about or derived from a request: 'request_stack', 'current_route_match', 'current_user', etc. At an API level, these are independent, meaning none of them have methods for getting another. ThemeNegotiatorInterface doesn't know which of these a particular negotiator will base its decision on. Therefore, it makes no sense to pass any of them to determineActiveTheme(). Instead, each negotiator can specify what it uniquely needs in its constructor.

I should be able to trigger theme negotiation for an arbitrary/ad-hoc request/context

What's an actual use case of that?

This may happen and be necessary for rendering e.g. theme-specific blocks (HTML fragments) as part of a master request. Sub-requests may or may not be another use-case.

Is there any legitimate use case to do the first without a sub-request? As to sub-requests, constructor injection is 100% compatible with that. Each negotiator's *.services.yml entry can either wire in services that are internally sub-request compatible ('request_stack', 'current_route_match', 'current_user' all are), or else, the service can be flagged as having request scope, in which case it will be automatically re-instantiated for subrequests (this is no longer Symfony's recommended approach, but it's an available option for contrib to use if necessary).

Dries’s picture

I agree with the reasoning in comment #24. It doesn't feel like now is the time to dramatically change RouteMatch (per #19.2). It is certainly worth exploring having a 'mega object' that gives access to everything, but maybe that is best left for Drupal 9?

Status: Needs review » Needs work

The last submitted patch, 12: theme-negotiator-2292217-12.patch, failed testing.

dawehner’s picture

Data always wins. Here is a list of theme negotiators in core (left out the test ones and ThemeNegiator itself):

negotiator requires the route match
AdminNegotiator
CustomThemeNegotiator
AdminDemoNegotiator
DefaultNegotiator
BatchNegotiator
AjaxBasePageNegotiator

We have multiple independent services for getting data about or derived from a request: 'request_stack', 'current_route_match', 'current_user', etc. At an API level, these are independent, meaning none of them have methods for getting another. ThemeNegotiatorInterface doesn't know which of these a particular negotiator will base its decision on. Therefore, it makes no sense to pass any of them to determineActiveTheme(). Instead, each negotiator can specify what it uniquely needs in its constructor.

As said before, the common case is to decide based upon the current request, so information coming from the $request and extracted into the $route_match. Applying your point the other way round, you could also argue that access(AccountInterface $account) should NOT get passed in the account. We added the $account on there because it is the better DX

effulgentsia’s picture

The table in #28 is for the applies() method. However, for determineActiveTheme() there's no such pattern. AdminDemo uses $route_match, Batch and Ajax use $request, and the rest use nothing. So, how about we continue to pass $route_match to applies(), but remove it from determineActiveTheme(), which is all this issue title is asking for anyway?

dawehner’s picture

Reviewing #1 because it is my favourite

  1. +++ b/core/lib/Drupal/Core/Theme/ChainThemeNegotiatorInterface.php
    @@ -0,0 +1,35 @@
    +  /**
    +   * Determines the active theme.
    +   *
    

    We should explicit document that this method is the method you want to use instead of determineActiveTheme

  2. +++ b/core/lib/Drupal/Core/Theme/ChainThemeNegotiatorInterface.php
    @@ -0,0 +1,35 @@
    +}
    \ No newline at end of file
    

    ups

  3. +++ b/core/lib/Drupal/Core/Theme/ThemeNegotiator.php
    @@ -108,4 +113,11 @@ public function determineActiveTheme(RouteMatchInterface $route_match) {
    +   * {@inheritdoc}
    +   */
    +  public function getActiveTheme() {
    +    return $this->determineActiveTheme($this->routeMatch);
    +  }
    +
    

    Should we maybe store the result somewhere?

star-szr’s picture

Stealing @webchick's tag, seems appropriate here.

andypost’s picture

#29 makes sense

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

joelpittet’s picture

Version: 8.1.x-dev » 8.2.x-dev
Status: Needs work » Needs review

Re-roll of #1 though it looks like part of this has been done already because there is no current_route_match on cache_context.theme service.

joelpittet’s picture

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.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.3.x-dev » 8.4.x-dev

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

joelpittet’s picture

dawehner’s picture

I'm quite confused as the title doesn't match the actual patch at all!

Note: #2472337: Provide a lazy alternative to service collectors which just detects service IDs will also make this interface not useful anymore.

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

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.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.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.

markhalliwell’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll

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.

vacho’s picture

andypost’s picture

Btw https://www.drupal.org/pift-ci-job/1153771 shows 7 coding standards messages

vacho’s picture

Solved, problems with coding standard.

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.

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.

joachim’s picture

> I'm quite confused as the title doesn't match the actual patch at all!

I'm seeing several approaches in patches. Tagging for this.

joachim’s picture

This also seems to be potentially at odds with #2471657: Just load the theme negotiators which are needed, which says:

> Convert \Drupal\Core\Theme\ThemeNegotiatorInterface::applies to accept a route object