Problem/Motivation

Interesting stuff is happening on Display Builder side: #3579299: Tidy context management which can inspire us to clean the context on UI Patterns side.

UI Patterns has a very specific way of dealing with contexts:

  • Derivable context plugin type:
    • src/Attribute/DerivableContext.php
    • src/DerivableContextInterface.php
    • src/DerivableContextPluginBase.php
    • src/DerivableContextPluginManager.php (and the related plugin.manager.ui_patterns_derivable_context service)
    • src/EventSubscriber/UiPatternsEntitySchemaSubscriber.php (and the related ui_patterns.entity_schema_subscriber service)
    • src/Plugin/Derivative/DerivableContextDeriver.php
    • src/Plugin/UiPatterns/DerivableContext/EntityFieldDerivableContext.php
    • src/Plugin/UiPatterns/DerivableContext/EntityReferencedDerivableContext.php
  • Requirements context plugin:
    • src/Plugin/Context/RequirementsContext.php
    • src/Plugin/Context/RequirementsContextDefinition.php
    • the attribute in \Drupal\ui_patterns\Attribute\Source
  • Resolvers:
    • src/Resolver/ChainContextEntityResolver.php (and the related ui_patterns.chain_context_entity_resolver service)
    • src/Resolver/ChainContextEntityResolverInterface.php
    • src/Resolver/ContextEntityResolverInterface.php
    • src/Resolver/FieldLayoutContextEntityResolver.php
    • src/Resolver/LayoutBuilderContextEntityResolver.php
    • tests/src/Kernel/Resolver/ChainContextEntityResolverTest.php
  • A specific constraint plugin used in ComponentFormatter:
    • src/Plugin/Validation/Constraint/RequiredArrayValuesConstraint.php
    • src/Plugin/Validation/Constraint/RequiredArrayValuesConstraintValidator.php
  • A trait used by SourcePluginManager:
    • src/ContextMatcherPluginManagerInterface.php
    • src/ContextMatcherPluginManagerTrait.php
  • and maybe more...

The main part of this mechanism is the context_requirements attributes which has been added because Core's ContextAwarePluginManagerTrait::getDefinitionsForContexts() (which is the reference implementation of ContextAwarePluginManagerInterface) doesn't check the context key, only the context data, as the Context API expects us to do.

It serves us well for 2 years, but is is complex.

Proposed resolution

We can rely on the Core's mechanism, checking the data instead of the keys, by:

  • never providing 2 or more contexts of the same type for a single source plugin
  • forbidding the any context and using entity only when not possible to target a specific entity type (most of the time, to target any fieldable content entity)
  • not being shy to create our own contexts with a context class or a typed data plugin if necessary

So, ContextAwarePluginManagerTrait will be able to load only the source plugins whose constraints are satisfied by the provide contexts.

Proposal: We may be able to do everything with only 4 contexts, while keeping it extensible for future usage:
proposal

Up-to-date source document: https://docs.google.com/spreadsheets/d/1-oAOLOEYaCaYhMPWKAuwNNTQ5YFeU_44...

We don' t need to create specific context data (so specific Typed Data plugins) with the current proposal, but it may be a good way of reducing the ambiguity of entity, string and integer. However, let's check what contrib space is already proposing before creating our own.

We can start by implementing ContextProviderInterface on the existing "consumer" plugins. This interface is for services, but it seems a good fit also for plugins. It is what we already do in Display Builder.

ComponentBlock

Which is passing the current context (blocks are already context aware):

  public function getRuntimeContexts(array $unqualified_context_ids) {
    return $this->getContexts();
  }

EntityComponentBlock

Same with the (manual? automatic?) addition of entity coming from the display building tool (Layout Builder, at least).

Because entity is already available in ComponentBlock with no value. It may be the opportunity to remove EntityComponentBlock and its deriver.

ComponentFormatter

Currently providing ui_patterns:lang_code (type: any), ui_patterns:field:items (type: any) and field_granularity:items

Proposal:

public function getRuntimeContexts(array $unqualified_context_ids) {
  $entity = $this->sampleEntityGenerator->get($this->fieldDefinition->getTargetEntityTypeId(), $this->fieldDefinition->getTargetBundle());
  return [
    'entity' => EntityContext::fromEntity($entity),
    'field_name' => new Context(new ContextDefinition('string'), $this->fieldDefinition->getName()),
  ];
}

ComponentPerItemFormatter

Currently providing ui_patterns:lang_code (type: any), ui_patterns:field:items (type: any) and ui_patterns:field:index (type: integer)

Proposal (same as ComponentFormatter with the addition of an integer):

