Updated: Comment #N

Problem/Motivation

Since Drupal 7, there is a hook_admin_paths() to define which menu links/routes are "admin" and which aren't. We use that logic for various things, including selecting the active theme and soon for route preloading. But that's silly to have a hook for, as it moves the definition of admin-path far away from the actual definition of the routes themselves.

Proposed resolution

Replace the hook with a boolean property on the route definition.

my.route:
  options:
    _admin_route: TRUE
  ...

Remaining tasks

Get the patch passing and committed.

One could argue that we shouldn't have yet-another-boolean-toggle and should instead define some way of letting routes classify themselves arbitrarily (to "admin", "front-end", "rest only", or whatever), but that requires more thought. @Crell is OK with proceeding as-is for now. (Adding route tagging/categories is probably an 8.1-safe thing to do.)

User interface changes

None.

API changes

hook_admin_paths() goes away, and is replaced by a property on the route definition.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

TR’s picture

I'm willing to write a patch to remove hook_admin_paths() and hook_admin_paths_alter() - I'd just like some feedback from the community first about whether I'm way off base here or not.

westie’s picture

Seems to make a lot of sense to me. Does anyone know if there is a technical reason the functions were spilt?

gagarine’s picture

This make lot more sens than the hook_admin_paths. With hook_admin_paths I can find a way to create set admin theme to false only for a content type on edit...

Damien Tournoud’s picture

That sounds good, but we currently do *not* save arbitrary metadata attached to menu routers. I would definitely support doing so.

RobLoach’s picture

What determines whether a path is an "admin path" though? I'd say that's up to the menu's user_access() callback to decide. We should get rid of the hook entirely and use the menu's user_access() context.

gagarine’s picture

admin path mean you use the admin theme it have nothing to do with access. So what about using a theme key.

/**
* Implements hook_menu().
*/
function user_menu() {
  $items['user/%user/edit'] = array(
    'title' => 'Edit',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('user_profile_form', 1),
    'access callback' => 'user_edit_access',
    'access arguments' => array(1),
    'theme' => admin,        //   <- this line has been added
    'type' => MENU_LOCAL_TASK,
    'file' => 'user.pages.inc',
  );
}

So in the theme key you can chose a theme or use "admin" to chose the admin theme and "default" for the default one (or just not give the key)

sun’s picture

Title: Why do we need hook_admin_paths()? » Convert hook_admin_paths() into declarative properties on routes
Component: menu system » routing system
Category: Feature request » Task
Issue summary: View changes
Related issues: +#1032700: Add caching to hook_admin_paths()

Related issue: #1032700-27: Add caching to hook_admin_paths()

In D8, we should convert these declarations into properties on routes, and only keep the alter hook (or equivalent) for dynamic or page/route-specific adjustments.

nod_’s picture

For info path_is_admin() is now used by the back to site button which replaces the overlay. We rely on it to know if the current page is an admin page or not and display the button appropriately. As long as there is a way to know if current_path() is admin or not we're cool.

dawehner’s picture

Status: Active » Needs review
Issue tags: +WSCCI, +Routing
FileSize
24.26 KB

I am not really sure how to deal with user/%/translations taxonomy_term/%/translations ... it feels like we might should just set it on all content/config translation routes.

Status: Needs review » Needs work

The last submitted patch, 9: admin_route-1316692.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
24.53 KB
425 bytes

I cannot even remember on how many issues I added that bit.

The last submitted patch, 11: admin_route-1316692.patch, failed testing.

tim.plunkett’s picture

The last patch seems like a good direction to go in. The issue summary is way out of date though.

Crell’s picture

Issue summary: View changes

I've updated the summary based on the latest patch. This is entirely sensible to me.

dawehner’s picture

FileSize
25.39 KB

Rerolled.

Status: Needs review » Needs work

The last submitted patch, 15: admin_paths-1316692.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
25.47 KB
4.5 KB

I fixed up a couple things...

It'd be nice to have the internals of path_is_admin moved somewhere, like AdminContext::isAdminPath($path), but I didn't want to make AdminContext dependent on the router...

Also we should probably go ahead and make an interface for AdminContext...

Status: Needs review » Needs work

The last submitted patch, 17: admin-path-1316692-17.patch, failed testing.

tim.plunkett’s picture

The new d.o comment form is awesome!

tim.plunkett’s picture

Except the part where it misattributes stuff. #19 and #20 are tim.plunkett.

Status: Needs review » Needs work

The last submitted patch, 19: admin-path-1316692-19.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
12.21 KB
34.63 KB

Okay, had to add code to handle the node setting.

Status: Needs review » Needs work

The last submitted patch, 22: hook_admin_path-1316692-22.patch, failed testing.

The last submitted patch, 22: hook_admin_path-1316692-22.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
35.4 KB
2.54 KB

Wrong interface!

Status: Needs review » Needs work

The last submitted patch, 25: admin-paths-1316692-25.patch, failed testing.

dawehner’s picture

25: admin-paths-1316692-25.patch queued for re-testing.

The last submitted patch, 25: admin-paths-1316692-25.patch, failed testing.

tim.plunkett’s picture

FileSize
36.22 KB
819 bytes

If LanguageRequestSubscriber runs before the router, it doesn't have the route object available, and then LanguageNegotiationUserAdmin will break when it calls isAdminRoute().

I'm not sure if there are other service priorities that need to be adjusted, or if this makes a loop between the router and language, but we'll find out soon.

tim.plunkett’s picture

Status: Needs work » Needs review
dawehner’s picture

+++ b/core/lib/Drupal/Core/Routing/AdminContext.php
@@ -0,0 +1,64 @@
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[RoutingEvents::ALTER] = array('onAlterRoutes', -10);
+    return $events;
+  }

Is that a leftover?

Status: Needs review » Needs work

The last submitted patch, 29: admin-paths-1316692-29.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
36.16 KB
4.11 KB

This is unfortunate, but it is only needed because the language *has* to come before the RouterListener. I think we're better off special casing the language here than making the AdminContext class dependent on the router.

Status: Needs review » Needs work

The last submitted patch, 33: admin-paths-1316692-33.patch, failed testing.

The last submitted patch, 33: admin-paths-1316692-33.patch, failed testing.

tim.plunkett’s picture

33: admin-paths-1316692-33.patch queued for re-testing.

dawehner’s picture

We seriously cannot run the router twice. Do we know whether we actually need to be able to set the language like that?

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
36.05 KB
3.6 KB

Trying this just for fun. I think I may have misunderstood @dawehner in IRC, but let's see what fails.

Status: Needs review » Needs work

The last submitted patch, 38: admin-paths-1316692-38.patch, failed testing.

dawehner’s picture

Just an idea: You could try to call LanguageNegotiatorInterface::reset() on the second running listener, this might fix some of the negotiation.

tim.plunkett’s picture

Assigned: Unassigned » tim.plunkett

I want to take a look at this again soon.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
36.29 KB

Reroll of #33, just to get back to passing.

tim.plunkett’s picture

