Commit 8847936f authored by larowlan's avatar larowlan

Issue #2904550 by tim.plunkett, Sam152, jwilson3, neclimdul, eelkeblok,...

Issue #2904550 by tim.plunkett, Sam152, jwilson3, neclimdul, eelkeblok, amateescu, DuaelFr, K3vin_nl, alexpott, tedbow, andypost: PluginDependencyTrait::calculatePluginDependencies() does not handle theme provided plugins
parent e07c2258
......@@ -33,20 +33,44 @@ trait PluginDependencyTrait {
protected function getPluginDependencies(PluginInspectionInterface $instance) {
$dependencies = [];
$definition = $instance->getPluginDefinition();
$provider = NULL;
$config_dependencies = [];
if ($definition instanceof PluginDefinitionInterface) {
$dependencies['module'][] = $definition->getProvider();
if ($definition instanceof DependentPluginDefinitionInterface && $config_dependencies = $definition->getConfigDependencies()) {
$dependencies = NestedArray::mergeDeep($dependencies, $config_dependencies);
$provider = $definition->getProvider();
if ($definition instanceof DependentPluginDefinitionInterface) {
$config_dependencies = $definition->getConfigDependencies();
}
}
elseif (is_array($definition)) {
$dependencies['module'][] = $definition['provider'];
// Plugins can declare additional dependencies in their definition.
$provider = $definition['provider'];
if (isset($definition['config_dependencies'])) {
$dependencies = NestedArray::mergeDeep($dependencies, $definition['config_dependencies']);
$config_dependencies = $definition['config_dependencies'];
}
}
// Add the provider as a dependency, taking into account if it's a module or
// a theme.
if ($provider) {
if ($provider === 'core' || $this->moduleHandler()->moduleExists($provider)) {
$dependencies['module'][] = $provider;
}
elseif ($this->themeHandler()->themeExists($provider)) {
$dependencies['theme'][] = $provider;
}
else {
@trigger_error('Declaring a dependency on an uninstalled module is deprecated in Drupal 8.7.0 and will not be supported in Drupal 9.0.0.', E_USER_DEPRECATED);
$dependencies['module'][] = $provider;
}
}
// Add the config dependencies.
if ($config_dependencies) {
$dependencies = NestedArray::mergeDeep($dependencies, $config_dependencies);
}
// If a plugin is dependent, calculate its dependencies.
if ($instance instanceof DependentPluginInterface && $plugin_dependencies = $instance->calculateDependencies()) {
$dependencies = NestedArray::mergeDeep($dependencies, $plugin_dependencies);
......@@ -69,4 +93,24 @@ protected function calculatePluginDependencies(PluginInspectionInterface $instan
$this->addDependencies($this->getPluginDependencies($instance));
}
/**
* Wraps the module handler.
*
* @return \Drupal\Core\Extension\ModuleHandlerInterface
* The module handler.
*/
protected function moduleHandler() {
return \Drupal::moduleHandler();
}
/**
* Wraps the theme handler.
*
* @return \Drupal\Core\Extension\ThemeHandlerInterface
* The theme handler.
*/
protected function themeHandler() {
return \Drupal::service('theme_handler');
}
}
......@@ -4,6 +4,8 @@
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Tests\Core\Plugin\Fixtures\TestConfigurablePlugin;
use Drupal\Tests\UnitTestCase;
......@@ -41,6 +43,20 @@ class BlockConfigEntityUnitTest extends UnitTestCase {
*/
protected $uuid;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface|\Prophecy\Prophecy\ProphecyInterface
*/
protected $moduleHandler;
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface|\Prophecy\Prophecy\ProphecyInterface
*/
protected $themeHandler;
/**
* {@inheritdoc}
*/
......@@ -60,8 +76,13 @@ protected function setUp() {
$this->uuid = $this->getMock('\Drupal\Component\Uuid\UuidInterface');
$this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class);
$this->themeHandler = $this->prophesize(ThemeHandlerInterface::class);
$container = new ContainerBuilder();
$container->set('entity_type.manager', $this->entityTypeManager);
$container->set('module_handler', $this->moduleHandler->reveal());
$container->set('theme_handler', $this->themeHandler->reveal());
$container->set('uuid', $this->uuid);
\Drupal::setContainer($container);
}
......@@ -70,6 +91,7 @@ protected function setUp() {
* @covers ::calculateDependencies
*/
public function testCalculateDependencies() {
$this->themeHandler->themeExists('stark')->willReturn(TRUE);
$values = ['theme' => 'stark'];
// Mock the entity under test so that we can mock getPluginCollections().
$entity = $this->getMockBuilder('\Drupal\block\Entity\Block')
......@@ -78,6 +100,7 @@ public function testCalculateDependencies() {
->getMock();
// Create a configurable plugin that would add a dependency.
$instance_id = $this->randomMachineName();
$this->moduleHandler->moduleExists('test')->willReturn(TRUE);
$instance = new TestConfigurablePlugin([], $instance_id, ['provider' => 'test']);
// Create a plugin collection to contain the instance.
......
......@@ -148,6 +148,7 @@ public function calculateDependencies() {
if ($this->getLayoutId()) {
$this->calculatePluginDependencies($this->getLayout());
}
return $this;
}
}
<?php
/**
* @file
* Post update functions for layout discovery.
*/
use Drupal\Core\Config\Entity\ConfigEntityUpdater;
/**
* Recalculate dependencies for the entity_form_display entity.
*/
function layout_discovery_post_update_recalculate_entity_form_display_dependencies(&$sandbox = NULL) {
\Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'entity_form_display');
}
/**
* Recalculate dependencies for the entity_view_display entity.
*/
function layout_discovery_post_update_recalculate_entity_view_display_dependencies(&$sandbox = NULL) {
\Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'entity_view_display');
}
<?php
/**
* @file
* Contains database additions to drupal-8.4.0.bare.standard.php.gz for testing
* upgrade path of https://www.drupal.org/project/drupal/issues/2904550.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
$connection->delete('config')
->condition('name', [
'core.extension',
'core.entity_view_display.node.page.default',
'core.entity_form_display.node.page.default',
], 'IN')
->execute();
$connection->insert('config')
->fields([
'collection',
'name',
'data',
])
->values([
'collection' => '',
'name' => 'core.extension',
'data' => 'a:4:{s:6:"module";a:44:{s:14:"automated_cron";i:0;s:5:"block";i:0;s:13:"block_content";i:0;s:10:"breakpoint";i:0;s:8:"ckeditor";i:0;s:5:"color";i:0;s:7:"comment";i:0;s:6:"config";i:0;s:7:"contact";i:0;s:10:"contextual";i:0;s:8:"datetime";i:0;s:5:"dblog";i:0;s:18:"dynamic_page_cache";i:0;s:6:"editor";i:0;s:5:"field";i:0;s:12:"field_layout";i:0;s:8:"field_ui";i:0;s:4:"file";i:0;s:6:"filter";i:0;s:4:"help";i:0;s:7:"history";i:0;s:5:"image";i:0;s:16:"layout_discovery";i:0;s:4:"link";i:0;s:7:"menu_ui";i:0;s:4:"node";i:0;s:7:"options";i:0;s:10:"page_cache";i:0;s:4:"path";i:0;s:9:"quickedit";i:0;s:3:"rdf";i:0;s:6:"search";i:0;s:8:"shortcut";i:0;s:6:"system";i:0;s:8:"taxonomy";i:0;s:4:"text";i:0;s:7:"toolbar";i:0;s:4:"tour";i:0;s:6:"update";i:0;s:4:"user";i:0;s:8:"views_ui";i:0;s:17:"menu_link_content";i:1;s:5:"views";i:10;s:8:"standard";i:1000;}s:5:"theme";a:5:{s:6:"stable";i:0;s:6:"classy";i:0;s:6:"bartik";i:0;s:5:"seven";i:0;s:17:"test_layout_theme";i:0;}s:7:"profile";s:8:"standard";s:5:"_core";a:1:{s:19:"default_config_hash";s:43:"R4IF-ClDHXxblLcG0L7MgsLvfBIMAvi_skumNFQwkDc";}}',
])
->values([
'collection' => '',
'name' => 'core.entity_view_display.node.page.default',
'data' => 'a:12:{s:4:"uuid";s:36:"bf0e7e89-41b9-4031-adef-09933affbfe0";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:2:{s:6:"config";a:2:{i:0;s:26:"field.field.node.page.body";i:1;s:14:"node.type.page";}s:6:"module";a:4:{i:0;s:12:"field_layout";i:1;s:17:"test_layout_theme";i:2;s:4:"text";i:3;s:4:"user";}}s:20:"third_party_settings";a:1:{s:12:"field_layout";a:2:{s:2:"id";s:17:"test_layout_theme";s:8:"settings";a:0:{}}}s:5:"_core";a:1:{s:19:"default_config_hash";s:43:"g1S3_GLaxq4l3I9RIca5Mlz02MxI2KmOquZpHw59akM";}s:2:"id";s:17:"node.page.default";s:16:"targetEntityType";s:4:"node";s:6:"bundle";s:4:"page";s:4:"mode";s:7:"default";s:7:"content";a:2:{s:4:"body";a:6:{s:5:"label";s:6:"hidden";s:4:"type";s:12:"text_default";s:6:"weight";i:100;s:6:"region";s:7:"content";s:8:"settings";a:0:{}s:20:"third_party_settings";a:0:{}}s:5:"links";a:4:{s:6:"weight";i:101;s:6:"region";s:7:"content";s:8:"settings";a:0:{}s:20:"third_party_settings";a:0:{}}}s:6:"hidden";a:0:{}}',
])
->values([
'collection' => '',
'name' => 'core.entity_form_display.node.page.default',
'data' => 'a:12:{s:4:"uuid";s:36:"6d390c8a-e5aa-41ee-98d3-1d422e497283";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:2:{s:6:"config";a:2:{i:0;s:26:"field.field.node.page.body";i:1;s:14:"node.type.page";}s:6:"module";a:4:{i:0;s:12:"field_layout";i:1;s:4:"path";i:2;s:17:"test_layout_theme";i:3;s:4:"text";}}s:20:"third_party_settings";a:1:{s:12:"field_layout";a:2:{s:2:"id";s:17:"test_layout_theme";s:8:"settings";a:0:{}}}s:5:"_core";a:1:{s:19:"default_config_hash";s:43:"sb0qCkzU_8mNq29NehYAU8jCBXWPLeX0UN8sYFVGVcw";}s:2:"id";s:17:"node.page.default";s:16:"targetEntityType";s:4:"node";s:6:"bundle";s:4:"page";s:4:"mode";s:7:"default";s:7:"content";a:8:{s:4:"body";a:5:{s:4:"type";s:26:"text_textarea_with_summary";s:6:"weight";i:31;s:6:"region";s:7:"content";s:8:"settings";a:3:{s:4:"rows";i:9;s:12:"summary_rows";i:3;s:11:"placeholder";s:0:"";}s:20:"third_party_settings";a:0:{}}s:7:"created";a:5:{s:4:"type";s:18:"datetime_timestamp";s:6:"weight";i:10;s:6:"region";s:7:"content";s:8:"settings";a:0:{}s:20:"third_party_settings";a:0:{}}s:4:"path";a:5:{s:4:"type";s:4:"path";s:6:"weight";i:30;s:6:"region";s:7:"content";s:8:"settings";a:0:{}s:20:"third_party_settings";a:0:{}}s:7:"promote";a:5:{s:4:"type";s:16:"boolean_checkbox";s:8:"settings";a:1:{s:13:"display_label";b:1;}s:6:"weight";i:15;s:6:"region";s:7:"content";s:20:"third_party_settings";a:0:{}}s:6:"status";a:5:{s:4:"type";s:16:"boolean_checkbox";s:8:"settings";a:1:{s:13:"display_label";b:1;}s:6:"weight";i:120;s:6:"region";s:7:"content";s:20:"third_party_settings";a:0:{}}s:6:"sticky";a:5:{s:4:"type";s:16:"boolean_checkbox";s:8:"settings";a:1:{s:13:"display_label";b:1;}s:6:"weight";i:16;s:6:"region";s:7:"content";s:20:"third_party_settings";a:0:{}}s:5:"title";a:5:{s:4:"type";s:16:"string_textfield";s:6:"weight";i:-5;s:6:"region";s:7:"content";s:8:"settings";a:2:{s:4:"size";i:60;s:11:"placeholder";s:0:"";}s:20:"third_party_settings";a:0:{}}s:3:"uid";a:5:{s:4:"type";s:29:"entity_reference_autocomplete";s:6:"weight";i:5;s:6:"region";s:7:"content";s:8:"settings";a:3:{s:14:"match_operator";s:8:"CONTAINS";s:4:"size";i:60;s:11:"placeholder";s:0:"";}s:20:"third_party_settings";a:0:{}}}s:6:"hidden";a:0:{}}',
])
->execute();
<?php
namespace Drupal\Tests\layout_discovery\Functional\Update;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* Tests the upgrade path for updating the layout discovery dependencies.
*
* @see layout_discovery_post_update_recalculate_entity_form_display_dependencies()
* @see layout_discovery_post_update_recalculate_entity_view_display_dependencies()
*
* @group layout_discovery
* @group legacy
*/
class LayoutDiscoveryDependenciesUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.4.0.bare.standard.php.gz',
__DIR__ . '/../../../fixtures/update/drupal-8.theme-dependencies-in-module-key-2904550.php',
];
}
/**
* Tests updating the dependencies for layout discovery based entity displays.
*/
public function testUpdatedLayoutDiscoveryDependencies() {
$entities = [
EntityFormDisplay::load('node.page.default'),
EntityViewDisplay::load('node.page.default'),
];
foreach ($entities as $entity) {
$dependencies = $entity->getDependencies();
$this->assertTrue(in_array('test_layout_theme', $dependencies['module']));
$this->assertFalse(isset($dependencies['theme']));
}
$this->runUpdates();
$updated_entities = [
EntityFormDisplay::load('node.page.default'),
EntityViewDisplay::load('node.page.default'),
];
foreach ($updated_entities as $updated_entity) {
$dependencies = $updated_entity->getDependencies();
$this->assertFalse(in_array('test_layout_theme', $dependencies['module']));
$this->assertTrue(in_array('test_layout_theme', $dependencies['theme']));
}
}
}
......@@ -11,6 +11,8 @@
use Drupal\Core\Config\Schema\SchemaIncompleteException;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Language\Language;
use Drupal\Core\Plugin\DefaultLazyPluginCollection;
use Drupal\Tests\Core\Config\Entity\Fixtures\ConfigEntityBaseWithPluginCollections;
......@@ -63,7 +65,7 @@ class ConfigEntityBaseUnitTest extends UnitTestCase {
*
* @var string
*/
protected $provider;
protected $provider = 'the_provider_of_the_entity_type';
/**
* The language manager.
......@@ -93,6 +95,20 @@ class ConfigEntityBaseUnitTest extends UnitTestCase {
*/
protected $typedConfigManager;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface|\Prophecy\Prophecy\ProphecyInterface
*/
protected $moduleHandler;
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface|\Prophecy\Prophecy\ProphecyInterface
*/
protected $themeHandler;
/**
* {@inheritdoc}
*/
......@@ -104,7 +120,6 @@ protected function setUp() {
'uuid' => '3bb9ee60-bea5-4622-b89b-a63319d10b3a',
];
$this->entityTypeId = $this->randomMachineName();
$this->provider = $this->randomMachineName();
$this->entityType = $this->getMock('\Drupal\Core\Config\Entity\ConfigEntityTypeInterface');
$this->entityType->expects($this->any())
->method('getProvider')
......@@ -131,12 +146,17 @@ protected function setUp() {
$this->typedConfigManager = $this->getMock('Drupal\Core\Config\TypedConfigManagerInterface');
$this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class);
$this->themeHandler = $this->prophesize(ThemeHandlerInterface::class);
$container = new ContainerBuilder();
$container->set('entity_type.manager', $this->entityTypeManager);
$container->set('uuid', $this->uuid);
$container->set('language_manager', $this->languageManager);
$container->set('cache_tags.invalidator', $this->cacheTagsInvalidator);
$container->set('config.typed', $this->typedConfigManager);
$container->set('module_handler', $this->moduleHandler->reveal());
$container->set('theme_handler', $this->themeHandler->reveal());
\Drupal::setContainer($container);
$this->entity = $this->getMockForAbstractClass('\Drupal\Core\Config\Entity\ConfigEntityBase', [$values, $this->entityTypeId]);
......@@ -163,6 +183,8 @@ public function testCalculateDependencies() {
* @covers ::preSave
*/
public function testPreSaveDuringSync() {
$this->moduleHandler->moduleExists('node')->willReturn(TRUE);
$query = $this->getMock('\Drupal\Core\Entity\Query\QueryInterface');
$storage = $this->getMock('\Drupal\Core\Config\Entity\ConfigEntityStorageInterface');
......@@ -224,6 +246,12 @@ public function testAddDependency() {
* @dataProvider providerCalculateDependenciesWithPluginCollections
*/
public function testCalculateDependenciesWithPluginCollections($definition, $expected_dependencies) {
$this->moduleHandler->moduleExists('the_provider_of_the_entity_type')->willReturn(TRUE);
$this->moduleHandler->moduleExists('test')->willReturn(TRUE);
$this->moduleHandler->moduleExists('test_theme')->willReturn(FALSE);
$this->themeHandler->themeExists('test_theme')->willReturn(TRUE);
$values = [];
$this->entity = $this->getMockBuilder('\Drupal\Tests\Core\Config\Entity\Fixtures\ConfigEntityBaseWithPluginCollections')
->setConstructorArgs([$values, $this->entityTypeId])
......@@ -269,11 +297,16 @@ public function providerCalculateDependenciesWithPluginCollections() {
['provider' => 'test'],
['module' => ['test']],
],
// Tests that the plugin provider is a theme dependency.
[
['provider' => 'test_theme'],
['theme' => ['test_theme']],
],
// Tests that a plugin that is provided by the same module as the config
// entity is not added to the dependencies array.
[
['provider' => $this->provider],
['module' => [NULL]],
[],
],
// Tests that a config entity that has a plugin which provides config
// dependencies in its definition has them.
......
......@@ -5,6 +5,8 @@
use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Plugin\Definition\DependentPluginDefinitionInterface;
use Drupal\Core\Plugin\PluginDependencyTrait;
use Drupal\Tests\UnitTestCase;
......@@ -24,6 +26,16 @@ class PluginDependencyTraitTest extends UnitTestCase {
public function testGetPluginDependencies(ProphecyInterface $plugin, $definition, array $expected) {
$test_class = new TestPluginDependency();
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
$module_handler->moduleExists('test_module1')->willReturn(TRUE);
$module_handler->moduleExists('test_theme1')->willReturn(FALSE);
$test_class->setModuleHandler($module_handler->reveal());
$theme_handler = $this->prophesize(ThemeHandlerInterface::class);
$theme_handler->themeExists('test_module1')->willReturn(FALSE);
$theme_handler->themeExists('test_theme1')->willReturn(TRUE);
$test_class->setThemeHandler($theme_handler->reveal());
$plugin->getPluginDefinition()->willReturn($definition);
$actual = $test_class->getPluginDependencies($plugin->reveal());
......@@ -46,6 +58,16 @@ public function testGetPluginDependencies(ProphecyInterface $plugin, $definition
public function testCalculatePluginDependencies(ProphecyInterface $plugin, $definition, array $expected) {
$test_class = new TestPluginDependency();
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
$module_handler->moduleExists('test_module1')->willReturn(TRUE);
$module_handler->moduleExists('test_theme1')->willReturn(FALSE);
$test_class->setModuleHandler($module_handler->reveal());
$theme_handler = $this->prophesize(ThemeHandlerInterface::class);
$theme_handler->themeExists('test_module1')->willReturn(FALSE);
$theme_handler->themeExists('test_theme1')->willReturn(TRUE);
$test_class->setThemeHandler($theme_handler->reveal());
$plugin->getPluginDefinition()->willReturn($definition);
$test_class->calculatePluginDependencies($plugin->reveal());
......@@ -65,7 +87,7 @@ public function providerTestPluginDependencies() {
'module' => ['test_module2'],
]);
$data['dependent_plugin'] = [
$data['dependent_plugin_from_module'] = [
$dependent_plugin,
['provider' => 'test_module1'],
[
......@@ -75,6 +97,28 @@ public function providerTestPluginDependencies() {
],
],
];
$data['dependent_plugin_from_core'] = [
$dependent_plugin,
['provider' => 'core'],
[
'module' => [
'core',
'test_module2',
],
],
];
$data['dependent_plugin_from_theme'] = [
$dependent_plugin,
['provider' => 'test_theme1'],
[
'module' => [
'test_module2',
],
'theme' => [
'test_theme1',
],
],
];
$data['array_with_config_dependencies'] = [
$plugin,
......@@ -120,6 +164,38 @@ public function providerTestPluginDependencies() {
return $data;
}
/**
* @covers ::getPluginDependencies
*
* @group legacy
* @expectedDeprecated Declaring a dependency on an uninstalled module is deprecated in Drupal 8.7.0 and will not be supported in Drupal 9.0.0.
*/
public function testNeitherThemeNorModule() {
$test_class = new TestPluginDependency();
$plugin = $this->prophesize(PluginInspectionInterface::class);
$definition = $this->prophesize(PluginDefinitionInterface::class);
$definition->getProvider()->willReturn('neither_theme_nor_module');
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
$module_handler->moduleExists('neither_theme_nor_module')->willReturn(FALSE);
$test_class->setModuleHandler($module_handler->reveal());
$theme_handler = $this->prophesize(ThemeHandlerInterface::class);
$theme_handler->themeExists('neither_theme_nor_module')->willReturn(FALSE);
$test_class->setThemeHandler($theme_handler->reveal());
$plugin->getPluginDefinition()->willReturn($definition);
$actual = $test_class->getPluginDependencies($plugin->reveal());
$expected = [
'module' => [
'neither_theme_nor_module',
],
];
$this->assertEquals($expected, $actual);
}
}
class TestPluginDependency {
......@@ -129,6 +205,26 @@ class TestPluginDependency {
getPluginDependencies as public;
}
protected $moduleHandler;
protected $themeHandler;
public function setModuleHandler(ModuleHandlerInterface $module_handler) {
$this->moduleHandler = $module_handler;
}
public function setThemeHandler(ThemeHandlerInterface $theme_handler) {
$this->themeHandler = $theme_handler;
}
protected function moduleHandler() {
return $this->moduleHandler;
}
protected function themeHandler() {
return $this->themeHandler;
}
/**
* @return array[]
*/
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment