Commit 149a310c authored by willzyx's avatar willzyx

Issue #2828116 by tlyngej, willzyx, lussoluca: Add Container info page

parent fb432f12
......@@ -69,6 +69,12 @@ devel.run_cron:
menu_name: devel
class: \Drupal\devel\Plugin\Menu\DestinationMenuLink
# Container info
devel.container_info.service:
title: 'Container Info'
route_name: devel.container_info.service
menu_name: devel
# Routes info
devel.route_info:
title: 'Routes Info'
......
......@@ -6,3 +6,13 @@ devel.admin_settings:
route_name: devel.admin_settings
base_route: devel.admin_settings
weight: 0
# Container info
devel.container_info.service:
title: 'Services'
route_name: devel.container_info.service
base_route: devel.container_info.service
devel.container_info.parameter:
title: 'Parameters'
route_name: devel.container_info.parameter
base_route: devel.container_info.service
......@@ -35,11 +35,19 @@ function devel_help($route_name, RouteMatchInterface $route_match) {
$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 Service Container') . '</dt>';
$output .= '<dd>' . t('The module allows you to inspect Services and Parameters registered in the Service Container. You can see those informations on <a href=":url">Container info</a> page.', [':url' => Url::fromRoute('devel.container_info.service')->toString()]) . '</dd>';
$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.container_info.service':
case 'devel.container_info.parameter':
$output = '';
$output .= '<p>' . t('Displays Services and Parameters registered in the Service Container. For more informations on the Service Container, see the <a href=":url">Symfony online documentation</a>.', [':url' => 'http://symfony.com/doc/current/service_container.html']) . '</p>';
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>';
......
......@@ -161,6 +161,47 @@ devel.run_cron:
_permission: 'administer site configuration'
_csrf_token: 'TRUE'
# Container info
devel.container_info.service:
path: '/devel/container/service'
defaults:
_controller: '\Drupal\devel\Controller\ContainerInfoController::serviceList'
_title: 'Container services'
options:
_admin_route: TRUE
requirements:
_permission: 'access devel information'
devel.container_info.service.detail:
path: '/devel/container/service/{service_id}'
defaults:
_controller: '\Drupal\devel\Controller\ContainerInfoController::serviceDetail'
_title: 'Service @service_id detail'
options:
_admin_route: TRUE
requirements:
_permission: 'access devel information'
devel.container_info.parameter:
path: '/devel/container/parameter'
defaults:
_controller: '\Drupal\devel\Controller\ContainerInfoController::parameterList'
_title: 'Container parameters'
options:
_admin_route: TRUE
requirements:
_permission: 'access devel information'
devel.container_info.parameter.detail:
path: '/devel/container/parameter/{parameter_name}'
defaults:
_controller: '\Drupal\devel\Controller\ContainerInfoController::parameterDetail'
_title: 'Parameter @parameter_name value'
options:
_admin_route: TRUE
requirements:
_permission: 'access devel information'
# Route info
devel.route_info:
path: '/devel/routes'
......
<?php
namespace Drupal\devel\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DrupalKernelInterface;
use Drupal\Core\Url;
use Drupal\devel\DevelDumperManagerInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Provides route responses for the container info pages.
*/
class ContainerInfoController extends ControllerBase implements ContainerAwareInterface {
use ContainerAwareTrait;
/**
* The drupal kernel.
*
* @var \Drupal\Core\DrupalKernelInterface
*/
protected $kernel;
/**
* The dumper manager service.
*
* @var \Drupal\devel\DevelDumperManagerInterface
*/
protected $dumper;
/**
* ServiceInfoController constructor.
*
* @param \Drupal\Core\DrupalKernelInterface $drupalKernel
* The drupal kernel.
* @param \Drupal\devel\DevelDumperManagerInterface $dumper
* The dumper manager service.
*/
public function __construct(DrupalKernelInterface $drupalKernel, DevelDumperManagerInterface $dumper) {
$this->kernel = $drupalKernel;
$this->dumper = $dumper;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('kernel'),
$container->get('devel.dumper')
);
}
/**
* Builds the services overview page.
*
* @return array
* A render array as expected by the renderer.
*/
public function serviceList() {
$headers = [
$this->t('ID'),
$this->t('Class'),
$this->t('Alias'),
$this->t('Operations'),
];
$rows = [];
if ($container = $this->kernel->getCachedContainerDefinition()) {
foreach ($container['services'] as $service_id => $definition) {
$service = unserialize($definition);
$row['id'] = [
'data' => $service_id,
'class' => 'table-filter-text-source',
];
$row['class'] = [
'data' => isset($service['class']) ? $service['class'] : '',
'class' => 'table-filter-text-source',
];
$row['alias'] = [
'data' => array_search($service_id, $container['aliases']) ?: '',
'class' => 'table-filter-text-source',
];
$row['operations']['data'] = [
'#type' => 'operations',
'#links' => [
'devel' => [
'title' => $this->t('Devel'),
'url' => Url::fromRoute('devel.container_info.service.detail', ['service_id' => $service_id]),
],
],
];
$rows[$service_id] = $row;
}
ksort($rows);
}
$output['#attached']['library'][] = 'system/drupal.system.modules';
$output['filters'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['table-filter', 'js-show'],
],
];
$output['filters']['text'] = [
'#type' => 'search',
'#title' => $this->t('Search'),
'#size' => 30,
'#placeholder' => $this->t('Enter service id, alias or class'),
'#attributes' => [
'class' => ['table-filter-text'],
'data-table' => '.devel-filter-text',
'autocomplete' => 'off',
'title' => $this->t('Enter a part of the service id, service alias or class to filter by.'),
],
];
$output['services'] = [
'#type' => 'table',
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No services found.'),
'#sticky' => TRUE,
'#attributes' => [
'class' => ['devel-service-list', 'devel-filter-text'],
],
];
return $output;
}
/**
* Returns a render array representation of the service.
*
* @param string $service_id
* The ID of the service to retrieve.
*
* @return array
* A render array containing the service detail.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* If the requested service is not defined.
*/
public function serviceDetail($service_id) {
$instance = $this->container->get($service_id, ContainerInterface::NULL_ON_INVALID_REFERENCE);
if ($instance === NULL) {
throw new NotFoundHttpException();
}
$output = [];
if ($cached_definitions = $this->kernel->getCachedContainerDefinition()) {
// Tries to retrieve the service definition from the kernel's cached
// container definition.
if (isset($cached_definitions['services'][$service_id])) {
$definition = unserialize($cached_definitions['services'][$service_id]);
// If the service has an alias add it to the definition.
if ($alias = array_search($service_id, $cached_definitions['aliases'])) {
$definition['alias'] = $alias;
}
$output['definition'] = $this->dumper->exportAsRenderable($definition, $this->t('Computed Definition'));
}
}
$output['instance'] = $this->dumper->exportAsRenderable($instance, $this->t('Instance'));
return $output;
}
/**
* Builds the parameters overview page.
*
* @return array
* A render array as expected by the renderer.
*/
public function parameterList() {
$headers = [
$this->t('Name'),
$this->t('Operations'),
];
$rows = [];
if ($container = $this->kernel->getCachedContainerDefinition()) {
foreach ($container['parameters'] as $parameter_name => $definition) {
$row['name'] = [
'data' => $parameter_name,
'class' => 'table-filter-text-source',
];
$row['operations']['data'] = [
'#type' => 'operations',
'#links' => [
'devel' => [
'title' => $this->t('Devel'),
'url' => Url::fromRoute('devel.container_info.parameter.detail', ['parameter_name' => $parameter_name]),
],
],
];
$rows[$parameter_name] = $row;
}
ksort($rows);
}
$output['#attached']['library'][] = 'system/drupal.system.modules';
$output['filters'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['table-filter', 'js-show'],
],
];
$output['filters']['text'] = [
'#type' => 'search',
'#title' => $this->t('Search'),
'#size' => 30,
'#placeholder' => $this->t('Enter parameter name'),
'#attributes' => [
'class' => ['table-filter-text'],
'data-table' => '.devel-filter-text',
'autocomplete' => 'off',
'title' => $this->t('Enter a part of the parameter name to filter by.'),
],
];
$output['parameters'] = [
'#type' => 'table',
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No parameters found.'),
'#sticky' => TRUE,
'#attributes' => [
'class' => ['devel-parameter-list', 'devel-filter-text'],
],
];
return $output;
}
/**
* Returns a render array representation of the parameter value.
*
* @param string $parameter_name
* The name of the parameter to retrieve.
*
* @return array
* A render array containing the parameter value.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* If the requested parameter is not defined.
*/
public function parameterDetail($parameter_name) {
try {
$parameter = $this->container->getParameter($parameter_name);
}
catch (ParameterNotFoundException $e) {
throw new NotFoundHttpException();
}
return $this->dumper->exportAsRenderable($parameter);
}
}
<?php
namespace Drupal\Tests\devel\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests container info pages and links.
*
* @group devel
*/
class DevelContainerInfoTest extends BrowserTestBase {
use DevelWebAssertHelper;
/**
* {@inheritdoc}
*/
public static $modules = ['devel', 'devel_test', 'block'];
/**
* The user for tests.
*
* @var \Drupal\user\UserInterface
*/
protected $develUser;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalPlaceBlock('page_title_block');
$this->develUser = $this->drupalCreateUser(['access devel information']);
$this->drupalLogin($this->develUser);
}
/**
* Tests container info menu link.
*/
public function testContainerInfoMenuLink() {
$this->drupalPlaceBlock('system_menu_block:devel');
// Ensures that the events info link is present on the devel menu and that
// it points to the correct page.
$this->drupalGet('');
$this->clickLink('Container Info');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('/devel/container/service');
$this->assertSession()->pageTextContains('Container services');
}
/**
* Tests service list page.
*/
public function testServiceList() {
$this->drupalGet('/devel/container/service');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Container services');
$this->assertContainerInfoLocalTasks();
$page = $this->getSession()->getPage();
// Ensures that the services table is found.
$table = $page->find('css', 'table.devel-service-list');
$this->assertNotNull($table);
// Ensures that the expected table headers are found.
/** @var $headers \Behat\Mink\Element\NodeElement[] */
$headers = $table->findAll('css', 'thead th');
$this->assertEquals(4, count($headers));
$expected_headers = ['ID', 'Class', 'Alias', 'Operations'];
$actual_headers = array_map(function ($element) {
return $element->getText();
}, $headers);
$this->assertSame($expected_headers, $actual_headers);
// Ensures that all the serivices are listed in the table.
$cached_definition = \Drupal::service('kernel')->getCachedContainerDefinition();
$this->assertNotNull($cached_definition);
$rows = $table->findAll('css', 'tbody tr');
$this->assertEquals(count($cached_definition['services']), count($rows));
// Tests the presence of some (arbitrarily chosen) services in the table.
$expected_services = [
'config.factory' => [
'class' => 'Drupal\Core\Config\ConfigFactory',
'alias' => '',
],
'devel.route_subscriber' => [
'class' => 'Drupal\devel\Routing\RouteSubscriber',
'alias' => '',
],
'plugin.manager.element_info' => [
'class' => 'Drupal\Core\Render\ElementInfoManager',
'alias' => 'element_info',
],
];
foreach ($expected_services as $service_id => $expected) {
$row = $table->find('css', sprintf('tbody tr:contains("%s")', $service_id));
$this->assertNotNull($row);
/** @var $cells \Behat\Mink\Element\NodeElement[] */
$cells = $row->findAll('css', 'td');
$this->assertEquals(4, count($cells));
$cell_service_id = $cells[0];
$this->assertEquals($service_id, $cell_service_id->getText());
$this->assertTrue($cell_service_id->hasClass('table-filter-text-source'));
$cell_class = $cells[1];
$this->assertEquals($expected['class'], $cell_class->getText());
$this->assertTrue($cell_class->hasClass('table-filter-text-source'));
$cell_alias = $cells[2];
$this->assertEquals($expected['alias'], $cell_alias->getText());
$this->assertTrue($cell_class->hasClass('table-filter-text-source'));
$cell_operations = $cells[3];
$actual_href = $cell_operations->findLink('Devel')->getAttribute('href');
$expected_href = Url::fromRoute('devel.container_info.service.detail', ['service_id' => $service_id])->toString();
$this->assertEquals($expected_href, $actual_href);
}
// Ensures that the page is accessible ony to users with the adequate
// permissions.
$this->drupalLogout();
$this->drupalGet('devel/container/service');
$this->assertSession()->statusCodeEquals(403);
}
/**
* Tests service detail page.
*/
public function testServiceDetail() {
$service_id = 'devel.dumper';
// Ensures that the page works as expected.
$this->drupalGet("/devel/container/service/$service_id");
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains("Service $service_id detail");
// Ensures that the page returns a 404 error if the requested service is
// not defined.
$this->drupalGet('/devel/container/service/not.exists');
$this->assertSession()->statusCodeEquals(404);
// Ensures that the page is accessible ony to users with the adequate
// permissions.
$this->drupalLogout();
$this->drupalGet("devel/container/service/$service_id");
$this->assertSession()->statusCodeEquals(403);
}
/**
* Tests parameter list page.
*/
public function testParameterList() {
// Ensures that the page works as expected.
$this->drupalGet('/devel/container/parameter');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Container parameters');
$this->assertContainerInfoLocalTasks();
$page = $this->getSession()->getPage();
// Ensures that the parameters table is found.
$table = $page->find('css', 'table.devel-parameter-list');
$this->assertNotNull($table);
// Ensures that the expected table headers are found.
/** @var $headers \Behat\Mink\Element\NodeElement[] */
$headers = $table->findAll('css', 'thead th');
$this->assertEquals(2, count($headers));
$expected_headers = ['Name', 'Operations'];
$actual_headers = array_map(function ($element) {
return $element->getText();
}, $headers);
$this->assertSame($expected_headers, $actual_headers);
// Ensures that all the parameters are listed in the table.
$cached_definition = \Drupal::service('kernel')->getCachedContainerDefinition();
$this->assertNotNull($cached_definition);
$rows = $table->findAll('css', 'tbody tr');
$this->assertEquals(count($cached_definition['parameters']), count($rows));
// Tests the presence of some parameters in the table.
$expected_parameters = [
'authentication_providers',
'cache_bins',
'factory.keyvalue',
'twig.config',
];
foreach ($expected_parameters as $parameter_name) {
$row = $table->find('css', sprintf('tbody tr:contains("%s")', $parameter_name));
$this->assertNotNull($row);
/** @var $cells \Behat\Mink\Element\NodeElement[] */
$cells = $row->findAll('css', 'td');
$this->assertEquals(2, count($cells));
$cell_parameter_name = $cells[0];
$this->assertEquals($parameter_name, $cell_parameter_name->getText());
$this->assertTrue($cell_parameter_name->hasClass('table-filter-text-source'));
$cell_operations = $cells[1];
$actual_href = $cell_operations->findLink('Devel')->getAttribute('href');
$expected_href = Url::fromRoute('devel.container_info.parameter.detail', ['parameter_name' => $parameter_name])->toString();
$this->assertEquals($expected_href, $actual_href);
}
// Ensures that the page is accessible ony to users with the adequate
// permissions.
$this->drupalLogout();
$this->drupalGet('devel/container/service');
$this->assertSession()->statusCodeEquals(403);
}
/**
* Tests parameter detail page.
*/
public function testParameterDetail() {
$parameter_name = 'cache_bins';
// Ensures that the page works as expected.
$this->drupalGet("/devel/container/parameter/$parameter_name");
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains("Parameter $parameter_name value");
// Ensures that the page returns a 404 error if the requested parameter is
// not defined.
$this->drupalGet('/devel/container/parameter/not_exists');
$this->assertSession()->statusCodeEquals(404);
// Ensures that the page is accessible ony to users with the adequate
// permissions.
$this->drupalLogout();
$this->drupalGet("devel/container/service/$parameter_name");
$this->assertSession()->statusCodeEquals(403);
}
/**
* Asserts that container info local tasks are present.
*/
protected function assertContainerInfoLocalTasks() {
$expected_local_tasks = [
['devel.container_info.service', []],
['devel.container_info.parameter', []],
];
$this->assertLocalTasks($expected_local_tasks);
}
}
<?php
namespace Drupal\Tests\devel\Functional;
use Drupal\Core\Url;
/**
* Provides convenience methods for assertions in browser tests.
*/
trait DevelWebAssertHelper {
/**
* Asserts local tasks in the page output.
*
* @param array $routes
* A list of expected local tasks, prepared as an array of route names and
* their associated route parameters, to assert on the page (in the given
* order).
* @param int $level
* (optional) The local tasks level to assert; 0 for primary, 1 for
* secondary. Defaults to 0.
*/
protected function assertLocalTasks(array $routes, $level = 0) {
$type_class = $level == 0 ? 'tabs primary' : 'tabs secondary';
$elements = $this->xpath('//*[contains(@class, :class)]//a', [':class' => $type_class]);
$this->assertTrue(count($elements), 'Local tasks found.');
foreach ($routes as $index => $route_info) {
list($route_name, $route_parameters) = $route_info;
$expected = Url::fromRoute($route_name, $route_parameters)->toString();
$this->assertEquals($expected, $elements[$index]->getAttribute('href'));
}
}
}
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