Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,37 @@
<?php
namespace Drupal\devel\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a DevelDumper annotation object.
*
* @Annotation
*
* @see \Drupal\devel\DevelDumperPluginManager
* @see \Drupal\devel\DevelDumperInterface
* @see \Drupal\devel\DevelDumperBase
* @see plugin_api
*/
class DevelDumper extends Plugin {
/**
* The human-readable name of the DevelDumper type.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
/**
* A short description of the DevelDumper type.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $description;
}

View File

@ -0,0 +1,240 @@
<?php
namespace Drupal\devel\Controller;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DrupalKernelInterface;
use Drupal\Core\Url;
use Drupal\devel\DevelDumperManagerInterface;
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 {
/**
* The drupal kernel.
*/
protected DrupalKernelInterface $kernel;
/**
* The dumper manager service.
*/
protected DevelDumperManagerInterface $dumper;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->kernel = $container->get('kernel');
$instance->dumper = $container->get('devel.dumper');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* Builds the services overview page.
*
* @return array
* A render array as expected by the renderer.
*/
public function serviceList(): array {
$headers = [
$this->t('ID'),
$this->t('Class'),
$this->t('Alias'),
$this->t('Operations'),
];
$rows = [];
if ($cached_definitions = $this->kernel->getCachedContainerDefinition()) {
foreach ($cached_definitions['services'] as $service_id => $definition) {
$service = unserialize($definition);
$row['id'] = [
'data' => $service_id,
'filter' => TRUE,
];
$row['class'] = [
'data' => $service['class'] ?? '',
'filter' => TRUE,
];
$row['alias'] = [
'data' => array_search($service_id, $cached_definitions['aliases'], TRUE) ?: '',
'filter' => TRUE,
];
$row['operations']['data'] = [
'#type' => 'operations',
'#links' => [
'devel' => [
'title' => $this->t('Devel'),
'url' => Url::fromRoute('devel.container_info.service.detail', ['service_id' => $service_id]),
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 700,
'minHeight' => 500,
]),
],
],
],
];
$rows[$service_id] = $row;
}
ksort($rows);
}
$output['services'] = [
'#type' => 'devel_table_filter',
'#filter_label' => $this->t('Search'),
'#filter_placeholder' => $this->t('Enter service id, alias or class'),
'#filter_description' => $this->t('Enter a part of the service id, service alias or class to filter by.'),
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No services found.'),
'#sticky' => TRUE,
'#attributes' => [
'class' => ['devel-service-list'],
],
];
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(string $service_id): array {
$container = $this->kernel->getContainer();
/** @var object|null $instance */
$instance = $container->get($service_id, ContainerInterface::NULL_ON_INVALID_REFERENCE);
if ($instance === NULL) {
throw new NotFoundHttpException();
}
$output = [];
// Tries to retrieve the service definition from the kernel's cached
// container definition.
$cached_definitions = $this->kernel->getCachedContainerDefinition();
if ($cached_definitions && 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'], TRUE)) {
$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(): array {
$headers = [
$this->t('Name'),
$this->t('Operations'),
];
$rows = [];
if ($cached_definitions = $this->kernel->getCachedContainerDefinition()) {
foreach ($cached_definitions['parameters'] as $parameter_name => $definition) {
$row['name'] = [
'data' => $parameter_name,
'filter' => TRUE,
];
$row['operations']['data'] = [
'#type' => 'operations',
'#links' => [
'devel' => [
'title' => $this->t('Devel'),
'url' => Url::fromRoute('devel.container_info.parameter.detail', ['parameter_name' => $parameter_name]),
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 700,
'minHeight' => 500,
]),
],
],
],
];
$rows[$parameter_name] = $row;
}
ksort($rows);
}
$output['parameters'] = [
'#type' => 'devel_table_filter',
'#filter_label' => $this->t('Search'),
'#filter_placeholder' => $this->t('Enter parameter name'),
'#filter_description' => $this->t('Enter a part of the parameter name to filter by.'),
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No parameters found.'),
'#sticky' => TRUE,
'#attributes' => [
'class' => ['devel-parameter-list'],
],
];
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(string $parameter_name): array {
$container = $this->kernel->getContainer();
try {
$parameter = $container->getParameter($parameter_name);
}
catch (ParameterNotFoundException) {
throw new NotFoundHttpException();
}
return $this->dumper->exportAsRenderable($parameter);
}
}

View File

@ -0,0 +1,208 @@
<?php
namespace Drupal\devel\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Field\FormatterPluginManager;
use Drupal\Core\Field\WidgetPluginManager;
use Drupal\Core\Theme\Registry;
use Drupal\Core\Url;
use Drupal\devel\DevelDumperManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Returns responses for devel module routes.
*/
class DevelController extends ControllerBase {
/**
* The dumper service.
*/
protected DevelDumperManagerInterface $dumper;
/**
* The entity type bundle info service.
*/
protected EntityTypeBundleInfoInterface $entityTypeBundleInfo;
/**
* The field type plugin manager service.
*/
protected FieldTypePluginManagerInterface $fieldTypeManager;
/**
* The field formatter plugin manager.
*/
protected FormatterPluginManager $formatterPluginManager;
/**
* The field widget plugin manager.
*/
protected WidgetPluginManager $widgetPluginManager;
/**
* The theme registry.
*/
protected Registry $themeRegistry;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->dumper = $container->get('devel.dumper');
$instance->entityTypeBundleInfo = $container->get('entity_type.bundle.info');
$instance->fieldTypeManager = $container->get('plugin.manager.field.field_type');
$instance->formatterPluginManager = $container->get('plugin.manager.field.formatter');
$instance->widgetPluginManager = $container->get('plugin.manager.field.widget');
$instance->currentUser = $container->get('current_user');
$instance->stringTranslation = $container->get('string_translation');
$instance->themeRegistry = $container->get('theme.registry');
$instance->entityTypeManager = $container->get('entity_type.manager');
return $instance;
}
/**
* Clears all caches, then redirects to the previous page.
*/
public function cacheClear() {
drupal_flush_all_caches();
// @todo Use DI for messenger once https://www.drupal.org/project/drupal/issues/2940148 is resolved.
$this->messenger()->addMessage($this->t('Cache cleared.'));
return $this->redirect('<front>');
}
/**
* Theme registry.
*
* @return array
* The complete theme registry as renderable.
*/
public function themeRegistry(): array {
$hooks = $this->themeRegistry->get();
ksort($hooks);
return $this->dumper->exportAsRenderable($hooks);
}
/**
* Builds the fields info overview page.
*
* @return array
* Array of page elements to render.
*/
public function fieldInfoPage() {
$fields = $this->entityTypeManager->getStorage('field_storage_config')
->loadMultiple();
ksort($fields);
$output['fields'] = $this->dumper->exportAsRenderable($fields, $this->t('Fields'));
$field_instances = $this->entityTypeManager->getStorage('field_config')
->loadMultiple();
ksort($field_instances);
$output['instances'] = $this->dumper->exportAsRenderable($field_instances, $this->t('Instances'));
$bundles = $this->entityTypeBundleInfo->getAllBundleInfo();
ksort($bundles);
$output['bundles'] = $this->dumper->exportAsRenderable($bundles, $this->t('Bundles'));
$field_types = $this->fieldTypeManager->getUiDefinitions();
ksort($field_types);
$output['field_types'] = $this->dumper->exportAsRenderable($field_types, $this->t('Field types'));
$formatter_types = $this->formatterPluginManager->getDefinitions();
ksort($formatter_types);
$output['formatter_types'] = $this->dumper->exportAsRenderable($formatter_types, $this->t('Formatter types'));
$widget_types = $this->widgetPluginManager->getDefinitions();
ksort($widget_types);
$output['widget_types'] = $this->dumper->exportAsRenderable($widget_types, $this->t('Widget types'));
return $output;
}
/**
* Builds the state variable overview page.
*
* @return array
* Array of page elements to render.
*/
public function stateSystemPage(): array {
$can_edit = $this->currentUser->hasPermission('administer site configuration');
$header = [
'name' => $this->t('Name'),
'value' => $this->t('Value'),
];
if ($can_edit) {
$header['edit'] = $this->t('Operations');
}
$rows = [];
// State class doesn't have getAll method so we get all states from the
// KeyValueStorage.
foreach ($this->keyValue('state')->getAll() as $state_name => $state) {
$rows[$state_name] = [
'name' => [
'data' => $state_name,
'class' => 'table-filter-text-source',
],
'value' => [
'data' => $this->dumper->export($state),
],
];
if ($can_edit) {
$operations['edit'] = [
'title' => $this->t('Edit'),
'url' => Url::fromRoute('devel.system_state_edit', ['state_name' => $state_name]),
];
$rows[$state_name]['edit'] = [
'data' => ['#type' => 'operations', '#links' => $operations],
];
}
}
$output['states'] = [
'#type' => 'devel_table_filter',
'#filter_label' => $this->t('Search'),
'#filter_placeholder' => $this->t('Enter state name'),
'#filter_title' => $this->t('Enter a part of the state name to filter by.'),
'#header' => $header,
'#rows' => $rows,
'#empty' => $this->t('No state variables found.'),
'#attributes' => [
'class' => ['devel-state-list'],
],
];
return $output;
}
/**
* Builds the session overview page.
*
* @return array
* Array of page elements to render.
*/
public function session() {
$output['description'] = [
'#markup' => '<p>' . $this->t('Here are the contents of your $_SESSION variable.') . '</p>',
];
$output['session'] = [
'#type' => 'table',
'#header' => [$this->t('Session name'), $this->t('Session ID')],
'#rows' => [[session_name(), session_id()]],
'#empty' => $this->t('No session available.'),
];
$output['data'] = $this->dumper->exportAsRenderable($_SESSION);
return $output;
}
}

View File

@ -0,0 +1,130 @@
<?php
namespace Drupal\devel\Controller;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Url;
use Drupal\devel\DevelDumperManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Provides route responses for the element info page.
*/
class ElementInfoController extends ControllerBase {
/**
* Element info manager service.
*/
protected ElementInfoManagerInterface $elementInfo;
/**
* The dumper service.
*/
protected DevelDumperManagerInterface $dumper;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->elementInfo = $container->get('element_info');
$instance->dumper = $container->get('devel.dumper');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* Builds the element overview page.
*
* @return array
* A render array as expected by the renderer.
*/
public function elementList(): array {
$headers = [
$this->t('Name'),
$this->t('Provider'),
$this->t('Class'),
$this->t('Operations'),
];
$rows = [];
foreach ($this->elementInfo->getDefinitions() as $element_type => $definition) {
$row['name'] = [
'data' => $element_type,
'filter' => TRUE,
];
$row['provider'] = [
'data' => $definition['provider'],
'filter' => TRUE,
];
$row['class'] = [
'data' => $definition['class'],
'filter' => TRUE,
];
$row['operations']['data'] = [
'#type' => 'operations',
'#links' => [
'devel' => [
'title' => $this->t('Devel'),
'url' => Url::fromRoute('devel.elements_page.detail', ['element_name' => $element_type]),
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 700,
'minHeight' => 500,
]),
],
],
],
];
$rows[$element_type] = $row;
}
ksort($rows);
$output['elements'] = [
'#type' => 'devel_table_filter',
'#filter_label' => $this->t('Search'),
'#filter_placeholder' => $this->t('Enter element id, provider or class'),
'#filter_description' => $this->t('Enter a part of the element id, provider or class to filter by.'),
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No elements found.'),
'#sticky' => TRUE,
'#attributes' => [
'class' => ['devel-element-list'],
],
];
return $output;
}
/**
* Returns a render array representation of the element.
*
* @param string $element_name
* The name of the element to retrieve.
*
* @return array
* A render array containing the element.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* If the requested element is not defined.
*/
public function elementDetail($element_name): array {
if (!$element = $this->elementInfo->getDefinition($element_name, FALSE)) {
throw new NotFoundHttpException();
}
$element += $this->elementInfo->getInfo($element_name);
return $this->dumper->exportAsRenderable($element, $element_name);
}
}

View File

@ -0,0 +1,237 @@
<?php
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\StringTranslation\TranslationManager;
use Drupal\devel\DevelDumperManagerInterface;
use Drupal\path_alias\PathAliasStorage;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Controller for devel entity debug.
*
* @see \Drupal\devel\Routing\RouteSubscriber
* @see \Drupal\devel\Plugin\Derivative\DevelLocalTask
*/
class EntityDebugController extends ControllerBase {
/**
* The dumper service.
*/
protected DevelDumperManagerInterface $dumper;
/**
* The translation manager.
*/
protected TranslationManager $translationManager;
/**
* The alias storage.
*/
protected PathAliasStorage $aliasStorage;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$entityTypeManager = $container->get('entity_type.manager');
$instance = parent::create($container);
$instance->dumper = $container->get('devel.dumper');
$instance->entityTypeManager = $entityTypeManager;
$instance->translationManager = $container->get('string_translation');
$instance->aliasStorage = $entityTypeManager->getStorage('path_alias');
return $instance;
}
/**
* Returns the entity type definition of the current entity.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* A RouteMatch object.
*
* @return array
* Array of page elements to render.
*/
public function entityTypeDefinition(RouteMatchInterface $route_match): array {
$entity = $this->getEntityFromRouteMatch($route_match);
if (!$entity instanceof EntityInterface) {
return [];
}
return $this->dumper->exportAsRenderable($entity->getEntityType());
}
/**
* Returns the loaded structure of the current entity.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* A RouteMatch object.
*
* @return array
* Array of page elements to render.
*/
public function entityLoad(RouteMatchInterface $route_match): array {
$output = [];
$entity = $this->getEntityWithFieldDefinitions($route_match);
if ($entity instanceof EntityInterface) {
// Field definitions are lazy loaded and are populated only when needed.
// By calling ::getFieldDefinitions() we are sure that field definitions
// are populated and available in the dump output.
// @see https://www.drupal.org/node/2311557
if ($entity instanceof FieldableEntityInterface) {
$entity->getFieldDefinitions();
}
$output = $this->dumper->exportAsRenderable($entity);
}
return $output;
}
/**
* Returns the loaded structure of the current entity with references.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* A RouteMatch object.
*
* @return array
* Array of page elements to render.
*/
public function entityLoadWithReferences(RouteMatchInterface $route_match): array {
$entity = $this->getEntityWithFieldDefinitions($route_match);
if (!$entity instanceof EntityInterface) {
return [];
}
return $this->dumper->exportAsRenderable($entity, NULL, NULL, TRUE);
}
/**
* Returns the render structure of the current entity.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* A RouteMatch object.
*
* @return array
* Array of page elements to render.
*/
public function entityRender(RouteMatchInterface $route_match): array {
$output = [];
$entity = $this->getEntityFromRouteMatch($route_match);
if ($entity instanceof EntityInterface) {
$entity_type_id = $entity->getEntityTypeId();
$view_hook = $entity_type_id . '_view';
$build = [];
// If module implements own {entity_type}_view() hook use it, otherwise
// fallback to the entity view builder if available.
if (function_exists($view_hook)) {
$build = $view_hook($entity);
}
elseif ($this->entityTypeManager->hasHandler($entity_type_id, 'view_builder')) {
$build = $this->entityTypeManager->getViewBuilder($entity_type_id)->view($entity);
}
$output = $this->dumper->exportAsRenderable($build);
}
return $output;
}
/**
* Return definitions for any related path aliases.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* A RouteMatch object.
*
* @return array
* Array of page elements to render.
*/
public function pathAliases(RouteMatchInterface $route_match): array {
$entity = $this->getEntityFromRouteMatch($route_match);
if ($entity === NULL) {
return [];
}
$path = sprintf('/%s/%s', $entity->getEntityTypeId(), $entity->id());
$aliases = $this->aliasStorage->loadByProperties(['path' => $path]);
$aliasCount = count($aliases);
if ($aliasCount > 0) {
$message = $this->translationManager->formatPlural(
$aliasCount,
'Found 1 alias with path "@path."',
'Found @count aliases with path "@path".',
['@path' => $path]
);
}
else {
$message = $this->t('Found no aliases with path "@path".', ['@path' => $path]);
}
$build['header'] = [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $message,
];
// Add alias dump to the response.
$build['aliases'] = [];
foreach ($aliases as $alias) {
$build['aliases'][] = $this->dumper->exportAsRenderable($alias);
}
return $build;
}
/**
* Retrieves entity from route match.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The entity object as determined from the passed-in route match.
*/
protected function getEntityFromRouteMatch(RouteMatchInterface $route_match) {
$parameter_name = $route_match->getRouteObject()->getOption('_devel_entity_type_id');
return $route_match->getParameter($parameter_name);
}
/**
* Returns an entity with field definitions from the given route match.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The entity object with field definitions as determined from the
* passed-in route match.
*/
protected function getEntityWithFieldDefinitions(RouteMatchInterface $route_match): ?EntityInterface {
$entity = $this->getEntityFromRouteMatch($route_match);
if (!$entity instanceof EntityInterface) {
return NULL;
}
// Field definitions are lazy loaded and are populated only when needed.
// By calling ::getFieldDefinitions() we are sure that field definitions
// are populated and available in the dump output.
// @see https://www.drupal.org/node/2311557
if ($entity instanceof FieldableEntityInterface) {
$entity->getFieldDefinitions();
}
return $entity;
}
}

