domain_views_display-1.x-dev/src/DomainViewsDisplay.php
src/DomainViewsDisplay.php
<?php
declare(strict_types=1);
namespace Drupal\domain_views_display;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\DependencyInjection\AutowireTrait;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\domain\DomainNegotiatorInterface;
use Drupal\domain_views_display\Plugin\views\display_extender\DomainViewsDisplayExtender;
use Drupal\views\Entity\View;
use Drupal\views\ViewExecutable;
use Drupal\views\Views;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
/**
* Overrides a view's display as appropriate.
*
* @see \Drupal\domain_views_display\Plugin\views\display_extender\DomainViewsDisplayExtender
*/
class DomainViewsDisplay implements ContainerInjectionInterface, TrustedCallbackInterface {
use AutowireTrait;
/**
* The options keys to get the extender's options from a view display.
*
* @var non-empty-list<string>
*/
protected const array OPTIONS_KEYS = [
'display_options',
'display_extenders',
DomainViewsDisplayExtender::ID,
];
/**
* Creates the domain views display helper.
*
* @param \Drupal\domain\DomainNegotiatorInterface $domainNegotiator
* The domain negotiator.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
*/
public function __construct(
#[Autowire(service: 'domain.negotiator')]
protected readonly DomainNegotiatorInterface $domainNegotiator,
protected readonly EntityTypeManagerInterface $entityTypeManager,
) {}
/**
* Responds to hook_module_implements_alter().
*
* You can't use service calls in hook_module_implements_alter() so the method
* is static.
*
* @param array<string, string|false> $implementations
* The implementations.
* @param string $hook
* The hook.
*/
public static function moduleImplementsAlter(array &$implementations, string $hook): void {
if ($hook === 'views_pre_view') {
// Have the hook run early so later hook implementations already have the
// correct display set.
$group = $implementations['domain_views_display'];
$implementations = ['domain_views_display' => $group] + $implementations;
}
if ($hook === 'element_info_alter') {
// Have the hook run late so our pre-render is inserted into the front of
// the list.
$group = $implementations['domain_views_display'];
$implementations += ['domain_views_display' => $group];
}
}
/**
* Responds to hook_element_info_alter().
*
* @param array{view: array{'#pre_render': list<callable|string>}} $info
* The element info.
*/
public function elementInfoAlter(array &$info): void {
array_unshift($info['view']['#pre_render'], [static::class, 'preRender']);
}
/**
* Pre-renders a view element, changing display as appropriate.
*
* @param array{'#view'?: ViewExecutable, '#name'?: string, '#display_id': string} $element
* The element.
*
* @return array<string, mixed>
* The element.
*/
public static function preRender(array $element): array {
assert(isset($element['#view']) || isset($element['#name']));
// @phpstan-ignore offsetAccess.notFound
$view = $element['#view'] ?? Views::getView($element['#name']);
assert($view !== NULL);
// View config entities aren't statically cached so store in the element for
// use by the element's pre-render.
$element['#view'] = $view;
if (!$view->access($element['#display_id'])) {
return $element;
}
if (!$view->current_display) {
$view->setDisplay($element['#display_id']);
}
$instance = \Drupal::classResolver(static::class);
assert($instance instanceof DomainViewsDisplay);
$cacheability = CacheableMetadata::createFromRenderArray($element);
$override = $instance->getActiveOverride($view, $cacheability);
$cacheability->applyTo($element);
if (!$override) {
return $element;
}
$element['#display_id'] = $override;
return $element;
}
/**
* Responds to a views_pre_view() hook.
*
* @param \Drupal\views\ViewExecutable $view
* The view.
*/
public function viewsPreView(ViewExecutable $view): void {
$this->ensureDisplay($view);
}
/**
* Ensures a view has the correct display ID for its domain.
*
* It will throw an exception if the current display isn't set.
*
* @param \Drupal\views\ViewExecutable $view
* A view with the display set.
*
* @throws \LogicException
* If the view doesn't have a current display set.
*/
public function ensureDisplay(ViewExecutable $view): void {
$cacheability = CacheableMetadata::createFromRenderArray($view->element);
$override = $this->getActiveOverride($view, $cacheability);
$cacheability->applyTo($view->element);
if (!$override) {
return;
}
$view->setDisplay($override);
}
/**
* Returns the active override for a view with its display set.
*
* @param \Drupal\views\ViewExecutable $view
* A view with the display set.
* @param \Drupal\Core\Cache\CacheableMetadata|null $cacheability
* (optional) An object to track cacheability metadata.
*
* @return string|null
* The display ID to use as an override if present; NULL otherwise.
*
* @throws \LogicException
* If the view doesn't have a current display set.
*/
public function getActiveOverride(ViewExecutable $view, ?CacheableMetadata $cacheability = NULL): ?string {
if ($view->current_display === NULL) {
throw new \LogicException("Can't get active override from view without display set.");
}
$overrides = $this->getOption($view, 'override_displays');
if (!$overrides) {
return NULL;
}
/** @var array<string, string> $overrides */
$cacheability?->addCacheContexts(['url.site']);
$override = $overrides[$this->domainNegotiator->getActiveId()] ?? NULL;
if (!$override || $override === $view->current_display || !$view->access($override)) {
return NULL;
}
return $override;
}
/**
* Returns a list of IDs of views that have domain overrides configured.
*
* @return array<string, string>
* An associative array of view IDs keyed by ID.
*/
public function getViewIdsWithOverrides(): array {
return $this->entityTypeManager->getStorage('view')
->getQuery()
->exists('display.*.display_options.display_extenders.' . DomainViewsDisplayExtender::ID . '.override_displays')
->execute();
}
/**
* Returns a map of view ID to its overridden displays.
*
* @return array<string, array<string, string>>
* An associative array keyed by view ID of associative arrays of display
* IDs keyed by display ID.
*/
public function getViewDisplaysWithOverrides(): array {
$out = [];
foreach (View::loadMultiple($this->getViewIdsWithOverrides()) as $view) {
$option_keys = [...static::OPTIONS_KEYS, 'override_displays'];
$has_overrides = fn(array $display): bool => NestedArray::keyExists($display, $option_keys);
/** @var array<string, array{id: string}> $displays */
$displays = $view->get('display');
$displays = array_filter($displays, $has_overrides);
$out[$view->id()] = array_map(fn (array $display): string => $display['id'], $displays);
}
// @phpstan-ignore return.type
return $out;
}
/**
* Returns the domain views display extender for a view.
*
* @param \Drupal\views\ViewExecutable $view
* The view.
*
* @return \Drupal\domain_views_display\Plugin\views\display_extender\DomainViewsDisplayExtender|null
* The display extender if present; NULL otherwise.
*/
protected function getExtender(ViewExecutable $view): ?DomainViewsDisplayExtender {
$extender = $view->getDisplay()
->getExtenders()[DomainViewsDisplayExtender::ID] ?? NULL;
assert($extender instanceof DomainViewsDisplayExtender);
return $extender;
}
/**
* Gets a domain views extender option.
*
* @param \Drupal\views\ViewExecutable $view
* The view.
* @param string $option
* The option name.
*
* @return mixed
* The views extender option if there is an extender and the option's set;
* NULL otherwise.
*/
protected function getOption(ViewExecutable $view, string $option): mixed {
return $this->getExtender($view)?->options[$option] ?? NULL;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks(): array {
return [
'process',
'preRender',
];
}
}