public function getRuntimeContexts(array $unqualified_context_ids) {
  $entity = $this->sampleEntityGenerator->get($this->fieldDefinition->getTargetEntityTypeId(), $this->fieldDefinition->getTargetBundle());
  return [
    'entity' => EntityContext::fromEntity($entity),
    'field_name' => new Context(new ContextDefinition('string'), $this->fieldDefinition->getName()),
    'delta' => new Context(new ContextDefinition('integer'), 0),
  ];
}

Then, delta can be dynamically updated

public function viewElements(FieldItemListInterface $items, $langcode) {
  $build = [];
  $contexts = $this->getAvailableContexts();
  foreach ($items as $delta => $item) {
    $contexts['delta'] = new Context(new ContextDefinition('integer'), $delta);
    $build[] = $this->buildComponentRenderable($this->getComponentConfiguration()['component_id'], $contexts);
  }
  return $build;
}

ComponentLayout

Currently providing ui_patterns:form_state (type: any), entity (type: entity).

Proposal: Nothing?

ComponentStyle

Currently providing ui_patterns_views:rows (type: any). And ui_patterns_views:view (type: any), ui_patterns_views:plugin:options (type: any), ui_patterns_views:plugin:display_handler (type: any), ui_patterns_views:view_entity (type: entity:view) through ViewsPluginUiPatternsTrait.

Proposal:

public function getRuntimeContexts(array $unqualified_context_ids) {
  return [
    'view' => EntityContext::fromEntity($this->view->storage),
    'display' => new Context(new ContextDefinition('string'), $this->view->current_display),
  ];
}

ComponentRow

Currently providing ui_patterns_views:row:index (type: integer), entity (type: entity). And ui_patterns_views:view (type: any), ui_patterns_views:plugin:options (type: any), ui_patterns_views:plugin:display_handler (type: any), ui_patterns_views:view_entity (type: entity:view) through ViewsPluginUiPatternsTrait.

Proposal (same as ComponentStyle with the addition of an integer):

public function getRuntimeContexts(array $unqualified_context_ids) {
  return [
    'view' => EntityContext::fromEntity($this->view->storage),
    'display' => new Context(new ContextDefinition('string'), $this->view->current_display),
    'position' => new Context(new ContextDefinition('integer'), $this->position ?? 0),
  ];
}

Context switchers

There are the source plugins actually extending DerivableContextSourceBase:

  • EntityFieldSource (entity_field) ([Entity] ➜ [Field]), without a deriver class?
  • EntityReferencedSource (entity_reference) ([Entity] ➜ Referenced [Entity])
  • EntityReferenceFieldPropertySource (entity:field_property) ([Field item] ➜ Referenced [Entity])
  • and its EntityReferenceFieldPropertyDerivableContextDeriver

They can be managed as source plugins implementing both:

  • ContextAwarePluginInterface like the other context aware source plugins
  • ContextProviderInterface like the configurable renderable plugins

For example, EntityFieldSource is aware of:

  context_definitions: [
    'entity' => new ContextDefinition('entity')
  ],

and is providing:

  public function getRuntimeContexts(array $unqualified_context_ids) {
    return [
      'entity' => $this->getContextValue('entity'),
      'field_name' => new Context(new ContextDefinition('string'), $this->getSetting('derivable_context')),
    ];
  }

Ecosystem

What about the other modules (DS, Field Group...)? Are we breaking them? Anyway, it will be a change for the next major version.

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

pdureau created an issue. See original summary.

pdureau’s picture

Issue summary: View changes
Issue tags: +ui_patterns-next
pdureau’s picture

Issue summary: View changes
pdureau’s picture

Issue summary: View changes
pdureau’s picture

Issue summary: View changes
StatusFileSize
new84.19 KB
pdureau’s picture

Issue summary: View changes
pdureau’s picture

Issue summary: View changes
pdureau’s picture

I have created the fork, just to mess around ;)

pdureau’s picture

Issue summary: View changes
pdureau’s picture

Issue summary: View changes
pdureau’s picture

Issue summary: View changes
pdureau’s picture

Issue summary: View changes
pdureau’s picture

Issue summary: View changes
pdureau’s picture

Issue summary: View changes
StatusFileSize
new102.8 KB
pdureau’s picture

Issue summary: View changes

A little review of the current context_requirements values and how we can replace them with Drupal Core contexts:

  • field_formatter (in FieldLabelSource): ✅ it was never used, let's drop it
  • views:style (ViewRowsSource and many Display Builder sources): ✅ replaced by entity:view + string
  • views:row (in ViewFieldSource): ✅ replaced by entity:view + string + integer