View File

@ -0,0 +1,168 @@
<?php
namespace Drupal\devel\Controller;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface;
use Drupal\Core\Url;
use Drupal\devel\DevelDumperManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Provides route responses for the entity types info page.
*/
class EntityTypeInfoController extends ControllerBase {
/**
* The dumper service.
*/
protected DevelDumperManagerInterface $dumper;
/**
* The installed entity definition repository service.
*/
protected EntityLastInstalledSchemaRepositoryInterface $entityLastInstalledSchemaRepository;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->dumper = $container->get('devel.dumper');
$instance->entityLastInstalledSchemaRepository = $container->get('entity.last_installed_schema.repository');
$instance->entityTypeManager = $container->get('entity_type.manager');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* Builds the entity types overview page.
*
* @return array
* A render array as expected by the renderer.
*/
public function entityTypeList(): array {
$headers = [
$this->t('ID'),
$this->t('Name'),
$this->t('Provider'),
$this->t('Class'),
$this->t('Operations'),
];
$rows = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
$row['id'] = [
'data' => $entity_type->id(),
'filter' => TRUE,
];
$row['name'] = [
'data' => $entity_type->getLabel(),
'filter' => TRUE,
];
$row['provider'] = [
'data' => $entity_type->getProvider(),
'filter' => TRUE,
];
$row['class'] = [
'data' => $entity_type->getClass(),
'filter' => TRUE,
];
$row['operations']['data'] = [
'#type' => 'operations',
'#links' => [
'devel' => [
'title' => $this->t('Devel'),
'url' => Url::fromRoute('devel.entity_info_page.detail', ['entity_type_id' => $entity_type_id]),
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 700,
'minHeight' => 500,
]),
],
],
'fields' => [
'title' => $this->t('Fields'),
'url' => Url::fromRoute('devel.entity_info_page.fields', ['entity_type_id' => $entity_type_id]),
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 700,
'minHeight' => 500,
]),
],
],
],
];
$rows[$entity_type_id] = $row;
}
ksort($rows);
$output['entities'] = [
'#type' => 'devel_table_filter',
'#filter_label' => $this->t('Search'),
'#filter_placeholder' => $this->t('Enter entity type id, provider or class'),
'#filter_description' => $this->t('Enter a part of the entity type id, provider or class to filter by.'),
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No entity types found.'),
'#sticky' => TRUE,
'#attributes' => [
'class' => ['devel-entity-type-list'],
],
];
return $output;
}
/**
* Returns a render array representation of the entity type.
*
* @param string $entity_type_id
* The name of the entity type to retrieve.
*
* @return array
* A render array containing the entity type.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* If the requested entity type is not defined.
*/
public function entityTypeDetail($entity_type_id): array {
if (!$entity_type = $this->entityTypeManager->getDefinition($entity_type_id, FALSE)) {
throw new NotFoundHttpException();
}
return $this->dumper->exportAsRenderable($entity_type, $entity_type_id);
}
/**
* Returns a render array representation of the entity type field definitions.
*
* @param string $entity_type_id
* The name of the entity type to retrieve.
*
* @return array
* A render array containing the entity type field definitions.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* If the requested entity type is not defined.
*/
public function entityTypeFields($entity_type_id): array {
if (!$this->entityTypeManager->getDefinition($entity_type_id, FALSE)) {
throw new NotFoundHttpException();
}
$field_storage_definitions = $this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($entity_type_id);
return $this->dumper->exportAsRenderable($field_storage_definitions, $entity_type_id);
}
}

View File

