Skip to content
Snippets Groups Projects
Commit dc7d627e authored by Mikael Meulle's avatar Mikael Meulle
Browse files

Issue #3512477 by just_like_good_vibes, christian.wiedemann: Improve the...

Issue #3512477 by just_like_good_vibes, christian.wiedemann: Improve the instantiation and rendering of Sources from outside the module
parent c3479f06
No related branches found
No related tags found
No related merge requests found
......@@ -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);
}
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') {
/**
* 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;
}
}
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_id] = $data;
$build['#props'][$prop_or_slot_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;
}
if (!$source_id) {
return NULL;
/** @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);
}
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());
}
}
......
......
......@@ -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;
}
}
......@@ -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"
......
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment