LayoutPluginManagerTest.php

Same filename in this branch
  1. 10 core/tests/Drupal/KernelTests/Core/Layout/LayoutPluginManagerTest.php
Same filename in other branches
  1. 9 core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php
  2. 8.9.x core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php
  3. 11.x core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php
  4. 11.x core/tests/Drupal/KernelTests/Core/Layout/LayoutPluginManagerTest.php

Namespace

Drupal\Tests\Core\Layout

File

core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php

View source
<?php

declare (strict_types=1);
namespace Drupal\Tests\Core\Layout;

use Composer\Autoload\ClassLoader;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\Layout\LayoutDefinition;
use Drupal\Core\Layout\LayoutInterface;
use Drupal\Core\Layout\LayoutPluginManager;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;
use Prophecy\Argument;
// cspell:ignore lorem, ipsum, consectetur, adipiscing

/**
 * @coversDefaultClass \Drupal\Core\Layout\LayoutPluginManager
 * @group Layout
 */
class LayoutPluginManagerTest extends UnitTestCase {
    
    /**
     * The module handler.
     *
     * @var \Drupal\Core\Extension\ModuleHandlerInterface
     */
    protected $moduleHandler;
    
    /**
     * The theme handler.
     *
     * @var \Drupal\Core\Extension\ThemeHandlerInterface
     */
    protected $themeHandler;
    
    /**
     * The theme manager.
     *
     * @var \Drupal\Core\Theme\ThemeManagerInterface
     */
    protected $themeManager;
    
    /**
     * Cache backend instance.
     *
     * @var \Drupal\Core\Cache\CacheBackendInterface
     */
    protected $cacheBackend;
    
    /**
     * The layout plugin manager.
     *
     * @var \Drupal\Core\Layout\LayoutPluginManagerInterface
     */
    protected $layoutPluginManager;
    
    /**
     * {@inheritdoc}
     */
    protected function setUp() : void {
        parent::setUp();
        $this->setUpFilesystem();
        $container = new ContainerBuilder();
        $container->set('string_translation', $this->getStringTranslationStub());
        $this->themeManager = $this->prophesize(ThemeManagerInterface::class);
        $container->set('theme.manager', $this->themeManager
            ->reveal());
        \Drupal::setContainer($container);
        $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class);
        $this->moduleHandler
            ->moduleExists('module_a')
            ->willReturn(TRUE);
        $this->moduleHandler
            ->moduleExists('theme_a')
            ->willReturn(FALSE);
        $this->moduleHandler
            ->moduleExists('core')
            ->willReturn(FALSE);
        $this->moduleHandler
            ->moduleExists('invalid_provider')
            ->willReturn(FALSE);
        $module_a = new Extension('vfs://root', 'module', 'modules/module_a/module_a.layouts.yml');
        $this->moduleHandler
            ->getModule('module_a')
            ->willReturn($module_a);
        $this->moduleHandler
            ->getModuleDirectories()
            ->willReturn([
            'module_a' => vfsStream::url('root/modules/module_a'),
        ]);
        $this->moduleHandler
            ->alter('layout', Argument::type('array'))
            ->shouldBeCalled();
        $this->themeHandler = $this->prophesize(ThemeHandlerInterface::class);
        $this->themeHandler
            ->themeExists('theme_a')
            ->willReturn(TRUE);
        $this->themeHandler
            ->themeExists('core')
            ->willReturn(FALSE);
        $this->themeHandler
            ->themeExists('invalid_provider')
            ->willReturn(FALSE);
        $theme_a = new Extension('vfs://root', 'theme', 'themes/theme_a/theme_a.layouts.yml');
        $this->themeHandler
            ->getTheme('theme_a')
            ->willReturn($theme_a);
        $this->themeHandler
            ->getThemeDirectories()
            ->willReturn([
            'theme_a' => vfsStream::url('root/themes/theme_a'),
        ]);
        $this->cacheBackend = $this->prophesize(CacheBackendInterface::class);
        $namespaces = new \ArrayObject([
            'Drupal\\Core' => vfsStream::url('root/core/lib/Drupal/Core'),
        ]);
        $class_loader = new ClassLoader();
        $class_loader->addPsr4("Drupal\\Core\\", vfsStream::url("root/core/lib/Drupal/Core"));
        $class_loader->register(TRUE);
        $this->layoutPluginManager = new LayoutPluginManager($namespaces, $this->cacheBackend
            ->reveal(), $this->moduleHandler
            ->reveal(), $this->themeHandler
            ->reveal());
    }
    
    /**
     * @covers ::getDefinitions
     * @covers ::providerExists
     */
    public function testGetDefinitions() : void {
        $expected = [
            'module_a_provided_layout',
            'theme_a_provided_layout',
            'plugin_provided_layout',
            'plugin_provided_by_annotation_layout',
        ];
        $layout_definitions = $this->layoutPluginManager
            ->getDefinitions();
        $this->assertEquals($expected, array_keys($layout_definitions));
        $this->assertContainsOnlyInstancesOf(LayoutDefinition::class, $layout_definitions);
    }
    
    /**
     * @covers ::getDefinition
     * @covers ::processDefinition
     */
    public function testGetDefinition() : void {
        $layout_definition = $this->layoutPluginManager
            ->getDefinition('theme_a_provided_layout');
        $this->assertSame('theme_a_provided_layout', $layout_definition->id());
        $this->assertSame('2 column layout', (string) $layout_definition->getLabel());
        $this->assertSame('Columns: 2', (string) $layout_definition->getCategory());
        $this->assertSame('A theme provided layout', (string) $layout_definition->getDescription());
        $this->assertInstanceOf(TranslatableMarkup::class, $layout_definition->getLabel());
        $this->assertInstanceOf(TranslatableMarkup::class, $layout_definition->getCategory());
        $this->assertInstanceOf(TranslatableMarkup::class, $layout_definition->getDescription());
        $this->assertSame('twocol', $layout_definition->getTemplate());
        $this->assertSame('themes/theme_a/templates', $layout_definition->getPath());
        $this->assertSame('theme_a/twocol', $layout_definition->getLibrary());
        $this->assertSame('twocol', $layout_definition->getThemeHook());
        $this->assertSame('themes/theme_a/templates', $layout_definition->getTemplatePath());
        $this->assertSame('theme_a', $layout_definition->getProvider());
        $this->assertSame('right', $layout_definition->getDefaultRegion());
        $this->assertSame(LayoutDefault::class, $layout_definition->getClass());
        $expected_regions = [
            'left' => [
                'label' => new TranslatableMarkup('Left region', [], [
                    'context' => 'layout_region',
                ]),
            ],
            'right' => [
                'label' => new TranslatableMarkup('Right region', [], [
                    'context' => 'layout_region',
                ]),
            ],
        ];
        $regions = $layout_definition->getRegions();
        $this->assertEquals($expected_regions, $regions);
        $this->assertInstanceOf(TranslatableMarkup::class, $regions['left']['label']);
        $this->assertInstanceOf(TranslatableMarkup::class, $regions['right']['label']);
        $layout_definition = $this->layoutPluginManager
            ->getDefinition('module_a_provided_layout');
        $this->assertSame('module_a_provided_layout', $layout_definition->id());
        $this->assertSame('1 column layout', (string) $layout_definition->getLabel());
        $this->assertSame('Columns: 1', (string) $layout_definition->getCategory());
        $this->assertSame('A module provided layout', (string) $layout_definition->getDescription());
        $this->assertInstanceOf(TranslatableMarkup::class, $layout_definition->getLabel());
        $this->assertInstanceOf(TranslatableMarkup::class, $layout_definition->getCategory());
        $this->assertInstanceOf(TranslatableMarkup::class, $layout_definition->getDescription());
        $this->assertNull($layout_definition->getTemplate());
        $this->assertSame('modules/module_a/layouts', $layout_definition->getPath());
        $this->assertSame('module_a/onecol', $layout_definition->getLibrary());
        $this->assertSame('onecol', $layout_definition->getThemeHook());
        $this->assertNull($layout_definition->getTemplatePath());
        $this->assertSame('module_a', $layout_definition->getProvider());
        $this->assertSame('top', $layout_definition->getDefaultRegion());
        $this->assertSame(LayoutDefault::class, $layout_definition->getClass());
        $expected_regions = [
            'top' => [
                'label' => new TranslatableMarkup('Top region', [], [
                    'context' => 'layout_region',
                ]),
            ],
            'bottom' => [
                'label' => new TranslatableMarkup('Bottom region', [], [
                    'context' => 'layout_region',
                ]),
            ],
        ];
        $regions = $layout_definition->getRegions();
        $this->assertEquals($expected_regions, $regions);
        $this->assertInstanceOf(TranslatableMarkup::class, $regions['top']['label']);
        $this->assertInstanceOf(TranslatableMarkup::class, $regions['bottom']['label']);
        // Check that arbitrary property value gets set correctly.
        $this->assertSame('ipsum', $layout_definition->get('lorem'));
        $core_path = '/core/lib/Drupal/Core';
        $layout_definition = $this->layoutPluginManager
            ->getDefinition('plugin_provided_layout');
        $this->assertSame('plugin_provided_layout', $layout_definition->id());
        $this->assertEquals('Layout plugin', $layout_definition->getLabel());
        $this->assertEquals('Columns: 1', $layout_definition->getCategory());
        $this->assertEquals('Test layout', $layout_definition->getDescription());
        $this->assertInstanceOf(TranslatableMarkup::class, $layout_definition->getLabel());
        $this->assertInstanceOf(TranslatableMarkup::class, $layout_definition->getCategory());
        $this->assertInstanceOf(TranslatableMarkup::class, $layout_definition->getDescription());
        $this->assertSame('plugin-provided-layout', $layout_definition->getTemplate());
        $this->assertSame($core_path, $layout_definition->getPath());
        $this->assertNull($layout_definition->getLibrary());
        $this->assertSame('plugin_provided_layout', $layout_definition->getThemeHook());
        $this->assertSame("{$core_path}/templates", $layout_definition->getTemplatePath());
        $this->assertSame('core', $layout_definition->getProvider());
        $this->assertSame('main', $layout_definition->getDefaultRegion());
        $this->assertSame('Drupal\\Core\\Plugin\\Layout\\TestLayout', $layout_definition->getClass());
        $expected_regions = [
            'main' => [
                'label' => new TranslatableMarkup('Main Region', [], [
                    'context' => 'layout_region',
                ]),
            ],
        ];
        $regions = $layout_definition->getRegions();
        $this->assertEquals($expected_regions, $regions);
        $this->assertInstanceOf(TranslatableMarkup::class, $regions['main']['label']);
        // Check that arbitrary property value gets set correctly.
        $this->assertSame('adipiscing', $layout_definition->get('consectetur'));
        $layout_definition = $this->layoutPluginManager
            ->getDefinition('plugin_provided_by_annotation_layout');
        $this->assertSame('plugin_provided_by_annotation_layout', $layout_definition->id());
        $this->assertEquals('Layout by annotation plugin', $layout_definition->getLabel());
        $this->assertEquals('Columns: 2', $layout_definition->getCategory());
        $this->assertEquals('Test layout provided by annotated plugin', $layout_definition->getDescription());
        $this->assertInstanceOf(TranslatableMarkup::class, $layout_definition->getLabel());
        $this->assertInstanceOf(TranslatableMarkup::class, $layout_definition->getCategory());
        $this->assertInstanceOf(TranslatableMarkup::class, $layout_definition->getDescription());
        $this->assertSame('plugin-provided-annotation-layout', $layout_definition->getTemplate());
        $this->assertSame($core_path, $layout_definition->getPath());
        $this->assertNull($layout_definition->getLibrary());
        $this->assertSame('plugin_provided_annotation_layout', $layout_definition->getThemeHook());
        $this->assertSame("{$core_path}/templates", $layout_definition->getTemplatePath());
        $this->assertSame('core', $layout_definition->getProvider());
        $this->assertSame('left', $layout_definition->getDefaultRegion());
        $this->assertSame('Drupal\\Core\\Plugin\\Layout\\TestAnnotationLayout', $layout_definition->getClass());
        $expected_regions = [
            'left' => [
                'label' => new TranslatableMarkup('Left Region', [], [
                    'context' => 'layout_region',
                ]),
            ],
            'right' => [
                'label' => new TranslatableMarkup('Right Region', [], [
                    'context' => 'layout_region',
                ]),
            ],
        ];
        $regions = $layout_definition->getRegions();
        $this->assertEquals($expected_regions, $regions);
        $this->assertInstanceOf(TranslatableMarkup::class, $regions['left']['label']);
        $this->assertInstanceOf(TranslatableMarkup::class, $regions['right']['label']);
    }
    
    /**
     * @covers ::processDefinition
     */
    public function testProcessDefinition() : void {
        $this->moduleHandler
            ->alter('layout', Argument::type('array'))
            ->shouldNotBeCalled();
        $this->expectException(InvalidPluginDefinitionException::class);
        $this->expectExceptionMessage('The "module_a_derived_layout:array_based" layout definition must extend ' . LayoutDefinition::class);
        $module_a_provided_layout = <<<'EOS'
module_a_derived_layout:
  deriver: \Drupal\Tests\Core\Layout\LayoutDeriver
  array_based: true
EOS;
        vfsStream::create([
            'modules' => [
                'module_a' => [
                    'module_a.layouts.yml' => $module_a_provided_layout,
                ],
            ],
        ]);
        $this->layoutPluginManager
            ->getDefinitions();
    }
    
    /**
     * @covers ::getThemeImplementations
     */
    public function testGetThemeImplementations() : void {
        $core_path = '/core/lib/Drupal/Core';
        $expected = [
            'layout' => [
                'render element' => 'content',
            ],
            'twocol' => [
                'render element' => 'content',
                'base hook' => 'layout',
                'template' => 'twocol',
                'path' => 'themes/theme_a/templates',
            ],
            'plugin_provided_layout' => [
                'render element' => 'content',
                'base hook' => 'layout',
                'template' => 'plugin-provided-layout',
                'path' => "{$core_path}/templates",
            ],
            'plugin_provided_annotation_layout' => [
                'render element' => 'content',
                'base hook' => 'layout',
                'template' => 'plugin-provided-annotation-layout',
                'path' => "{$core_path}/templates",
            ],
        ];
        $theme_implementations = $this->layoutPluginManager
            ->getThemeImplementations();
        $this->assertEquals($expected, $theme_implementations);
    }
    
    /**
     * @covers ::getCategories
     */
    public function testGetCategories() : void {
        $expected = [
            'Columns: 1',
            'Columns: 2',
        ];
        $categories = $this->layoutPluginManager
            ->getCategories();
        $this->assertEquals($expected, $categories);
    }
    
    /**
     * @covers ::getSortedDefinitions
     */
    public function testGetSortedDefinitions() : void {
        // Sorted by category first, then label.
        $expected = [
            'module_a_provided_layout',
            'plugin_provided_layout',
            'theme_a_provided_layout',
            'plugin_provided_by_annotation_layout',
        ];
        $layout_definitions = $this->layoutPluginManager
            ->getSortedDefinitions();
        $this->assertEquals($expected, array_keys($layout_definitions));
        $this->assertContainsOnlyInstancesOf(LayoutDefinition::class, $layout_definitions);
    }
    
    /**
     * @covers ::getGroupedDefinitions
     */
    public function testGetGroupedDefinitions() : void {
        $category_expected = [
            'Columns: 1' => [
                'module_a_provided_layout',
                'plugin_provided_layout',
            ],
            'Columns: 2' => [
                'theme_a_provided_layout',
                'plugin_provided_by_annotation_layout',
            ],
        ];
        $definitions = $this->layoutPluginManager
            ->getGroupedDefinitions();
        $this->assertEquals(array_keys($category_expected), array_keys($definitions));
        foreach ($category_expected as $category => $expected) {
            $this->assertArrayHasKey($category, $definitions);
            $this->assertEquals($expected, array_keys($definitions[$category]));
            $this->assertContainsOnlyInstancesOf(LayoutDefinition::class, $definitions[$category]);
        }
    }
    
    /**
     * @covers ::getLayoutOptions
     *
     * Test that modules and themes can alter the list of layouts.
     */
    public function testGetLayoutOptions() : void {
        $this->moduleHandler
            ->alter([
            'plugin_filter_layout',
            'plugin_filter_layout__layout',
        ], Argument::type('array'), [], 'layout')
            ->shouldBeCalled();
        $this->themeManager
            ->alter([
            'plugin_filter_layout',
            'plugin_filter_layout__layout',
        ], Argument::type('array'), [], 'layout')
            ->shouldBeCalled();
        $this->layoutPluginManager
            ->getLayoutOptions();
    }
    
    /**
     * Sets up the filesystem with YAML files and annotated plugins.
     */
    protected function setUpFilesystem() {
        $module_a_provided_layout = <<<'EOS'
module_a_provided_layout:
  label: 1 column layout
  category: 'Columns: 1'
  description: 'A module provided layout'
  theme_hook: onecol
  path: layouts
  library: module_a/onecol
  regions:
    top:
      label: Top region
    bottom:
      label: Bottom region
  lorem: ipsum
module_a_derived_layout:
  label: 'Invalid provider derived layout'
  deriver: \Drupal\Tests\Core\Layout\LayoutDeriver
  invalid_provider: true
EOS;
        $theme_a_provided_layout = <<<'EOS'
theme_a_provided_layout:
  class: '\Drupal\Core\Layout\LayoutDefault'
  label: 2 column layout
  category: 'Columns: 2'
  description: 'A theme provided layout'
  template: twocol
  path: templates
  library: theme_a/twocol
  default_region: right
  regions:
    left:
      label: Left region
    right:
      label: Right region
EOS;
        $plugin_provided_layout = <<<'EOS'
<?php
namespace Drupal\Core\Plugin\Layout;
use Drupal\Core\Layout\Attribute\Layout;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
 * The TestLayout Class.
 */
#[Layout(
  id: 'plugin_provided_layout',
  label: new TranslatableMarkup('Layout plugin'),
  category: new TranslatableMarkup('Columns: 1'),
  description: new TranslatableMarkup('Test layout'),
  path: "core/lib/Drupal/Core",
  template: "templates/plugin-provided-layout",
  regions: [
    "main" => [
      "label" => new TranslatableMarkup("Main Region", [], ["context" => "layout_region"]),
    ],
  ],
  consectetur: 'adipiscing',
)]
class TestLayout extends LayoutDefault {}
EOS;
        $plugin_provided_by_annotation_layout = <<<'EOS'
