Commit ab9c396b authored by effulgentsia's avatar effulgentsia

Issue #2937799 by EclipseGc, tim.plunkett, effulgentsia, tedbow, Kristen Pol:...

Issue #2937799 by EclipseGc, tim.plunkett, effulgentsia, tedbow, Kristen Pol: Allow greater flexibility within SectionComponent::toRenderArray()
parent 5fd11861
......@@ -31,3 +31,8 @@ services:
layout_builder.sample_entity_generator:
class: Drupal\layout_builder\Entity\LayoutBuilderSampleEntityGenerator
arguments: ['@tempstore.shared', '@entity_type.manager']
layout_builder.render_block_component_subscriber:
class: Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray
arguments: ['@current_user']
tags:
- { name: event_subscriber }
<?php
namespace Drupal\layout_builder\Event;
use Drupal\Core\Cache\CacheableResponseTrait;
use Drupal\layout_builder\SectionComponent;
use Symfony\Component\EventDispatcher\Event;
/**
* Event fired when a section component's render array is being built.
*
* Subscribers to this event should manipulate the cacheability object and the
* build array in this event.
*
* @see \Drupal\layout_builder\LayoutBuilderEvents::SECTION_COMPONENT_BUILD_RENDER_ARRAY
*
* @internal
* Layout Builder is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
class SectionComponentBuildRenderArrayEvent extends Event {
use CacheableResponseTrait;
/**
* The section component whose render array is being built.
*
* @var \Drupal\layout_builder\SectionComponent
*/
protected $component;
/**
* The available contexts.
*
* @var \Drupal\Core\Plugin\Context\ContextInterface[]
*/
protected $contexts;
/**
* The plugin for the section component being built.
*
* @var \Drupal\Component\Plugin\PluginInspectionInterface
*/
protected $plugin;
/**
* Whether the component is in preview mode or not.
*
* @var bool
*/
protected $inPreview;
/**
* The render array built by the event subscribers.
*
* @var array
*/
protected $build = [];
/**
* Creates a new SectionComponentBuildRenderArrayEvent object.
*
* @param \Drupal\layout_builder\SectionComponent $component
* The section component whose render array is being built.
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
* The available contexts.
* @param bool $in_preview
* (optional) Whether the component is in preview mode or not.
*/
public function __construct(SectionComponent $component, array $contexts, $in_preview = FALSE) {
$this->component = $component;
$this->contexts = $contexts;
$this->plugin = $component->getPlugin($contexts);
$this->inPreview = $in_preview;
}
/**
* Get the section component whose render array is being built.
*
* @return \Drupal\layout_builder\SectionComponent
* The section component whose render array is being built.
*/
public function getComponent() {
return $this->component;
}
/**
* Get the available contexts.
*
* @return array|\Drupal\Core\Plugin\Context\ContextInterface[]
* The available contexts.
*/
public function getContexts() {
return $this->contexts;
}
/**
* Get the plugin for the section component being built.
*
* @return \Drupal\Component\Plugin\PluginInspectionInterface
* The plugin for the section component being built.
*/
public function getPlugin() {
return $this->plugin;
}
/**
* Determine if the component is in preview mode.
*
* @return bool
* Whether the component is in preview mode or not.
*/
public function inPreview() {
return $this->inPreview;
}
/**
* Get the render array in its current state.
*
* @return array
* The render array built by the event subscribers.
*/
public function getBuild() {
return $this->build;
}
/**
* Set the render array.
*
* @param array $build
* A render array.
*/
public function setBuild(array $build) {
$this->build = $build;
}
}
<?php
namespace Drupal\layout_builder\EventSubscriber;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
use Drupal\layout_builder\LayoutBuilderEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Builds render arrays and handles access for all block components.
*
* @internal
* Layout Builder is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
class BlockComponentRenderArray implements EventSubscriberInterface {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Creates a BlockComponentRenderArray object.
*
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(AccountInterface $current_user) {
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[LayoutBuilderEvents::SECTION_COMPONENT_BUILD_RENDER_ARRAY] = ['onBuildRender', 100];
return $events;
}
/**
* Builds render arrays for block plugins and sets it on the event.
*
* @param \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent $event
* The section component render event.
*/
public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) {
$block = $event->getPlugin();
if (!$block instanceof BlockPluginInterface) {
return;
}
// Only check access if the component is not being previewed.
if ($event->inPreview()) {
$access = AccessResult::allowed()->setCacheMaxAge(0);
}
else {
$access = $block->access($this->currentUser, TRUE);
}
$event->addCacheableDependency($access);
if ($access->isAllowed()) {
$event->addCacheableDependency($block);
$build = [
// @todo Move this to BlockBase in https://www.drupal.org/node/2931040.
'#theme' => 'block',
'#configuration' => $block->getConfiguration(),
'#plugin_id' => $block->getPluginId(),
'#base_plugin_id' => $block->getBaseId(),
'#derivative_plugin_id' => $block->getDerivativeId(),
'#weight' => $event->getComponent()->getWeight(),
'content' => $block->build(),
];
$event->setBuild($build);
}
}
}
<?php
namespace Drupal\layout_builder;
/**
* Defines events for the layout_builder module.
*
* @see \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent
*
* @internal
* Layout Builder is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
final class LayoutBuilderEvents {
/**
* Name of the event fired when a component's render array is built.
*
* This event allows modules to collaborate on creating the render array of
* the SectionComponent object. The event listener method receives a
* \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent
* instance.
*
* @Event
*
* @see \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent
* @see \Drupal\layout_builder\SectionComponent::toRenderArray()
*
* @var string
*/
const SECTION_COMPONENT_BUILD_RENDER_ARRAY = 'section_component.build.render_array';
}
......@@ -3,10 +3,8 @@
namespace Drupal\layout_builder;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
/**
* Provides a value object for a section component.
......@@ -98,37 +96,10 @@ public function __construct($uuid, $region, array $configuration = [], array $ad
* A renderable array representing the content of the component.
*/
public function toRenderArray(array $contexts = [], $in_preview = FALSE) {
$output = [];
$plugin = $this->getPlugin($contexts);
// @todo Figure out the best way to unify fields and blocks and components
// in https://www.drupal.org/node/1875974.
if ($plugin instanceof BlockPluginInterface) {
$cacheability = CacheableMetadata::createFromObject($plugin);
// Only check access if the component is not being previewed.
if ($in_preview) {
$access = AccessResult::allowed()->setCacheMaxAge(0);
}
else {
$access = $plugin->access($this->currentUser(), TRUE);
}
$cacheability->addCacheableDependency($access);
if ($access->isAllowed()) {
// @todo Move this to BlockBase in https://www.drupal.org/node/2931040.
$output = [
'#theme' => 'block',
'#configuration' => $plugin->getConfiguration(),
'#plugin_id' => $plugin->getPluginId(),
'#base_plugin_id' => $plugin->getBaseId(),
'#derivative_plugin_id' => $plugin->getDerivativeId(),
'#weight' => $this->getWeight(),
'content' => $plugin->build(),
];
}
$cacheability->applyTo($output);
}
$event = new SectionComponentBuildRenderArrayEvent($this, $contexts, $in_preview);
$this->eventDispatcher()->dispatch(LayoutBuilderEvents::SECTION_COMPONENT_BUILD_RENDER_ARRAY, $event);
$output = $event->getBuild();
$event->getCacheableMetadata()->applyTo($output);
return $output;
}
......@@ -293,6 +264,8 @@ public function getPlugin(array $contexts = []) {
* The plugin manager.
*/
protected function pluginManager() {
// @todo Figure out the best way to unify fields and blocks and components
// in https://www.drupal.org/node/1875974.
return \Drupal::service('plugin.manager.block');
}
......@@ -307,13 +280,13 @@ protected function contextHandler() {
}
/**
* Wraps the current user.
* Wraps the event dispatcher.
*
* @return \Drupal\Core\Session\AccountInterface
* The current user.
* @return \Symfony\Component\EventDispatcher\EventDispatcherInterface
* The event dispatcher.
*/
protected function currentUser() {
return \Drupal::currentUser();
protected function eventDispatcher() {
return \Drupal::service('event_dispatcher');
}
/**
......
<?php
namespace Drupal\Tests\layout_builder\Unit;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
use Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray;
use Drupal\layout_builder\SectionComponent;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray
* @group layout_builder
*/
class BlockComponentRenderArrayTest extends UnitTestCase {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* The block plugin manager.
*
* @var \Drupal\Core\Block\BlockManagerInterface
*/
protected $blockManager;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->blockManager = $this->prophesize(BlockManagerInterface::class);
$this->account = $this->prophesize(AccountInterface::class);
$container = new ContainerBuilder();
$container->set('plugin.manager.block', $this->blockManager->reveal());
\Drupal::setContainer($container);
}
/**
* @covers ::onBuildRender
*/
public function testOnBuildRender() {
$block = $this->prophesize(BlockPluginInterface::class);
$access_result = AccessResult::allowed();
$block->access($this->account->reveal(), TRUE)->willReturn($access_result)->shouldBeCalled();
$block->getCacheContexts()->willReturn([]);
$block->getCacheTags()->willReturn(['test']);
$block->getCacheMaxAge()->willReturn(Cache::PERMANENT);
$block->getConfiguration()->willReturn([]);
$block->getPluginId()->willReturn('block_plugin_id');
$block->getBaseId()->willReturn('block_plugin_id');
$block->getDerivativeId()->willReturn(NULL);
$block_content = ['#markup' => 'The block content.'];
$block->build()->willReturn($block_content);
$this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal());
$component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
$contexts = [];
$in_preview = FALSE;
$event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview);
$subscriber = new BlockComponentRenderArray($this->account->reveal());
$expected_build = [
'#theme' => 'block',
'#weight' => 0,
'#configuration' => [],
'#plugin_id' => 'block_plugin_id',
'#base_plugin_id' => 'block_plugin_id',
'#derivative_plugin_id' => NULL,
'content' => $block_content,
];
$expected_cache = $expected_build + [
'#cache' => [
'contexts' => [],
'tags' => ['test'],
'max-age' => -1,
],
];
$subscriber->onBuildRender($event);
$result = $event->getBuild();
$this->assertEquals($expected_build, $result);
$event->getCacheableMetadata()->applyTo($result);
$this->assertEquals($expected_cache, $result);
}
/**
* @covers ::onBuildRender
*/
public function testOnBuildRenderDenied() {
$block = $this->prophesize(BlockPluginInterface::class);
$access_result = AccessResult::forbidden();
$block->access($this->account->reveal(), TRUE)->willReturn($access_result)->shouldBeCalled();
$block->getCacheContexts()->shouldNotBeCalled();
$block->getCacheTags()->shouldNotBeCalled();
$block->getCacheMaxAge()->shouldNotBeCalled();
$block->getConfiguration()->shouldNotBeCalled();
$block->getPluginId()->shouldNotBeCalled();
$block->getBaseId()->shouldNotBeCalled();
$block->getDerivativeId()->shouldNotBeCalled();
$block_content = ['#markup' => 'The block content.'];
$block->build()->willReturn($block_content);
$this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal());
$component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
$contexts = [];
$in_preview = FALSE;
$event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview);
$subscriber = new BlockComponentRenderArray($this->account->reveal());
$expected_build = [];
$expected_cache = [
'#cache' => [
'contexts' => [],
'tags' => [],
'max-age' => -1,
],
];
$subscriber->onBuildRender($event);
$result = $event->getBuild();
$this->assertEquals($expected_build, $result);
$event->getCacheableMetadata()->applyTo($result);
$this->assertEquals($expected_cache, $result);
}
/**
* @covers ::onBuildRender
*/
public function testOnBuildRenderInPreview() {
$block = $this->prophesize(BlockPluginInterface::class);
$block->access($this->account->reveal(), TRUE)->shouldNotBeCalled();
$block->getCacheContexts()->willReturn([]);
$block->getCacheTags()->willReturn(['test']);
$block->getCacheMaxAge()->willReturn(Cache::PERMANENT);
$block->getConfiguration()->willReturn([]);
$block->getPluginId()->willReturn('block_plugin_id');
$block->getBaseId()->willReturn('block_plugin_id');
$block->getDerivativeId()->willReturn(NULL);
$block_content = ['#markup' => 'The block content.'];
$block->build()->willReturn($block_content);
$this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal());
$component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
$contexts = [];
$in_preview = TRUE;
$event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview);
$subscriber = new BlockComponentRenderArray($this->account->reveal());
$expected_build = [
'#theme' => 'block',
'#weight' => 0,
'#configuration' => [],
'#plugin_id' => 'block_plugin_id',
'#base_plugin_id' => 'block_plugin_id',
'#derivative_plugin_id' => NULL,
'content' => $block_content,
];
$expected_cache = $expected_build + [
'#cache' => [
'contexts' => [],
'tags' => ['test'],
'max-age' => 0,
],
];
$subscriber->onBuildRender($event);
$result = $event->getBuild();
$this->assertEquals($expected_build, $result);
$event->getCacheableMetadata()->applyTo($result);
$this->assertEquals($expected_cache, $result);
}
/**
* @covers ::onBuildRender
*/
public function testOnBuildRenderNoBlock() {
$this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn(NULL);
$component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
$contexts = [];
$in_preview = FALSE;
$event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview);
$subscriber = new BlockComponentRenderArray($this->account->reveal());
$expected_build = [];
$expected_cache = [
'#cache' => [
'contexts' => [],
'tags' => [],
'max-age' => -1,
],
];
$subscriber->onBuildRender($event);
$result = $event->getBuild();
$this->assertEquals($expected_build, $result);
$event->getCacheableMetadata()->applyTo($result);
$this->assertEquals($expected_cache, $result);
}
}
<?php
namespace Drupal\Tests\layout_builder\Unit;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Layout\LayoutInterface;
use Drupal\Core\Layout\LayoutPluginManagerInterface;
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
use Drupal\layout_builder\LayoutBuilderEvents;
use Drupal\layout_builder\SectionComponent;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* @coversDefaultClass \Drupal\layout_builder\SectionComponent
* @group layout_builder
*/
class SectionComponentTest extends UnitTestCase {
/**
* @covers ::toRenderArray
*/
public function testToRenderArray() {
$existing_block = $this->prophesize(BlockPluginInterface::class);
$existing_block->getPluginId()->willReturn('block_plugin_id');
$block_manager = $this->prophesize(BlockManagerInterface::class);
$block_manager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($existing_block->reveal());
// Imitate an event subscriber by setting a resulting build on the event.
$event_dispatcher = $this->prophesize(EventDispatcherInterface::class);
$event_dispatcher
->dispatch(LayoutBuilderEvents::SECTION_COMPONENT_BUILD_RENDER_ARRAY, Argument::type(SectionComponentBuildRenderArrayEvent::class))
->shouldBeCalled()
->will(function ($args) {
/** @var \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent $event */
$event = $args[1];
$event->setBuild(['#markup' => $event->getPlugin()->getPluginId()]);
return;
});
$layout_plugin = $this->prophesize(LayoutInterface::class);
$layout_plugin->build(Argument::type('array'))->willReturnArgument(0);
$layout_manager = $this->prophesize(LayoutPluginManagerInterface::class);
$layout_manager->createInstance('layout_onecol', [])->willReturn($layout_plugin->reveal());
$container = new ContainerBuilder();
$container->set('plugin.manager.block', $block_manager->reveal());
$container->set('event_dispatcher', $event_dispatcher->reveal());
$container->set('plugin.manager.core.layout', $layout_manager->reveal());
\Drupal::setContainer($container);
$expected = [
'#cache' => [
'contexts' => [],
'tags' => [],
'max-age' => -1,
],
'#markup' => 'block_plugin_id',
];
$component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
$result = $component->toRenderArray();
$this->assertEquals($expected, $result);
}
}
......@@ -2,6 +2,7 @@
namespace Drupal\Tests\layout_builder\Unit;
use Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockManagerInterface;
......@@ -15,6 +16,7 @@
use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
use Drupal\Tests\UnitTestCase;
......@@ -54,17 +56,29 @@ class SectionRenderTest extends UnitTestCase {
*/
protected $contextRepository;
/**
* The event dispatcher.
*
* @var \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher
*/
protected $eventDispatcher;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->account = $this->prophesize(AccountInterface::class);
$layout_plugin_manager = $this->prophesize(LayoutPluginManagerInterface::class);
$this->blockManager = $this->prophesize(BlockManagerInterface::class);
$this->contextHandler = $this->prophesize(ContextHandlerInterface::class);
$this->contextRepository = $this->prophesize(ContextRepositoryInterface::class);
// @todo Refactor this into some better tests in https://www.drupal.org/node/2942605.
$this->eventDispatcher = (new \ReflectionClass(ContainerAwareEventDispatcher::class))->newInstanceWithoutConstructor();
$this->account = $this->prophesize(AccountInterface::class);
$subscriber = new BlockComponentRenderArray($this->account->reveal());
$this->eventDispatcher->addSubscriber($subscriber);
$layout = $this->prophesize(LayoutInterface::class);
$layout->getPluginDefinition()->willReturn(new LayoutDefinition([]));
......@@ -77,6 +91,7 @@ protected function setUp() {
$container->set('plugin.manager.core.layout', $layout_plugin_manager->reveal());
$container->set('context.handler', $this->contextHandler->reveal());
$container->set('context.repository', $this->contextRepository->reveal());
$container->set('event_dispatcher', $this->eventDispatcher);