Assigned: tim.plunkett » Crell
Priority: Normal » Major
+++ b/core/modules/user/lib/Drupal/user/Plugin/LanguageNegotiation/LanguageNegotiationUserAdmin.php
@@ -29,6 +34,46 @@ class LanguageNegotiationUserAdmin extends LanguageNegotiationMethodBase {
+  /**
+   * The router.
+   *
+   * This is only used when called from an event subscriber, before the request
+   * has been populated with the route info.
+   *
+   * @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface
+   */
+  protected $router;
...
+   * @param \Symfony\Component\Routing\Matcher\UrlMatcherInterface $router
+   *   The router.
...
+    $this->router = $router;
...
+      $container->get('router')

@@ -51,17 +96,22 @@ public function getLangcode(Request $request = NULL) {
+      // If called from an event subscriber, the request may not the route info
+      // yet, so use the router to look up the path first.
+      if (!$route_object = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) {
+        $attributes = $this->router->match('/' . urldecode(trim($request->getPathInfo(), '/')));
+        $route_object = $attributes[RouteObjectInterface::ROUTE_OBJECT];
+      }

These are the parts @dawehner objects to, and I have no idea how to work around.

Crell’s picture

  1. +++ b/core/lib/Drupal/Core/Routing/AdminContext.php
    @@ -0,0 +1,56 @@
    +  public function isAdminRoute(Route $route = NULL) {
    +    if (!$route) {
    +      $route = $this->route;
    +      if (!$route) {
    +        return FALSE;
    +      }
    +    }
    +    return (bool) $route->getOption('_admin_route');
    +  }
    

    This is a service. Pass in the route. Don't provide a stateful, state-changing default. That way lies madness.

  2. +++ b/core/lib/Drupal/Core/Routing/RouteSubscriberBase.php
    @@ -25,8 +25,7 @@
    -  protected function alterRoutes(RouteCollection $collection, $provider) {
    -  }
    +  abstract protected function alterRoutes(RouteCollection $collection, $provider);
    

    Why? Does this not force people to implement it?

  3. +++ b/core/modules/node/lib/Drupal/node/EventSubscriber/NodeAdminRouteSubscriber.php
    @@ -0,0 +1,49 @@
    +  protected function alterRoutes(RouteCollection $collection, $provider) {
    +    if ($this->configFactory->get('node.settings')->get('use_admin_theme')) {
    +      foreach ($collection->all() as $route) {
    +        if ($route->hasOption('_node_admin_route')) {
    +          $route->setOption('_admin_route', TRUE);
    +        }
    +      }
    +    }
    +  }
    +
    

    Hrm. This makes me wonder if we really do need a route_type or something instead of multiple booleans. I won't block this issue on that question but I think we'll be there soon.

  4. +++ b/core/modules/user/lib/Drupal/user/Plugin/LanguageNegotiation/LanguageNegotiationUserAdmin.php
    @@ -51,17 +96,22 @@ public function getLangcode(Request $request = NULL) {
    +    if ($request && $this->adminContext) {
    +      // If called from an event subscriber, the request may not the route info
    +      // yet, so use the router to look up the path first.
    +      if (!$route_object = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) {
    +        $attributes = $this->router->match('/' . urldecode(trim($request->getPathInfo(), '/')));
    +        $route_object = $attributes[RouteObjectInterface::ROUTE_OBJECT];
    +      }
    +      $result = $this->adminContext->isAdminRoute($route_object);
    

    I agree this feels very wrong. The problem is that this class is depending on where in the request it's called. That suggests the problem is elsewhere; eg, why is this class being called from unknown places? Should it be different classes then?

David_Rothstein’s picture

The original rationale for a hook was performance (https://drupal.org/node/517688?page=1#comment-2171240).

Does the patch here avoid that by removing the path_get_admin_paths() functionality altogether? ... So would something like Overlay be impossible in Drupal 8?

dawehner’s picture

No, it uses routes instead of paths. For admin paths it sets a flag during compile-time of the routes, so there is no performance needed during runtime.

Crell’s picture

Assigned: Crell » tim.plunkett
tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned
Issue tags: +D8MI
Related issues: +#2103301: Add a helper class to introspect a given request
FileSize
36.26 KB

#44.1
Yes, it's a service now, but it probably doesn't need to be. I think it could actually build on top of #2103301: Add a helper class to introspect a given request nicely...

#44.2
RouteSubscriberBase has devolved into a base class that is only useful for those wanting to alter routes. But that said, I don't know why @dawehner made that change here.

#44.3
In all of core and contrib, this is the only other very common pattern (did the user choose to use the admin theme for content creation).
And classes like this will all boil it down to _admin_route anyway.

#44.4

eg, why is this class being called from unknown places

The places are not unknown. In fact, I state right here where the problem is: If called from an event subscriber
LanguageNegotiationUserAdmin determines what language to use, which is required by the URL generator any other early services (think /de/admin/content), and yet LanguageNegotiationUserAdmin also needs to know if this is an admin path.

So what do we do? How do we break that circle?

This is just a reroll of #42 to keep it fresh, no changes.

catch’s picture

@David the reason there's no longer a performance penalty is because we already have that penalty - routes have to be loaded to generate internal URLs, which is exactly what the hook was avoiding in 7.x. #2058845: Pre-load non-admin routes and #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees should claw some of that back though.

David_Rothstein’s picture

@David the reason there's no longer a performance penalty is because we already have that penalty - routes have to be loaded to generate internal URLs, which is exactly what the hook was avoiding in 7.x

Ah, thanks, that's what I was missing. It looks like even calling url('some/path') now loads the route...

So assuming that's there to stay, I agree there's no performance hit on top of it here, nor any need for path_get_admin_paths() to stay (which would presumably be a big performance hit if it did because it would need to load, well, everything)... modules which want to identify every admin path link on the current page should have other ways to do that going forward if the route is already available when the link is generated.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Yeah, in case we get the other issue in we can easily reiterate.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 48: hook_admin_paths-1316692-48.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
36.27 KB

Patch context conflict with #2219891: Duplicate declaration of hook_menu_link_defaults(), no changes.

dawehner’s picture

back to RTBC

catch’s picture

+++ b/core/modules/node/lib/Drupal/node/EventSubscriber/NodeAdminRouteSubscriber.php
@@ -0,0 +1,49 @@
+  protected function alterRoutes(RouteCollection $collection, $provider) {
+    if ($this->configFactory->get('node.settings')->get('use_admin_theme')) {
+      foreach ($collection->all() as $route) {
+        if ($route->hasOption('_node_admin_route')) {
+          $route->setOption('_admin_route', TRUE);
+        }
+      }
+    }
+  }
+
+}

It's a bit minor but could this use something like _node_operation_route instead of _node_admin_route? I didn't immediately figure out why we need both _admin_route and _node_admin_route. Cannot think of another way to do it that's better than this, but something without 'admin' would make it clearer that this might or might not be treated as administrative depending on the site.

tim.plunkett’s picture

FileSize
36.31 KB
3.33 KB

Fine by me. And _node_operation_route is only converted to _admin_route when configured to, otherwise its non-admin. Makes sense.

dawehner’s picture

+1 for the better name.

  • Commit 2c2c1ad on 8.x by catch:
    Issue #1316692 by tim.plunkett, dawehner: Convert hook_admin_paths()...
catch’s picture

Title: Convert hook_admin_paths() into declarative properties on routes » Change record: Convert hook_admin_paths() into declarative properties on routes
Priority: Major » Critical
Status: Reviewed & tested by the community » Active
Issue tags: +Missing change record

I committed/pushed this to 8.x, then ten seconds later remembered to look for a draft change notice, which this does not have. So doing old style I guess..

znerol’s picture

index 802d143..c881a36 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -276,6 +276,10 @@ services:
     class: Symfony\Component\Routing\RequestContext
     calls:
       - [fromRequest, ['@request']]
+  router.admin_context:
+    class: Drupal\Core\Routing\AdminContext
+    calls:
+      - [setRequest, ['@?request=']]
   router.route_provider:
     class: Drupal\Core\Routing\RouteProvider
     arguments: ['@database', '@router.builder']

New code should use the request_stack service instead of the request service. Can we have a (trivial) follow-up on that?

andypost’s picture

also field ui subscriber does not set admin option

kim.pepper’s picture

Title: Change record: Convert hook_admin_paths() into declarative properties on routes » Convert hook_admin_paths() into declarative properties on routes
Status: Active » Fixed
Issue tags: -Missing change record

Change record created [#2224207]

dawehner’s picture

@znerol
You are totally right here. You can create one and tag it as "Quickfix".

@kim
It would be actually also be great if your example could include an addition to menu links instead of just the removal part of it.

kim.pepper’s picture

@dawehner Done!

swentel’s picture

We forgot admin/content and all manage fields / form / display paths, they don't show up in the admin screen.

andypost’s picture

kfritsche’s picture

New follow-up for translations paths #2225577: Add _admin_route for translations

znerol’s picture

Follow up for the request to request_stack conversion: #2225539: Use request stack in admin context service

Status: Fixed » Closed (fixed)

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