<?php
namespace Drupal\Core\Plugin\Layout;
use Drupal\Core\Layout\LayoutDefault;
/**
 * @Layout(
 *   id = "plugin_provided_by_annotation_layout",
 *   label = @Translation("Layout by annotation plugin"),
 *   category = @Translation("Columns: 2"),
 *   description = @Translation("Test layout provided by annotated plugin"),
 *   path = "core/lib/Drupal/Core",
 *   template = "templates/plugin-provided-annotation-layout",
 *   default_region = "left",
 *   regions = {
 *     "left" = {
 *       "label" = @Translation("Left Region", context = "layout_region")
 *     },
 *     "right" = {
 *        "label" = @Translation("Right Region", context = "layout_region")
 *     }
 *   }
 * )
 */
class TestAnnotationLayout extends LayoutDefault {}
EOS;
        vfsStream::setup('root');
        vfsStream::create([
            'modules' => [
                'module_a' => [
                    'module_a.layouts.yml' => $module_a_provided_layout,
                ],
            ],
        ]);
        vfsStream::create([
            'themes' => [
                'theme_a' => [
                    'theme_a.layouts.yml' => $theme_a_provided_layout,
                ],
            ],
        ]);
        vfsStream::create([
            'core' => [
                'lib' => [
                    'Drupal' => [
                        'Core' => [
                            'Plugin' => [
                                'Layout' => [
                                    'TestLayout.php' => $plugin_provided_layout,
                                    'TestAnnotationLayout.php' => $plugin_provided_by_annotation_layout,
                                ],
                            ],
                        ],
                    ],
                ],
            ],
        ]);
    }

}

/**
 * Provides a dynamic layout deriver for the test.
 */
class LayoutDeriver extends DeriverBase {
    
    /**
     * {@inheritdoc}
     */
    public function getDerivativeDefinitions($base_plugin_definition) {
        if ($base_plugin_definition->get('array_based')) {
            $this->derivatives['array_based'] = [];
        }
        if ($base_plugin_definition->get('invalid_provider')) {
            $this->derivatives['invalid_provider'] = new LayoutDefinition([
                'id' => 'invalid_provider',
                'provider' => 'invalid_provider',
            ]);
            $this->derivatives['invalid_provider']
                ->setClass(LayoutInterface::class);
        }
        return $this->derivatives;
    }

}

Classes

Title Deprecated Summary
LayoutDeriver Provides a dynamic layout deriver for the test.
LayoutPluginManagerTest @coversDefaultClass \Drupal\Core\Layout\LayoutPluginManager @group Layout

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.