Problem/Motivation

The Drupal 8 routing system was specifically designed with features to allow Page Manager (and similar modules) to be implemented without introducing a multi-stage routing system.

As you can see in the original issue summary below by @sdboyer, there are a number of compelling reasons to avoid this.

However, currently, we create a route for each Page, and then have PageViewBuilder do the variant selection (the 2nd stage router) before rendering the variant. So, we're doing exactly the thing to be avoided!

Proposed resolution

  • Create a route for each variant at the same path, and attach enough information to do run the selection rules in a "route filter"
  • When we override an existing route, rather than removing it, we should simply put it at the end of the list so we fallback on the original, if none of our variants selection rules pass
  • Create a "route filter" which looks for any route that came from Page Manager, runs the selection rules, and keeps only the first one which succeeeds (or none)

Remaining tasks

  • Write patch
  • Review
  • Commit!

User interface changes

None.

API changes

None. This would just be an implementation detail around how we interact with the routing system.

Data model changes

None.

Original issue summary "Avoid introducing a multi-stage routing system" by @sdboyer

It's a little tough to define an issue in the negative, but nevertheless, here we go.

Whatever decisions we make about how to ultimately perform 'selection' (think in terms of page_manager, but to a lesser extent also Context) of a resource to render, those decisions should be made fully and completely within the scope of the the routing process. Specifically, that means that such decisions should be made within one of the defined subrouting procedures that occur under KernelEvents::REQUEST.

There's a fair bit of wiggle room here, especially given that our HTML resources are sewn together from data created by a hodgepodge of different participants. But the key guideline should be taken from the basic structure of HTTP - we should emerge from routing knowing exactly WHICH resource we are going to represent. There might be different representational forms for a single resource, but basic resource selection is the responsibility of routing. And while it is almost a certainty that contrib will blow this up, that absolutely does not make it permissible for core.

This is important for the following reasons:

  • Layers and separation of responsibility are good, and the basic notion that resource selection is performed by the router is a ubiquitously understood - and correct - way of solving that problem in a web application.
  • From the outset, the whole art of implementing Symfony effectively has been putting the right responsibilities into the right places. This is one of the most crucial examples of that. "It works" is not a philosophy of software design.
  • This idea is not new. Far from it. We've argued, discussed, and planned around this routing system for years, with the specific intention of ensuring that it can support exactly this sort of logic.
  • Violating this expectation is a de-encapsulation. The primary value of that encapsulation is to any code wanting to inspect a given Drupal system's set of request vectors for the purpose of of performing meta-operations (though there are other uses). And we WANT this. Examples include:
    • Inspecting the full route set (in an out-of-band operation) for their required assets (css/js) in order to generate a sitewide optimal asset aggregation strategy
    • Creating generic UIs that allow the user to visually inspect the logical IA of their site by introspecting it from the routing system. This is an (attainable) holy grail; contrast it with the cascading UIs provided by context or page_manager, which require descent through at least three separate UIs to see the full picture.
    • Whether or not you agree with subrequests as an approach, one of their important aspects is that they *bypass routing* on the subrequest's pass through the kernel. This is because they operate on the (reasonable) assumption that the parent request knows the resource being accessed, and has already specified it on the request object being sent through to the subrequest. If routing is de-encapsulated, then it's no longer possible to bypass it cleanly. That raises the complexity of everyone's implementation - both those wanting to manipulate routing, and those engaging in routing behaviors outside of the routing context.

At least some of these examples probably represent unfamiliar cases. That's because we've never had such introspection capabilities before, so such things haven't really been feasible. Remember that unfamiliar != unuseful.

Comments

sdboyer’s picture

note that this is largely in reference to the potential approaches that could arise from the work being done in issues like #2286357: Introduce Display Variants, use for the block rendering flow.

larowlan’s picture

Use case four, auto building documentation for a rest api

Crell’s picture

I don't know how we turn this issue into an actionable task, but I agree with the premise of it. The one caveat I'd add is that, in the Symfony model, the result of routing is not "the resource being displayed" but "the controller/action responsible". The resource to be displayed is, frequently, a route parameter at that point. That is, Symfony routing is not a full on WebMachine model but we can sort of use it as one, and we should.

