class ComponentNegotiator

Same name in this branch
  1. 11.x core/modules/sdc/src/ComponentNegotiator.php \Drupal\sdc\ComponentNegotiator
Same name in other branches
  1. 10 core/modules/sdc/src/ComponentNegotiator.php \Drupal\sdc\ComponentNegotiator
  2. 10 core/lib/Drupal/Core/Theme/ComponentNegotiator.php \Drupal\Core\Theme\ComponentNegotiator

Determines which component should be used.

Hierarchy

Expanded class hierarchy of ComponentNegotiator

2 files declare their use of ComponentNegotiator
ComponentKernelTestBase.php in core/tests/Drupal/Tests/Core/Theme/Component/ComponentKernelTestBase.php
ComponentKernelTestBase.php in core/tests/Drupal/KernelTests/Components/ComponentKernelTestBase.php

File

core/lib/Drupal/Core/Theme/ComponentNegotiator.php, line 11

Namespace

Drupal\Core\Theme
View source
class ComponentNegotiator {
    
    /**
     * Holds the component IDs from previous negotiations.
     *
     * @var array
     */
    protected array $cache = [];
    
    /**
     * Constructs ComponentNegotiator objects.
     *
     * @param \Drupal\Core\Theme\ThemeManagerInterface $themeManager
     *   The theme manager.
     * @param \Drupal\Core\Extension\ModuleExtensionList $moduleExtensionList
     *   The module extension list.
     */
    public function __construct(ThemeManagerInterface $themeManager, ModuleExtensionList $moduleExtensionList) {
    }
    
    /**
     * Negotiates the active component for the current request.
     *
     * @param string $component_id
     *   The requested component id. Ex: 'my-button', 'my-button--primary',
     *   'component_example:my-button--primary', ...
     * @param array[] $all_definitions
     *   All the plugin definitions for components keyed by plugin ID.
     *
     * @return string|null
     *   The negotiated plugin ID or null if no negotiation was successful.
     */
    public function negotiate(string $component_id, array $all_definitions) : ?string {
        $cache_key = $this->generateCacheKey($component_id);
        $cached_data = $this->cache[$cache_key] ?? NULL;
        if (isset($cached_data)) {
            return $cached_data;
        }
        $negotiated = $this->doNegotiate($component_id, $all_definitions);
        $this->cache[$cache_key] = $negotiated;
        return $negotiated;
    }
    
    /**
     * Negotiates the active component for the current request.
     *
     * @param string $component_id
     *   The requested component id. Ex: 'my-button', 'my-button--primary',
     *   'component_example:my-button--primary', ...
     * @param array[] $all_definitions
     *   All the plugin definitions for components keyed by plugin ID.
     *
     * @return string|null
     *   The negotiated plugin ID or null if no negotiation was successful.
     */
    private function doNegotiate(string $component_id, array $all_definitions) : ?string {
        // Consider only the component definitions matching the component ID in the
        // 'replaces' key.
        $matches = array_filter($all_definitions, static fn(array $definition) => $component_id === ($definition['replaces'] ?? NULL));
        $negotiated_plugin_id = $this->maybeNegotiateByTheme($matches);
        if ($negotiated_plugin_id) {
            return $negotiated_plugin_id;
        }
        return $this->maybeNegotiateByModule($matches);
    }
    
    /**
     * See if there is a candidate in the theme hierarchy.
     *
     * @param array[] $candidates
     *   All the components that might be a match.
     *
     * @return string|null
     *   The plugin ID for the negotiated component, or NULL if none was found.
     */
    private function maybeNegotiateByTheme(array $candidates) : ?string {
        // Prepare the error message.
        $theme_name = $this->themeManager
            ->getActiveTheme()
            ->getName();
        // Let's do theme based negotiation.
        $base_theme_names = array_map(static fn(Extension $extension) => $extension->getName(), $this->themeManager
            ->getActiveTheme()
            ->getBaseThemeExtensions());
        $considered_themes = [
            $theme_name,
            $base_theme_names,
        ];
        // Only consider components in the theme hierarchy tree.
        $candidates = array_filter($candidates, static fn(array $definition) => $definition['extension_type'] === ExtensionType::Theme && in_array($definition['provider'], $considered_themes, TRUE));
        if (empty($candidates)) {
            return NULL;
        }
        $theme_weights = array_flip($considered_themes);
        $sort_by_theme_weight = static fn(array $definition_a, array $definition_b) => $theme_weights[$definition_a['provider']] <=> $theme_weights[$definition_b['provider']];
        // Sort the candidates by weight and choose the one with the lowest weight.
        uasort($candidates, $sort_by_theme_weight);
        $definition = reset($candidates);
        return $definition['id'] ?? NULL;
    }
    
    /**
     * Negotiate the component from the list of candidates for a module.
     *
     * @param array[] $candidates
     *   The candidate definitions.
     *
     * @return string|null
     *   The negotiated plugin ID, or NULL if none found.
     */
    private function maybeNegotiateByModule(array $candidates) : ?string {
        $module_list = $this->moduleExtensionList
            ->getList();
        if (!$module_list) {
            return NULL;
        }
        $candidates = array_filter($candidates, static fn(array $definition) => $definition['extension_type'] === ExtensionType::Module);
        $sort_by_module_weight_and_name = static function (array $definition_a, array $definition_b) use ($module_list) {
            $a_weight = $module_list[$definition_a['provider']]?->weight ?? 999;
            $b_weight = $module_list[$definition_b['provider']]?->weight ?? 999;
            return $a_weight !== $b_weight ? $a_weight <=> $b_weight : $definition_a['provider'] <=> $definition_b['provider'];
        };
        uasort($candidates, $sort_by_module_weight_and_name);
        $definition = reset($candidates);
        return $definition ? $definition['id'] ?? NULL : NULL;
    }
    
    /**
     * Generates the cache key for the current theme and the provided component.
     *
     * @param string $component_id
     *   The component ID.
     *
     * @return string
     *   The cache key.
     */
    private function generateCacheKey(string $component_id) : string {
        return sprintf('component-negotiation::%s::%s', $component_id, $this->themeManager
            ->getActiveTheme()
            ->getName());
    }
    
    /**
     * Clears the negotiation cache.
     */
    public function clearCache() : void {
        $this->cache = [];
    }

}

Members

Title Sort descending Modifiers Object type Summary
ComponentNegotiator::$cache protected property Holds the component IDs from previous negotiations.
ComponentNegotiator::clearCache public function Clears the negotiation cache.
ComponentNegotiator::doNegotiate private function Negotiates the active component for the current request.
ComponentNegotiator::generateCacheKey private function Generates the cache key for the current theme and the provided component.
ComponentNegotiator::maybeNegotiateByModule private function Negotiate the component from the list of candidates for a module.
ComponentNegotiator::maybeNegotiateByTheme private function See if there is a candidate in the theme hierarchy.
ComponentNegotiator::negotiate public function Negotiates the active component for the current request.
ComponentNegotiator::__construct public function Constructs ComponentNegotiator objects.

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