Commit 44892524 authored by catch's avatar catch

Issue #2349011 by Fabianx, Wim Leers: Support placeholder render strategies so...

Issue #2349011 by Fabianx, Wim Leers: Support placeholder render strategies so contrib can support BigPipe, ESI…
parent 54b60851
......@@ -1503,6 +1503,20 @@ services:
arguments: ['@controller_resolver', '@renderer']
tags:
- { name: event_subscriber }
# Placeholder strategies for rendering placeholders.
html_response.placeholder_strategy_subscriber:
class: Drupal\Core\EventSubscriber\HtmlResponsePlaceholderStrategySubscriber
tags:
- { name: event_subscriber }
arguments: ['@placeholder_strategy']
placeholder_strategy:
class: Drupal\Core\Render\Placeholder\ChainedPlaceholderStrategy
tags:
- { name: service_collector, tag: placeholder_strategy, call: addPlaceholderStrategy }
placeholder_strategy.single_flush:
class: Drupal\Core\Render\Placeholder\SingleFlushStrategy
tags:
- { name: placeholder_strategy, priority: -1000 }
email.validator:
class: Egulias\EmailValidator\EmailValidator
......
<?php
/**
* @file
* Contains \Drupal\Core\EventSubscriber\HtmlResponsePlaceholderStrategySubscriber.
*/
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\Render\HtmlResponse;
use Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* HTML response subscriber to allow for different placeholder strategies.
*
* This allows core and contrib to coordinate how to render placeholders;
* e.g. an EsiRenderStrategy could replace the placeholders with ESI tags,
* while e.g. a BigPipeRenderStrategy could store the placeholders in a
* BigPipe service and render them after the main content has been sent to
* the client.
*/
class HtmlResponsePlaceholderStrategySubscriber implements EventSubscriberInterface {
/**
* The placeholder strategy to use.
*
* @var \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface
*/
protected $placeholderStrategy;
/**
* Constructs a HtmlResponsePlaceholderStrategySubscriber object.
*
* @param \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface $placeholder_strategy
* The placeholder strategy to use.
*/
public function __construct(PlaceholderStrategyInterface $placeholder_strategy) {
$this->placeholderStrategy = $placeholder_strategy;
}
/**
* Processes placeholders for HTML responses.
*
* @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
* The event to process.
*/
public function onRespond(FilterResponseEvent $event) {
if (!$event->isMasterRequest()) {
return;
}
$response = $event->getResponse();
if (!$response instanceof HtmlResponse) {
return;
}
$attachments = $response->getAttachments();
if (empty($attachments['placeholders'])) {
return;
}
$attachments['placeholders'] = $this->placeholderStrategy->processPlaceholders($attachments['placeholders']);
$response->setAttachments($attachments);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// Run shortly before HtmlResponseSubscriber.
$events[KernelEvents::RESPONSE][] = ['onRespond', 5];
return $events;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Render\Placeholder\ChainedPlaceholderStrategy.
*/
namespace Drupal\Core\Render\Placeholder;
/**
* Renders placeholders using a chain of placeholder strategies.
*/
class ChainedPlaceholderStrategy implements PlaceholderStrategyInterface {
/**
* An ordered list of placeholder strategy services.
*
* Ordered according to service priority.
*
* @var \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface[]
*/
protected $placeholderStrategies = [];
/**
* Adds a placeholder strategy to use.
*
* @param \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface $strategy
* The strategy to add to the placeholder strategies.
*/
public function addPlaceholderStrategy(PlaceholderStrategyInterface $strategy) {
$this->placeholderStrategies[] = $strategy;
}
/**
* {@inheritdoc}
*/
public function processPlaceholders(array $placeholders) {
if (empty($placeholders)) {
return [];
}
// Assert that there is at least one strategy.
assert('!empty($this->placeholderStrategies)', 'At least one placeholder strategy must be present; by default the fallback strategy \Drupal\Core\Render\Placeholder\SingleFlushStrategy is always present.');
$new_placeholders = [];
// Give each placeholder strategy a chance to replace all not-yet replaced
// placeholders. The order of placeholder strategies is well defined
// and this uses a variation of the "chain of responsibility" design pattern.
foreach ($this->placeholderStrategies as $strategy) {
$processed_placeholders = $strategy->processPlaceholders($placeholders);
assert('array_intersect_key($processed_placeholders, $placeholders) === $processed_placeholders', 'Processed placeholders must be a subset of all placeholders.');
$placeholders = array_diff_key($placeholders, $processed_placeholders);
$new_placeholders += $processed_placeholders;
if (empty($placeholders)) {
break;
}
}
return $new_placeholders;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface.
*/
namespace Drupal\Core\Render\Placeholder;
/**
* Provides an interface for defining a placeholder strategy service.
*/
interface PlaceholderStrategyInterface {
/**
* Processes placeholders to render them with different strategies.
*
* @param array $placeholders
* The placeholders to process, with the keys being the markup for the
* placeholders and the values the corresponding render array describing the
* data to be rendered.
*
* @return array
* The resulting placeholders, with a subset of the keys of $placeholders
* (and those being the markup for the placeholders) but with the
* corresponding render array being potentially modified to render e.g. an
* ESI or BigPipe placeholder.
*/
public function processPlaceholders(array $placeholders);
}
<?php
/**
* @file
* Contains \Drupal\Core\Render\Placeholder\SingleFlushStrategy
*/
namespace Drupal\Core\Render\Placeholder;
/**
* Defines the 'single_flush' placeholder strategy.
*
* This is designed to be the fallback strategy, so should have the lowest
* priority. All placeholders that are not yet replaced at this point will be
* rendered as is and delivered directly.
*/
class SingleFlushStrategy implements PlaceholderStrategyInterface {
/**
* {@inheritdoc}
*/
public function processPlaceholders(array $placeholders) {
// Return all placeholders as is; they should be rendered directly.
return $placeholders;
}
}
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Render\Placeholder\ChainedPlaceholderStrategyTest.
*/
namespace Drupal\Tests\Core\Render\Placeholder;
use Drupal\Core\Render\Placeholder\ChainedPlaceholderStrategy;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Core\Render\Placeholder\ChainedPlaceholderStrategy
* @group Render
*/
class ChainedPlaceholderStrategyTest extends UnitTestCase {
/**
* @covers ::addPlaceholderStrategy
* @covers ::processPlaceholders
*
* @dataProvider providerProcessPlaceholders
*/
public function testProcessPlaceholders($strategies, $placeholders, $result) {
$chained_placeholder_strategy = new ChainedPlaceholderStrategy();
foreach ($strategies as $strategy) {
$chained_placeholder_strategy->addPlaceholderStrategy($strategy);
}
$this->assertEquals($result, $chained_placeholder_strategy->processPlaceholders($placeholders));
}
/**
* Provides a list of render strategies, placeholders and results.
*
* @return array
*/
public function providerProcessPlaceholders() {
$data = [];
// Empty placeholders.
$data['empty placeholders'] = [[], [], []];
// Placeholder removing strategy.
$placeholders = [
'remove-me' => ['#markup' => 'I-am-a-llama-that-will-be-removed-sad-face.'],
];
$prophecy = $this->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
$prophecy->processPlaceholders($placeholders)->willReturn([]);
$dev_null_strategy = $prophecy->reveal();
$data['placeholder removing strategy'] = [[$dev_null_strategy], $placeholders, []];
// Fake Single Flush strategy.
$placeholders = [
'67890' => ['#markup' => 'special-placeholder'],
];
$prophecy = $this->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
$prophecy->processPlaceholders($placeholders)->willReturn($placeholders);
$single_flush_strategy = $prophecy->reveal();
$data['fake single flush strategy'] = [[$single_flush_strategy], $placeholders, $placeholders];
// Fake ESI strategy.
$placeholders = [
'12345' => ['#markup' => 'special-placeholder-for-esi'],
];
$result = [
'12345' => ['#markup' => '<esi:include src="/fragment/12345" />'],
];
$prophecy = $this->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
$prophecy->processPlaceholders($placeholders)->willReturn($result);
$esi_strategy = $prophecy->reveal();
$data['fake esi strategy'] = [[$esi_strategy], $placeholders, $result];
// ESI + SingleFlush strategy (ESI replaces all).
$prophecy = $this->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
$prophecy->processPlaceholders($placeholders)->willReturn($result);
$esi_strategy = $prophecy->reveal();
$prophecy = $this->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
$prophecy->processPlaceholders($placeholders)->shouldNotBeCalled();
$prophecy->processPlaceholders($result)->shouldNotBeCalled();
$prophecy->processPlaceholders([])->shouldNotBeCalled();
$single_flush_strategy = $prophecy->reveal();
$data['fake esi and single_flush strategy - esi replaces all'] = [[$esi_strategy, $single_flush_strategy], $placeholders, $result];
// ESI + SingleFlush strategy (mixed).
$placeholders = [
'12345' => ['#markup' => 'special-placeholder-for-ESI'],
'67890' => ['#markup' => 'special-placeholder'],
'foo' => ['#markup' => 'bar'],
];
$esi_result = [
'12345' => ['#markup' => '<esi:include src="/fragment/12345" />'],
];
$normal_result = [
'67890' => ['#markup' => 'special-placeholder'],
'foo' => ['#markup' => 'bar'],
];
$result = $esi_result + $normal_result;
$prophecy = $this->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
$prophecy->processPlaceholders($placeholders)->willReturn($esi_result);
$esi_strategy = $prophecy->reveal();
$prophecy = $this->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
$prophecy->processPlaceholders($normal_result)->willReturn($normal_result);
$single_flush_strategy = $prophecy->reveal();
$data['fake esi and single_flush strategy - mixed'] = [[$esi_strategy, $single_flush_strategy], $placeholders, $result];
return $data;
}
/**
* @covers ::processPlaceholders
*
* @expectedException \AssertionError
* @expectedExceptionMessage At least one placeholder strategy must be present; by default the fallback strategy \Drupal\Core\Render\Placeholder\SingleFlushStrategy is always present.
*/
public function testProcessPlaceholdersNoStrategies() {
// Placeholders but no strategies defined.
$placeholders = [
'assert-me' => ['#markup' => 'I-am-a-llama-that-will-lead-to-an-assertion-by-the-chained-placeholder-strategy.'],
];
$chained_placeholder_strategy = new ChainedPlaceholderStrategy();
$chained_placeholder_strategy->processPlaceholders($placeholders);
}
/**
* @covers ::processPlaceholders
*
* @expectedException \AssertionError
* @expectedExceptionMessage Processed placeholders must be a subset of all placeholders.
*/
public function testProcessPlaceholdersWithRoguePlaceholderStrategy() {
// Placeholders but no strategies defined.
$placeholders = [
'assert-me' => ['#markup' => 'llama'],
];
$result = [
'assert-me' => ['#markup' => 'llama'],
'new-placeholder' => ['#markup' => 'rogue llama'],
];
$prophecy = $this->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
$prophecy->processPlaceholders($placeholders)->willReturn($result);
$rogue_strategy = $prophecy->reveal();
$chained_placeholder_strategy = new ChainedPlaceholderStrategy();
$chained_placeholder_strategy->addPlaceholderStrategy($rogue_strategy);
$chained_placeholder_strategy->processPlaceholders($placeholders);
}
}
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