Only in Display Builder:

  • layout_discovery: ✅ nevermind, we will drop it anyway in #3571009: Add SourceWithSlotsInterface and LayoutSource
  • page (in PageTitleSource, MainPageContentSource and PageLayoutSource): we can let PageTitleSource be context free, so available everywhere, but we need to constraint the others. ⚠️ Can we define this at UI Patterns level and address this in the current issue?
  • content (in ContentEntitySource): ⚠️ this one is tricky because we need to tell we are in the context of a content entity (so the generic entity context is enough) but we are saving the display in a content storage (so entity view overrides). This is important to avoid storing a reference to content from config. How do we express this? See also #3501797: Add a ContentEntity source for slots with a Content entity context
pdureau’s picture

Issue summary: View changes

Tags are addressed in a dedicated ticket: #3590492: Tidy the source plugins tags

pdureau’s picture

Status: Active » Needs work

I have opened a MR and pushed a first commit to help discussion and safely apply the upcoming changes: https://git.drupalcode.org/project/ui_patterns/-/commit/8c8890297163ce59...

With 3 enums:

ContextNames with the IDs of the contexts we will keep because they are correctly typed and are enough for ContextAwarePluginManagerTrait to work

  • case Entity = 'entity';
  • case FieldName = 'field_name';
  • case FieldDelta = 'ui_patterns:field:index'; (we maybe rename it at the end)
  • case View = 'ui_patterns_views:view_entity'; (we maybe rename it at the end)

2 are still missing: view display and view rows.

LegacyContextNames with the IDs of the context we will get rid off because too loosely typed are useless to load source plugins (more details in the file):

  • case Bundle = 'bundle';
  • case Field = 'ui_patterns:field:items';
  • case Requirements = 'context_requirements';
  • case FormState = 'ui_patterns:form_state';
  • case LangCode = 'ui_patterns:lang_code';
  • case ViewRows = 'ui_patterns_views:rows';

RequirementsValues to catalog the found values of context_requirements:

  • case Field = 'field_formatter';
  • case FieldItem = 'field_granularity:item';
  • case View = 'views:style';
  • case ViewRow = 'views:row';

It is a bit counter-intuitive to catalog context names like that because:

  • context names are useless in context providers side (they are here only to label them, and are never shared or processed, with the only exception of the context mapping mechanism if my understanding is right)
  • context names are an internal convention of each context aware plugins

However, I hope those 3 enums will hep for the work, because they are making the context easier to find in the codebase and we will less rely on "magical strings".

Also, I have added a (still unused and may not be kept at the end) ContextDefinitionsRepository helper to easily test the context structures defined in the issue.

pdureau’s picture

I may have finally understand the use case of context_requirement and some specific mechanism we currently have ;)

Let's say we have a source selector where the root level contexts are: entity + field name, so for example in ComponentFormatter. Because I am in the root, I want all sources, , so for a slot:

  • without contexts, so for a slot: Component, Wysiwyg...
  • with only entity context: Entity Link, "Entity -> Field"...
  • with only field name context: none.
  • with both contexts: Field Formatter, Field Label...

Now I am switching contexts thanks to "Entity -> Field", my context is also entity + field name with the same entity but a different field name, so i will access to the same sources:

  • without contexts: Component, Wysiwyg...
  • with only entity context: Entity Link, "Entity -> Field"...
  • with only field name context: none.
  • with both contexts: Field Formatter, Field Label...

But i don't want all of them here, I just switched contexts, I want only the ones which differ from the original context:

  • with both contexts: Field Formatter, Field Label...

It works like that today, that's great, we need to keep this.

pdureau’s picture

Status: Needs work » Needs review

Hi Mikael,

I have a proposal for the current state of this ticket:

  1. We first merge #3590426: Reduce the API scope of our plugin managers, #3590492: Tidy the source plugins tags and #3591167: Tidy metadata plugin attributes, in this order, and we rebase the MR
  2. We remove the unfinished or temporary parts of the MR:
    • modules/ui_patterns_temp/*
    • src/Plugin/Context/ContextDefinitionsRepository.php
    • src/Plugin/DataType/*
  3. We merge like that in branch 2.x

Because I believe this work is just tidying up without messing with compatibility and can be deliver ASAP (statement to careful check together):

  • Use PHP enums for contexts
  • Better use of ContextAwarePluginInterface
  • ui_patterns_views:plugin:display_handler, ui_patterns:field:items and ui_patterns:lang_code contexts removal

So, we can start a new ticket with the more impactful changes and more focused MR.

pdureau’s picture

We remove the unfinished or temporary parts of the MR:

  • modules/ui_patterns_temp/*
  • src/Plugin/Context/ContextDefinitionsRepository.php
  • src/Plugin/DataType/*

Done. Removed from MR #501 (to review). Kept in MR # 509 (for future use).

The phpmd pipeline fail is fixed in #3591167: Tidy metadata plugin attributes will be merged before.