Commit 049ce37b authored by willzyx's avatar willzyx

Issue #2834390 by willzyx: Add Routes info page

parent d3d4eae8
......@@ -23,11 +23,6 @@ devel.menu_rebuild:
route_name: devel.menu_rebuild
menu_name: devel
class: \Drupal\devel\Plugin\Menu\DestinationMenuLink
devel.menu_item:
title: 'Menu Item'
route_name: devel.menu_item
menu_name: devel
class: \Drupal\devel\Plugin\Menu\MenuItemMenuLink
devel.state_system_page:
title: 'State editor'
description: 'Edit state system values.'
......@@ -73,3 +68,14 @@ devel.run_cron:
route_name: devel.run_cron
menu_name: devel
class: \Drupal\devel\Plugin\Menu\DestinationMenuLink
# Routes info
devel.route_info:
title: 'Routes Info'
route_name: devel.route_info
menu_name: devel
devel.route_info.item:
title: 'Current route info'
route_name: devel.route_info.item
menu_name: devel
class: \Drupal\devel\Plugin\Menu\RouteDetailMenuLink
......@@ -20,13 +20,31 @@ use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\Core\Utility\Error;
/**
* Implements hook_help().
*/
function devel_help($route_name) {
function devel_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.devel':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Devel module provides a suite of modules containing fun for module developers and themers. For more information, see the <a href=":url">online documentation for the Devel module</a>.', [':url' => 'https://www.drupal.org/docs/8/modules/devel']) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Inspecting Routes') . '</dt>';
$output .= '<dd>' . t('The module allows you to inspect routes information, gathering all routing data from <em>.routing.yml</em> files and from classes which subscribe to the route build/alter events. You can see those informations on <a href=":url">Routes info</a> page.', [':url' => Url::fromRoute('devel.route_info')->toString()]) . '</dd>';
$output .= '</dl>';
return $output;
case 'devel.route_info';
$output = '';
$output .= '<p>' . t('Displays registered routes for the site. For a complete overview of the routing system, see the <a href=":url">online documentation</a>.', [':url' => 'https://www.drupal.org/docs/8/api/routing-system']) . '</p>';
return $output;
case 'devel.reinstall':
$output = '<p>' . t('<strong>Warning</strong> - will delete your module tables and configuration.') . '</p>';
$output .= '<p>' . t('Uninstall and then install the selected modules. <code>hook_uninstall()</code> and <code>hook_install()</code> will be executed and the schema version number will be set to the most recent update number.') . '</p>';
......
......@@ -67,16 +67,6 @@ devel.system_state_edit:
requirements:
_permission: 'administer site configuration'
devel.menu_item:
path: '/devel/menu/item'
defaults:
_controller: '\Drupal\devel\Controller\DevelController::menuItem'
_title: 'Menu item'
options:
_admin_route: TRUE
requirements:
_permission: 'access devel information'
devel.theme_registry:
path: '/devel/theme/registry'
defaults:
......@@ -170,3 +160,24 @@ devel.run_cron:
requirements:
_permission: 'administer site configuration'
_csrf_token: 'TRUE'
# Route info
devel.route_info:
path: '/devel/routes'
defaults:
_controller: '\Drupal\devel\Controller\RouteInfoController::routeList'
_title: 'Routes'
options:
_admin_route: TRUE
requirements:
_permission: 'access devel information'
devel.route_info.item:
path: '/devel/routes/item'
defaults:
_controller: '\Drupal\devel\Controller\RouteInfoController::routeDetail'
_title: 'Route detail'
options:
_admin_route: TRUE
requirements:
_permission: 'access devel information'
......@@ -3,13 +3,9 @@
namespace Drupal\devel\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Symfony\Component\HttpFoundation\Request;
/**
* Returns responses for devel module routes.
......@@ -25,41 +21,6 @@ class DevelController extends ControllerBase {
return $this->redirect('<front>');
}
/**
* Returns a dump of a route object.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* Page request object.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*
* @return array
* A render array containing the route object.
*/
public function menuItem(Request $request, RouteMatchInterface $route_match) {
$output = [];
// Get the route object from the path query string if available.
if ($path = $request->query->get('path')) {
try {
/* @var \Symfony\Cmf\Component\Routing\ChainRouter $router */
$router = \Drupal::service('router');
$route = $router->match($path);
$output['route'] = ['#markup' => kpr($route, TRUE)];
}
catch (\Exception $e) {
drupal_set_message($this->t("Unable to load route for url '%url'", ['%url' => $path]), 'warning');
}
}
// No path specified, get the current route.
else {
$route = $route_match->getRouteObject();
$output['route'] = ['#markup' => kpr($route, TRUE)];
}
return $output;
}
public function themeRegistry() {
$hooks = theme_get_registry();
ksort($hooks);
......
<?php
namespace Drupal\devel\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Url;
use Drupal\devel\DevelDumperManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouterInterface;
/**
* Provides route responses for the route info pages.
*/
class RouteInfoController extends ControllerBase {
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The router service.
*
* @var \Symfony\Component\Routing\RouterInterface
*/
protected $router;
/**
* The dumper service.
*
* @var \Drupal\devel\DevelDumperManagerInterface
*/
protected $dumper;
/**
* RouterInfoController constructor.
*
* @param \Drupal\Core\Routing\RouteProviderInterface $provider
* The route provider.
* @param \Symfony\Component\Routing\RouterInterface $router
* The router service.
* @param \Drupal\devel\DevelDumperManagerInterface $dumper
* The dumper service.
*/
public function __construct(RouteProviderInterface $provider, RouterInterface $router, DevelDumperManagerInterface $dumper) {
$this->routeProvider = $provider;
$this->router = $router;
$this->dumper = $dumper;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('router.route_provider'),
$container->get('router.no_access_checks'),
$container->get('devel.dumper')
);
}
/**
* Builds the routes overview page.
*
* @return array
* A render array as expected by the renderer.
*/
public function routeList() {
$headers = [
$this->t('Route Name'),
$this->t('Path'),
$this->t('Allowed Methods'),
$this->t('Operations'),
];
$rows = [];
foreach ($this->routeProvider->getAllRoutes() as $route_name => $route) {
$row['name'] = [
'data' => $route_name,
'class' => 'table-filter-text-source',
];
$row['path'] = [
'data' => $route->getPath(),
'class' => 'table-filter-text-source',
];
$row['methods']['data'] = [
'#theme' => 'item_list',
'#items' => $route->getMethods(),
'#empty' => $this->t('ANY'),
'#context' => ['list_style' => 'comma-list'],
];
// We cannot resolve routes with dynamic parameters from route path. For
// these routes we pass the route name.
// @see ::routeItem()
if (strpos($route->getPath(), '{') !== FALSE) {
$parameters = ['query' => ['route_name' => $route_name]];
}
else {
$parameters = ['query' => ['path' => $route->getPath()]];
}
$row['operations']['data'] = [
'#type' => 'operations',
'#links' => [
'devel' => [
'title' => $this->t('Devel'),
'url' => Url::fromRoute('devel.route_info.item', [], $parameters),
],
],
];
$rows[] = $row;
}
$output['#attached']['library'][] = 'system/drupal.system.modules';
$output['filters'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['table-filter', 'js-show'],
],
];
$output['filters']['name'] = [
'#type' => 'search',
'#title' => $this->t('Search'),
'#size' => 30,
'#placeholder' => $this->t('Enter route name or path'),
'#attributes' => [
'class' => ['table-filter-text'],
'data-table' => '.devel-filter-text',
'autocomplete' => 'off',
'title' => $this->t('Enter a part of the route name or path to filter by.'),
],
];
$output['routes'] = [
'#type' => 'table',
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No routes found.'),
'#sticky' => TRUE,
'#attributes' => [
'class' => ['devel-route-list', 'devel-filter-text'],
],
];
return $output;
}
/**
* Returns a render array representation of the route object.
*
* The method tries to resolve the route from the 'path' or the 'route_name'
* query string value if available. If no route is retrieved from the query
* string parameters it fallbacks to the current route.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*
* @return array
* A render array as expected by the renderer.
*/
public function routeDetail(Request $request, RouteMatchInterface $route_match) {
$route = NULL;
// Get the route object from the path query string if available.
if ($path = $request->query->get('path')) {
try {
$route = $this->router->match($path);
}
catch (\Exception $e) {
drupal_set_message($this->t("Unable to load route for url '%url'", ['%url' => $path]), 'warning');
}
}
// Get the route object from the route name query string if available and
// the route is not retrieved by path.
if ($route === NULL && $route_name = $request->query->get('route_name')) {
try {
$route = $this->routeProvider->getRouteByName($route_name);
}
catch (\Exception $e) {
drupal_set_message($this->t("Unable to load route '%name'", ['%name' => $route_name]), 'warning');
}
}
// No route retrieved from path or name specified, get the current route.
if ($route === NULL) {
$route = $route_match->getRouteObject();
}
return $this->dumper->exportAsRenderable($route);
}
}
......@@ -2,28 +2,10 @@
namespace Drupal\devel\Plugin\Menu;
use Drupal\Core\Menu\MenuLinkDefault;
use Drupal\Core\Url;
/**
* Modifies the menu link to add current route path.
*
* @deprecated in Devel 8.1.0-beta1, will be removed before Devel 8.1.0.
* Use \Drupal\devel\Plugin\Menu\RouteDetailMenuLink instead.
*/
class MenuItemMenuLink extends MenuLinkDefault {
/**
* {@inheritdoc}
*/
public function getOptions() {
$options = parent::getOptions();
$options['query']['path'] = '/' . Url::fromRoute('<current>')->getInternalPath();
return $options;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return 0;
}
}
class MenuItemMenuLink extends RouteDetailMenuLink {}
<?php
namespace Drupal\devel\Plugin\Menu;
use Drupal\Core\Menu\MenuLinkDefault;
use Drupal\Core\Url;
/**
* Modifies the menu link to add current route path.
*/
class RouteDetailMenuLink extends MenuLinkDefault {
/**
* {@inheritdoc}
*/
public function getOptions() {
$options = parent::getOptions();
$options['query']['path'] = '/' . Url::fromRoute('<current>')->getInternalPath();
return $options;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return 0;
}
}
......@@ -106,42 +106,4 @@ class DevelMenuLinksTest extends WebTestBase {
$this->assertUrl($url);
}
/**
* Tests menu item link.
*/
public function testMenuItemLink() {
// Ensures that devel menu item works properly.
$url = $this->develUser->toUrl();
$path = '/' . $url->getInternalPath();
$this->drupalGet($url);
$this->clickLink(t('Menu Item'));
$this->assertResponse(200);
$this->assertText('Menu item');
$this->assertUrl('devel/menu/item', ['query' => ['path' => $path]]);
// Ensures that devel menu item works properly even when dynamic cache is
// enabled.
$url = Url::fromRoute('devel.simple_page');
$path = '/' . $url->getInternalPath();
$this->drupalGet($url);
$this->clickLink(t('Menu Item'));
$this->assertResponse(200);
$this->assertText('Menu item');
$this->assertUrl('devel/menu/item', ['query' => ['path' => $path]]);
// Ensures that if no 'path' query string is passed devel menu item does
// not return errors.
$this->drupalGet('devel/menu/item');
$this->assertResponse(200);
$this->assertText('Menu item');
// Ensures that devel menu item is accessible ony to users with the
// adequate permissions.
$this->drupalLogout();
$this->drupalGet('devel/menu/item');
$this->assertResponse(403);
}
}
<?php
namespace Drupal\Tests\devel\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests routes info pages and links.
*
* @group devel
*/
class DevelRouteInfoTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['devel', 'devel_test', 'block'];
/**
* The user for the test.
*
* @var \Drupal\user\UserInterface
*/
protected $develUser;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('system_menu_block:devel');
$this->drupalPlaceBlock('page_title_block');
$this->develUser = $this->drupalCreateUser(['access devel information']);
$this->drupalLogin($this->develUser);
}
/**
* Tests routes info.
*/
public function testRouteList() {
// Ensures that the routes info link is present on the devel menu and that
// it points to the correct page.
$this->drupalGet('');
$this->clickLink('Routes Info');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('/devel/routes');
$this->assertSession()->pageTextContains('Routes');
$page = $this->getSession()->getPage();
// Ensures that the expected table headers are found.
/** @var $headers \Behat\Mink\Element\NodeElement[] */
$headers = $page->findAll('css', 'table.devel-route-list thead th');
$this->assertEquals(4, count($headers));
$expected_items = ['Route Name', 'Path', 'Allowed Methods', 'Operations'];
foreach ($headers as $key => $element) {
$this->assertSame($element->getText(), $expected_items[$key]);
}
// Ensures that all the routes are listed in the table.
$routes = \Drupal::service('router.route_provider')->getAllRoutes();
$rows = $page->findAll('css', 'table.devel-route-list tbody tr');
$this->assertEquals(count($routes), count($rows));
// Tests the presence of some (arbitrarily chosen) routes in the table.
$expected_routes = [
'<current>' => [
'path' => '/<current>',
'methods' => ['GET', 'POST'],
'dynamic' => FALSE,
],
'user.login.http' => [
'path' => '/user/login',
'methods' => ['POST'],
'dynamic' => FALSE,
],
'entity.user.canonical' => [
'path' => '/user/{user}',
'methods' => ['GET', 'POST'],
'dynamic' => TRUE,
],
'entity.user.devel_load' => [
'path' => '/devel/user/{user}',
'methods' => ['ANY'],
'dynamic' => TRUE,
],
];
foreach ($expected_routes as $route_name => $expected) {
$row = $page->find('css', sprintf('table.devel-route-list tbody tr:contains("%s")', $route_name));
$this->assertNotNull($row);
/** @var $cells \Behat\Mink\Element\NodeElement[] */
$cells = $row->findAll('css', 'td');
$this->assertEquals(4, count($cells));
$cell_route_name = $cells[0];
$this->assertEquals($route_name, $cell_route_name->getText());
$this->assertTrue($cell_route_name->hasClass('table-filter-text-source'));
$cell_path = $cells[1];
$this->assertEquals($expected['path'], $cell_path->getText());
$this->assertTrue($cell_path->hasClass('table-filter-text-source'));
$cell_methods = $cells[2];
$this->assertEquals(implode('', $expected['methods']), $cell_methods->getText());
$cell_operations = $cells[3];
$actual_href = $cell_operations->findLink('Devel')->getAttribute('href');
if ($expected['dynamic']) {
$parameters = ['query' => ['route_name' => $route_name]];
}
else {
$parameters = ['query' => ['path' => $expected['path']]];
}
$expected_href = Url::fromRoute('devel.route_info.item', [], $parameters)->toString();
$this->assertEquals($expected_href, $actual_href);
}
// Ensures that the page is accessible only to the users with the adequate
// permissions.
$this->drupalLogout();
$this->drupalGet('devel/routes');
$this->assertSession()->statusCodeEquals(403);
}
/**
* Tests route detail page.
*/
public function testRouteDetail() {
$expected_title = 'Route detail';
$xpath_warning_messages = '//div[contains(@class, "messages--warning")]';
// Ensures that devel route detail link in the menu works properly.
$url = $this->develUser->toUrl();
$path = '/' . $url->getInternalPath();
$this->drupalGet($url);
$this->clickLink('Current route info');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($expected_title);
$expected_url = Url::fromRoute('devel.route_info.item', [], ['query' => ['path' => $path]]);
$this->assertSession()->addressEquals($expected_url);
$this->assertSession()->elementNotExists('xpath', $xpath_warning_messages);
// Ensures that devel route detail works properly even when dynamic cache
// is enabled.
$url = Url::fromRoute('devel.simple_page');
$path = '/' . $url->getInternalPath();
$this->drupalGet($url);
$this->clickLink('Current route info');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($expected_title);
$expected_url = Url::fromRoute('devel.route_info.item', [], ['query' => ['path' => $path]]);
$this->assertSession()->addressEquals($expected_url);
$this->assertSession()->elementNotExists('xpath', $xpath_warning_messages);
// Ensures that if a non existent path is passed as input, a warning
// message is shown.
$this->drupalGet('devel/routes/item', ['query' => ['path' => '/undefined']]);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($expected_title);
$this->assertSession()->elementExists('xpath', $xpath_warning_messages);
// Ensures that the route detail page works properly when a valid route
// name input is passed.
$this->drupalGet('devel/routes/item', ['query' => ['route_name' => 'devel.simple_page']]);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($expected_title);
$this->assertSession()->elementNotExists('xpath', $xpath_warning_messages);
// Ensures that if a non existent route name is passed as input a warning
// message is shown.
$this->drupalGet('devel/routes/item', ['query' => ['route_name' => 'not.exists']]);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($expected_title);
$this->assertSession()->elementExists('xpath', $xpath_warning_messages);
// Ensures that if no 'path' nor 'name' query string is passed as input,
// devel route detail page does not return errors.
$this->drupalGet('devel/routes/item');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($expected_title);
// Ensures that the page is accessible ony to the users with the adequate
// permissions.
$this->drupalLogout();
$this->drupalGet('devel/routes/item');
$this->assertSession()->statusCodeEquals(403);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment