diff --git a/modules/ui_patterns_views/src/Plugin/UiPatterns/Source/ViewFieldSource.php b/modules/ui_patterns_views/src/Plugin/UiPatterns/Source/ViewFieldSource.php index 306a019ea2ab6db3e3969b2fad8372418aab0a25..1f06bbd33b1fd13ad89c4db691ea5c4641595671 100644 --- a/modules/ui_patterns_views/src/Plugin/UiPatterns/Source/ViewFieldSource.php +++ b/modules/ui_patterns_views/src/Plugin/UiPatterns/Source/ViewFieldSource.php @@ -21,7 +21,7 @@ use Drupal\views\ViewExecutable; tags: ['views'], context_requirements: ['views:row'], context_definitions: [ - 'entity' => new EntityContextDefinition('entity:view', label: new TranslatableMarkup('View')), + 'ui_patterns_views:view_entity' => new EntityContextDefinition('entity:view', label: new TranslatableMarkup('View')), ] )] class ViewFieldSource extends ViewsSourceBase { diff --git a/modules/ui_patterns_views/src/Plugin/UiPatterns/Source/ViewRowsSource.php b/modules/ui_patterns_views/src/Plugin/UiPatterns/Source/ViewRowsSource.php index e890bba6492e9bdfa3261dfd847c2eb40f6c8266..17457dc614d54e9b609b1a3e6a94b299f97ab14d 100644 --- a/modules/ui_patterns_views/src/Plugin/UiPatterns/Source/ViewRowsSource.php +++ b/modules/ui_patterns_views/src/Plugin/UiPatterns/Source/ViewRowsSource.php @@ -20,7 +20,7 @@ use Drupal\views\ViewExecutable; prop_types: ['slot'], tags: ['views'], context_requirements: ['views:style'], context_definitions: [ - 'entity' => new EntityContextDefinition('entity:view', label: new TranslatableMarkup('View')), + 'ui_patterns_views:view_entity' => new EntityContextDefinition('entity:view', label: new TranslatableMarkup('View')), ] )] class ViewRowsSource extends ViewsSourceBase { diff --git a/modules/ui_patterns_views/src/Plugin/UiPatterns/Source/ViewTitleSource.php b/modules/ui_patterns_views/src/Plugin/UiPatterns/Source/ViewTitleSource.php index 795e7e2ab5b42c32f66529c70abc1f0090c4e02e..326e54560aa81218814621d0c7c841154582dfbb 100644 --- a/modules/ui_patterns_views/src/Plugin/UiPatterns/Source/ViewTitleSource.php +++ b/modules/ui_patterns_views/src/Plugin/UiPatterns/Source/ViewTitleSource.php @@ -18,7 +18,7 @@ use Drupal\views\ViewExecutable; description: new TranslatableMarkup('The title of the view.'), prop_types: ['string'], context_definitions: [ - 'entity' => new EntityContextDefinition('entity:view', label: new TranslatableMarkup('View')), + 'ui_patterns_views:view_entity' => new EntityContextDefinition('entity:view', label: new TranslatableMarkup('View')), ] )] class ViewTitleSource extends ViewsSourceBase { diff --git a/modules/ui_patterns_views/src/Plugin/UiPatterns/Source/ViewsSourceBase.php b/modules/ui_patterns_views/src/Plugin/UiPatterns/Source/ViewsSourceBase.php index 827de1276fdfa7bd09341d86da27ca69d997cc96..70bc1661b2c1b9d44f73d7092ea55b93d2a0663c 100644 --- a/modules/ui_patterns_views/src/Plugin/UiPatterns/Source/ViewsSourceBase.php +++ b/modules/ui_patterns_views/src/Plugin/UiPatterns/Source/ViewsSourceBase.php @@ -57,11 +57,13 @@ abstract class ViewsSourceBase extends SourcePluginBase { if (isset($this->context["ui_patterns_views:view"])) { return $this->getContextValue("ui_patterns_views:view"); } - return $this->getViewExecutable($this->getContextValue("entity")); + if (isset($this->context["ui_patterns_views:view_entity"])) { + return $this->getViewExecutable($this->getContextValue("ui_patterns_views:view_entity")); + } } catch (ContextException) { - return NULL; } + return NULL; } /** diff --git a/modules/ui_patterns_views/src/Plugin/views/row/ComponentRow.php b/modules/ui_patterns_views/src/Plugin/views/row/ComponentRow.php index 26555ac67bbd7620087a156f70bf64b267c717c5..17789117aa685cf4c337a0bde20c3259181a1040 100644 --- a/modules/ui_patterns_views/src/Plugin/views/row/ComponentRow.php +++ b/modules/ui_patterns_views/src/Plugin/views/row/ComponentRow.php @@ -4,9 +4,12 @@ declare(strict_types=1); namespace Drupal\ui_patterns_views\Plugin\views\row; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\Context\Context; use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\Core\Plugin\Context\EntityContext; use Drupal\Core\Render\Element; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Url; @@ -32,6 +35,13 @@ class ComponentRow extends Fields { use ViewsPluginUiPatternsTrait; + /** + * The sample entity generator. + * + * @var \Drupal\ui_patterns\Entity\SampleEntityGenerator + */ + protected $sampleEntityGenerator; + /** * {@inheritdoc} */ @@ -42,6 +52,7 @@ class ComponentRow extends Fields { $plugin_definition, ); $instance->initialize($container); + $instance->sampleEntityGenerator = $container->get('ui_patterns.sample_entity_generator'); return $instance; } @@ -83,7 +94,7 @@ class ComponentRow extends Fields { } } // Build ui patterns component form. - $form['ui_patterns'] = $this->componentSettingsForm($form, $form_state, $this->getComponentSourceContexts()); + $form['ui_patterns'] = $this->componentSettingsForm($form, $form_state, $this->getComponentSourceContextsCustom()); $form['ui_patterns']["#component_validation"] = FALSE; } @@ -96,9 +107,41 @@ class ComponentRow extends Fields { * {@inheritdoc} */ public function render($row) { + return $this->buildComponentRenderable($this->getComponentConfiguration()['component_id'], $this->getComponentSourceContextsCustom($row)); + } + + /** + * Get the source contexts for the component. + * + * @param mixed $row + * The view row if relevant. + * + * @return array + * Source contexts. + */ + protected function getComponentSourceContextsCustom(mixed $row = NULL): array { $context = $this->getComponentSourceContexts(); - $context['ui_patterns_views:row:index'] = new Context(new ContextDefinition('integer'), $row->index ?? 0); - return $this->buildComponentRenderable($this->getComponentConfiguration()['component_id'], $context); + $entity = NULL; + $bundle = NULL; + $view = $this->view; + if ($row === NULL) { + $base_entity_type = $view->getBaseEntityType(); + if ($base_entity_type instanceof EntityTypeInterface) { + $base_entity_type_id = "" . $base_entity_type->id(); + $entity = $this->sampleEntityGenerator->get($base_entity_type_id, $this->findEntityBundle($base_entity_type_id)); + $bundle = ""; + } + } + else { + $context['ui_patterns_views:row:index'] = new Context(new ContextDefinition('integer'), $row->index ?? 0); + $entity = $row->_entity; + $bundle = ($row->_entity instanceof EntityInterface) ? $row->_entity->bundle() : ""; + } + if ($entity instanceof EntityInterface) { + $context['entity'] = EntityContext::fromEntity($entity); + $context['bundle'] = new Context(new ContextDefinition('string'), $bundle); + } + return $context; } /** diff --git a/modules/ui_patterns_views/src/ViewsPluginUiPatternsTrait.php b/modules/ui_patterns_views/src/ViewsPluginUiPatternsTrait.php index 7fcc495b6a5d3e6cc99cba4eda2f3aac817e6ce0..46a990ddc31355070d9006b69cb2ff73f0501f80 100644 --- a/modules/ui_patterns_views/src/ViewsPluginUiPatternsTrait.php +++ b/modules/ui_patterns_views/src/ViewsPluginUiPatternsTrait.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Drupal\ui_patterns_views; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\Context\Context; use Drupal\Core\Plugin\Context\ContextDefinition; @@ -71,7 +72,7 @@ trait ViewsPluginUiPatternsTrait { // Build view entity context. $view_entity = $this->entityTypeManager->getStorage('view')->load($view->id()); if ($view_entity instanceof ViewEntityInterface) { - $context['entity'] = EntityContext::fromEntity($view_entity); + $context['ui_patterns_views:view_entity'] = EntityContext::fromEntity($view_entity); } return $context; } @@ -116,4 +117,39 @@ trait ViewsPluginUiPatternsTrait { return NULL; } + /** + * Find an entity bundle. + * + * @param string $entity_type_id + * The entity type id. + * + * @return string + * The bundle. + * + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + */ + protected function findEntityBundle(string $entity_type_id) : string { + // @todo better implementation with service 'entity_type.bundle.info' + $bundle = $entity_type_id; + $entity_type_definition = $this->entityTypeManager->getDefinition($entity_type_id); + if (!($entity_type_definition instanceof EntityTypeInterface)) { + return $bundle; + } + $bundle_entity_type = $entity_type_definition->getBundleEntityType(); + if (NULL !== $bundle_entity_type) { + $bundle_list = $this->entityTypeManager->getStorage($bundle_entity_type)->loadMultiple(); + if (count($bundle_list) > 0) { + foreach ($bundle_list as $bundle_entity) { + $bundle_to_test = "" . $bundle_entity->id(); + if ($bundle_to_test) { + $bundle = $bundle_to_test; + break; + } + } + } + } + return $bundle; + } + } diff --git a/src/AdvancedPluginManagerTrait.php b/src/AdvancedPluginManagerTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..6d55a75e6b4f7094e986076bf8f79743dcc785f5 --- /dev/null +++ b/src/AdvancedPluginManagerTrait.php @@ -0,0 +1,118 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\ui_patterns; + +/** + * Trait for sources handling enum values. + */ +trait AdvancedPluginManagerTrait { + /** + * The static cache. + * + * @var array<string, mixed> + */ + protected array $staticCache = []; + + /** + * Advanced method to get source definitions for contexts. + * + * In addition to getDefinitionsForContexts(), this method + * checks context_definitions of plugins according to their keys. + * When required in def, a context must be present with same key, + * and it must satisfy the context definition. + * + * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts + * Contexts. + * @param array<string, bool> $tag_filter + * Filter results by tags. + * The array keys are the tags, and the values are boolean. + * If the value is TRUE, the tag is required. + * If the value is FALSE, the tag is forbidden. + * + * @return array<string, array<string, mixed> > + * Plugin definitions + */ + public function getDefinitionsForContextsRefined(array $contexts = [], ?array $tag_filter = NULL) : array { + $cacheKey = $this->getHashKey(__FUNCTION__, [$contexts, $tag_filter]); + if (isset($this->staticCache[$cacheKey])) { + return $this->staticCache[$cacheKey]; + } + + $definitions = $this->getDefinitionsForContexts($contexts); + $checked_context_by_keys = []; + foreach (array_keys($contexts) as $key) { + $checked_context_by_keys[$key] = []; + } + $definitions = array_filter($definitions, function ($definition) use ($contexts, &$checked_context_by_keys) { + $context_definitions = isset($definition['context_definitions']) ? $definition['context_definitions'] ?? [] : []; + foreach ($context_definitions as $key => $context_definition) { + if (!$context_definition->isRequired()) { + continue; + } + if (!array_key_exists($key, $contexts)) { + return FALSE; + } + $context_definition_key = hash('sha256', serialize($context_definition)); + if (!isset($checked_context_by_keys[$key][$context_definition_key])) { + $checked_context_by_keys[$key][$context_definition_key] = $context_definition->isSatisfiedBy($contexts[$key]); + } + if (!$checked_context_by_keys[$key][$context_definition_key]) { + return FALSE; + } + } + return TRUE; + }); + if (is_array($tag_filter)) { + $definitions = static::filterDefinitionsByTags($definitions, $tag_filter); + } + $this->staticCache[$cacheKey] = $definitions; + return $definitions; + } + + /** + * Filters definitions by tags. + * + * @param array $definitions + * The definitions. + * @param array<string, bool> $tag_filter + * Filter results by tags. + * The array keys are the tags, and the values are boolean. + * If the value is TRUE, the tag is required. + * If the value is FALSE, the tag is forbidden. + * + * @return array + * The filtered definitions. + */ + protected static function filterDefinitionsByTags(array $definitions, array $tag_filter): array { + return array_filter($definitions, static function ($definition) use ($tag_filter) { + $tags = array_key_exists("tags", $definition) ? $definition['tags'] : []; + if (count($tag_filter) > 0) { + foreach ($tag_filter as $tag => $tag_required) { + $found = in_array($tag, $tags); + if (($tag_required && !$found) || (!$tag_required && $found)) { + return FALSE; + } + } + } + return TRUE; + }); + } + + /** + * Get a hash key for caching. + * + * @param string $key + * A key. + * @param array $contexts + * An array of contexts. + * + * @return string + * The hash key. + */ + private function getHashKey(string $key, array $contexts = []) : string { + return hash("sha256", serialize([$key, $contexts])); + } + +} diff --git a/src/DerivableContextInterface.php b/src/DerivableContextInterface.php index fe29007f7e1406cdfe4b5455166c4ff928b1c544..02b40551e1baa049062828d109bd797a606d295b 100644 --- a/src/DerivableContextInterface.php +++ b/src/DerivableContextInterface.php @@ -21,9 +21,9 @@ interface DerivableContextInterface extends ConfigurableInterface, PluginInspect /** * Returns the derived context. * - * @return array<string, \Drupal\Core\Plugin\Context\ContextInterface> - * The derived context. + * @return array< array<string, \Drupal\Core\Plugin\Context\ContextInterface> > + * An array of derived contexts. */ - public function getDerivedContext(): array; + public function getDerivedContexts(): array; } diff --git a/src/DerivableContextPluginBase.php b/src/DerivableContextPluginBase.php index 2d6a02c4e753759f5bfb37d41acf83af4215c8b6..ac1cea064fa0a133b5dc160ff69f10f1df970d03 100644 --- a/src/DerivableContextPluginBase.php +++ b/src/DerivableContextPluginBase.php @@ -95,7 +95,7 @@ abstract class DerivableContextPluginBase extends PluginBase implements /** * {@inheritdoc} */ - abstract public function getDerivedContext(): array; + abstract public function getDerivedContexts(): array; /** * {@inheritdoc} diff --git a/src/DerivableContextPluginManager.php b/src/DerivableContextPluginManager.php index 6ace207cee6ca4f061c4d37d217bf0a3667b1b30..39f47f69a7c38b4c5f4ec2979fb22c6ec0fd6665 100644 --- a/src/DerivableContextPluginManager.php +++ b/src/DerivableContextPluginManager.php @@ -20,6 +20,7 @@ use Drupal\ui_patterns\Plugin\Context\RequirementsContextDefinition; class DerivableContextPluginManager extends DefaultPluginManager implements ContextAwarePluginManagerInterface { use ContextAwarePluginManagerTrait; + use AdvancedPluginManagerTrait; /** * Constructs the object. diff --git a/src/Element/ComponentElementBuilder.php b/src/Element/ComponentElementBuilder.php index c807cdfa62817bad666019bdf8c8f7935eb79300..a7cfe72d51f82e7c0ac89121c8458e68f4d57e5e 100644 --- a/src/Element/ComponentElementBuilder.php +++ b/src/Element/ComponentElementBuilder.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\ui_patterns\Element; 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; @@ -171,14 +172,34 @@ class ComponentElementBuilder implements TrustedCallbackInterface { continue; } $build = $source->alterComponent($build); - $build["#slots"][$slot_id][] = $source->getValue($slot_prop_type); + $source_value = $source->getValue($slot_prop_type); + if (Element::isRenderArray($source_value)) { + $build["#slots"][$slot_id][] = $this->isSingletonRenderArray($source_value) ? array_values($source_value)[0] : $source_value; + } } - if (count($build["#slots"][$slot_id]) === 1) { + if ($this->isSingletonRenderArray($build["#slots"][$slot_id])) { $build["#slots"][$slot_id] = $build["#slots"][$slot_id][0]; } return $build; } + /** + * Check if the render array is a singleton. + * + * @param array $candidate + * The render array to check. + * + * @return bool + * TRUE if the render array is a singleton, FALSE otherwise. + */ + protected function isSingletonRenderArray(array $candidate): bool { + if (count($candidate) !== 1) { + return FALSE; + } + $key = array_key_first($candidate); + return (is_int($key) || ($key === '') || $key[0] !== '#'); + } + /** * Calculate a component dependencies. * diff --git a/src/Element/ComponentForm.php b/src/Element/ComponentForm.php index 639997d7b5f927822ed4adf2b9462db86f4bddd1..92afec67b57aa6f2b820679ba82ef5f2ed1adbb8 100644 --- a/src/Element/ComponentForm.php +++ b/src/Element/ComponentForm.php @@ -336,7 +336,7 @@ class ComponentForm extends ComponentFormBase { $form_state->setErrorByName('', $e->getMessage()); } else { - $form_state->setError($element['component_id'], $e->getMessage()); + // $form_state->setError($element['component_id'], $e->getMessage()); } } } diff --git a/src/Plugin/Derivative/EntityReferencedDerivableContextDeriver.php b/src/Plugin/Derivative/EntityReferencedDerivableContextDeriver.php new file mode 100644 index 0000000000000000000000000000000000000000..d6d8646745788e4abc43a4d8383d5b2f121cbbda --- /dev/null +++ b/src/Plugin/Derivative/EntityReferencedDerivableContextDeriver.php @@ -0,0 +1,94 @@ +<?php + +namespace Drupal\ui_patterns\Plugin\Derivative; + +use Drupal\Component\Plugin\PluginBase; +use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\Core\Plugin\Context\EntityContextDefinition; + +/** + * Provides derivable context for every field from referenced entities. + */ +class EntityReferencedDerivableContextDeriver extends EntityFieldSourceDeriverBase { + + /** + * {@inheritdoc} + */ + protected function getDerivativeDefinitionsForEntityBundleField(string $entity_type_id, string $bundle, string $field_name, array $base_plugin_derivative): void { + $entity_reference_data = $this->entityFieldsMetadata[$entity_type_id]["bundles"][$bundle]["fields"][$field_name]["entity_reference"]; + if (!is_array($entity_reference_data) || (count($entity_reference_data) === 0) || + !isset($entity_reference_data["fieldable"]) || !$entity_reference_data["fieldable"] || + !isset($entity_reference_data["bundles"]) || (count($entity_reference_data["bundles"]) === 0)) { + return; + } + $base_label = $base_plugin_derivative["label"]; + $field_data = $this->entityFieldsMetadata[$entity_type_id]["bundles"][$bundle]["fields"][$field_name]; + /*if ($field_data["metadata"]["cardinality"] !== 1) { + return; + }*/ + unset($base_plugin_derivative["context_definitions"]["field_name"]); + $target_entity_type_id = $entity_reference_data["entity_type_id"]; + $target_entity_label = $this->entityFieldsMetadata[$target_entity_type_id]["label"] ?? ""; + $target_bundles = $entity_reference_data["bundles"]; + if (count($target_bundles) > 1) { + $target_bundles[] = ""; + } + $no_bundle_context = (new ContextDefinition('string')) + ->setRequired() + ->setLabel("Bundle") + ->addConstraint('AllowedValues', [""]); + $entity_context = EntityContextDefinition::fromEntityTypeId($entity_type_id) + ->setRequired() + ->setLabel((string) ($this->entityFieldsMetadata[$entity_type_id]["label"] ?? "")); + foreach ($target_bundles as $target_bundle) { + $target_entity_type_fields_data = $this->entityFieldsMetadata[$target_entity_type_id]; + $target_bundle_data = []; + if (isset($target_entity_type_fields_data["bundles"]) && is_array($target_entity_type_fields_data["bundles"])) { + $target_bundle_data = $target_entity_type_fields_data["bundles"][$target_bundle] ?? []; + } + $id = implode(PluginBase::DERIVATIVE_SEPARATOR, [ + $entity_type_id, + $bundle, + $field_name, + $target_entity_type_id, + $target_bundle, + ]); + $entity_bundle_label = $target_bundle_data["label"] ?? $this->entityFieldsMetadata[$target_entity_type_id]["label"]; + $this->derivatives[$id] = array_merge($base_plugin_derivative, [ + "id" => $id, + "label" => t("@bundle referenced by @field", [ + "@field" => $base_label, + "@bundle" => ($target_bundle == "") ? $target_entity_label : sprintf("%s (%s)", $entity_bundle_label, $target_entity_label), + ]), + ]); + if (isset($this->derivatives[$id]['tags']) && is_array($this->derivatives[$id]['tags'])) { + $this->derivatives[$id]['tags'][] = "entity_referenced"; + } + $this->derivatives[$id]['metadata']['group'] = t("@entity from @field", [ + "@entity" => $entity_bundle_label, + "@field" => $field_data["label"], + ]); + // Check plugin exists for host entity type without bundle. + $id_no_bundle = implode(PluginBase::DERIVATIVE_SEPARATOR, [ + $entity_type_id, + "", + $field_name, + $target_entity_type_id, + $target_bundle, + ]); + if (!isset($this->derivatives[$id_no_bundle])) { + $field_storage_metadata = $this->entityFieldsMetadata[$entity_type_id]["field_storages"][$field_name]; + $this->derivatives[$id_no_bundle] = array_merge($this->derivatives[$id], [ + "id" => $id_no_bundle, + "label" => t("@target referenced by @field", [ + "@field" => $field_storage_metadata["label"] ?? $field_name, + "@target" => $target_entity_label, + ]), + ]); + $this->derivatives[$id_no_bundle]["context_definitions"]["entity"] = $entity_context; + $this->derivatives[$id_no_bundle]["context_definitions"]["bundle"] = $no_bundle_context; + } + } + } + +} diff --git a/src/Plugin/UiPatterns/DerivableContext/EntityFieldDerivableContext.php b/src/Plugin/UiPatterns/DerivableContext/EntityFieldDerivableContext.php index 6c7553b6b979f9d922ed507c958121109f679cab..77b437d0e7864cf52e1902818d53d603f5c2b01e 100644 --- a/src/Plugin/UiPatterns/DerivableContext/EntityFieldDerivableContext.php +++ b/src/Plugin/UiPatterns/DerivableContext/EntityFieldDerivableContext.php @@ -27,7 +27,7 @@ class EntityFieldDerivableContext extends DerivableContextPluginBase { /** * {@inheritdoc} */ - public function getDerivedContext(): array { + public function getDerivedContexts(): array { $contexts = $this->context; $split_plugin_id = explode(PluginBase::DERIVATIVE_SEPARATOR, $this->getPluginId()); $field_name = array_pop($split_plugin_id); @@ -44,7 +44,7 @@ class EntityFieldDerivableContext extends DerivableContextPluginBase { $contexts = RequirementsContext::addToContext(["field_granularity:item"], $contexts); } } - return $contexts; + return [$contexts]; } } diff --git a/src/Plugin/UiPatterns/DerivableContext/EntityReferencedDerivableContext.php b/src/Plugin/UiPatterns/DerivableContext/EntityReferencedDerivableContext.php new file mode 100644 index 0000000000000000000000000000000000000000..5c63163759af9c16488665fbcedf0381f3cc6cb5 --- /dev/null +++ b/src/Plugin/UiPatterns/DerivableContext/EntityReferencedDerivableContext.php @@ -0,0 +1,151 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\ui_patterns\Plugin\UiPatterns\DerivableContext; + +use Drupal\Component\Plugin\PluginBase; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Plugin\Context\Context; +use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\Core\Plugin\Context\EntityContextDefinition; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\ui_patterns\Attribute\DerivableContext; +use Drupal\ui_patterns\DerivableContextPluginBase; +use Drupal\ui_patterns\Plugin\Derivative\EntityReferencedDerivableContextDeriver; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Derivable context plugins for entity Reference fields. + */ +#[DerivableContext( + id: 'entity_reference', + label: new TranslatableMarkup('Entity Referenced from fields'), + description: new TranslatableMarkup('Derived contexts for Entity Reference Fields.'), + deriver: EntityReferencedDerivableContextDeriver::class +)] +class EntityReferencedDerivableContext extends DerivableContextPluginBase { + + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The entity field manager. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface + */ + protected $entityFieldManager; + + /** + * The sample entity generator. + * + * @var \Drupal\ui_patterns\Entity\SampleEntityGenerator + */ + protected $sampleEntityGenerator; + + /** + * {@inheritdoc} + */ + public static function create( + ContainerInterface $container, + array $configuration, + $plugin_id, + $plugin_definition, + ) { + $instance = new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('context.repository'), + ); + $instance->entityTypeManager = $container->get('entity_type.manager'); + $instance->entityFieldManager = $container->get('entity_field.manager'); + $instance->sampleEntityGenerator = $container->get('ui_patterns.sample_entity_generator'); + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getDerivedContexts(): array { + $contexts = $this->context; + // 0 = base id, 1 = entity_type_id, 2 = bundle + $split_plugin_id = explode(PluginBase::DERIVATIVE_SEPARATOR, $this->getPluginId()); + // 0 = ref_bundle, 1 = ref_entity_type_id + $split_plugin_id_reverse = array_reverse($split_plugin_id); + $bundle = $split_plugin_id_reverse[0]; + $entity_type_id = $split_plugin_id_reverse[1]; + $ref_field_name = $split_plugin_id_reverse[2]; + $bundle_context_definition = new ContextDefinition("string", "Bundle"); + $contexts['bundle'] = new Context($bundle_context_definition, $bundle); + $entity = $this->context["entity"]->getContextValue(); + $referenced_entities = []; + $entity_context_definition = new EntityContextDefinition($entity_type_id); + $entity_context_definition->addConstraint('Bundle', [$bundle]); + if ($entity->hasField($ref_field_name)) { + $field_reference = $entity->get($ref_field_name); + if (!$field_reference->isEmpty()) { + for ($i = 0; $i < $field_reference->count(); $i++) { + $field_item = $field_reference->get($i); + $referenced_entity_test = $field_item->entity; + if (($referenced_entity_test instanceof EntityInterface) && (empty($bundle) || $referenced_entity_test->bundle() === $bundle)) { + $referenced_entities[] = $referenced_entity_test; + } + } + } + } + if ((count($referenced_entities) === 0) && !$entity->id()) { + // Case when the entity is a sample (we are probably in a form) + // we generate a sample referenced entity. + $referenced_entities[] = $this->sampleEntityGenerator->get($entity_type_id, empty($bundle) ? $this->findEntityBundleWithField($entity_type_id, $ref_field_name) : $bundle); + } + $returned_contexts = []; + foreach ($referenced_entities as $referenced_entity) { + $returned_contexts[] = array_merge($contexts, [ + "entity" => new Context($entity_context_definition, $referenced_entity), + ]); + } + return $returned_contexts; + } + + /** + * Find an entity bundle which eventually has a field. + * + * @param string $entity_type_id + * The entity type id. + * @param string $field_name + * The field name to be found in searched bundle. + * + * @return string + * The bundle. + */ + protected function findEntityBundleWithField(string $entity_type_id, ?string $field_name = NULL) : string { + // @todo better implementation with service 'entity_type.bundle.info' + $bundle = $entity_type_id; + $bundle_entity_type = $this->entityTypeManager->getDefinition($entity_type_id)->getBundleEntityType(); + if (NULL !== $bundle_entity_type) { + $bundle_list = $this->entityTypeManager->getStorage($bundle_entity_type)->loadMultiple(); + if (count($bundle_list) > 0) { + foreach ($bundle_list as $bundle_entity) { + $bundle_to_test = (string) $bundle_entity->id(); + if ($field_name === NULL) { + $bundle = $bundle_to_test; + break; + } + $definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle_to_test); + if (array_key_exists($field_name, $definitions)) { + $bundle = $bundle_to_test; + break; + } + } + } + } + return $bundle; + } + +} diff --git a/src/Plugin/UiPatterns/Source/DerivableContextSourceBase.php b/src/Plugin/UiPatterns/Source/DerivableContextSourceBase.php index c0381f357ef284cd5762a37b347838e6339fc0b1..a65015ce4941f043bd6377854bbedb78ac604b9e 100644 --- a/src/Plugin/UiPatterns/Source/DerivableContextSourceBase.php +++ b/src/Plugin/UiPatterns/Source/DerivableContextSourceBase.php @@ -9,7 +9,6 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Form\FormStateInterface; use Drupal\ui_patterns\DerivableContextPluginBase; use Drupal\ui_patterns\Plugin\UiPatterns\PropType\SlotPropType; -use Drupal\ui_patterns\SourceInterface; use Drupal\ui_patterns\SourcePluginBase; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -48,11 +47,11 @@ abstract class DerivableContextSourceBase extends SourcePluginBase { protected ?array $derivableContexts = NULL; /** - * The source plugin rendered. + * The source plugins rendered. * - * @var \Drupal\ui_patterns\SourceInterface|null + * @var array<\Drupal\ui_patterns\SourceInterface> */ - protected $sourcePlugin = NULL; + protected $sourcePlugins = NULL; /** * {@inheritdoc} @@ -87,39 +86,56 @@ abstract class DerivableContextSourceBase extends SourcePluginBase { /** * {@inheritdoc} + * + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ public function getPropValue(): mixed { - $source_plugin = $this->getSourcePlugin(); - return ($source_plugin) ? $source_plugin->getPropValue() : []; + $definition = $this->propDefinition; + $prop_type = $definition['ui_patterns']['type_definition']; + $source_plugins = $this->getSourcePlugins(); + if (!$this->isSlot()) { + $source_plugin = (count($source_plugins) > 0) ? $source_plugins[0] : NULL; + return ($source_plugin) ? $source_plugin->getValue($prop_type) : NULL; + } + $returned = []; + foreach ($source_plugins as $source_plugin) { + $returned[] = $source_plugin->getValue($prop_type); + } + return empty($returned) ? NULL : $returned; } /** * Set the source plugin according to configuration. + * + * @return array<\Drupal\ui_patterns\SourceInterface> + * Source plugins */ - private function getSourcePlugin(): ?SourceInterface { - $this->sourcePlugin = NULL; + private function getSourcePlugins(): array { + $this->sourcePlugins = []; $derivable_context = $this->getSetting('derivable_context') ?? NULL; if (!$derivable_context) { - return $this->sourcePlugin; + return $this->sourcePlugins; } /** @var \Drupal\ui_patterns\DerivableContextInterface $derivable_context_plugin */ $derivable_context_plugin = $this->derivableContextManager->createInstance($derivable_context, DerivableContextPluginBase::buildConfiguration($this->context)); if (!$derivable_context_plugin) { - return $this->sourcePlugin; + return $this->sourcePlugins; } - $derived_context = $derivable_context_plugin->getDerivedContext(); + $derived_contexts = $derivable_context_plugin->getDerivedContexts(); $sources = $this->getSetting($derivable_context) ?? []; if (!is_array($sources) || !array_key_exists("value", $sources)) { - return $this->sourcePlugin; + return $this->sourcePlugins; } $sources = $sources["value"]; $source_configuration = $this->isSlot() ? array_values($sources["sources"])[0] : $sources; - $target_plugin_configuration = array_merge($source_configuration["source"] ?? [], [ - "context" => $derived_context, - ]); - $this->sourcePlugin = $this->createSourcePlugin($source_configuration["source_id"], $target_plugin_configuration, $derived_context); - return $this->sourcePlugin; + foreach ($derived_contexts as $derived_context) { + $target_plugin_configuration = array_merge($source_configuration["source"] ?? [], [ + "context" => $derived_context, + ]); + $this->sourcePlugins[] = $this->createSourcePlugin($source_configuration["source_id"], $target_plugin_configuration, $derived_context); + } + return $this->sourcePlugins; } /** @@ -196,7 +212,11 @@ abstract class DerivableContextSourceBase extends SourcePluginBase { $source = $source["value"] ?? []; /** @var \Drupal\ui_patterns\DerivableContextInterface $derivable_context_plugin */ $derivable_context_plugin = $this->derivableContextManager->createInstance($derivable_context, DerivableContextPluginBase::buildConfiguration($this->context)); - $derived_context = $derivable_context_plugin->getDerivedContext(); + $derived_contexts = $derivable_context_plugin->getDerivedContexts(); + if (count($derived_contexts) === 0) { + return $form; + } + $derived_context = reset($derived_contexts); $component_id = $derived_context["component_id"]->getContextValue(); $is_slot = $this->isSlot(); $form[$derivable_context] = $source_container; @@ -297,22 +317,10 @@ abstract class DerivableContextSourceBase extends SourcePluginBase { if ($this->derivableContexts) { return $this->derivableContexts; } - $this->derivableContexts = $this->derivableContextManager->getDefinitionsForContexts($this->context); + $this->derivableContexts = $this->derivableContextManager->getDefinitionsForContextsRefined($this->context, $this->getDerivationTagFilter()); return $this->derivableContexts; } - /** - * {@inheritdoc} - */ - public function alterComponent(array $element): array { - $source_plugin = $this->getSourcePlugin(); - if (!$source_plugin) { - return $element; - } - // @todo ? - return $element; - } - /** * {@inheritdoc} */ @@ -328,12 +336,21 @@ abstract class DerivableContextSourceBase extends SourcePluginBase { return $dependencies; } SourcePluginBase::mergeConfigDependencies($dependencies, $this->getPluginDependencies($derivable_context_plugin)); - $source_plugin = $this->getSourcePlugin(); - if (!$source_plugin) { - return $dependencies; + $source_plugins = $this->getSourcePlugins(); + foreach ($source_plugins as $source_plugin) { + SourcePluginBase::mergeConfigDependencies($dependencies, $this->getPluginDependencies($source_plugin)); } - SourcePluginBase::mergeConfigDependencies($dependencies, $this->getPluginDependencies($source_plugin)); return $dependencies; } + /** + * Get tag filter for plugin derivation. + * + * @return array|null + * Tag filter or NULL. + */ + protected function getDerivationTagFilter(): ?array { + return NULL; + } + } diff --git a/src/Plugin/UiPatterns/Source/EntityReferencedSource.php b/src/Plugin/UiPatterns/Source/EntityReferencedSource.php new file mode 100644 index 0000000000000000000000000000000000000000..211e309357a36eff5d02dbf33df212f8c56b1df2 --- /dev/null +++ b/src/Plugin/UiPatterns/Source/EntityReferencedSource.php @@ -0,0 +1,58 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\ui_patterns\Plugin\UiPatterns\Source; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\ui_patterns\Attribute\Source; + +/** + * Plugin implementation of the source. + */ +#[Source( + id: 'entity_reference', + label: new TranslatableMarkup('From referenced entities'), + description: new TranslatableMarkup('Data from a referenced entities'), + context_definitions: [ + 'entity' => new ContextDefinition('entity', label: new TranslatableMarkup('Entity'), required: TRUE), + ] +)] +class EntityReferencedSource extends DerivableContextSourceBase { + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state): array { + $form = parent::settingsForm($form, $form_state); + $form["derivable_context"]["#title"] = $this->t("Entity reference field"); + // When no derivable contexts exist, allow this form still be valid. + if (isset($form["derivable_context"]["#options"]) && empty($form["derivable_context"]["#options"])) { + $form["derivable_context"]["#required"] = FALSE; + } + return $form; + } + + /** + * {@inheritDoc} + */ + protected function getSourcesTagFilter(): array { + return [ + "widget:dismissible" => FALSE, + "widget" => FALSE, + ]; + } + + /** + * {@inheritDoc} + */ + protected function getDerivationTagFilter(): ?array { + return [ + // "entity" => TRUE, + "entity_referenced" => TRUE, + ]; + } + +} diff --git a/src/Plugin/UiPatterns/Source/FieldValueSourceBase.php b/src/Plugin/UiPatterns/Source/FieldValueSourceBase.php index 84cc13de70ed0c5ebf0f82c204e1c121a53341c6..9e0fa0b083fa0b90b5af66a6dfdd3c5c78409b80 100644 --- a/src/Plugin/UiPatterns/Source/FieldValueSourceBase.php +++ b/src/Plugin/UiPatterns/Source/FieldValueSourceBase.php @@ -64,12 +64,14 @@ abstract class FieldValueSourceBase extends FieldSourceBase implements SourceInt if ($entity instanceof EntityInterface) { return $entity; } - // Useful in the context of views. - $field_items = $this->getContextValue("ui_patterns:field:items"); - if ($field_items instanceof FieldItemListInterface) { - return $field_items->getEntity(); + if (isset($this->context["ui_patterns:field:items"])) { + // Useful in the context of views. + $field_items = $this->getContextValue("ui_patterns:field:items"); + if ($field_items instanceof FieldItemListInterface) { + return $field_items->getEntity(); + } } - return $this->sampleEntityGenerator->get($this->getEntityTypeId(), $this->getBundle()); + return NULL; } /** @@ -85,7 +87,7 @@ abstract class FieldValueSourceBase extends FieldSourceBase implements SourceInt } /** @var \Drupal\Core\Entity\ContentEntityBase $entity */ $entity = $this->getEntity(); - if (!$entity) { + if (!$entity && isset($this->context["ui_patterns:field:items"])) { $field_items = $this->getContextValue('ui_patterns:field:items'); if ($field_items instanceof FieldItemListInterface) { if ($field_items->getFieldDefinition()->getName() == $field_name) { diff --git a/src/SourcePluginManager.php b/src/SourcePluginManager.php index 3a25653b03bfdd061b21dfbaaec6915b8b5f6ce1..89bda7e251341614b7dbed1e5b9ff1bf8e73a45a 100644 --- a/src/SourcePluginManager.php +++ b/src/SourcePluginManager.php @@ -21,13 +21,7 @@ use Drupal\ui_patterns\Plugin\Context\RequirementsContextDefinition; class SourcePluginManager extends DefaultPluginManager implements ContextAwarePluginManagerInterface { use ContextAwarePluginManagerTrait; - - /** - * The static cache. - * - * @var array<string, mixed> - */ - protected array $staticCache = []; + use AdvancedPluginManagerTrait; /** * Constructs the object. @@ -189,35 +183,6 @@ class SourcePluginManager extends DefaultPluginManager implements ContextAwarePl }); } - /** - * Filters definitions by tags. - * - * @param array $definitions - * The definitions. - * @param array<string, bool> $tag_filter - * Filter results by tags. - * The array keys are the tags, and the values are boolean. - * If the value is TRUE, the tag is required. - * If the value is FALSE, the tag is forbidden. - * - * @return array - * The filtered definitions. - */ - protected static function filterDefinitionsByTags(array $definitions, array $tag_filter): array { - return array_filter($definitions, static function ($definition) use ($tag_filter) { - $tags = array_key_exists("tags", $definition) ? $definition['tags'] : []; - if (count($tag_filter) > 0) { - foreach ($tag_filter as $tag => $tag_required) { - $found = in_array($tag, $tags); - if (($tag_required && !$found) || (!$tag_required && $found)) { - return FALSE; - } - } - } - return TRUE; - }); - } - /** * Returns convertible source definitions for a prop type. * @@ -360,67 +325,4 @@ class SourcePluginManager extends DefaultPluginManager implements ContextAwarePl return isset($definitions[$source_id]); } - /** - * Get a hash key for caching. - * - * @param string $key - * A key. - * @param array $contexts - * An array of contexts. - * - * @return string - * The hash key. - */ - private function getHashKey(string $key, array $contexts = []) : string { - return hash("sha256", serialize([$key, $contexts])); - } - - /** - * Advanced method to get source definitions for contexts. - * - * In addition to getDefinitionsForContexts(), this method - * checks context_definitions of plugins according to their keys. - * When required in def, a context must be present with same key, - * and it must satisfy the context definition. - * - * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts - * Contexts. - * - * @return array<string, array<string, mixed> > - * Plugin definitions - */ - public function getDefinitionsForContextsRefined(array $contexts = []) : array { - $cacheKey = $this->getHashKey(__FUNCTION__, $contexts); - if (isset($this->staticCache[$cacheKey])) { - return $this->staticCache[$cacheKey]; - } - - $definitions = $this->getDefinitionsForContexts($contexts); - $checked_context_by_keys = []; - foreach (array_keys($contexts) as $key) { - $checked_context_by_keys[$key] = []; - } - $definitions = array_filter($definitions, function ($definition) use ($contexts, &$checked_context_by_keys) { - $context_definitions = isset($definition['context_definitions']) ? $definition['context_definitions'] ?? [] : []; - foreach ($context_definitions as $key => $context_definition) { - if (!$context_definition->isRequired()) { - continue; - } - if (!array_key_exists($key, $contexts)) { - return FALSE; - } - $context_definition_key = hash('sha256', serialize($context_definition)); - if (!isset($checked_context_by_keys[$key][$context_definition_key])) { - $checked_context_by_keys[$key][$context_definition_key] = $context_definition->isSatisfiedBy($contexts[$key]); - } - if (!$checked_context_by_keys[$key][$context_definition_key]) { - return FALSE; - } - } - return TRUE; - }); - $this->staticCache[$cacheKey] = $definitions; - return $definitions; - } - } diff --git a/transform.py b/transform.py new file mode 100644 index 0000000000000000000000000000000000000000..c827f190176512b3ccd2cfd8823b0c26903d0db2 --- /dev/null +++ b/transform.py @@ -0,0 +1,20 @@ +import os +import re +os.chdir("/Users/mikael/Work/ui_patterns_2/11/ui_patterns/src/Plugin/UiPatterns/Source") +srcs = os.listdir() +t = [x[:-4] for x in srcs] +os.chdir("/Users/mikael/Work/ui_patterns_2/11/ui_patterns/tests/src/Kernel/Source") + +for e in t: + f = "%sTest.php" % e + dest = os.path.join("/Users/mikael/Work/ui_patterns_2/11/ui_patterns/tests/src/Kernel/Source", f) + if not os.path.exists(dest): + os.system("cp %s %s" % ("FieldPropertySourceTest.php", f)) + data = open(dest,"r").read().replace("FieldPropertySource", e) + snake = re.sub(r'(?<!^)(?=[A-Z])', '_', e.replace("Widget","").replace("Source", "")).lower() + data = data.replace("field_property_", "%s_" % snake) + f = open(dest,"w") + f.write(data) + f.close() + print("Created %s" % dest) +