sdboyer’s picture

@Crell - yeah, i'm trying to strengthen that requirement a bit. indeed, they just require a controller be defined...so really, what i'm actually arguing for here is a limit on the acceptable range of behaviors for controllers. it amounts to the same thing.

it's not really gonna be an actionable task, as it's defined in the negative. there's a lot of possible ways in which this might happen, and by having one ticket with the reasons not to do it (and, as @larowlan has nicely added, the nice things we get if we avoid it), it could make it easier to make the counterarguments in assorted issues.

wim leers’s picture

Status: Active » Closed (duplicate)

AFAICT, this was fixed in #2352155: Remove HtmlFragment/HtmlPage, where a new RenderEvents::SELECT_PAGE_DISPLAY_VARIANT was introduced.

However, this issue feels a bit more broad than that, so if this is a premature closing, feel free to reopen.

dsnopek’s picture

Status: Closed (duplicate) » Active

Personally, I think we should keep this open for now. This was more a "warning" about what we shouldn't do when moving functionality from page_manager into Drupal core. And since the book still isn't closed on that (even if it's 8.1.x or 8.2.x, etc), I think we need to still keep this at the back of our minds.

A quick question for @Wim: Looking at the diagram, it appears as though the main content is already selected and built before page variant selection, right?

Anyway, when orginally discussing this issue, we decided that the standards for page_manager in contrib and page_manager in core are different. In contrib, a multi-stage router is not ideal, but acceptable. But it's something we shouldn't introduce to core!

dsnopek’s picture

Title: Avoid introducing a multi-stage routing system » Create a route for every variant and use a "route filter" to apply selection rules
Project: Drupal core » Page Manager
Version: 8.0.x-dev » 8.x-1.x-dev
Component: routing system » Code
Category: Task » Bug report
Issue summary: View changes

At MWDS, @Crell explained to me that some of the new Drupal 8 routing system features were designed specifically to allow Page Manager to be implemented without introducing a multi-stage router.

Moving this issue to Page Manager (because core can already handle this) and updating the issue summary with a concrete plan!

wim leers’s picture

use a "route filter" to apply selection rules
-> this screams 'route' cache context. i.e. please ensure that anything Page Manager caches that happens after (and depends on) this filtering/selection logic, has the 'route' cache context in its cacheability metadata. Definitely any response generated by Page Manager that depends on this filtering/selection logic should have that cache context.

(This is just an FYI for when you reach that point. See https://www.drupal.org/developing/api/8/cache/contexts and related handbook pages for more information.)

dsnopek’s picture

Thanks, Wim! :-)

tim.plunkett’s picture

Priority: Normal » Major

TL;DR
#2551633: Make variants into their own config entity should be blocked on this
#2561339: Consider referring to "routes" rather than "pages" in the UI? could be a direct result on this
This issue will diverge us from D7 architecture and really change the way we think of Page Manager

----

After many lengthy discussions yesterday about the merits of #2551633: Make variants into their own config entity, I think it is important to pursue this issue first.
One major thing we need to think about here is whether we still need the distinct concepts of pages vs variants.
At what point do they just become the same thing?

I'd like to have a call about this, or make it the major focus of our next team call, we need to make a decision on this soon.

wim leers’s picture

One major thing we need to think about here is whether we still need the distinct concepts of pages vs variants.

So, conceptually, if multiple pages exist for the same route, but with different selection criteria, that then just means we go from "selecting a variant of a page to show" to "selecting a page to show".

As a total noob in this area, that sounds much simpler, and probably also easier to explain to site builders, and hopefully can also result in a slightly simpler UI.

dsnopek’s picture

One major thing we need to think about here is whether we still need the distinct concepts of pages vs variants.
At what point do they just become the same thing?

That is a really interesting idea!

I personally can't manage a call this weekend (staying at my in-laws) but we should for sure discuss this at the next SCOTCH meeting.

Crell’s picture

I'd be happy to be part of this conversation, but I'm still in Bulgaria for the next week and Milwaukee next week. When is the next SCOTCH meeting?

tim.plunkett’s picture

Assigned: Unassigned » tim.plunkett

I land in philly about halfway through the meeting, not going to make that

However, working on a patch for this

dsnopek’s picture

Ok, let's discuss at the SCOTCH meeting (Tuesday @ 12pm CDT) with whoever is there, and then try to put together another meeting where we can get Tim (for sure) and Crell (if it works out). Here's a Doodle:

http://doodle.com/poll/qsu4cet8g7nd63y9

If folks are available, we could even do later in the day on Tuesday?

Hopefully, it doesn't have to be next week because it'd be great to have a decision on this as it affects work on lots of other things...

tim.plunkett’s picture

StatusFileSize
new9.71 KB

Just a WIP. Unit tests will fail, so not testing.

dsnopek’s picture

We discussed this issue on a call today, and someone (Kris?) asked the question about how you create a menu item that refers to a Page if the page is made up of multiple routes with unique route names since the menu item takes a route name. We should attempt to first make sure this is possible before merging this patch, and if it is possible, we should create some automated tests to make sure it works.

tim.plunkett’s picture

Status: Active » Needs review
StatusFileSize
new6.3 KB
new15.53 KB

More progress.

phenaproxima’s picture

Here, at @EclipseGc's request, is a test module proving that route filtering will work for this -- https://github.com/phenaproxima/route-filter-test

eclipsegc’s picture

Ok, so my specific worries here are totally allayed by working with Phenaproxima's code. ++ to this issue.

Eclipse

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new15.25 KB
new2.35 KB

Just a small tweak to allow menu links to have somewhere consistent to point to.

tim.plunkett’s picture

VariantRouteFilter has this code:
$contexts = $page->getExecutable()->getContexts();

That dispatches an event that triggers RouteParamContext, which tries to pull the node value from the request attributes.

Just one problem.

Request attributes are not populated until AFTER route filters finish running.

See \Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher::matchRequest().
$attributes = $this->finalMatcher->finalMatch($collection, $request); is what populates $request->attributes, and that is directly after route filtering.

No idea what to do now.

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new16.42 KB
new3.06 KB

Let's implement our own "routing"!

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new16.91 KB
new1.11 KB

We gather the contexts from the event and call setContexts() on the display_variant during selection.

In HEAD, that happens within the controller, and by the time our #pre_render runs it is still the same instance of the display_variant.

With this patch, selection runs during routing, and by the time our #pre_render runs it is a DIFFERENT instance of the display variant, so it has ZERO contexts.

---

So, here's a hack!

tim.plunkett’s picture

StatusFileSize
new4.89 KB
new18.73 KB

Updating a bunch of docs.

tim.plunkett’s picture

StatusFileSize
new28.76 KB
new4.89 KB

Wrote some tests, which revealed some improvements to be made.

tim.plunkett’s picture

StatusFileSize
new10.92 KB

Wrong interdiff.

tim.plunkett’s picture

StatusFileSize
new28.82 KB
new8.83 KB

Consolidated the get/setContext calls.
Also, ensured that we always load the correct page entity.

The only remaining @todos are in \Drupal\page_manager\Routing\VariantRouteFilter::prepareRequest(), will hopefully track down Crell.

tim.plunkett’s picture

StatusFileSize
new8.83 KB
new30.79 KB

Replaced the param converter with running all of the route enhancers.
Next up is the last @todo of preventing request munging.

tim.plunkett’s picture

StatusFileSize
new5.55 KB

Ughhh wrong interdiff again.

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new30.08 KB
new5.78 KB

Fixing tests, duh.

tim.plunkett’s picture

StatusFileSize
new2.46 KB
new30.38 KB

One less @todo, one remaining!

tim.plunkett’s picture

StatusFileSize
new32.3 KB
new4.09 KB

Found a major bug.

If you provide a page with one variant for /node/{node}, it will run on /node/add, despite that route having no page/variant info on it.
The route collection would have 2 routes in it, the correct route for /node/add, and the variant for /node/{node}. The correct route would be removed.
If the /node/{node} variant has a selection condition depending on {node}, it will throw a ContextException.
If it doesn't have any problematic selection conditions, it will be selected, and will 404.

With this patch, when we encounter a route with NO page/variant info, we don't remove it, we move it to the end of the collection.
Additionally, we catch all ContextExceptions and remove those routes from the collection.

This gets us 2/3 of the way there. The final problem is when you have a route with no problematic selection conditions, as stated above...

Status: Needs review » Needs work

The last submitted patch, 38: 2305199-pm-38.patch, failed testing.

dawehner’s picture

+++ b/src/Routing/VariantRouteFilter.php
@@ -0,0 +1,142 @@
+    if ($collection->count()) {
+      $this->prepareRequest($collection, $request);
+    }
+

In the other case you could totally do an early eturn so we maybe could make prepareRequest the non conditional test

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new32.31 KB
new1.18 KB

Great idea!
Still figuring out the fail.

Status: Needs review » Needs work

The last submitted patch, 41: 2305199-pm-41.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new34.96 KB
new5.44 KB

This prevents custom page_manager access from running on overridden routes. We should update the UI to hide that section in that case.

It also does not remove non-page_manager routes from the collection, it just moves the to the end. This helps with the /node/add problem.

Our blocker for that is #2600666: Route definitions using entity type paramconverters with serial IDs should add an integer requirement, which fixes /node/add completely.

Status: Needs review » Needs work

The last submitted patch, 43: 2305199-pm-42.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new37.22 KB
new3.03 KB

Working around 2600666, and adjusting the tests for the access change mentioned in #43.

tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned
StatusFileSize
new42.85 KB
new20.92 KB

Removed the call to finalMatch thanks to some code from @dawehner. \Drupal\page_manager\Routing\RouteAttributes should probably move to CTools (or really to Symfony CMF).

Once I got it all passing, I was able to refactor filter() to not be a rat's nest of nested conditions. Still not ideal using the $page_manager_route_found flag, but it works.

I think this is ready for commit.

  • tim.plunkett committed 555ccec on 8.x-1.x
    Issue #2305199 by tim.plunkett, dsnopek, sdboyer, dawehner, EclipseGc:...
tim.plunkett’s picture

Status: Needs review » Fixed

Committed after in-person review from @EclipseGc.
Thanks to @EclipseGc, @dsnopek, @dawehner, and @sdboyer for help with this!

wim leers’s picture

Status: Fixed » Active

#21+#22: So @phenaproxima's module proves route filtering works. But this is not called at runtime. So… how would this work? Wow, apparently route filters are executed at runtime. I find that quite frightening performance-wise. But D8 core itself uses it too, so… I guess it's okay.

#27: "own routing" -> even more frightening! Especially if this means routing (and param converting) happens twice… Does it happen twice? If not, great, but let's prove that using test coverage? #37 seems to confirm it does happen twice…

I think it's crucial to look at the performance for a route X:

  • without Page Manager
  • with Page Manager not overriding route X
  • with Page Manager overriding route X and having a single variant
  • with Page Manager overriding route X and having multiple variants
dsnopek’s picture

@Wim: There's some things that will run twice because of this change, due to limitations in some core and/or Symfony CMF code. It was decided at the BADCamp sprint that this was acceptable for now, and that we'd open some issues afterwards to fix it. Can we do general performance profiling on a new issue?

@tim.plunkett: Since you're the most familiar with this code, could you create the necessary follow-ups to prevent doing stuff twice? Thanks!

wim leers’s picture

Can we do general performance profiling on a new issue?

Of course.

But I think it's critical to know what the performance impact is. Are we talking say ~1 ms or ~10 ms? Magnitude of impact is what matters most here.

tim.plunkett’s picture

Status: Active » Fixed

I opened #2605282: Ensure that upcasting only runs one extra time. to help us mitigate the impact of this change.

Also opened #2605286: Profile the recent changes to Page Manager routing to do some investigation.

Status: Fixed » Closed (fixed)

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