diff --git a/src/Plugin/DisplayVariant/BlockDisplayVariant.php b/src/Plugin/DisplayVariant/BlockDisplayVariant.php index 238aa46..87ce680 100644 --- a/src/Plugin/DisplayVariant/BlockDisplayVariant.php +++ b/src/Plugin/DisplayVariant/BlockDisplayVariant.php @@ -68,6 +68,15 @@ protected $token; /** + * An array of collected contexts. + * + * This is only used on runtime, and is not stored. + * + * @var \Drupal\Component\Plugin\Context\ContextInterface[] + */ + protected $contexts = []; + + /** * Constructs a new BlockDisplayVariant. * * @param array $configuration @@ -112,26 +121,6 @@ public static function create(ContainerInterface $container, array $configuratio /** * {@inheritdoc} */ - public function build() { - // Set default page cache keys that include the display. - $build['#cache']['keys'] = [ - 'ctools_block_display', - // The UUID of this display. - // @todo should have an API for this? - $this->configuration['uuid'], - ]; - $build['#pre_render'][] = [$this, 'buildRegions']; - return $build; - } - - /** - * #pre_render callback for building the regions. - */ - abstract public function buildRegions(array $build); - - /** - * {@inheritdoc} - */ public function access(AccountInterface $account = NULL) { // Delegate to the conditions. return $this->determineSelectionAccess($this->getContexts()); @@ -180,6 +169,29 @@ public function getSelectionLogic() { } /** + * Gets the contexts. + * + * @return \Drupal\Component\Plugin\Context\ContextInterface[] + * An array of set contexts, keyed by context name. + */ + public function getContexts() { + return $this->contexts; + } + + /** + * Sets the contexts. + * + * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts + * An array of contexts, keyed by context name. + * + * @return $this + */ + public function setContexts(array $contexts) { + $this->contexts = $contexts; + return $this; + } + + /** * {@inheritdoc} */ protected function contextHandler() { diff --git a/tests/src/Unit/BlockDisplayVariantTest.php b/tests/src/Unit/BlockDisplayVariantTest.php index b2dd2d7..282b116 100644 --- a/tests/src/Unit/BlockDisplayVariantTest.php +++ b/tests/src/Unit/BlockDisplayVariantTest.php @@ -8,18 +8,12 @@ namespace Drupal\Tests\ctools\Unit; use Drupal\Component\Uuid\UuidInterface; -use Drupal\Core\Access\AccessResult; -use Drupal\Core\Block\BlockPluginInterface; -use Drupal\Core\Cache\Context\CacheContextsManager; use Drupal\Core\Plugin\Context\ContextHandlerInterface; -use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\Form\FormState; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Utility\Token; -use Drupal\ctools\Plugin\BlockPluginCollection; use Drupal\ctools\Plugin\DisplayVariant\BlockDisplayVariant; use Drupal\Tests\UnitTestCase; -use Symfony\Component\DependencyInjection\ContainerBuilder; /** * Tests the block display variant plugin. @@ -56,162 +50,6 @@ public function testAccess() { } /** - * Tests the build() method when a block is empty. - * - * @covers ::build - */ - public function testBuildEmptyBlock() { - $account = $this->prophesize(AccountInterface::class); - $block1 = $this->prophesize(BlockPluginInterface::class); - $block1->access($account)->willReturn(TRUE); - - // Building a block with empty content. - $block1->build()->willReturn(['#cache' => [ 'tags' => [ 0 => 'tag_to_be_merged']]]); - - $context_handler = $this->prophesize(ContextHandlerInterface::class); - $uuid_generator = $this->prophesize(UuidInterface::class); - $token = $this->prophesize(Token::class); - - $display_variant = new TestBlockDisplayVariant([], '', [], $context_handler->reveal(), $account->reveal(), $uuid_generator->reveal(), $token->reveal()); - - // Empty block. - $expected_build = [ - '#markup' => '', - '#cache' => [ - 'tags' => [ - 'block_plugin:block_plugin_id', - 'page:page_id', - 'tag_to_be_merged', - ], - 'contexts' => [], - 'max-age' => -1, - ], - ]; - - $build = [ - '#block_plugin' => $block1->reveal(), - '#cache' => [ - 'tags' => [ - 'page:page_id', - 'block_plugin:block_plugin_id', - ], - ], - ]; - - $build = $display_variant->buildBlock($build); - - // Assert that cacheability metadata is merged. - $this->assertSame($expected_build, $build); - } - - /** - * Tests the build() method when blocks can be cached. - * - * @covers ::build - */ - public function testBuild() { - $container = new ContainerBuilder(); - $cache_contexts = $this->prophesize(CacheContextsManager::class); - $container->set('cache_contexts_manager', $cache_contexts->reveal()); - \Drupal::setContainer($container); - - $account = $this->prophesize(AccountInterface::class); - - // Define one block that allows access, access varies by permissions. - $block1 = $this->prophesize(BlockPluginInterface::class); - $block1->access($account, TRUE)->willReturn(AccessResult::allowed()->cachePerPermissions()); - $block1->getConfiguration()->willReturn(['label' => 'Block label']); - $block1->getPluginId()->willReturn('block_plugin_id'); - $block1->getBaseId()->willReturn('block_base_plugin_id'); - $block1->getDerivativeId()->willReturn('block_derivative_plugin_id'); - $block1->getCacheTags()->willReturn(['block_plugin1:block_plugin_id']); - $block1->getCacheMaxAge()->willReturn(3600); - $block1->getCacheContexts()->willReturn(['url']); - - // Define another block that doesn't allow access - $block2 = $this->prophesize()->willImplement(ContextAwarePluginInterface::class)->willImplement(BlockPluginInterface::class); - $block2->access($account, TRUE)->willReturn(AccessResult::forbidden()->cachePerUser()); - $block2->getConfiguration()->willReturn([]); - $block2->getPluginId()->willReturn('block_plugin_id'); - $block2->getBaseId()->willReturn('block_base_plugin_id'); - $block2->getDerivativeId()->willReturn('block_derivative_plugin_id'); - // The block is not shown, so cacheability metadata is not collected. - $block2->getCacheContexts()->shouldNotBeCalled(); - $block2->getCacheMaxAge()->shouldNotBeCalled(); - $block2->getCacheTags()->shouldNotBeCalled(); - $blocks = [ - 'top' => [ - 'block1' => $block1->reveal(), - 'block2' => $block2->reveal(), - ], - ]; - $block_collection = $this->getMockBuilder(BlockPluginCollection::class) - ->disableOriginalConstructor() - ->getMock(); - $block_collection->expects($this->once()) - ->method('getAllByRegion') - ->willReturn($blocks); - - $context_handler = $this->prophesize(ContextHandlerInterface::class); - $context_handler->applyContextMapping($block2->reveal(), [])->shouldBeCalledTimes(1); - - $uuid_generator = $this->prophesize(UuidInterface::class); - $page_title = 'Page title'; - $token = $this->getMockBuilder(Token::class) - ->disableOriginalConstructor() - ->getMock(); - $display_variant = $this->getMockBuilder(BlockDisplayVariant::class) - ->setConstructorArgs([['page_title' => $page_title, 'uuid' => 'UUID'], 'test', [], $context_handler->reveal(), $account->reveal(), $uuid_generator->reveal(), $token]) - ->setMethods(['getBlockCollection', 'renderPageTitle']) - ->getMock(); - - $page = $this->prophesize(PageInterface::class); - $page->id()->willReturn('page_id'); - - $display_variant->expects($this->once()) - ->method('getBlockCollection') - ->willReturn($block_collection); - $display_variant->expects($this->once()) - ->method('renderPageTitle') - ->with($page_title) - ->willReturn($page_title); - - $expected_cache_block1 = [ - 'keys' => ['page_manager_block_display', 'UUID', 'block', 'block1'], - 'tags' => ['block_plugin1:block_plugin_id'], - 'contexts' => ['url'], - 'max-age' => 3600, - ]; - - // The page cacheability metadata contains the access cacheability metadata - // of accessible and non-accessible blocks. Additionally, the cacheability - // metadata of accessible blocks is merged to avoid cache redirects when - // possible. - $expected_cache_page = [ - 'keys' => ['page_manager_block_display', 'UUID'], - 'contexts' => ['url', 'user', 'user.permissions'], - 'tags' => ['block_plugin1:block_plugin_id'], - 'max-age' => 3600, - ]; - - // Build the variant and ensure that pre_render is set only for the first - // block. - $build = $display_variant->build(); - $build = $display_variant->buildRegions($build); - $this->assertSame([$display_variant, 'buildBlock'], $build['top']['block1']['#pre_render'][0]); - $this->assertTrue(empty($build['top']['block2'])); - $this->assertSame($expected_cache_block1, $build['top']['block1']['#cache']); - $this->assertSame($expected_cache_page, $build['#cache']); - - // Ensure that building the block returns the correct markup. - $block1->build()->willReturn([ - '#markup' => 'block1_build_value', - ]); - $block1_build = $display_variant->buildBlock($build['top']['block1']); - $this->assertSame(['#markup' => 'block1_build_value'], $block1_build['content']); - } - - /** * Tests the submitConfigurationForm() method. * * @covers ::submitConfigurationForm @@ -224,7 +62,7 @@ public function testSubmitConfigurationForm($values) { $uuid_generator = $this->prophesize(UuidInterface::class); $token = $this->prophesize(Token::class); - $display_variant = new BlockDisplayVariant([], '', [], $context_handler->reveal(), $account->reveal(), $uuid_generator->reveal(), $token->reveal()); + $display_variant = new TestBlockDisplayVariant([], '', [], $context_handler->reveal(), $account->reveal(), $uuid_generator->reveal(), $token->reveal()); $form = []; $form_state = (new FormState())->setValues($values); @@ -265,158 +103,14 @@ class TestBlockDisplayVariant extends BlockDisplayVariant { * {@inheritdoc} */ public function build() { - // Set default page cache keys that include the display. - $build['#cache']['keys'] = [ - 'test_block_display', - // The UUID of this display. - // @todo should have an API for this? - $this->configuration['uuid'], - ]; - $build['#pre_render'][] = [$this, 'buildRegions']; - return $build; + return []; } - /** - * #pre_render callback for building the regions. - */ - public function buildRegions(array $build) { - $cacheability = CacheableMetadata::createFromRenderArray($build) - ->addCacheableDependency($this); - - $contexts = $this->getContexts(); - foreach ($this->getRegionAssignments() as $region => $blocks) { - if (!$blocks) { - continue; - } - - $region_name = Html::getClass("block-region-$region"); - $build[$region]['#prefix'] = '
'; - $build[$region]['#suffix'] = '
'; - - /** @var $blocks \Drupal\Core\Block\BlockPluginInterface[] */ - $weight = 0; - foreach ($blocks as $block_id => $block) { - if ($block instanceof ContextAwarePluginInterface) { - $this->contextHandler()->applyContextMapping($block, $contexts); - } - $access = $block->access($this->account, TRUE); - $cacheability->addCacheableDependency($access); - if (!$access->isAllowed()) { - continue; - } - - $block_build = [ - '#theme' => 'block', - '#attributes' => [], - '#weight' => $weight++, - '#configuration' => $block->getConfiguration(), - '#plugin_id' => $block->getPluginId(), - '#base_plugin_id' => $block->getBaseId(), - '#derivative_plugin_id' => $block->getDerivativeId(), - '#block_plugin' => $block, - '#pre_render' => [[$this, 'buildBlock']], - '#cache' => [ - 'keys' => ['page_manager_block_display', $this->configuration['uuid'], 'block', $block_id], - // Each block needs cache tags of the page and the block plugin, as - // only the page is a config entity that will trigger cache tag - // invalidations in case of block configuration changes. - 'tags' => Cache::mergeTags($this->getCacheTags(), $block->getCacheTags()), - 'contexts' => $block->getCacheContexts(), - 'max-age' => $block->getCacheMaxAge(), - ], - ]; - - // Merge the cacheability metadata of blocks into the page. This helps - // to avoid cache redirects if the blocks have more cache contexts than - // the page, which the page must respect as well. - $cacheability->addCacheableDependency($block); - - $build[$region][$block_id] = $block_build; - } - } - - $build['#title'] = $this->renderPageTitle($this->configuration['page_title']); - - $cacheability->applyTo($build); - - return $build; - } - - /** - * {@inheritdoc} - */ - public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - $form = parent::buildConfigurationForm($form, $form_state); - - // Allow to configure the page title, even when adding a new display. - // Default to the page label in that case. - $form['page_title'] = [ - '#type' => 'textfield', - '#title' => $this->t('Page title'), - '#description' => $this->t('Configure the page title that will be used for this display.'), - '#default_value' => $this->configuration['page_title'] ?: '', - ]; - - return $form; - } - - /** - * {@inheritdoc} - */ - public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { - parent::submitConfigurationForm($form, $form_state); - - if ($form_state->hasValue('page_title')) { - $this->configuration['page_title'] = $form_state->getValue('page_title'); - } - } - - /** - * {@inheritdoc} - */ - public function defaultConfiguration() { - return parent::defaultConfiguration() + [ - 'blocks' => [], - 'selection_conditions' => [], - 'selection_logic' => 'and', - 'page_title' => '', + public function getRegionNames() { + return [ + 'top' => 'Top', + 'bottom' => 'Bottom', ]; } - /** - * Renders the page title and replaces tokens. - * - * @param string $page_title - * The page title that should be rendered. - * - * @return string - * The page title after replacing any tokens. - */ - protected function renderPageTitle($page_title) { - $data = $this->getContextAsTokenData(); - return $this->token->replace($page_title, $data); - } - - /** - * Returns available context as token data. - * - * @return array - * An array with token data values keyed by token type. - */ - protected function getContextAsTokenData() { - $data = []; - foreach ($this->getContexts() as $context) { - // @todo Simplify this when token and typed data types are unified in - // https://drupal.org/node/2163027. - if (strpos($context->getContextDefinition()->getDataType(), 'entity:') === 0) { - $token_type = substr($context->getContextDefinition()->getDataType(), 7); - if ($token_type == 'taxonomy_term') { - $token_type = 'term'; - } - $data[$token_type] = $context->getContextValue(); - } - } - return $data; - } } -