Commit feac050a authored by willzyx's avatar willzyx

Issue #2836958 by willzyx: Add Event(dispatcher) info page

parent 065551bf
......@@ -85,3 +85,9 @@ devel.route_info.item:
route_name: devel.route_info.item
menu_name: devel
class: \Drupal\devel\Plugin\Menu\RouteDetailMenuLink
# Event info
devel.event_info:
title: 'Events Info'
route_name: devel.event_info
menu_name: devel
......@@ -39,6 +39,8 @@ function devel_help($route_name, RouteMatchInterface $route_match) {
$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 .= '<dt>' . t('Inspecting Events') . '</dt>';
$output .= '<dd>' . t('The module allow you to inspect listeners registered in the event dispatcher. You can see those informations on <a href=":url">Events info</a> page.', [':url' => Url::fromRoute('devel.event_info')->toString()]) . '</dd>';
$output .= '</dl>';
return $output;
......@@ -53,6 +55,11 @@ function devel_help($route_name, RouteMatchInterface $route_match) {
$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.event_info':
$output = '';
$output .= '<p>' . t('Displays events and listeners registered in the event dispatcher. For a complete overview of the event system, see the <a href=":url">Symfony online documentation</a>.', [':url' => 'http://symfony.com/doc/current/components/event_dispatcher.html']) . '</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>';
......
......@@ -222,3 +222,14 @@ devel.route_info.item:
_admin_route: TRUE
requirements:
_permission: 'access devel information'
# Event info
devel.event_info:
path: '/devel/events'
defaults:
_controller: '\Drupal\devel\Controller\EventInfoController::eventList'
_title: 'Events'
options:
_admin_route: TRUE
requirements:
_permission: 'access devel information'
<?php
namespace Drupal\devel\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Provides route responses for the event info page.
*/
class EventInfoController extends ControllerBase {
/**
* Event dispatcher service.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* EventInfoController constructor.
*
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* Event dispatcher service.
*/
public function __construct(EventDispatcherInterface $event_dispatcher) {
$this->eventDispatcher = $event_dispatcher;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('event_dispatcher')
);
}
/**
* Builds the events overview page.
*
* @return array
* A render array as expected by the renderer.
*/
public function eventList() {
$headers = [
'name' => [
'data' => $this->t('Event Name'),
'class' => 'visually-hidden',
],
'callable' => $this->t('Callable'),
'priority' => $this->t('Priority'),
];
$event_listeners = $this->eventDispatcher->getListeners();
ksort($event_listeners);
$rows = [];
foreach ($event_listeners as $event_name => $listeners) {
$rows[][] = [
'data' => $event_name,
'class' => 'table-filter-text-source devel-event-name-header',
'colspan' => '3',
'header' => TRUE,
];
foreach ($listeners as $priority => $listener) {
$row['name'] = [
'data' => $event_name,
'class' => 'table-filter-text-source visually-hidden',
];
$row['class'] = [
'data' => $this->resolveCallableName($listener),
];
$row['priority'] = [
'data' => $priority,
];
$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 event name'),
'#attributes' => [
'class' => ['table-filter-text'],
'data-table' => '.devel-filter-text',
'autocomplete' => 'off',
'title' => $this->t('Enter a part of the event name to filter by.'),
],
];
$output['events'] = [
'#type' => 'table',
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No events found.'),
'#attributes' => [
'class' => ['devel-event-list', 'devel-filter-text'],
],
];
return $output;
}
/**
* Helper function for resolve callable name.
*
* @param mixed $callable
* The for which resolve the name. Can be either the name of a function
* stored in a string variable, or an object and the name of a method
* within the object.
*
* @return string
* The resolved callable name or an empty string.
*/
protected function resolveCallableName($callable) {
if (is_callable($callable, TRUE, $callable_name)) {
return $callable_name;
}
return '';
}
}
<?php
namespace Drupal\Tests\devel\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests event info pages and links.
*
* @group devel
*/
class DevelEventInfoTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['devel', 'block'];
/**
* The user for the test.
*
* @var \Drupal\user\UserInterface
*/
protected $develUser;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('page_title_block');
$this->develUser = $this->drupalCreateUser(['access devel information']);
$this->drupalLogin($this->develUser);
}
/**
* Tests event info menu link.
*/
public function testEventsInfoMenuLink() {
$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('Events Info');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('/devel/events');
$this->assertSession()->pageTextContains('Events');
}
/**
* Tests event info page.
*/
public function testEventList() {
$event_dispatcher = \Drupal::service('event_dispatcher');
$this->drupalGet('/devel/events');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Events');
$page = $this->getSession()->getPage();
// Ensures that the event table is found.
$table = $page->find('css', 'table.devel-event-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(3, count($headers));
$expected_headers = ['Event Name', 'Callable', 'Priority'];
$actual_headers = array_map(function ($element) {
return $element->getText();
}, $headers);
$this->assertSame($expected_headers, $actual_headers);
// Ensures that all the events are listed in the table.
$events = $event_dispatcher->getListeners();
$event_header_row = $table->findAll('css', 'tbody tr th.devel-event-name-header');
$this->assertEquals(count($events), count($event_header_row));
// Tests the presence of some (arbitrarily chosen) events and related
// listeners in the table. The event items are tested dynamically so no
// test failures are expected if listeners change.
$expected_events = [
'config.delete',
'kernel.request',
'routing.route_alter',
];
foreach ($expected_events as $event_name) {
$listeners = $event_dispatcher->getListeners($event_name);
// Ensures that the event header is present in the table.
$event_header_row = $table->findAll('css', sprintf('tbody tr th:contains("%s")', $event_name));
$this->assertNotNull($event_header_row);
$this->assertEquals(1, count($event_header_row));
// Ensures that all the event listener are listed in the table.
/** @var $event_rows \Behat\Mink\Element\NodeElement[] */
$event_rows = $table->findAll('css', sprintf('tbody tr:contains("%s")', $event_name));
// Remove the header row.
array_shift($event_rows);
$this->assertEquals(count($listeners), count($event_rows));
foreach ($listeners as $index => $listener) {
/** @var $cells \Behat\Mink\Element\NodeElement[] */
$cells = $event_rows[$index]->findAll('css', 'td');
$this->assertEquals(3, count($cells));
$cell_event_name = $cells[0];
$this->assertEquals($event_name, $cell_event_name->getText());
$this->assertTrue($cell_event_name->hasClass('table-filter-text-source'));
$this->assertTrue($cell_event_name->hasClass('visually-hidden'));
$cell_callable = $cells[1];
is_callable($listener, TRUE, $callable_name);
$this->assertEquals($callable_name, $cell_callable->getText());
$cell_methods = $cells[2];
$this->assertEquals($index, $cell_methods->getText());
}
}
// Ensures that the page is accessible only to the users with the adequate
// permissions.
$this->drupalLogout();
$this->drupalGet('devel/events');
$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