diff --git a/src/Element/ComponentElementBuilder.php b/src/Element/ComponentElementBuilder.php index d890b3599b5123ebcde07d5bba5639816d3a6f7f..e2948b34e57d6a2739782c9dd7d8389d1eca881f 100644 --- a/src/Element/ComponentElementBuilder.php +++ b/src/Element/ComponentElementBuilder.php @@ -4,14 +4,14 @@ declare(strict_types=1); namespace Drupal\ui_patterns\Element; +use Drupal\ui_patterns\Plugin\UiPatterns\PropType\SlotPropType; use Drupal\Component\Plugin\Exception\ContextException; use Drupal\Core\Plugin\Component; use Drupal\Core\Render\Element; use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Theme\ComponentPluginManager; use Drupal\ui_patterns\ComponentPluginManager as UiPatternsComponentPluginManager; -use Drupal\ui_patterns\PropTypePluginManager; -use Drupal\ui_patterns\SourceInterface; +use Drupal\ui_patterns\PropTypeInterface; use Drupal\ui_patterns\SourcePluginBase; use Drupal\ui_patterns\SourcePluginManager; use Psr\Log\LoggerInterface; @@ -34,7 +34,6 @@ class ComponentElementBuilder implements TrustedCallbackInterface { */ public function __construct( protected SourcePluginManager $sourcesManager, - protected PropTypePluginManager $propTypeManager, protected ComponentPluginManager $componentPluginManager, protected ModuleHandlerInterface $moduleHandler, protected LoggerInterface $logger, @@ -79,75 +78,86 @@ class ComponentElementBuilder implements TrustedCallbackInterface { * Add a single prop to the renderable. */ protected function buildProp(array $build, string $prop_id, array $definition, array $configuration, array $source_contexts): array { - if (isset($build["#props"][$prop_id])) { + if (isset($build['#props'][$prop_id])) { // Keep existing props. No known use case yet. return $build; } - $source = $this->getSource($prop_id, $definition, $configuration, $source_contexts); - if (!$source) { - return $build; + return $this->buildSource($build, $prop_id, $definition, $configuration, $source_contexts); + } + + /** + * Add data to a prop or a slot. + */ + protected function addDataToComponent(array &$build, string $prop_or_slot_id, PropTypeInterface $prop_type, mixed $data): void { + if ($prop_type instanceof SlotPropType) { + if ($data !== NULL && Element::isRenderArray($data)) { + if ($this->isSingletonRenderArray($data)) { + $data = array_values($data)[0]; + } + $build['#slots'][$prop_or_slot_id][] = $data; + } } - try { - $build = $source->alterComponent($build); - $prop_type = $definition['ui_patterns']['type_definition']; - $data = $source->getValue($prop_type); - $this->moduleHandler->alter('ui_patterns_source_value', $data, $source, $configuration); - if (empty($data) && $prop_type->getPluginId() !== 'attributes') { + else { + if (!empty($data) || $prop_type->getPluginId() === 'attributes') { // For JSON Schema validator, empty value is not the same as missing // value, and we want to prevent some of the prop types rules to be - // applied on empty values: string pattern, string format, enum, number - // min/max... + // applied on empty values: string pattern, string format, + // enum, number min/max... // However, we don't remove empty attributes to avoid an error with // Drupal\Core\Template\TwigExtension::createAttribute() when themers // forget to use the default({}) filter. - return $build; + $build['#props'][$prop_or_slot_id] = $data; } - $build["#props"][$prop_id] = $data; } - catch (ContextException $e) { - // ContextException is thrown when a required context is missing. - // We don't want to break the render process, so we just ignore the prop. - $error_message = t("Context error for prop '@prop_id' in component '@component_id': @message", [ - '@prop_id' => $prop_id, - '@component_id' => $build['#component'], - '@message' => $e->getMessage(), - ]); - $this->logger->error($error_message); - } - return $build; } /** - * Get Source plugin for a prop. + * Update the build array for a configured source on a prop/slot. * + * @param array $build + * The build array. * @param string $prop_or_slot_id * Prop ID or slot ID. * @param array $definition * Definition. * @param array $configuration * Configuration. - * @param array $source_contexts + * @param array $contexts * Source contexts. * - * @return \Drupal\ui_patterns\SourceInterface|null - * The source found or NULL. - * - * @throws \Drupal\Component\Plugin\Exception\PluginException + * @return mixed + * The updated build array. */ - protected function getSource(string $prop_or_slot_id, array $definition, array $configuration, array $source_contexts) : ?SourceInterface { - $source_id = $configuration["source_id"] ?? NULL; - if (!$source_id && isset($definition['ui_patterns']['type_definition'])) { - $source_id = $this->sourcesManager->getPropTypeDefault($definition['ui_patterns']['type_definition']->getPluginId(), $source_contexts); + public function buildSource(array $build, string $prop_or_slot_id, array $definition, array $configuration, array $contexts) : mixed { + try { + if (empty($configuration['source_id'])) { + return $build; + } + $source = $this->sourcesManager->getSource($prop_or_slot_id, $definition, $configuration, $contexts); + if (!$source) { + return $build; + } + /** @var \Drupal\ui_patterns\PropTypeInterface $prop_type */ + $prop_type = $source->getPropDefinition()['ui_patterns']['type_definition']; + // Alter the build array before getting the value. + $build = $source->alterComponent($build); + // Get the value from the source. + $data = $source->getValue($prop_type); + // Alter the value by hook implementations. + $this->moduleHandler->alter('ui_patterns_source_value', $data, $source, $configuration); + $this->addDataToComponent($build, $prop_or_slot_id, $prop_type, $data); } - if (!$source_id) { - return NULL; + catch (ContextException $e) { + // ContextException is thrown when a required context is missing. + // We don't want to break the render process, so we just ignore the prop. + $error_message = t("Context error for '@prop_id' in component '@component_id': @message", [ + '@prop_id' => $prop_or_slot_id, + '@component_id' => $build['#component'], + '@message' => $e->getMessage(), + ]); + $this->logger->error($error_message); } - /** @var \Drupal\ui_patterns\SourceInterface $source */ - $source = $this->sourcesManager->createInstance( - $source_id, - SourcePluginBase::buildConfiguration($prop_or_slot_id, $definition, $configuration, $source_contexts) - ); - return $source; + return $build; } /** @@ -167,34 +177,22 @@ class ComponentElementBuilder implements TrustedCallbackInterface { * Add a single slot to the renderable. */ protected function buildSlot(array $build, string $slot_id, array $definition, array $configuration, array $contexts): array { - if (isset($build["#slots"][$slot_id])) { + if (isset($build['#slots'][$slot_id])) { // Keep existing slots. Used by ComponentLayout for example. return $build; } - if (!isset($configuration["sources"])) { + if (!isset($configuration['sources'])) { return $build; } // Slots can have many sources while props can have only one. - $build["#slots"][$slot_id] = []; - /** @var \Drupal\ui_patterns\PropTypeInterface $slot_prop_type */ - $slot_prop_type = $this->propTypeManager->createInstance("slot", []); + $build['#slots'][$slot_id] = []; // Add sources data to the slot. - foreach ($configuration["sources"] as $source_configuration) { - $source = $this->getSource($slot_id, $definition, $source_configuration, $contexts); - if (!$source) { - continue; - } - $build = $source->alterComponent($build); - $source_value = $source->getValue($slot_prop_type) ?? []; - $this->moduleHandler->alter('ui_patterns_source_value', $source_value, $source, $source_configuration); - if (Element::isRenderArray($source_value)) { - $build["#slots"][$slot_id][] = $this->isSingletonRenderArray($source_value) ? array_values($source_value)[0] : $source_value; - } + foreach ($configuration['sources'] as $source_configuration) { + $build = $this->buildSource($build, $slot_id, $definition, $source_configuration, $contexts); } - if ($this->isSingletonRenderArray($build["#slots"][$slot_id])) { - $build["#slots"][$slot_id] = $build["#slots"][$slot_id][0]; + if ($this->isSingletonRenderArray($build['#slots'][$slot_id])) { + $build['#slots'][$slot_id] = $build['#slots'][$slot_id][0]; } - return $build; } @@ -269,7 +267,7 @@ class ComponentElementBuilder implements TrustedCallbackInterface { if ($prop_id === 'variant') { continue; } - if ($source = $this->getSource($prop_id, $definition, $configuration['props'][$prop_id] ?? [], $contexts)) { + if ($source = $this->sourcesManager->getSource($prop_id, $definition, $configuration['props'][$prop_id] ?? [], $contexts)) { SourcePluginBase::mergeConfigDependencies($dependencies, $source->calculateDependencies()); } } @@ -294,11 +292,11 @@ class ComponentElementBuilder implements TrustedCallbackInterface { $slots = $component->metadata->slots ?? []; foreach ($slots as $slot_id => $definition) { $slot_configuration = $configuration['slots'][$slot_id] ?? []; - if (!isset($slot_configuration["sources"]) || !is_array($slot_configuration["sources"])) { + if (!isset($slot_configuration['sources']) || !is_array($slot_configuration['sources'])) { continue; } - foreach ($slot_configuration["sources"] as $source_configuration) { - if ($source = $this->getSource($slot_id, $definition, $source_configuration, $contexts)) { + foreach ($slot_configuration['sources'] as $source_configuration) { + if ($source = $this->sourcesManager->getSource($slot_id, $definition, $source_configuration, $contexts)) { SourcePluginBase::mergeConfigDependencies($dependencies, $source->calculateDependencies()); } } diff --git a/src/SourcePluginManager.php b/src/SourcePluginManager.php index 32a42c61efe3234537fad3279071e4fdaa39154c..de07ef0df79b3550aed78da1dcc152deb91ebc29 100644 --- a/src/SourcePluginManager.php +++ b/src/SourcePluginManager.php @@ -325,4 +325,55 @@ class SourcePluginManager extends DefaultPluginManager implements ContextAwarePl return isset($definitions[$source_id]); } + /** + * Get a source plugin Instance. + * + * A source instance is always related to a prop or a slot. + * That's why we pass first the prop or slot id and the associated definition. + * If definition is empty, the slot will be automatically assumed. + * The configuration passed is the source configuration. + * It has a key 'source_id' that is the source plugin identifier. + * When no source_id is provided, + * the default source for the prop type is used. + * The source contexts are the contexts currently in use, + * maybe needed for that source or not. + * The form array parents are the form array parents, needed + * when dealing with the source settingsForm. + * + * @param string $prop_or_slot_id + * Prop ID or slot ID. + * @param array $definition + * Definition (if empty, slot will be automatically set). + * @param array $configuration + * Configuration for the source. + * @param array $source_contexts + * Source contexts. + * @param array $form_array_parents + * Form array parents. + * + * @return \Drupal\ui_patterns\SourceInterface|null + * The source found and instantiated or NULL. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public function getSource(string $prop_or_slot_id, array $definition, array $configuration, array $source_contexts = [], array $form_array_parents = []) : ?SourceInterface { + if (empty($definition)) { + // We consider a slot if no definition is provided. + $definition = ['ui_patterns' => ['type_definition' => $this->propTypeManager->createInstance('slot')]]; + } + $source_id = $configuration['source_id'] ?? NULL; + if (!$source_id && isset($definition['ui_patterns']['type_definition'])) { + $source_id = $this->getPropTypeDefault($definition['ui_patterns']['type_definition']->getPluginId(), $source_contexts); + } + if (!$source_id) { + return NULL; + } + /** @var \Drupal\ui_patterns\SourceInterface $source */ + $source = $this->createInstance( + $source_id, + SourcePluginBase::buildConfiguration($prop_or_slot_id, $definition, $configuration, $source_contexts, $form_array_parents) + ); + return $source; + } + } diff --git a/ui_patterns.services.yml b/ui_patterns.services.yml index ac131fbe71b9ee6e89c909bbbf4b87204a807612..fc03ba97b7e551760eee99f5ca889bee305288c5 100644 --- a/ui_patterns.services.yml +++ b/ui_patterns.services.yml @@ -44,7 +44,6 @@ services: class: Drupal\ui_patterns\Element\ComponentElementBuilder arguments: - "@plugin.manager.ui_patterns_source" - - "@plugin.manager.ui_patterns_prop_type" - "@plugin.manager.sdc" - "@module_handler" - "@logger.channel.ui_patterns"