@ -0,0 +1,112 @@
<?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.
*/
protected EventDispatcherInterface $eventDispatcher;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->eventDispatcher = $container->get('event_dispatcher');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* Builds the events overview page.
*
* @return array
* A render array as expected by the renderer.
*/
public function eventList(): array {
$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' => ['devel-event-name-header'],
'filter' => TRUE,
'colspan' => '3',
'header' => TRUE,
];
foreach ($listeners as $listener) {
$row['name'] = [
'data' => $event_name,
'class' => ['visually-hidden'],
'filter' => TRUE,
];
$row['class'] = [
'data' => $this->resolveCallableName($listener),
];
$row['priority'] = [
'data' => $this->eventDispatcher->getListenerPriority($event_name, $listener),
];
$rows[] = $row;
}
}
$output['events'] = [
'#type' => 'devel_table_filter',
'#filter_label' => $this->t('Search'),
'#filter_placeholder' => $this->t('Enter event name'),
'#filter_description' => $this->t('Enter a part of the event name to filter by.'),
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No events found.'),
'#attributes' => [
'class' => ['devel-event-list'],
],
];
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(mixed $callable) {
if (is_callable($callable, TRUE, $callable_name)) {
return $callable_name;
}
return '';
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace Drupal\devel\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Layout\LayoutPluginManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Returns response for Layout Info route.
*/
class LayoutInfoController extends ControllerBase {
/**
* The Layout Plugin Manager.
*/
protected LayoutPluginManagerInterface $layoutPluginManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->layoutPluginManager = $container->get('plugin.manager.core.layout');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* Builds the Layout Info page.
*
* @return array
* Array of page elements to render.
*/
public function layoutInfoPage(): array {
$headers = [
$this->t('Icon'),
$this->t('Label'),
$this->t('Description'),
$this->t('Category'),
$this->t('Regions'),
$this->t('Provider'),
];
$rows = [];
foreach ($this->layoutPluginManager->getDefinitions() as $layout) {
$rows[] = [
'icon' => ['data' => $layout->getIcon()],
'label' => $layout->getLabel(),
'description' => $layout->getDescription(),
'category' => $layout->getCategory(),
'regions' => implode(', ', $layout->getRegionLabels()),
'provider' => $layout->getProvider(),
];
}
$output['layouts'] = [
'#type' => 'table',
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No layouts available.'),
'#attributes' => [
'class' => ['devel-layout-list'],
],
];
return $output;
}
}

View File

@ -0,0 +1,176 @@
<?php
namespace Drupal\devel\Controller;
use Drupal\Component\Serialization\Json;
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.
*/
protected RouteProviderInterface $routeProvider;
/**
* The router service.
*/
protected RouterInterface $router;
/**
* The dumper service.
*/
protected DevelDumperManagerInterface $dumper;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->routeProvider = $container->get('router.route_provider');
$instance->router = $container->get('router.no_access_checks');
$instance->dumper = $container->get('devel.dumper');
$instance->messenger = $container->get('messenger');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* Builds the routes overview page.
*
* @return array
* A render array as expected by the renderer.
*/
public function routeList(): array {
$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,
'filter' => TRUE,
];
$row['path'] = [
'data' => $route->getPath(),
'filter' => TRUE,
];
$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 (str_contains($route->getPath(), '{')) {
$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),
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 700,
'minHeight' => 500,
]),
],
],
],
];
$rows[] = $row;
}
$output['routes'] = [
'#type' => 'devel_table_filter',
'#filter_label' => $this->t('Search'),
'#filter_placeholder' => $this->t('Enter route name or path'),
'#filter_description' => $this->t('Enter a part of the route name or path to filter by.'),
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No routes found.'),
'#sticky' => TRUE,
'#attributes' => [
'class' => ['devel-route-list'],
],
];
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): array {
$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) {
$this->messenger->addWarning($this->t("Unable to load route for url '%url'", ['%url' => $path]));
}
}
// 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) {
$this->messenger->addWarning($this->t("Unable to load route '%name'", ['%name' => $route_name]));
}
}
// No route retrieved from path or name specified, get the current route.
if ($route === NULL) {
$route = $route_match->getRouteObject();
}
return $this->dumper->exportAsRenderable($route);
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace Drupal\devel\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Session\SessionManagerInterface;
use Drupal\user\UserStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Controller for switch to another user account.
*/
class SwitchUserController extends ControllerBase {
/**
* The current user.
*/
protected AccountProxyInterface $account;
/**
* The user storage.
*/
protected UserStorageInterface $userStorage;
/**
* The session manager service.
*/
protected SessionManagerInterface $sessionManager;
/**
* The session.
*/
protected Session $session;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->account = $container->get('current_user');
$instance->userStorage = $container->get('entity_type.manager')->getStorage('user');
$instance->moduleHandler = $container->get('module_handler');
$instance->sessionManager = $container->get('session_manager');
$instance->session = $container->get('session');
return $instance;
}
/**
* Switches to a different user.
*
* We don't call session_save_session() because we really want to change
* users. Usually unsafe!
*
* @param string|null $name
* The username to switch to, or NULL to log out.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A redirect response object.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function switchUser(?string $name = NULL) {
if (empty($name) || !($account = $this->userStorage->loadByProperties(['name' => $name]))) {
throw new AccessDeniedHttpException();
}
$account = reset($account);
// Call logout hooks when switching from original user.
$this->moduleHandler->invokeAll('user_logout', [$this->account]);
// Regenerate the session ID to prevent against session fixation attacks.
$this->sessionManager->regenerate();
// Based off masquarade module as:
// https://www.drupal.org/node/218104 doesn't stick and instead only
// keeps context until redirect.
$this->account->setAccount($account);
$this->session->set('uid', $account->id());
// Call all login hooks when switching to masquerading user.
$this->moduleHandler->invokeAll('user_login', [$account]);
return $this->redirect('<front>');
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace Drupal\devel;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\devel\Render\FilteredMarkup;
use Drupal\devel\Twig\Extension\Debug;
/**
* Defines a base devel dumper implementation.
*
* @see \Drupal\devel\Annotation\DevelDumper
* @see \Drupal\devel\DevelDumperInterface
* @see \Drupal\devel\DevelDumperPluginManager
* @see plugin_api
*/
abstract class DevelDumperBase extends PluginBase implements DevelDumperInterface {
/**
* {@inheritdoc}
*/
public function dump($input, ?string $name = NULL): void {
echo (string) $this->export($input, $name);
}
/**
* {@inheritdoc}
*/
public function exportAsRenderable($input, ?string $name = NULL): array {
return ['#markup' => $this->export($input, $name)];
}
/**
* Wrapper for \Drupal\Core\Render\Markup::create().
*
* @param mixed $input
* The input to mark as a safe string.
*
* @return \Drupal\Component\Render\MarkupInterface|string
* The unaltered input value.
*/
protected function setSafeMarkup(mixed $input): MarkupInterface|string {
return FilteredMarkup::create($input);
}
/**
* Returns a list of internal functions.
*
* The list returned from this method can be used to exclude internal
* functions from the backtrace output.
*
* @return array
* An array of internal functions.
*/
protected function getInternalFunctions(): array {
$class_name = static::class;
$manager_class_name = DevelDumperManager::class;
return [
[$class_name, 'dump'],
[$class_name, 'export'],
[$manager_class_name, 'dump'],
[$manager_class_name, 'export'],
[$manager_class_name, 'exportAsRenderable'],
[$manager_class_name, 'message'],
[Debug::class, 'dump'],
'devel_export',
'devel_message',
'devel_debug',
'dpm',
'dvm',
'dsm',
'dpr',
'dvr',
'kpr',
'dargs',
'dcp',
'dfb',
'dfbt',
'dpq',
'ddebug_backtrace',
'kdevel_print_object',
'backtrace_error_handler',
];
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Drupal\devel;
use Drupal\Component\Render\MarkupInterface;
/**
* Base interface definition for DevelDumper plugins.
*
* @see \Drupal\devel\Annotation\DevelDumper
* @see \Drupal\devel\DevelDumperPluginManager
* @see \Drupal\devel\DevelDumperBase
* @see plugin_api
*/
interface DevelDumperInterface {
/**
* Dumps information about a variable.
*
* @param mixed $input
* The variable to dump.
* @param string|null $name
* (optional) The label to output before variable, defaults to NULL.
*/
public function dump(mixed $input, ?string $name = NULL);
/**
* Returns a string representation of a variable.
*
* @param mixed $input
* The variable to export.
* @param string|null $name
* (optional) The label to output before variable, defaults to NULL.
*
* @return \Drupal\Component\Render\MarkupInterface|string
* String representation of a variable.
*/
public function export(mixed $input, ?string $name = NULL): MarkupInterface|string;
/**
* Returns a string representation of a variable wrapped in a render array.
*
* @param mixed $input
* The variable to export.
* @param string|null $name
* (optional) The label to output before variable, defaults to NULL.
*
* @return array
* String representation of a variable wrapped in a render array.
*/
public function exportAsRenderable(mixed $input, ?string $name = NULL): array;
/**
* Checks if requirements for this plugin are satisfied.
*
* @return bool
* TRUE is requirements are satisfied, FALSE otherwise.
*/
public static function checkRequirements(): bool;
}

View File

@ -0,0 +1,277 @@
<?php
namespace Drupal\devel;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\TranslatableInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
/**
* Manager class for DevelDumper.
*/
class DevelDumperManager implements DevelDumperManagerInterface {
use StringTranslationTrait;
/**
* The devel config.
*/
protected ImmutableConfig $config;
/**
* The current account.
*/
protected AccountProxyInterface $account;
/**
* The devel dumper plugin manager.
*/
protected DevelDumperPluginManagerInterface $dumperManager;
/**
* The entity type manager.
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The messenger.
*/
protected MessengerInterface $messenger;
/**
* Constructs a DevelDumperPluginManager object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Core\Session\AccountProxyInterface $account
* The current account.
* @param \Drupal\devel\DevelDumperPluginManagerInterface $dumper_manager
* The devel dumper plugin manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager service.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation manager.
*/
public function __construct(
ConfigFactoryInterface $config_factory,
AccountProxyInterface $account,
DevelDumperPluginManagerInterface $dumper_manager,
EntityTypeManagerInterface $entityTypeManager,
MessengerInterface $messenger,
TranslationInterface $string_translation,
) {
$this->config = $config_factory->get('devel.settings');
$this->account = $account;
$this->dumperManager = $dumper_manager;
$this->entityTypeManager = $entityTypeManager;
$this->messenger = $messenger;
$this->stringTranslation = $string_translation;
}
/**
* Instances a new dumper plugin.
*
* @param string|null $plugin_id
* (optional) The plugin ID, defaults to NULL.
*
* @return \Drupal\devel\DevelDumperInterface
* Returns the devel dumper plugin instance.
*/
protected function createInstance(?string $plugin_id = NULL) {
if (!$plugin_id || !$this->dumperManager->isPluginSupported($plugin_id)) {
$plugin_id = $this->config->get('devel_dumper');
}
return $this->dumperManager->createInstance($plugin_id);
}
/**
* {@inheritdoc}
*/
public function dump($input, ?string $name = NULL, $plugin_id = NULL): void {
if ($this->hasAccessToDevelInformation()) {
$this->createInstance($plugin_id)->dump($input, $name);
}
}
/**
* {@inheritdoc}
*/
public function export(mixed $input, ?string $name = NULL, ?string $plugin_id = NULL, bool $load_references = FALSE): MarkupInterface|string {
if (!$this->hasAccessToDevelInformation()) {
return '';
}
if ($load_references && $input instanceof EntityInterface) {
$input = $this->entityToArrayWithReferences($input);
}
return $this->createInstance($plugin_id)->export($input, $name);
}
/**
* {@inheritdoc}
*/
public function message($input, ?string $name = NULL, $type = MessengerInterface::TYPE_STATUS, ?string $plugin_id = NULL, $load_references = FALSE): void {
if ($this->hasAccessToDevelInformation()) {
$output = $this->export($input, $name, $plugin_id, $load_references);
$this->messenger->addMessage($output, $type, TRUE);
}
}
/**
* {@inheritdoc}
*/
public function debug($input, ?string $name = NULL, ?string $plugin_id = NULL) {
$output = $this->createInstance($plugin_id)->export($input, $name) . "\n";
// The temp directory does vary across multiple simpletest instances.
$file = $this->config->get('debug_logfile');
if (empty($file)) {
$file = 'temporary://drupal_debug.txt';
}
if (file_put_contents($file, $output, FILE_APPEND) === FALSE && $this->hasAccessToDevelInformation()) {
$this->messenger->addError($this->t('Devel was unable to write to %file.', ['%file' => $file]));
return FALSE;
}
}
/**
* {@inheritdoc}
*/
public function dumpOrExport($input, ?string $name = NULL, $export = TRUE, ?string $plugin_id = NULL) {
if ($this->hasAccessToDevelInformation()) {
$dumper = $this->createInstance($plugin_id);
if ($export) {
return $dumper->export($input, $name);
}
$dumper->dump($input, $name);
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function exportAsRenderable($input, ?string $name = NULL, $plugin_id = NULL, $load_references = FALSE): array {
if ($this->hasAccessToDevelInformation()) {
if ($load_references && $input instanceof EntityInterface) {
$input = $this->entityToArrayWithReferences($input);
}
return $this->createInstance($plugin_id)->exportAsRenderable($input, $name);
}
return [];
}
/**
* Checks whether a user has access to devel information.
*
* @return bool
* TRUE if the user has the permission, FALSE otherwise.
*/
protected function hasAccessToDevelInformation(): bool {
return $this->account->hasPermission('access devel information');
}
/**
* Converts the given entity to an array with referenced entities loaded.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The target entity.
* @param int $depth
* Internal. Track the recursion.
* @param array $array_path
* Internal. Track where we first say this entity.
*
* @return mixed[]
* An array of field names and deep values.
*/
protected function entityToArrayWithReferences(EntityInterface $entity, int $depth = 0, array $array_path = []) {
// Note that we've now seen this entity.
$seen = &drupal_static(__FUNCTION__);
$seen_key = $entity->getEntityTypeId() . '-' . $entity->id();
if (!isset($seen[$seen_key])) {
$seen[$seen_key] = $array_path;
}
$array = $entity->toArray();
// Prevent out of memory and too deep traversing.
if ($depth > 20) {
return $array;
}
if (!$entity instanceof FieldableEntityInterface) {
return $array;
}
foreach ($array as $field => &$value) {
if (is_array($value)) {
$fieldDefinition = $entity->getFieldDefinition($field);
$target_type = $fieldDefinition->getSetting('target_type');
if (!$target_type) {
continue;
}
try {
$storage = $this->entityTypeManager->getStorage($target_type);
}
catch (InvalidPluginDefinitionException | PluginNotFoundException) {
continue;
}
foreach ($value as $delta => &$item) {
if (is_array($item)) {
$referenced_entity = NULL;
if (isset($item['target_id'])) {
$referenced_entity = $storage->load($item['target_id']);
}
elseif (isset($item['target_revision_id'])) {
/** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
$referenced_entity = $storage->loadRevision($item['target_revision_id']);
}
$langcode = $entity->language()->getId();
if ($referenced_entity instanceof TranslatableInterface
&& $referenced_entity->hasTranslation($langcode)) {
$referenced_entity = $referenced_entity->getTranslation($langcode);
}
if (empty($referenced_entity)) {
continue;
}
$seen_id = $referenced_entity->getEntityTypeId() . '-' . $referenced_entity->id();
if (isset($seen[$seen_id])) {
$item['message'] = 'Recursion detected.';
$item['array_path'] = implode('.', $seen[$seen_id]);
continue;
}
$item['entity'] = $this->entityToArrayWithReferences($referenced_entity, $depth++, array_merge($array_path, [$field, $delta, 'entity']));
$item['bundle'] = $referenced_entity->bundle();
}
}
}
}
return $array;
}
}

View File

@ -0,0 +1,115 @@
<?php
namespace Drupal\devel;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Core\Messenger\MessengerInterface;
/**
* Interface for DevelDumper manager.
*
* @package Drupal\devel
*/
interface DevelDumperManagerInterface {
/**
* Dumps information about a variable.
*
* @param mixed $input
* The variable to dump.
* @param string|null $name
* (optional) The label to output before variable, defaults to NULL.
* @param string|null $plugin_id
* (optional) The plugin ID, defaults to NULL.
*/
public function dump(mixed $input, ?string $name = NULL, ?string $plugin_id = NULL);
/**
* Returns a string representation of a variable.
*
* @param mixed $input
* The variable to dump.
* @param string|null $name
* (optional) The label to output before variable.
* @param string|null $plugin_id
* (optional) The plugin ID, defaults to NULL.
* @param bool $load_references
* If the input is an entity, load the referenced entities.
*
* @return \Drupal\Component\Render\MarkupInterface|string
* String representation of a variable.
*/
public function export(mixed $input, ?string $name = NULL, ?string $plugin_id = NULL, bool $load_references = FALSE): MarkupInterface|string;
/**
* Sets a message with a string representation of a variable.
*
* @param mixed $input
* The variable to dump.
* @param string|null $name
* The label to output before variable.
* @param string $type
* (optional) The message's type. Defaults to
* MessengerInterface::TYPE_STATUS.
* @param string|null $plugin_id
* (optional) The plugin ID. Defaults to NULL.
* @param bool $load_references
* (optional) If the input is an entity, load the referenced entities.
* Defaults to FALSE.
*/
public function message(mixed $input, ?string $name = NULL, $type = MessengerInterface::TYPE_STATUS, ?string $plugin_id = NULL, $load_references = FALSE);
/**
* Logs a variable to a drupal_debug.txt in the site's temp directory.
*
* @param mixed $input
* The variable to log to the drupal_debug.txt log file.
* @param string|null $name
* (optional) If set, a label to output before $data in the log file.
* @param string|null $plugin_id
* (optional) The plugin ID, defaults to NULL.
*
* @return void|false
* Empty if successful, FALSE if the log file could not be written.
*
* @see dd()
* @see http://drupal.org/node/314112
*/
public function debug(mixed $input, ?string $name = NULL, ?string $plugin_id = NULL);
/**
* Wrapper for ::dump() and ::export().
*
* @param mixed $input
* The variable to dump.
* @param string|null $name
* (optional) The label to output before variable, defaults to NULL.
* @param bool $export
* (optional) Whether return string representation of a variable.
* @param string|null $plugin_id
* (optional) The plugin ID, defaults to NULL.
*
* @return string|null
* String representation of a variable if $export is set to TRUE,
* NULL otherwise.
*/
public function dumpOrExport(mixed $input, ?string $name = NULL, $export = TRUE, ?string $plugin_id = NULL);
/**
* Returns a render array representation of a variable.
*
* @param mixed $input
* The variable to export.
* @param string|null $name
* The label to output before variable.
* @param string|null $plugin_id
* The plugin ID.
* @param bool $load_references
* If the input is an entity, also load the referenced entities.
*
* @return array
* String representation of a variable wrapped in a render array.
*/
public function exportAsRenderable(mixed $input, ?string $name = NULL, ?string $plugin_id = NULL, $load_references = FALSE): array;
}

View File

@ -0,0 +1,72 @@
<?php
namespace Drupal\devel;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\devel\Annotation\DevelDumper;
/**
* Plugin type manager for Devel Dumper plugins.
*
* @see \Drupal\devel\Annotation\DevelDumper
* @see \Drupal\devel\DevelDumperInterface
* @see \Drupal\devel\DevelDumperBase
* @see plugin_api
*/
class DevelDumperPluginManager extends DefaultPluginManager implements DevelDumperPluginManagerInterface {
/**
* Constructs a DevelDumperPluginManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/Devel/Dumper', $namespaces, $module_handler, DevelDumperInterface::class, DevelDumper::class);
$this->setCacheBackend($cache_backend, 'devel_dumper_plugins');
$this->alterInfo('devel_dumper_info');
}
/**
* {@inheritdoc}
*/
public function processDefinition(&$definition, $plugin_id): void {
parent::processDefinition($definition, $plugin_id);
$definition['supported'] = (bool) call_user_func([$definition['class'], 'checkRequirements']);
}
/**
* {@inheritdoc}
*/
public function isPluginSupported($plugin_id): bool {
$definition = $this->getDefinition($plugin_id, FALSE);
return $definition && $definition['supported'];
}
/**
* {@inheritdoc}
*/
public function createInstance($plugin_id, array $configuration = []) {
if (!$this->isPluginSupported($plugin_id)) {
$plugin_id = $this->getFallbackPluginId($plugin_id);
}
return parent::createInstance($plugin_id, $configuration);
}
/**
* {@inheritdoc}
*/
public function getFallbackPluginId($plugin_id, array $configuration = []): string {
return 'var_dumper';
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\devel;
use Drupal\Component\Plugin\FallbackPluginManagerInterface;
use Drupal\Component\Plugin\PluginManagerInterface;
/**
* Interface for DevelDumper plugin manager.
*/
interface DevelDumperPluginManagerInterface extends PluginManagerInterface, FallbackPluginManagerInterface {
/**
* Checks if plugin has a definition and is supported.
*
* @param string $plugin_id
* The ID of the plugin to check.
*
* @return bool
* TRUE if the plugin is supported, FALSE otherwise.
*/
public function isPluginSupported($plugin_id): bool;
}

View File

@ -0,0 +1,106 @@
<?php
namespace Drupal\devel;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Security\TrustedCallbackInterface;
/**
* Lazy builders for the devel module.
*/
class DevelLazyBuilders implements TrustedCallbackInterface {
/**
* The menu link tree service.
*/
protected MenuLinkTreeInterface $menuLinkTree;
/**
* The devel toolbar config.
*/
protected ImmutableConfig $config;
/**
* Constructs a new ShortcutLazyBuilders object.
*
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_link_tree
* The menu link tree service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*/
public function __construct(
MenuLinkTreeInterface $menu_link_tree,
ConfigFactoryInterface $config_factory,
) {
$this->menuLinkTree = $menu_link_tree;
$this->config = $config_factory->get('devel.toolbar.settings');
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks(): array {
return ['renderMenu'];
}
/**
* Lazy builder callback for the devel menu toolbar.
*
* @return array
* The renderable array rapresentation of the devel menu.
*/
public function renderMenu(): array {
$parameters = new MenuTreeParameters();
$parameters->onlyEnabledLinks()->setTopLevelOnly();
$tree = $this->menuLinkTree->load('devel', $parameters);
$manipulators = [
['callable' => 'menu.default_tree_manipulators:checkAccess'],
['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
[
'callable' => function (array $tree): array {
return $this->processTree($tree);
},
],
];
$tree = $this->menuLinkTree->transform($tree, $manipulators);
$build = $this->menuLinkTree->build($tree);
$build['#attributes']['class'] = ['toolbar-menu'];
CacheableMetadata::createFromRenderArray($build)
->addCacheableDependency($this->config)
->applyTo($build);
return $build;
}
/**
* Adds toolbar-specific attributes to the menu link tree.
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The menu link tree to manipulate.
*
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
* The manipulated menu link tree.
*/
public function processTree(array $tree): array {
$visible_items = $this->config->get('toolbar_items') ?: [];
foreach ($tree as $element) {
$plugin_id = $element->link->getPluginId();
if (!in_array($plugin_id, $visible_items)) {
// Add a class that allow to hide the non prioritized menu items when
// the toolbar has horizontal orientation.
$element->options['attributes']['class'][] = 'toolbar-horizontal-item-hidden';
}
}
return $tree;
}
}

View File

@ -0,0 +1,304 @@
<?php
namespace Drupal\devel\Drush\Commands;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
use Consolidation\SiteAlias\SiteAliasManagerInterface;
use Consolidation\SiteProcess\Util\Escape;
use Drupal\Component\Uuid\Php;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Utility\Token;
use Drush\Attributes as CLI;
use Drush\Commands\AutowireTrait;
use Drush\Commands\DrushCommands;
use Drush\Commands\pm\PmCommands;
use Drush\Drush;
use Drush\Exceptions\UserAbortException;
use Drush\Exec\ExecTrait;
use Drush\Utils\StringUtils;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Input\Input;
use Symfony\Component\Console\Output\Output;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
final class DevelCommands extends DrushCommands {
use AutowireTrait;
use ExecTrait;
const REINSTALL = 'devel:reinstall';
const HOOK = 'devel:hook';
const EVENT = 'devel:event';
const TOKEN = 'devel:token';
const UUID = 'devel:uuid';
const SERVICES = 'devel:services';
/**
* Constructs a new DevelCommands object.
*/
public function __construct(
protected Token $token,
protected EventDispatcherInterface $eventDispatcher,
protected ModuleHandlerInterface $moduleHandler,
private readonly SiteAliasManagerInterface $siteAliasManager,
) {
parent::__construct();
}
/**
* Gets the module handler.
*
* @return \Drupal\Core\Extension\ModuleHandlerInterface
* The moduleHandler.
*/
public function getModuleHandler(): ModuleHandlerInterface {
return $this->moduleHandler;
}
/**
* Gets the event dispatcher.
*
* @return \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
* The eventDispatcher.
*/
public function getEventDispatcher(): EventDispatcherInterface {
return $this->eventDispatcher;
}
/**
* Gets the container.
*
* @return \Drupal\Component\DependencyInjection\ContainerInterface
* The container.
*/
public function getContainer(): ContainerInterface {
return Drush::getContainer()->get('service_container');
}
/**
* Gets the token.
*
* @return \Drupal\Core\Utility\Token
* The token.
*/
public function getToken(): Token {
return $this->token;
}
/**
* Uninstall, and Install modules.
*/
#[CLI\Command(name: self::REINSTALL, aliases: ['dre', 'devel-reinstall'])]
#[CLI\Argument(name: 'modules', description: 'A comma-separated list of module names.')]
public function reinstall($modules): void {
/** @var \Drush\SiteAlias\ProcessManager $process_manager */
$process_manager = $this->processManager();
$modules = StringUtils::csvToArray($modules);
$modules_str = implode(',', $modules);
$process = $process_manager->drush($this->siteAliasManager->getSelf(), PmCommands::UNINSTALL, [$modules_str]);
$process->mustRun();
$process = $process_manager->drush($this->siteAliasManager->getSelf(), PmCommands::INSTALL, [$modules_str]);
$process->mustRun();
}
/**
* List implementations of a given hook and optionally edit one.
*/
#[CLI\Command(name: self::HOOK, aliases: ['fnh', 'fn-hook', 'hook', 'devel-hook'])]
#[CLI\Argument(name: 'hook', description: 'The name of the hook to explore.')]
#[CLI\Argument(name: 'implementation', description: 'The name of the implementation to edit. Usually omitted')]
#[CLI\Usage(name: 'devel:hook cron', description: 'List implementations of hook_cron().')]
#[CLI\OptionsetGetEditor()]
public function hook(string $hook, string $implementation): void {
// Get implementations in the .install files as well.
include_once DRUPAL_ROOT . '/core/includes/install.inc';
drupal_load_updates();
$info = $this->codeLocate($implementation . ('_' . $hook));
$exec = self::getEditor('');
$cmd = sprintf($exec, Escape::shellArg($info['file']));
$process = $this->processManager()->shell($cmd);
$process->setTty(TRUE);
$process->mustRun();
}
/**
* Asks the user to select a hook implementation.
*/
#[CLI\Hook(type: HookManager::INTERACT, target: self::HOOK)]
public function hookInteract(Input $input, Output $output): void {
$hook_implementations = [];
if (!$input->getArgument('implementation')) {
foreach (array_keys($this->moduleHandler->getModuleList()) as $key) {
if ($this->moduleHandler->hasImplementations($input->getArgument('hook'), [$key])) {
$hook_implementations[] = $key;
}
}
if ($hook_implementations !== []) {
if (!$choice = $this->io()->select('Enter the number of the hook implementation you wish to view.', array_combine($hook_implementations, $hook_implementations))) {
throw new UserAbortException();
}
$input->setArgument('implementation', $choice);
}
else {
throw new \Exception(dt('No implementations'));
}
}
}
/**
* List implementations of a given event and optionally edit one.
*/
#[CLI\Command(name: self::EVENT, aliases: ['fne', 'fn-event', 'event'])]
#[CLI\Argument(name: 'event', description: 'The name of the event to explore. If omitted, a list of events is shown.')]
#[CLI\Argument(name: 'implementation', description: 'The name of the implementation to show. Usually omitted.')]
#[CLI\Usage(name: 'drush devel:event', description: 'Pick a Kernel event, then pick an implementation, and then view its source code')]
#[CLI\Usage(name: 'devel-event kernel.terminate', description: 'Pick a terminate subscribers implementation and view its source code.')]
public function event($event, $implementation): void {
$info = $this->codeLocate($implementation);
$exec = self::getEditor('');
$cmd = sprintf($exec, Escape::shellArg($info['file']));
$process = $this->processManager()->shell($cmd);
$process->setTty(TRUE);
$process->mustRun();
}
/**
* Asks the user to select an event and the event's implementation.
*/
#[CLI\Hook(type: HookManager::INTERACT, target: self::EVENT)]
public function interactEvent(Input $input, Output $output): void {
$event = $input->getArgument('event');
if (!$event) {
// @todo Expand this list.
$events = [
'kernel.controller',
'kernel.exception',
'kernel.request',
'kernel.response',
'kernel.terminate',
'kernel.view',
];
$events = array_combine($events, $events);
if (!$event = $this->io()->select('Enter the event you wish to explore.', $events)) {
throw new UserAbortException();
}
$input->setArgument('event', $event);
}
/** @var \Symfony\Component\EventDispatcher\EventDispatcher $event_dispatcher */
$event_dispatcher = $this->eventDispatcher;
if ($implementations = $event_dispatcher->getListeners($event)) {
$choices = [];
foreach ($implementations as $implementation) {
$callable = $implementation[0]::class . '::' . $implementation[1];
$choices[$callable] = $callable;
}
if (!$choice = $this->io()->select('Enter the number of the implementation you wish to view.', $choices)) {
throw new UserAbortException();
}
$input->setArgument('implementation', $choice);
}
else {
throw new \Exception(dt('No implementations.'));
}
}
/**
* List available tokens.
*/
#[CLI\Command(name: self::TOKEN, aliases: ['token', 'devel-token'])]
#[CLI\FieldLabels(labels: ['group' => 'Group', 'token' => 'Token', 'name' => 'Name'])]
#[CLI\DefaultTableFields(fields: ['group', 'token', 'name'])]
public function token($options = ['format' => 'table']): RowsOfFields {
$rows = [];
$all = $this->token->getInfo();
foreach ($all['tokens'] as $group => $tokens) {
foreach ($tokens as $key => $token) {
$rows[] = [
'group' => $group,
'token' => $key,
'name' => $token['name'],
];
}
}
return new RowsOfFields($rows);
}
/**
* Generate a Universally Unique Identifier (UUID).
*/
#[CLI\Command(name: self::UUID, aliases: ['uuid', 'devel-uuid'])]
public function uuid(): string {
$uuid = new Php();
return $uuid->generate();
}
/**
* Get source code line for specified function or method.
*/
public function codeLocate($function_name): array {
// Get implementations in the .install files as well.
include_once DRUPAL_ROOT . '/core/includes/install.inc';
drupal_load_updates();
if (!str_contains($function_name, '::')) {
if (!function_exists($function_name)) {
throw new \Exception(dt('Function not found'));
}
$reflect = new \ReflectionFunction($function_name);
}
else {
[$class, $method] = explode('::', $function_name);
if (!method_exists($class, $method)) {
throw new \Exception(dt('Method not found'));
}
$reflect = new \ReflectionMethod($class, $method);
}
return [
'file' => $reflect->getFileName(),
'startline' => $reflect->getStartLine(),
'endline' => $reflect->getEndLine(),
];
}
/**
* Get a list of available container services.
*/
#[CLI\Command(name: self::SERVICES, aliases: ['devel-container-services', 'dcs', 'devel-services'])]
#[CLI\Argument(name: 'prefix', description: 'Optional prefix to filter the service list by.')]
#[CLI\Usage(name: 'drush devel-services', description: 'Gets a list of all available container services')]
#[CLI\Usage(name: 'drush dcs plugin.manager', description: 'Get all services containing "plugin.manager"')]
public function services(?string $prefix = NULL, array $options = ['format' => 'yaml']): array {
$container = $this->getContainer();
$services = $container->getServiceIds();
// If there is a prefix, try to find matches.
if (isset($prefix)) {
$services = preg_grep(sprintf('/%s/', $prefix), $services);
}
if (empty($services)) {
throw new \Exception(dt('No container services found.'));
}
sort($services);
return $services;
}
}

View File

@ -0,0 +1,131 @@
<?php
namespace Drupal\devel\Element;
use Drupal\Component\Utility\Html;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\Element\RenderElementBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a render element for filterable table data.
*
* Usage example:
*
* @code
* $build['item'] = [
* '#type' => 'devel_table_filter',
* '#filter_label' => $this->t('Search'),
* '#filter_placeholder' => $this->t('Enter element name.'),
* '#filter_description' => $this->t('Enter a part of name to filter by.'),
* '#header' => $headers,
* '#rows' => $rows,
* '#empty' => $this->t('No element found.'),
* ];
* @endcode
*
* @RenderElement("devel_table_filter")
*/
class ClientSideFilterTable extends RenderElementBase implements ContainerFactoryPluginInterface {
// phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod.Found
final public function __construct(array $configuration, string $plugin_id, string|array $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
$instance = new static($configuration, $plugin_id, $plugin_definition);
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getInfo(): array {
$class = static::class;
return [
'#filter_label' => $this->t('Search'),
'#filter_placeholder' => $this->t('Search'),
'#filter_description' => $this->t('Search'),
'#header' => [],
'#rows' => [],
'#empty' => '',
'#sticky' => FALSE,
'#responsive' => TRUE,
'#attributes' => [],
'#pre_render' => [
[$class, 'preRenderTable'],
],
];
}
/**
* Pre-render callback: Assemble render array for the filterable table.
*
* @param array $element
* An associative array containing the properties of the element.
*
* @return array
* The $element with prepared render array ready for rendering.
*/
public static function preRenderTable(array $element): array {
$build['#attached']['library'][] = 'devel/devel-table-filter';
$identifier = Html::getUniqueId('js-devel-table-filter');
$build['filters'] = [
'#type' => 'container',
'#weight' => -1,
'#attributes' => [
'class' => ['table-filter', 'js-show'],
],
];
$build['filters']['name'] = [
'#type' => 'search',
'#size' => 30,
'#title' => $element['#filter_label'],
'#placeholder' => $element['#filter_placeholder'],
'#attributes' => [
'class' => ['table-filter-text'],
'data-table' => '.' . $identifier,
'autocomplete' => 'off',
'title' => $element['#filter_description'],
],
];
foreach ($element['#rows'] as &$row) {
foreach ($row as &$cell) {
if (!isset($cell['data'])) {
continue;
}
if (empty($cell['filter'])) {
continue;
}
$cell['class'][] = 'table-filter-text-source';
}
}
$build['table'] = [
'#type' => 'table',
'#header' => $element['#header'],
'#rows' => $element['#rows'],
'#empty' => $element['#empty'],
'#sticky' => $element['#sticky'],
'#responsive' => $element['#responsive'],
'#attributes' => $element['#attributes'],
];
$build['table']['#attributes']['class'][] = $identifier;
$build['table']['#attributes']['class'][] = 'devel-table-filter';
return $build;
}
}

View File

@ -0,0 +1,165 @@
<?php
namespace Drupal\devel;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Manipulates entity type information.
*
* This class contains primarily bridged hooks for compile-time or
* cache-clear-time hooks. Runtime hooks should be placed in EntityOperations.
*/
class EntityTypeInfo implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* The current user.
*/
protected AccountInterface $currentUser;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
$instance = new self();
$instance->currentUser = $container->get('current_user');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* Adds devel links to appropriate entity types.
*
* This is an alter hook bridge.
*
* @param \Drupal\Core\Entity\EntityTypeInterface[] $entity_types
* The master entity type list to alter.
*
* @see hook_entity_type_alter()
*/
public function entityTypeAlter(array &$entity_types): void {
foreach ($entity_types as $entity_type_id => $entity_type) {
// Make devel-load and devel-load-with-references subtasks. The edit-form
// template is used to extract and set additional parameters dynamically.
// If there is no 'edit-form' template then still create the link using
// 'entity_type_id/{entity_type_id}' as the link. This allows devel info
// to be viewed for any entity, even if the url has to be typed manually.
// @see https://gitlab.com/drupalspoons/devel/-/issues/377
$entity_link = $entity_type->getLinkTemplate('edit-form') ?: $entity_type_id . sprintf('/{%s}', $entity_type_id);
$this->setEntityTypeLinkTemplate($entity_type, $entity_link, 'devel-load', '/devel/' . $entity_type_id);
$this->setEntityTypeLinkTemplate($entity_type, $entity_link, 'devel-load-with-references', '/devel/load-with-references/' . $entity_type_id);
$this->setEntityTypeLinkTemplate($entity_type, $entity_link, 'devel-path-alias', '/devel/path-alias/' . $entity_type_id);
// Create the devel-render subtask.
if ($entity_type->hasViewBuilderClass() && $entity_type->hasLinkTemplate('canonical')) {
// We use canonical template to extract and set additional parameters
// dynamically.
$entity_link = $entity_type->getLinkTemplate('canonical');
$this->setEntityTypeLinkTemplate($entity_type, $entity_link, 'devel-render', '/devel/render/' . $entity_type_id);
}
// Create the devel-definition subtask.
if ($entity_type->hasLinkTemplate('devel-render') || $entity_type->hasLinkTemplate('devel-load')) {
// We use canonical or edit-form template to extract and set additional
// parameters dynamically.
$entity_link = $entity_type->getLinkTemplate('edit-form');
if (empty($entity_link)) {
$entity_link = $entity_type->getLinkTemplate('canonical');
}
$this->setEntityTypeLinkTemplate($entity_type, $entity_link, 'devel-definition', '/devel/definition/' . $entity_type_id);
}
}
}
/**
* Sets entity type link template.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* Entity type.
* @param string $entity_link
* Entity link.
* @param string $devel_link_key
* Devel link key.
* @param string $base_path
* Base path for devel link key.
*/
protected function setEntityTypeLinkTemplate(EntityTypeInterface $entity_type, $entity_link, $devel_link_key, string $base_path) {
// Extract all route parameters from the given template and set them to
// the current template.
// Some entity templates can contain not only entity id,
// for example /user/{user}/documents/{document}
// /group/{group}/content/{group_content}
// We use canonical or edit-form templates to get these parameters and set
// them for devel entity link templates.
$path_parts = $this->getPathParts($entity_link);
$entity_type->setLinkTemplate($devel_link_key, $base_path . $path_parts);
}
/**
* Get path parts.
*
* @param string $entity_path
* Entity path.
*
* @return string
* Path parts.
*/
protected function getPathParts($entity_path): string {
$path = '';
if (preg_match_all('/{\w*}/', $entity_path, $matches)) {
foreach ($matches[0] as $match) {
$path .= '/' . $match;
}
}
return $path;
}
/**
* Adds devel operations on entity that supports it.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity on which to define an operation.
*
* @return array
* An array of operation definitions.
*
* @see hook_entity_operation()
*/
public function entityOperation(EntityInterface $entity): array {
$operations = $parameters = [];
if ($this->currentUser->hasPermission('access devel information')) {
if ($entity->hasLinkTemplate('canonical')) {
$parameters = $entity->toUrl('canonical')->getRouteParameters();
}
if ($entity->hasLinkTemplate('devel-load')) {
$url = $entity->toUrl('devel-load');
$operations['devel'] = [
'title' => $this->t('Devel'),
'weight' => 100,
'url' => $parameters ? $url->setRouteParameters($parameters) : $url,
];
}
elseif ($entity->hasLinkTemplate('devel-render')) {
$url = $entity->toUrl('devel-render');
$operations['devel'] = [
'title' => $this->t('Devel'),
'weight' => 100,
'url' => $parameters ? $url->setRouteParameters($parameters) : $url,
];
}
}
return $operations;
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace Drupal\devel\EventSubscriber;
use Drupal\Core\Session\AccountProxyInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Listener for handling PHP errors.
*/
class ErrorHandlerSubscriber implements EventSubscriberInterface {
/**
* The current user.
*/
protected AccountProxyInterface $account;
/**
* ErrorHandlerSubscriber constructor.
*
* @param \Drupal\Core\Session\AccountProxyInterface $account
* The current user.
*/
public function __construct(AccountProxyInterface $account) {
$this->account = $account;
}
/**
* Register devel error handler.
*
* @param \Symfony\Component\HttpKernel\Event\RequestEvent|null $event
* The event to process.
*/
public function registerErrorHandler(?RequestEvent $event = NULL): void {
if (!$this->account->hasPermission('access devel information')) {
return;
}
devel_set_handler(devel_get_handlers());
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
// Runs as soon as possible in the request but after
// AuthenticationSubscriber (priority 300) because you need to access to
// the current user for determine whether register the devel error handler
// or not.
$events[KernelEvents::REQUEST][] = ['registerErrorHandler', 256];
return $events;
}
}

View File

@ -0,0 +1,139 @@
<?php
namespace Drupal\devel\EventSubscriber;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Theme\Registry;
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Subscriber for force the system to rebuild the theme registry.
*/
class ThemeInfoRebuildSubscriber implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* Internal flag for handle user notification.
*/
protected string $notificationFlag = 'devel.rebuild_theme_warning';
/**
* The devel config.
*/
protected Config $config;
/**
* The current user.
*/
protected AccountProxyInterface $account;
/**
* The theme handler.
*/
protected ThemeHandlerInterface $themeHandler;
/**
* The messenger.
*/
protected MessengerInterface $messenger;
/**
* The theme registry.
*/
protected Registry $themeRegistry;
/**
* Constructs a ThemeInfoRebuildSubscriber object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config
* The config factory.
* @param \Drupal\Core\Session\AccountProxyInterface $account
* The current user.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation manager.
* @param \Drupal\Core\Theme\Registry $theme_registry
* The theme registry.
*/
public function __construct(
ConfigFactoryInterface $config,
AccountProxyInterface $account,
ThemeHandlerInterface $theme_handler,
MessengerInterface $messenger,
TranslationInterface $string_translation,
Registry $theme_registry,
) {
$this->config = $config->get('devel.settings');
$this->account = $account;
$this->themeHandler = $theme_handler;
$this->messenger = $messenger;
$this->stringTranslation = $string_translation;
$this->themeRegistry = $theme_registry;
}
/**
* Forces the system to rebuild the theme registry.
*
* @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
* The event to process.
*/
public function rebuildThemeInfo(RequestEvent $event): void {
if ($this->config->get('rebuild_theme')) {
// Update the theme registry.
$this->themeRegistry->reset();
// Refresh theme data.
$this->themeHandler->refreshInfo();
// Resets the internal state of the theme handler and clear the 'system
// list' cache; this allow to properly register, if needed, PSR-4
// namespaces for theme extensions after refreshing the info data.
$this->themeHandler->reset();
// Notify the user that the theme info are rebuilt on every request.
$this->triggerWarningIfNeeded($event->getRequest());
}
}
/**
* Notifies the user that the theme info are rebuilt on every request.
*
* The warning message is shown only to users with adequate permissions and
* only once per session.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*/
protected function triggerWarningIfNeeded(Request $request) {
if ($this->account->hasPermission('access devel information')) {
$session = $request->getSession();
if (!$session->has($this->notificationFlag)) {
$session->set($this->notificationFlag, TRUE);
$message = $this->t('The theme information is being rebuilt on every request. Remember to <a href=":url">turn off</a> this feature on production websites.', [':url' => Url::fromRoute('devel.admin_settings')->toString()]);
$this->messenger->addWarning($message);
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
// Set high priority value to start as early as possible.
$events[KernelEvents::REQUEST][] = ['rebuildThemeInfo', 256];
return $events;
}
}

View File

@ -0,0 +1,147 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Core\Form\ConfirmFormHelper;
use Drupal\Core\Form\ConfirmFormInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Edit config variable form.
*/
class ConfigDeleteForm extends FormBase implements ConfirmFormInterface {
/**
* Logger service.
*/
protected LoggerInterface $logger;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->messenger = $container->get('messenger');
$instance->logger = $container->get('logger.channel.devel');
$instance->configFactory = $container->get('config.factory');
$instance->requestStack = $container->get('request_stack');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_config_system_delete_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $config_name = ''): array {
$config = $this->configFactory->get($config_name);
if ($config->isNew()) {
$this->messenger->addError($this->t('Config @name does not exist in the system.', ['@name' => $config_name]));
return $form;
}
$form['#title'] = $this->getQuestion();
$form['#attributes']['class'][] = 'confirmation';
$form['description'] = ['#markup' => $this->getDescription()];
$form[$this->getFormName()] = ['#type' => 'hidden', '#value' => 1];
// By default, render the form using theme_confirm_form().
if (!isset($form['#theme'])) {
$form['#theme'] = 'confirm_form';
}
$form['name'] = [
'#type' => 'value',
'#value' => $config_name,
];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->getConfirmText(),
'#submit' => [
function (array &$form, FormStateInterface $form_state): void {
$this->submitForm($form, $form_state);
},
],
];
$form['actions']['cancel'] = ConfirmFormHelper::buildCancelLink($this, $this->requestStack->getCurrentRequest());
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$config_name = $form_state->getValue('name');
try {
$this->configFactory->getEditable($config_name)->delete();
$this->messenger->addStatus($this->t('Configuration variable %variable was successfully deleted.', ['%variable' => $config_name]));
$this->logger->info('Configuration variable %variable was successfully deleted.', ['%variable' => $config_name]);
$form_state->setRedirectUrl($this->getCancelUrl());
}
catch (\Exception $e) {
$this->messenger->addError($e->getMessage());
$this->logger->error('Error deleting configuration variable %variable : %error.', [
'%variable' => $config_name,
'%error' => $e->getMessage(),
]);
}
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return Url::fromRoute('devel.configs_list');
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete this configuration?');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('This action cannot be undone.');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Confirm');
}
/**
* {@inheritdoc}
*/
public function getCancelText() {
return $this->t('Cancel');
}
/**
* {@inheritdoc}
*/
public function getFormName(): string {
return 'confirm';
}
}

View File

@ -0,0 +1,187 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\devel\DevelDumperManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Edit config variable form.
*/
class ConfigEditor extends FormBase {
/**
* Logger service.
*/
protected LoggerInterface $logger;
/**
* The dumper service.
*/
protected DevelDumperManagerInterface $dumper;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->messenger = $container->get('messenger');
$instance->logger = $container->get('logger.channel.devel');
$instance->configFactory = $container->get('config.factory');
$instance->requestStack = $container->get('request_stack');
$instance->stringTranslation = $container->get('string_translation');
$instance->dumper = $container->get('devel.dumper');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_config_system_edit_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $config_name = ''): array {
$config = $this->configFactory->get($config_name);
if ($config->isNew()) {
$this->messenger->addError($this->t('Config @name does not exist in the system.', ['@name' => $config_name]));
return $form;
}
$data = $config->getOriginal();
if (empty($data)) {
$this->messenger->addWarning($this->t('Config @name exists but has no data.', ['@name' => $config_name]));
return $form;
}
try {
$output = Yaml::encode($data);
}
catch (InvalidDataTypeException $e) {
$this->messenger->addError($this->t('Invalid data detected for @name : %error', [
'@name' => $config_name,
'%error' => $e->getMessage(),
]));
return $form;
}
$form['current'] = [
'#type' => 'details',
'#title' => $this->t('Current value for %variable', ['%variable' => $config_name]),
'#attributes' => ['class' => ['container-inline']],
];
$form['current']['value'] = [
'#type' => 'item',
'#markup' => $this->dumper->dumpOrExport(input: $output, plugin_id: 'default'),
];
$form['name'] = [
'#type' => 'value',
'#value' => $config_name,
];
$form['new'] = [
'#type' => 'textarea',
'#title' => $this->t('New value'),
'#default_value' => $output,
'#rows' => 24,
'#required' => TRUE,
];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
];
$form['actions']['cancel'] = [
'#type' => 'link',
'#title' => $this->t('Cancel'),
'#url' => $this->buildCancelLinkUrl(),
];
$form['actions']['delete'] = [
'#type' => 'link',
'#title' => $this->t('Delete'),
'#url' => Url::fromRoute('devel.config_delete', ['config_name' => $config_name]),
'#attributes' => [
'class' => ['button', 'button--danger'],
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
$value = $form_state->getValue('new');
// Try to parse the new provided value.
try {
$parsed_value = Yaml::decode($value);
// Config::setData needs array for the new configuration and
// a simple string is valid YAML for any reason.
if (is_array($parsed_value)) {
$form_state->setValue('parsed_value', $parsed_value);
}
else {
$form_state->setErrorByName('new', $this->t('Invalid input'));
}
}
catch (InvalidDataTypeException $e) {
$form_state->setErrorByName('new', $this->t('Invalid input: %error', ['%error' => $e->getMessage()]));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$values = $form_state->getValues();
try {
$this->configFactory->getEditable($values['name'])
->setData($values['parsed_value'])
->save();
$this->messenger->addMessage($this->t('Configuration variable %variable was successfully saved.', ['%variable' => $values['name']]));
$this->logger->info('Configuration variable %variable was successfully saved.', ['%variable' => $values['name']]);
$form_state->setRedirectUrl(Url::fromRoute('devel.configs_list'));
}
catch (\Exception $e) {
$this->messenger->addError($e->getMessage());
$this->logger->error('Error saving configuration variable %variable : %error.', [
'%variable' => $values['name'],
'%error' => $e->getMessage(),
]);
}
}
/**
* Builds the cancel link url for the form.
*
* @return \Drupal\Core\Url
* Cancel url
*/
private function buildCancelLinkUrl(): Url {
$query = $this->requestStack->getCurrentRequest()->query;
if ($query->has('destination')) {
$options = UrlHelper::parse($query->get('destination'));
return Url::fromUserInput('/' . ltrim($options['path'], '/'), $options);
}
return Url::fromRoute('devel.configs_list');
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form that displays all the config variables to edit them.
*/
class ConfigsList extends FormBase {
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->configFactory = $container->get('config.factory');
$instance->redirectDestination = $container->get('redirect.destination');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_config_system_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $filter = ''): array {
$form['filter'] = [
'#type' => 'details',
'#title' => $this->t('Filter variables'),
'#attributes' => ['class' => ['container-inline']],
'#open' => isset($filter) && trim($filter) !== '',
];
$form['filter']['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Variable name'),
'#title_display' => 'invisible',
'#default_value' => $filter,
];
$form['filter']['actions'] = ['#type' => 'actions'];
$form['filter']['actions']['show'] = [
'#type' => 'submit',
'#value' => $this->t('Filter'),
];
$header = [
'name' => ['data' => $this->t('Name')],
'edit' => ['data' => $this->t('Operations')],
];
$rows = [];
$destination = $this->redirectDestination->getAsArray();
// List all the variables filtered if any filter was provided.
$names = $this->configFactory->listAll($filter);
foreach ($names as $config_name) {
$operations['edit'] = [
'title' => $this->t('Edit'),
'url' => Url::fromRoute('devel.config_edit', ['config_name' => $config_name]),
'query' => $destination,
];
$rows[] = [
'name' => $config_name,
'operation' => [
'data' => [
'#type' => 'operations',
'#links' => $operations,
],
],
];
}
$form['variables'] = [
'#type' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => $this->t('No variables found'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$filter = $form_state->getValue('name');
$form_state->setRedirectUrl(Url::FromRoute('devel.configs_list', ['filter' => Html::escape($filter)]));
}
}

View File

@ -0,0 +1,159 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Update\UpdateHookRegistry;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Display a dropdown of installed modules with the option to reinstall them.
*/
class DevelReinstall extends FormBase {
/**
* The module installer.
*/
protected ModuleInstallerInterface $moduleInstaller;
/**
* The module extension list.
*/
protected ModuleExtensionList $moduleExtensionList;
/**
* The update hook registry service.
*/
protected UpdateHookRegistry $updateHookRegistry;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->moduleInstaller = $container->get('module_installer');
$instance->moduleExtensionList = $container->get('extension.list.module');
$instance->updateHookRegistry = $container->get('update.update_hook_registry');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_reinstall_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
// Get a list of all available modules.
$modules = $this->moduleExtensionList->reset()->getList();
$uninstallable = array_filter($modules, fn($module): bool => empty($modules[$module->getName()]->info['required'])
&& $this->updateHookRegistry->getInstalledVersion($module->getName()) > UpdateHookRegistry::SCHEMA_UNINSTALLED
&& $module->getName() !== 'devel');
$form['filters'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['table-filter', 'js-show'],
],
];
$form['filters']['text'] = [
'#type' => 'search',
'#title' => $this->t('Search'),
'#size' => 30,
'#placeholder' => $this->t('Enter module name'),
'#attributes' => [
'class' => ['table-filter-text'],
'data-table' => '#devel-reinstall-form',
'autocomplete' => 'off',
'title' => $this->t('Enter a part of the module name or description to filter by.'),
],
];
// Only build the rest of the form if there are any modules available to
// uninstall.
if ($uninstallable === []) {
return $form;
}
$header = [
'name' => $this->t('Name'),
'description' => $this->t('Description'),
];
$rows = [];
foreach ($uninstallable as $module) {
$name = $module->info['name'] ?: $module->getName();
$rows[$module->getName()] = [
'name' => [
'data' => [
'#type' => 'inline_template',
'#template' => '<label class="module-name table-filter-text-source">{{ module_name }}</label>',
'#context' => ['module_name' => $name],
],
],
'description' => [
'data' => $module->info['description'],
'class' => ['description'],
],
];
}
$form['reinstall'] = [
'#type' => 'tableselect',
'#header' => $header,
'#options' => $rows,
'#js_select' => FALSE,
'#empty' => $this->t('No modules are available to uninstall.'),
];
$form['#attached']['library'][] = 'system/drupal.system.modules';
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Reinstall'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
// Form submitted, but no modules selected.
if (array_filter($form_state->getValue('reinstall')) === []) {
$form_state->setErrorByName('reinstall', $this->t('No modules selected.'));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
try {
$modules = $form_state->getValue('reinstall');
$reinstall = array_keys(array_filter($modules));
$this->moduleInstaller->uninstall($reinstall, FALSE);
$this->moduleInstaller->install($reinstall, FALSE);
// @todo Revisit usage of DI once https://www.drupal.org/project/drupal/issues/2940148 is resolved.
$this->messenger()->addMessage($this->t('Uninstalled and installed: %names.', ['%names' => implode(', ', $reinstall)]));
}
catch (\Exception $e) {
$this->messenger()->addError($this->t('Unable to reinstall modules. Error: %error.', ['%error' => $e->getMessage()]));
}
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteBuilderInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides confirmation form for rebuilding the routes.
*/
class RouterRebuildConfirmForm extends ConfirmFormBase {
/**
* The route builder service.
*/
protected RouteBuilderInterface $routeBuilder;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->routeBuilder = $container->get('router.builder');
$instance->messenger = $container->get('messenger');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_menu_rebuild';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to rebuild the router?');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl(): Url {
return new Url('<front>');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Rebuilds the routes information gathering all routing data from .routing.yml files and from classes which subscribe to the route build events. This action cannot be undone.');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Rebuild');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$this->routeBuilder->rebuild();
$this->messenger->addMessage($this->t('The router has been rebuilt.'));
$form_state->setRedirect('<front>');
}
}

View File

@ -0,0 +1,208 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Core\Config\Config;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\devel\DevelDumperPluginManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Defines a form that configures devel settings.
*/
class SettingsForm extends ConfigFormBase {
protected DevelDumperPluginManagerInterface $dumperManager;
/**
* The 'devel.settings' config object.
*/
protected Config $config;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->dumperManager = $container->get('plugin.manager.devel_dumper');
$instance->config = $container->get('config.factory')->getEditable('devel.settings');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_admin_settings_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames(): array {
return [
'devel.settings',
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?Request $request = NULL): array {
$current_url = Url::createFromRequest($request);
$form['page_alter'] = [
'#type' => 'checkbox',
'#title' => $this->t('Display $attachments array'),
'#default_value' => $this->config->get('page_alter'),
'#description' => $this->t('Display $attachments array from <a href="https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21theme.api.php/function/hook_page_attachments_alter/10">hook_page_attachments_alter()</a> in the messages area of each page.'),
];
$form['raw_names'] = [
'#type' => 'checkbox',
'#title' => $this->t('Display machine names of permissions and modules'),
'#default_value' => $this->config->get('raw_names'),
'#description' => $this->t('Display the language-independent machine names of the permissions in mouse-over hints on the <a href=":permissions_url">Permissions</a> page and the module base file names on the Permissions and <a href=":modules_url">Modules</a> pages.', [
':permissions_url' => Url::fromRoute('user.admin_permissions')->toString(),
':modules_url' => Url::fromRoute('system.modules_list')->toString(),
]),
];
$form['rebuild_theme'] = [
'#type' => 'checkbox',
'#title' => $this->t('Rebuild the theme registry on every page load'),
'#description' => $this->t('New templates, theme overrides, and changes to the theme.info.yml need the theme registry to be rebuilt in order to appear on the site.'),
'#default_value' => $this->config->get('rebuild_theme'),
];
$error_handlers = devel_get_handlers();
$form['error_handlers'] = [
'#type' => 'select',
'#title' => $this->t('Error handlers'),
'#options' => [
DEVEL_ERROR_HANDLER_NONE => $this->t('None'),
DEVEL_ERROR_HANDLER_STANDARD => $this->t('Standard Drupal'),
DEVEL_ERROR_HANDLER_BACKTRACE_DPM => $this->t('Backtrace in the message area'),
DEVEL_ERROR_HANDLER_BACKTRACE_KINT => $this->t('Backtrace above the rendered page'),
],
'#multiple' => TRUE,
'#default_value' => empty($error_handlers) ? DEVEL_ERROR_HANDLER_NONE : $error_handlers,
'#description' => [
[
'#markup' => $this->t('Select the error handler(s) to use, in case you <a href=":choose">choose to show errors on screen</a>.', [':choose' => Url::fromRoute('system.logging_settings')->toString()]),
],
[
'#theme' => 'item_list',
'#items' => [
$this->t('<em>None</em> is a good option when stepping through the site in your debugger.'),
$this->t('<em>Standard Drupal</em> does not display all the information that is often needed to resolve an issue.'),
$this->t('<em>Backtrace</em> displays nice debug information when any type of error is noticed, but only to users with the %perm permission.', ['%perm' => $this->t('Access developer information')]),
],
],
[
'#markup' => $this->t('Depending on the situation, the theme, the size of the call stack and the arguments, etc., some handlers may not display their messages, or display them on the subsequent page. Select <em>Standard Drupal</em> <strong>and</strong> <em>Backtrace above the rendered page</em> to maximize your chances of not missing any messages.') . '<br />' .
$this->t('Demonstrate the current error handler(s):') . ' ' .
Link::fromTextAndUrl('notice', $current_url->setOption('query', ['demo' => 'notice']))->toString() . ', ' .
Link::fromTextAndUrl('notice+warning', $current_url->setOption('query', ['demo' => 'warning']))->toString() . ', ' .
Link::fromTextAndUrl('notice+warning+error', $current_url->setOption('query', ['demo' => 'error']))->toString() . ' (' .
$this->t('The presentation of the @error is determined by PHP.', ['@error' => 'error']) . ')',
],
],
];
$form['error_handlers']['#size'] = count($form['error_handlers']['#options']);
if ($request->query->has('demo')) {
if ($request->getMethod() === 'GET') {
$this->demonstrateErrorHandlers($request->query->get('demo'));
}
$request->query->remove('demo');
}
$dumper = $this->config->get('devel_dumper');
$default = $this->dumperManager->isPluginSupported($dumper) ? $dumper : $this->dumperManager->getFallbackPluginId('');
$form['dumper'] = [
'#type' => 'radios',
'#title' => $this->t('Variables Dumper'),
'#options' => [],
'#default_value' => $default,
'#description' => $this->t('Select the debugging tool used for formatting and displaying the variables inspected through the debug functions of Devel. <strong>NOTE</strong>: Some of these plugins require external libraries for to be enabled. Learn how install external libraries with <a href=":url">Composer</a>.', [
':url' => 'https://www.drupal.org/node/2404989',
]),
];
foreach ($this->dumperManager->getDefinitions() as $id => $definition) {
$form['dumper']['#options'][$id] = $definition['label'];
$supported = $this->dumperManager->isPluginSupported($id);
$form['dumper'][$id]['#disabled'] = !$supported;
$form['dumper'][$id]['#description'] = [
'#type' => 'inline_template',
'#template' => '{{ description }}{% if not supported %}<div><small>{% trans %}<strong>Not available</strong>. You may need to install external dependencies for use this plugin.{% endtrans %}</small></div>{% endif %}',
'#context' => [
'description' => $definition['description'],
'supported' => $supported,
],
];
}
// Allow custom debug filename for use in DevelDumperManager::debug()
$default_file = $this->config->get('debug_logfile') ?: 'temporary://drupal_debug.txt';
$form['debug_logfile'] = [
'#type' => 'textfield',
'#title' => $this->t('Debug Log File'),
'#description' => $this->t('This is the log file that Devel functions such as ddm() write to. Use temporary:// to represent your systems temporary directory. Save with a blank filename to revert to the default.'),
'#default_value' => $default_file,
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$values = $form_state->getValues();
$this->config
->set('page_alter', $values['page_alter'])
->set('raw_names', $values['raw_names'])
->set('error_handlers', $values['error_handlers'])
->set('rebuild_theme', $values['rebuild_theme'])
->set('devel_dumper', $values['dumper'])
->set('debug_logfile', $values['debug_logfile'] ?: 'temporary://drupal_debug.txt')
->save();
parent::submitForm($form, $form_state);
}
/**
* Demonstrates the capabilities of the error handler.
*
* @param string $severity
* The severity level for which demonstrate the error handler capabilities.
*/
protected function demonstrateErrorHandlers(string $severity): void {
switch ($severity) {
case 'notice':
trigger_error('This is an example notice', E_USER_NOTICE);
break;
case 'warning':
trigger_error('This is an example notice', E_USER_NOTICE);
trigger_error('This is an example warning', E_USER_WARNING);
break;
case 'error':
trigger_error('This is an example notice', E_USER_NOTICE);
trigger_error('This is an example warning', E_USER_WARNING);
trigger_error('This is an example error', E_USER_ERROR);
}
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\user\UserStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Define a form to allow the user to switch and become another user.
*/
class SwitchUserForm extends FormBase {
/**
* The csrf token generator.
*/
protected CsrfTokenGenerator $csrfToken;
/**
* The user storage.
*/
protected UserStorageInterface $userStorage;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->csrfToken = $container->get('csrf_token');
$instance->userStorage = $container->get('entity_type.manager')->getStorage('user');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_switchuser_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
$form['autocomplete'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['container-inline'],
],
];
$form['autocomplete']['userid'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Username'),
'#placeholder' => $this->t('Enter username'),
'#target_type' => 'user',
'#selection_settings' => [
'include_anonymous' => FALSE,
],
'#process_default_value' => FALSE,
'#title_display' => 'invisible',
'#required' => TRUE,
'#size' => '28',
];
$form['autocomplete']['actions'] = ['#type' => 'actions'];
$form['autocomplete']['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Switch'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
$userId = $form_state->getValue('userid');
if ($userId === NULL) {
$form_state->setErrorByName('userid', $this->t('Username not found'));
return;
}
/** @var \Drupal\user\UserInterface|null $account */
$account = $this->userStorage->load($userId);
if ($account === NULL) {
$form_state->setErrorByName('userid', $this->t('Username not found'));
}
else {
$form_state->setValue('username', $account->getAccountName());
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
// We cannot rely on automatic token creation, since the csrf seed changes
// after the redirect and the generated token is not more valid.
// @todo find another way to do this.
$url = Url::fromRoute('devel.switch', ['name' => $form_state->getValue('username')]);
$url->setOption('query', ['token' => $this->csrfToken->get($url->getInternalPath())]);
$form_state->setRedirectUrl($url);
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\devel\SwitchUserListHelper;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Define an accessible form to switch the user.
*/
class SwitchUserPageForm extends FormBase {
/**
* The FormBuilder object.
*/
protected FormBuilderInterface $formBuilder;
/**
* A helper for creating the user list form.
*/
protected SwitchUserListHelper $switchUserListHelper;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->switchUserListHelper = $container->get('devel.switch_user_list_helper');
$instance->formBuilder = $container->get('form_builder');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_switchuser_page_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
if ($accounts = $this->switchUserListHelper->getUsers()) {
$form['devel_links'] = $this->switchUserListHelper->buildUserList($accounts);
$form['devel_form'] = $this->formBuilder->getForm(SwitchUserForm::class);
}
else {
$this->messenger->addStatus('There are no user accounts present!');
}
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
// Nothing to do here. This is delegated to devel.switch via http call.
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
// Nothing to do here. This is delegated to devel.switch via http call.
}
}

View File

@ -0,0 +1,194 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Url;
use Drupal\devel\DevelDumperManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form API form to edit a state.
*/
class SystemStateEdit extends FormBase {
/**
* The state store.
*/
protected StateInterface $state;
/**
* Logger service.
*/
protected LoggerInterface $logger;
/**
* The dumper service.
*/
protected DevelDumperManagerInterface $dumper;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->state = $container->get('state');
$instance->messenger = $container->get('messenger');
$instance->logger = $container->get('logger.channel.devel');
$instance->stringTranslation = $container->get('string_translation');
$instance->dumper = $container->get('devel.dumper');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_state_system_edit_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $state_name = ''): array {
// Get the old value.
$old_value = $this->state->get($state_name);
if (!isset($old_value)) {
$this->messenger->addWarning($this->t('State @name does not exist in the system.', ['@name' => $state_name]));
return $form;
}
// Only simple structures are allowed to be edited.
$disabled = !$this->checkObject($old_value);
if ($disabled) {
$this->messenger->addWarning($this->t('Only simple structures are allowed to be edited. State @name contains objects.', ['@name' => $state_name]));
}
// First we show the user the content of the variable about to be edited.
$form['value'] = [
'#type' => 'item',
'#title' => $this->t('Current value for %name', ['%name' => $state_name]),
'#markup' => $this->dumper->dumpOrExport(input: $old_value),
];
$transport = 'plain';
if (!$disabled && is_array($old_value)) {
try {
$old_value = Yaml::encode($old_value);
$transport = 'yaml';
}
catch (InvalidDataTypeException $e) {
$this->messenger->addError($this->t('Invalid data detected for @name : %error', ['@name' => $state_name, '%error' => $e->getMessage()]));
return $form;
}
}
// Store in the form the name of the state variable.
$form['state_name'] = [
'#type' => 'value',
'#value' => $state_name,
];
// Set the transport format for the new value. Values:
// - plain
// - yaml.
$form['transport'] = [
'#type' => 'value',
'#value' => $transport,
];
$form['new_value'] = [
'#type' => 'textarea',
'#title' => $this->t('New value'),
'#default_value' => $disabled ? '' : $old_value,
'#disabled' => $disabled,
'#rows' => 15,
];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
'#disabled' => $disabled,
];
$form['actions']['cancel'] = [
'#type' => 'link',
'#title' => $this->t('Cancel'),
'#url' => Url::fromRoute('devel.state_system_page'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
$values = $form_state->getValues();
if ($values['transport'] == 'yaml') {
// Try to parse the new provided value.
try {
$parsed_value = Yaml::decode($values['new_value']);
$form_state->setValue('parsed_value', $parsed_value);
}
catch (InvalidDataTypeException $e) {
$form_state->setErrorByName('new_value', $this->t('Invalid input: %error', ['%error' => $e->getMessage()]));
}
}
else {
$form_state->setValue('parsed_value', $values['new_value']);
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
// Save the state.
$values = $form_state->getValues();
$this->state->set($values['state_name'], $values['parsed_value']);
$form_state->setRedirectUrl(Url::fromRoute('devel.state_system_page'));
$this->messenger->addMessage($this->t('Variable %variable was successfully edited.', ['%variable' => $values['state_name']]));
$this->logger->info('Variable %variable was successfully edited.', ['%variable' => $values['state_name']]);
}
/**
* Helper function to determine if a variable is or contains an object.
*
* @param mixed $data
* Input data to check.
*
* @return bool
* TRUE if the variable is not an object and does not contain one.
*/
protected function checkObject(mixed $data): bool {
if (is_object($data)) {
return FALSE;
}
if (is_array($data)) {
// If the current object is an array, then check recursively.
foreach ($data as $value) {
// If there is an object the whole container is "contaminated".
if (!$this->checkObject($value)) {
return FALSE;
}
}
}
// All checks pass.
return TRUE;
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configures devel toolbar settings.
*/
class ToolbarSettingsForm extends ConfigFormBase {
/**
* The menu link tree service.
*/
protected MenuLinkTreeInterface $menuLinkTree;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->menuLinkTree = $container->get('menu.link_tree');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_toolbar_settings_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames(): array {
return [
'devel.toolbar.settings',
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->configFactory->getEditable('devel.toolbar.settings');
$form['toolbar_items'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Menu items always visible'),
'#options' => $this->getLinkLabels(),
'#default_value' => $config->get('toolbar_items') ?: [],
'#required' => TRUE,
'#description' => $this->t('Select the menu items always visible in devel toolbar tray. All the items not selected in this list will be visible only when the toolbar orientation is vertical.'),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$values = $form_state->getValues();
$toolbar_items = array_keys(array_filter($values['toolbar_items']));
$this->configFactory->getEditable('devel.toolbar.settings')
->set('toolbar_items', $toolbar_items)
->save();
parent::submitForm($form, $form_state);
}
/**
* Provides an array of available menu items.
*
* @return array
* Associative array of devel menu item labels keyed by plugin ID.
*/
protected function getLinkLabels(): array {
$options = [];
$parameters = new MenuTreeParameters();
$parameters->onlyEnabledLinks()->setTopLevelOnly();
$tree = $this->menuLinkTree->load('devel', $parameters);
foreach ($tree as $element) {
$link = $element->link;
$options[$link->getPluginId()] = $link->getTitle();
}
asort($options);
return $options;
}
}

View File

@ -0,0 +1,130 @@
<?php
namespace Drupal\devel\Plugin\Block;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\devel\Form\SwitchUserForm;
use Drupal\devel\SwitchUserListHelper;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a block for switching users.
*
* @Block(
* id = "devel_switch_user",
* admin_label = @Translation("Switch user"),
* category = "Devel"
* )
*/
class SwitchUserBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* The FormBuilder object.
*/
protected FormBuilderInterface $formBuilder;
/**
* A helper for creating the user list form.
*/
protected SwitchUserListHelper $switchUserListHelper;
// phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod.Found
final public function __construct(array $configuration, string $plugin_id, array $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
$instance = new static($configuration, $plugin_id, $plugin_definition);
$instance->formBuilder = $container->get('form_builder');
$instance->switchUserListHelper = $container->get('devel.switch_user_list_helper');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration(): array {
return [
'list_size' => 12,
'include_anon' => FALSE,
'show_form' => TRUE,
];
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
return AccessResult::allowedIfHasPermission($account, 'switch users');
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$anonymous = new AnonymousUserSession();
$form['list_size'] = [
'#type' => 'number',
'#title' => $this->t('Number of users to display in the list'),
'#default_value' => $this->configuration['list_size'],
'#min' => 1,
'#max' => 50,
];
$form['include_anon'] = [
'#type' => 'checkbox',
'#title' => $this->t('Include %anonymous', ['%anonymous' => $anonymous->getDisplayName()]),
'#default_value' => $this->configuration['include_anon'],
];
$form['show_form'] = [
'#type' => 'checkbox',
'#title' => $this->t('Allow entering any user name'),
'#default_value' => $this->configuration['show_form'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state): void {
$this->configuration['list_size'] = $form_state->getValue('list_size');
$this->configuration['include_anon'] = $form_state->getValue('include_anon');
$this->configuration['show_form'] = $form_state->getValue('show_form');
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge(): int {
return 0;
}
/**
* {@inheritdoc}
*/
public function build(): array {
$build = [];
if ($accounts = $this->switchUserListHelper->getUsers($this->configuration['list_size'], $this->configuration['include_anon'])) {
$build['devel_links'] = $this->switchUserListHelper->buildUserList($accounts);
if ($this->configuration['show_form']) {
$build['devel_form'] = $this->formBuilder->getForm(SwitchUserForm::class);
}
}
return $build;
}
}

View File

@ -0,0 +1,104 @@
<?php
namespace Drupal\devel\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides local task definitions for all entity bundles.
*
* @see \Drupal\devel\Controller\EntityDebugController
* @see \Drupal\devel\Routing\RouteSubscriber
*/
class DevelLocalTask extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The entity manager.
*/
protected EntityTypeManagerInterface $entityTypeManager;
final public function __construct() {}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id): static {
$instance = new static();
$instance->entityTypeManager = $container->get('entity_type.manager');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
$has_edit_path = $entity_type->hasLinkTemplate('edit-form');
$has_canonical_path = $entity_type->hasLinkTemplate('devel-render');
if ($has_edit_path || $has_canonical_path) {
$this->derivatives[$entity_type_id . '.devel_tab'] = [
'route_name' => sprintf('entity.%s.', $entity_type_id) . ($has_edit_path ? 'devel_load' : 'devel_render'),
'title' => $this->t('Devel'),
'base_route' => sprintf('entity.%s.', $entity_type_id) . ($has_canonical_path ? "canonical" : "edit_form"),
'weight' => 100,
];
$this->derivatives[$entity_type_id . '.devel_definition_tab'] = [
'route_name' => sprintf('entity.%s.devel_definition', $entity_type_id),
'title' => $this->t('Definition'),
'parent_id' => sprintf('devel.entities:%s.devel_tab', $entity_type_id),
'weight' => 100,
];
$this->derivatives[$entity_type_id . 'devel_path_alias_tab'] = [
'route_name' => sprintf('entity.%s.devel_path_alias', $entity_type_id),
'title' => $this->t('Path alias'),
'parent_id' => sprintf('devel.entities:%s.devel_tab', $entity_type_id),
'weight' => 100,
];
if ($has_canonical_path) {
$this->derivatives[$entity_type_id . '.devel_render_tab'] = [
'route_name' => sprintf('entity.%s.devel_render', $entity_type_id),
'weight' => 100,
'title' => $this->t('Render'),
'parent_id' => sprintf('devel.entities:%s.devel_tab', $entity_type_id),
];
}
if ($has_edit_path) {
$this->derivatives[$entity_type_id . '.devel_load_tab'] = [
'route_name' => sprintf('entity.%s.devel_load', $entity_type_id),
'weight' => 100,
'title' => $this->t('Load'),
'parent_id' => sprintf('devel.entities:%s.devel_tab', $entity_type_id),
];
$this->derivatives[$entity_type_id . '.devel_load_with_references_tab'] = [
'route_name' => sprintf('entity.%s.devel_load_with_references', $entity_type_id),
'weight' => 100,
'title' => $this->t('Load (with references)'),
'parent_id' => sprintf('devel.entities:%s.devel_tab', $entity_type_id),
];
}
}
}
foreach ($this->derivatives as &$entry) {
$entry += $base_plugin_definition;
}
return $this->derivatives;
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace Drupal\devel\Plugin\Devel\Dumper;
use Drupal\Component\Render\MarkupInterface;
use Drupal\devel\DevelDumperBase;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
/**
* Provides a Symfony VarDumper dumper plugin.
*
* @DevelDumper(
* id = "var_dumper",
* label = @Translation("Symfony var-dumper"),
* description = @Translation("Wrapper for <a href='https://github.com/symfony/var-dumper'>Symfony var-dumper</a> debugging tool."),
* )
*/
class VarDumper extends DevelDumperBase {
/**
* {@inheritdoc}
*/
public function export(mixed $input, ?string $name = NULL): MarkupInterface|string {
$cloner = new VarCloner();
$dumper = 'cli' === PHP_SAPI ? new CliDumper() : new HtmlDumper();
$output = fopen('php://memory', 'r+b');
$dumper->dump($cloner->cloneVar($input), $output);
$output = stream_get_contents($output, -1, 0);
if ($name !== NULL && $name !== '') {
$output = $name . ' => ' . $output;
}
return $this->setSafeMarkup($output);
}
/**
* {@inheritdoc}
*/
public static function checkRequirements(): bool {
return class_exists(VarCloner::class, TRUE);
}
}

View File

@ -0,0 +1,183 @@
<?php
namespace Drupal\devel\Plugin\Mail;
use Drupal\Component\FileSecurity\FileSecurity;
use Drupal\Core\Config\Config;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Mail\MailFormatHelper;
use Drupal\Core\Mail\MailInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Site\Settings;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a mail backend that saves emails as temporary files.
*
* To enable, save a variable in settings.php (or otherwise) whose value
* can be as simple as:
*
* @code
* $config['system.mail']['interface']['default'] = 'devel_mail_log';
* @endcode
*
* By default, the mails are saved in 'temporary://devel-mails'. This setting
* can be changed using 'debug_mail_directory' config setting. For example:
* @code
* $config['devel.settings']['debug_mail_directory'] =
* 'temporary://my-directory';
* @endcode
*
* The default filename pattern used is '%to-%subject-%datetime.mail.txt'. This
* setting can be changed using 'debug_mail_directory' config setting.
* For example:
* @code
* $config['devel.settings']['debug_mail_file_format'] =
* 'devel-mail-%to-%subject-%datetime.mail.txt';
* @endcode
*
* The following placeholders can be used in the filename pattern:
* - %to: the email recipient.
* - %subject: the email subject.
* - %datetime: the current datetime in 'y-m-d_his' format.
*
* @Mail(
* id = "devel_mail_log",
* label = @Translation("Devel Logging Mailer"),
* description = @Translation("Outputs the message as a file in the temporary
* directory.")
* )
*/
class DevelMailLog implements MailInterface, ContainerFactoryPluginInterface {
/**
* The 'devel.settings' config object.
*/
protected Config $config;
/**
* The file system service.
*/
protected FileSystemInterface $fileSystem;
final public function __construct() {}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
$instance = new static();
$instance->config = $container->get('config.factory')->get('devel.settings');
$instance->fileSystem = $container->get('file_system');
return $instance;
}
/**
* {@inheritdoc}
*/
public function mail(array $message): bool {
$directory = $this->config->get('debug_mail_directory');
if (!$this->prepareDirectory($directory)) {
return FALSE;
}
$pattern = $this->config->get('debug_mail_file_format');
$filename = $this->replacePlaceholders($pattern, $message);
$output = $this->composeMessage($message);
return (bool) file_put_contents($directory . '/' . $filename, $output);
}
/**
* {@inheritdoc}
*/
public function format(array $message): array {
// Join the body array into one string.
$message['body'] = implode("\n\n", $message['body']);
// Convert any HTML to plain-text.
$message['body'] = MailFormatHelper::htmlToText($message['body']);
// Wrap the mail body for sending.
$message['body'] = MailFormatHelper::wrapMail($message['body']);
return $message;
}
/**
* Compose the output message.
*
* @param array $message
* A message array, as described in hook_mail_alter().
*
* @return string
* The output message.
*/
protected function composeMessage(array $message): string {
$mimeheaders = [];
$message['headers']['To'] = $message['to'];
foreach ($message['headers'] as $name => $value) {
$mimeheaders[] = $name . ': ' . iconv_mime_decode($value);
}
$line_endings = Settings::get('mail_line_endings', PHP_EOL);
$output = implode($line_endings, $mimeheaders) . $line_endings;
// 'Subject:' is a mail header and should not be translated.
$output .= 'Subject: ' . $message['subject'] . $line_endings;
// Blank line to separate headers from body.
$output .= $line_endings;
return $output . preg_replace('@\r?\n@', $line_endings, $message['body']);
}
/**
* Replaces placeholders with sanitized values in a string.
*
* @param string $filename
* The string that contains the placeholders. The following placeholders
* are considered in the replacement:
* - %to: replaced by the email recipient value.
* - %subject: replaced by the email subject value.
* - %datetime: replaced by the current datetime in 'y-m-d_his' format.
* @param array $message
* A message array, as described in hook_mail_alter().
*
* @return string
* The formatted string.
*/
protected function replacePlaceholders(string $filename, array $message): string {
$tokens = [
'%to' => $message['to'],
'%subject' => $message['subject'],
'%datetime' => date('y-m-d_his'),
];
$filename = str_replace(array_keys($tokens), array_values($tokens), $filename);
return preg_replace('/[^a-zA-Z0-9_\-\.@]/', '_', $filename) ?? '';
}
/**
* Checks that the directory exists and is writable.
*
* Public directories will be protected by adding an .htaccess which
* indicates that the directory is private.
*
* @param string $directory
* A string reference containing the name of a directory path or URI.
*
* @return bool
* TRUE if the directory exists (or was created), is writable and is
* protected (if it is public). FALSE otherwise.
*/
protected function prepareDirectory(string $directory): bool {
if (!$this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY)) {
return FALSE;
}
if (str_starts_with($directory, 'public://')) {
return FileSecurity::writeHtaccess($directory);
}
return TRUE;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Drupal\devel\Plugin\Menu;
use Drupal\Core\Menu\MenuLinkDefault;
use Drupal\Core\Url;
/**
* Modifies the menu link to add destination.
*/
class DestinationMenuLink extends MenuLinkDefault {
/**
* {@inheritdoc}
*/
public function getOptions() {
$options = parent::getOptions();
// Append the current path as destination to the query string.
$options['query']['destination'] = Url::fromRoute('<current>')->toString();
return $options;
}
/**
* {@inheritdoc}
*
* @todo Make cacheable once https://www.drupal.org/node/2582797 lands.
*/
public function getCacheMaxAge(): int {
return 0;
}
}

View File

@ -0,0 +1,29 @@
<?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(): int {
return 0;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Drupal\devel\Render;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Render\MarkupTrait;
/**
* Defines an object that passes safe strings through the Devel system.
*
* This object should only be constructed with a known safe string. If there is
* any risk that the string contains user-entered data that has not been
* filtered first, it must not be used.
*
* @internal
* This object is marked as internal because it should only be used in the
* Devel module.
* @see \Drupal\Core\Render\Markup
*/
final class FilteredMarkup implements MarkupInterface, \Countable {
use MarkupTrait;
}

View File

@ -0,0 +1,307 @@
<?php
namespace Drupal\devel\Routing;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
use Drupal\devel\Controller\EntityDebugController;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Subscriber for Devel routes.
*
* @see \Drupal\devel\Controller\EntityDebugController
* @see \Drupal\devel\Plugin\Derivative\DevelLocalTask
*/
class RouteSubscriber extends RouteSubscriberBase {
/**
* The entity type manager service.
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The router service.
*/
protected RouteProviderInterface $routeProvider;
/**
* Constructs a new RouteSubscriber object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager
* The entity type manager.
* @param \Drupal\Core\Routing\RouteProviderInterface $router_provider
* The router service.
*/
public function __construct(EntityTypeManagerInterface $entity_manager, RouteProviderInterface $router_provider) {
$this->entityTypeManager = $entity_manager;
$this->routeProvider = $router_provider;
}
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection): void {
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
$route = $this->getEntityLoadRoute($entity_type);
if ($route instanceof Route) {
$collection->add(sprintf('entity.%s.devel_load', $entity_type_id), $route);
}
$route = $this->getEntityLoadWithReferencesRoute($entity_type);
if ($route instanceof Route) {
$collection->add(sprintf('entity.%s.devel_load_with_references', $entity_type_id), $route);
}
$route = $this->getEntityRenderRoute($entity_type);
if ($route instanceof Route) {
$collection->add(sprintf('entity.%s.devel_render', $entity_type_id), $route);
}
$route = $this->getEntityTypeDefinitionRoute($entity_type);
if ($route instanceof Route) {
$collection->add(sprintf('entity.%s.devel_definition', $entity_type_id), $route);
}
$route = $this->getPathAliasesRoute($entity_type);
if ($route instanceof Route) {
$collection->add(sprintf('entity.%s.devel_path_alias', $entity_type_id), $route);
}
}
}
/**
* Gets the entity load route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getEntityLoadRoute(EntityTypeInterface $entity_type): ?Route {
if ($devel_load = $entity_type->getLinkTemplate('devel-load')) {
$route = (new Route($devel_load))
->addDefaults([
'_controller' => EntityDebugController::class . '::entityLoad',
'_title' => 'Devel Load',
])
->addRequirements([
'_permission' => 'access devel information',
])
->setOption('_admin_route', TRUE)
->setOption('_devel_entity_type_id', $entity_type->id());
// Set the parameters of the new route using the existing 'edit-form'
// route parameters. If there are none (for example, where Devel creates
// a link for entities with no edit-form) then we need to set the basic
// parameter [entity_type_id => [type => 'entity:entity_type_id']].
// @see https://gitlab.com/drupalspoons/devel/-/issues/377
$parameters = $this->getRouteParameters($entity_type, 'edit-form') !== [] ? $this->getRouteParameters($entity_type, 'edit-form') : [$entity_type->id() => ['type' => 'entity:' . $entity_type->id()]];
$route->setOption('parameters', $parameters);
return $route;
}
return NULL;
}
/**
* Gets the entity load route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getEntityLoadWithReferencesRoute(EntityTypeInterface $entity_type): Route|null {
$devel_load = $entity_type->getLinkTemplate('devel-load-with-references');
if ($devel_load === FALSE) {
return NULL;
}
$entity_type_id = $entity_type->id();
$route = new Route($devel_load);
$route
->addDefaults([
'_controller' => EntityDebugController::class . '::entityLoadWithReferences',
'_title' => 'Devel Load (with references)',
])
->addRequirements([
'_permission' => 'access devel information',
])
->setOption('_admin_route', TRUE)
->setOption('_devel_entity_type_id', $entity_type_id)
->setOption('parameters', [
$entity_type_id => ['type' => 'entity:' . $entity_type_id],
]);
return $route;
}
/**
* Gets the entity render route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getEntityRenderRoute(EntityTypeInterface $entity_type): ?Route {
if ($devel_render = $entity_type->getLinkTemplate('devel-render')) {
$route = (new Route($devel_render))
->addDefaults([
'_controller' => EntityDebugController::class . '::entityRender',
'_title' => 'Devel Render',
])
->addRequirements([
'_permission' => 'access devel information',
])
->setOption('_admin_route', TRUE)
->setOption('_devel_entity_type_id', $entity_type->id());
if (($parameters = $this->getRouteParameters($entity_type, 'canonical')) !== []) {
$route->setOption('parameters', $parameters);
}
return $route;
}
return NULL;
}
/**
* Gets the entity type definition route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getEntityTypeDefinitionRoute(EntityTypeInterface $entity_type): ?Route {
if ($devel_definition = $entity_type->getLinkTemplate('devel-definition')) {
$route = (new Route($devel_definition))
->addDefaults([
'_controller' => EntityDebugController::class . '::entityTypeDefinition',
'_title' => 'Entity type definition',
])
->addRequirements([
'_permission' => 'access devel information',
])
->setOption('_admin_route', TRUE)
->setOption('_devel_entity_type_id', $entity_type->id());
$link_template = $entity_type->getLinkTemplate('edit-form') ? 'edit-form' : 'canonical';
if (($parameters = $this->getRouteParameters($entity_type, $link_template)) !== []) {
$route->setOption('parameters', $parameters);
}
return $route;
}
return NULL;
}
/**
* Gets the path aliases route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getPathAliasesRoute(EntityTypeInterface $entity_type): ?Route {
$path_alias_definition = $entity_type->getLinkTemplate('devel-path-alias');
if ($path_alias_definition === FALSE) {
return NULL;
}
$route = new Route($path_alias_definition);
$route
->addDefaults([
'_controller' => EntityDebugController::class . '::pathAliases',
'_title' => 'Path aliases',
])
->addRequirements([
'_permission' => 'access devel information',
])
->setOption('_admin_route', TRUE)
->setOption('_devel_entity_type_id', $entity_type->id());
$link_template = $entity_type->getLinkTemplate('edit-form') ? 'edit-form' : 'canonical';
$parameters = $this->getRouteParameters($entity_type, $link_template);
if ($parameters !== []) {
$route->setOption('parameters', $parameters);
}
return $route;
}
/**
* Gets the route parameters from the template.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param string $link_template
* The link template.
*
* @return array[]
* A list of route of parameters.
*/
protected function getRouteParameters(EntityTypeInterface $entity_type, string $link_template): array {
$parameters = [];
if (!$path = $entity_type->getLinkTemplate($link_template)) {
return $parameters;
}
$original_route_parameters = [];
$candidate_routes = $this->routeProvider->getRoutesByPattern($path);
if ($candidate_routes->count()) {
// Guess the best match. There could be more than one route sharing the
// same path. Try first an educated guess based on the route name. If we
// can't find one, pick-up the first from the list.
$name = 'entity.' . $entity_type->id() . '.' . str_replace('-', '_', $link_template);
if (!$original_route = $candidate_routes->get($name)) {
$iterator = $candidate_routes->getIterator();
$iterator->rewind();
$original_route = $iterator->current();
}
$original_route_parameters = $original_route->getOption('parameters') ?? [];
}
if (preg_match_all('/{\w*}/', $path, $matches)) {
foreach ($matches[0] as $match) {
$match = str_replace(['{', '}'], '', $match);
// This match has an original route parameter definition.
if (isset($original_route_parameters[$match])) {
$parameters[$match] = $original_route_parameters[$match];
}
// It could be an entity type?
elseif ($this->entityTypeManager->hasDefinition($match)) {
$parameters[$match] = ['type' => 'entity:' . $match];
}
}
}
return $parameters;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events = parent::getSubscribedEvents();
$events[RoutingEvents::ALTER] = ['onAlterRoutes', 100];
return $events;
}
}

View File

@ -0,0 +1,206 @@
<?php
namespace Drupal\devel;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RedirectDestinationInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Url;
use Drupal\user\RoleInterface;
use Drupal\user\RoleStorageInterface;
use Drupal\user\UserStorageInterface;
/**
* Switch user helper service.
*/
class SwitchUserListHelper {
use StringTranslationTrait;
/**
* The Current User object.
*/
protected AccountInterface $currentUser;
/**
* The user storage.
*/
protected UserStorageInterface $userStorage;
/**
* The redirect destination service.
*/
protected RedirectDestinationInterface $redirectDestination;
/**
* The role storage.
*/
protected RoleStorageInterface $roleStorage;
/**
* Constructs a new SwitchUserListHelper service.
*
* @param \Drupal\Core\Session\AccountInterface $current_user
* Current user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Routing\RedirectDestinationInterface $redirect_destination
* The redirect destination service.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation manager.
*/
public function __construct(
AccountInterface $current_user,
EntityTypeManagerInterface $entity_type_manager,
RedirectDestinationInterface $redirect_destination,
TranslationInterface $string_translation,
) {
$this->currentUser = $current_user;
$this->userStorage = $entity_type_manager->getStorage('user');
$this->redirectDestination = $redirect_destination;
$this->stringTranslation = $string_translation;
$this->roleStorage = $entity_type_manager->getStorage('user_role');
}
/**
* Provides the list of accounts that can be used for the user switch.
*
* Inactive users are omitted from all of the following db selects. Users
* with 'switch users' permission and anonymous user if include_anon property
* is set to TRUE, are prioritized.
*
* @param int $limit
* The number of accounts to use for the list.
* @param bool $include_anonymous
* Whether or not to include the anonymous user.
*
* @return \Drupal\Core\Session\AccountInterface[]
* List of accounts to be used for the switch.
*/
public function getUsers(int $limit = 50, bool $include_anonymous = FALSE) {
$limit = $include_anonymous ? $limit - 1 : $limit;
// Users with 'switch users' permission are prioritized so get these first.
$query = $this->userStorage->getQuery()
->condition('uid', 0, '>')
->condition('status', 0, '>')
->sort('access', 'DESC')
->accessCheck(FALSE)
->range(0, $limit);
/** @var array<string, RoleInterface> $roles */
$roles = $this->roleStorage->loadMultiple();
unset($roles[AccountInterface::ANONYMOUS_ROLE]);
$roles = array_filter($roles, static fn($role): bool => $role->hasPermission('switch users'));
if ($roles !== [] && !isset($roles[RoleInterface::AUTHENTICATED_ID])) {
$query->condition('roles', array_keys($roles), 'IN');
}
$user_ids = $query->execute();
// If we don't have enough users with 'switch users' permission, add more
// users until we hit $limit.
if (count($user_ids) < $limit) {
$query = $this->userStorage->getQuery()
->condition('uid', 0, '>')
->condition('status', 0, '>')
->sort('access', 'DESC')
->accessCheck(FALSE)
->range(0, $limit);
// Exclude the prioritized user ids if the previous query returned some.
if (!empty($user_ids)) {
$query->condition('uid', array_keys($user_ids), 'NOT IN');
$query->range(0, $limit - count($user_ids));
}
$user_ids += $query->execute();
}
/** @var \Drupal\Core\Session\AccountInterface[] $accounts */
$accounts = $this->userStorage->loadMultiple($user_ids);
if ($include_anonymous) {
$anonymous = new AnonymousUserSession();
$accounts[$anonymous->id()] = $anonymous;
}
// Syntax comes from https://php.watch/versions/8.2/partially-supported-callable-deprecation.
uasort($accounts, self::class . '::sortUserList');
return $accounts;
}
/**
* Builds the user listing as renderable array.
*
* @param \Drupal\Core\Session\AccountInterface[] $accounts
* The accounts to be rendered in the list.
*
* @return array
* A renderable array.
*/
public function buildUserList(array $accounts): array {
$links = [];
foreach ($accounts as $account) {
$links[$account->id()] = [
'title' => $account->getDisplayName(),
'url' => Url::fromRoute('devel.switch', ['name' => $account->getAccountName()]),
'query' => $this->redirectDestination->getAsArray(),
'attributes' => [
'title' => $account->hasPermission('switch users') ? $this->t('This user can switch back.') : $this->t('Caution: this user will be unable to switch back.'),
],
];
if ($account->isAnonymous()) {
$links[$account->id()]['url'] = Url::fromRoute('user.logout');
}
if ($this->currentUser->id() === $account->id()) {
$links[$account->id()]['title'] = new FormattableMarkup('<strong>%user</strong>', ['%user' => $account->getDisplayName()]);
}
}
return [
'#theme' => 'links',
'#links' => $links,
'#attached' => ['library' => ['devel/devel']],
];
}
/**
* Helper callback for uasort() to sort accounts by last access.
*
* @param \Drupal\Core\Session\AccountInterface $a
* First account.
* @param \Drupal\Core\Session\AccountInterface $b
* Second account.
*
* @return int
* Result of comparing the last access times:
* - -1 if $a was more recently accessed
* - 0 if last access times compare equal
* - 1 if $b was more recently accessed
*/
public static function sortUserList(AccountInterface $a, AccountInterface $b): int {
$a_access = (int) $a->getLastAccessedTime();
$b_access = (int) $b->getLastAccessedTime();
if ($a_access === $b_access) {
return 0;
}
// User never access to site.
if ($a_access === 0) {
return 1;
}
return ($a_access > $b_access) ? -1 : 1;
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace Drupal\devel;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Toolbar integration handler.
*/
class ToolbarHandler implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* The current user.
*/
protected AccountProxyInterface $account;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
$instance = new self();
$instance->account = $container->get('current_user');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* Hook bridge.
*
* @return array
* The devel toolbar items render array.
*
* @see hook_toolbar()
*/
public function toolbar() {
$items['devel'] = [
'#cache' => [
'contexts' => ['user.permissions'],
],
];
if ($this->account->hasPermission('access devel information')) {
$items['devel'] += [
'#type' => 'toolbar_item',
'#weight' => 999,
'tab' => [
'#type' => 'link',
'#title' => $this->t('Devel'),
'#url' => Url::fromRoute('devel.admin_settings'),
'#attributes' => [
'title' => $this->t('Development menu'),
'class' => ['toolbar-icon', 'toolbar-icon-devel'],
],
],
'tray' => [
'#heading' => $this->t('Development menu'),
'devel_menu' => [
// Currently devel menu is uncacheable, so instead of poisoning the
// entire page cache we use a lazy builder.
// @see \Drupal\devel\Plugin\Menu\DestinationMenuLink
// @see \Drupal\devel\Plugin\Menu\RouteDetailMenuItem
'#lazy_builder' => ['devel.lazy_builders:renderMenu' , []],
// Force the creation of the placeholder instead of rely on the
// automatical placeholdering or otherwise the page results
// uncacheable when max-age 0 is bubbled up.
'#create_placeholder' => TRUE,
],
'configuration' => [
'#type' => 'link',
'#title' => $this->t('Configure'),
'#url' => Url::fromRoute('devel.toolbar.settings_form'),
'#options' => [
'attributes' => ['class' => ['edit-devel-toolbar']],
],
],
],
'#attached' => [
'library' => 'devel/devel-toolbar',
],
];
}
return $items;
}
}

View File

@ -0,0 +1,261 @@
<?php
namespace Drupal\devel\Twig\Extension;
use Drupal\devel\DevelDumperManagerInterface;
use Twig\Environment;
use Twig\Extension\AbstractExtension;
use Twig\Template;
use Twig\TwigFunction;
/**
* Provides the Devel debugging function within Twig templates.
*
* NOTE: This extension doesn't do anything unless twig_debug is enabled.
* The twig_debug setting is read from the Twig environment, not Drupal
* Settings, so a container rebuild is necessary when toggling twig_debug on
* and off.
*/
class Debug extends AbstractExtension {
/**
* The devel dumper service.
*/
protected DevelDumperManagerInterface $dumper;
/**
* Constructs a Debug object.
*
* @param \Drupal\devel\DevelDumperManagerInterface $dumper
* The devel dumper service.
*/
public function __construct(DevelDumperManagerInterface $dumper) {
$this->dumper = $dumper;
}
/**
* {@inheritdoc}
*/
public function getName(): string {
return 'devel_debug';
}
/**
* {@inheritdoc}
*/
public function getFunctions() {
$options = [
'is_safe' => ['html'],
'needs_environment' => TRUE,
'needs_context' => TRUE,
'is_variadic' => TRUE,
];
return [
new TwigFunction('devel_dump', [$this, 'dump'], $options),
new TwigFunction('devel_message', [$this, 'message'], $options),
new TwigFunction('dpm', [$this, 'message'], $options),
new TwigFunction('dsm', [$this, 'message'], $options),
new TwigFunction('devel_breakpoint', [$this, 'breakpoint'], [
'needs_environment' => TRUE,
'needs_context' => TRUE,
'is_variadic' => TRUE,
]),
];
}
/**
* Provides debug function to Twig templates.
*
* Handles 0, 1, or multiple arguments.
*
* @param \Twig\Environment $env
* The twig environment instance.
* @param array $context
* An array of parameters passed to the template.
* @param array $args
* An array of parameters passed the function.
*
* @return string
* String representation of the input variables.
*
* @see \Drupal\devel\DevelDumperManager::dump()
*/
public function dump(Environment $env, array $context, array $args = []): string|false|null {
return $this->doDump($env, $context, $args);
}
/**
* Writes the debug information for Twig templates.
*
* @param \Twig\Environment $env
* The twig environment instance.
* @param array $context
* An array of parameters passed to the template.
* @param array $args
* An array of parameters passed the function.
* @param string|null $plugin_id
* The plugin id. Defaults to null.
*
* @return false|string|null
* String representation of the input variables, or null if twig_debug mode
* is tunred off.
*/
private function doDump(Environment $env, array $context, array $args = [], ?string $plugin_id = NULL): false|string|null {
if (!$env->isDebug()) {
return NULL;
}
ob_start();
// No arguments passed, display full Twig context.
if ($args === []) {
$context_variables = $this->getContextVariables($context);
$this->dumper->dump($context_variables, 'Twig context', $plugin_id);
}
else {
$parameters = $this->guessTwigFunctionParameters();
foreach ($args as $index => $variable) {
$name = empty($parameters[$index]) ? NULL : $parameters[$index];
$this->dumper->dump($variable, $name, $plugin_id);
}
}
return ob_get_clean();
}
/**
* Provides debug function to Twig templates.
*
* Handles 0, 1, or multiple arguments.
*
* @param \Twig\Environment $env
* The twig environment instance.
* @param array $context
* An array of parameters passed to the template.
* @param array $args
* An array of parameters passed the function.
*
* @see \Drupal\devel\DevelDumperManager::message()
*/
public function message(Environment $env, array $context, array $args = []): void {
if (!$env->isDebug()) {
return;
}
// No arguments passed, display full Twig context.
if ($args === []) {
$context_variables = $this->getContextVariables($context);
$this->dumper->message($context_variables, 'Twig context');
}
else {
$parameters = $this->guessTwigFunctionParameters();
foreach ($args as $index => $variable) {
$name = empty($parameters[$index]) ? NULL : $parameters[$index];
$this->dumper->message($variable, $name);
}
}
}
/**
* Provides XDebug integration for Twig templates.
*
* To use this features simply put the following statement in the template
* of interest:
*
* @code
* {{ devel_breakpoint() }}
* @endcode
*
* When the template is evaluated is made a call to a dedicated method in
* devel twig debug extension in which is used xdebug_break(), that emits a
* breakpoint to the debug client (the debugger break on the specific line as
* if a normal file/line breakpoint was set on this line).
* In this way you'll be able to inspect any variables available in the
* template (environment, context, specific variables etc..) in your IDE.
*
* @param \Twig\Environment $env
* The twig environment instance.
* @param array $context
* An array of parameters passed to the template.
* @param array $args
* An array of parameters passed the function.
*/
public function breakpoint(Environment $env, array $context, array $args = []): void {
if (!$env->isDebug()) {
return;
}
if (function_exists('xdebug_break')) {
xdebug_break();
}
}
/**
* Filters the Twig context variable.
*
* @param array $context
* The Twig context.
*
* @return array
* An array Twig context variables.
*/
protected function getContextVariables(array $context): array {
$context_variables = [];
foreach ($context as $key => $value) {
if (!$value instanceof Template) {
$context_variables[$key] = $value;
}
}
return $context_variables;
}
/**
* Gets the twig function parameters for the current invocation.
*
* @return array
* The detected twig function parameters.
*/
protected function guessTwigFunctionParameters(): array {
$callee = NULL;
$template = NULL;
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT);
foreach ($backtrace as $index => $trace) {
if (isset($trace['object']) && $trace['object'] instanceof Template) {
$template = $trace['object'];
$callee = $backtrace[$index - 1];
break;
}
}
$parameters = [];
if ($template !== NULL && $callee !== NULL) {
$line_number = $callee['line'];
$debug_infos = $template->getDebugInfo();
if (isset($debug_infos[$line_number])) {
$source_line = $debug_infos[$line_number];
$source_file_name = $template->getTemplateName();
if (is_readable($source_file_name)) {
$source = file($source_file_name, FILE_IGNORE_NEW_LINES);
$line = $source[$source_line - 1];
preg_match('/\((.+)\)/', $line, $matches);
if (isset($matches[1])) {
$parameters = array_map('trim', explode(',', $matches[1]));
}
}
}
}
return $parameters;
}
}