Problem/Motivation

Contrib modules extend Display Builder: they add islands, derive existing ones, add buildables for new integrations, add sources, and change instances from their own code. display_builder_ai already adds an island and changes instances. Four families of interfaces carry that contract:

  • Islands: IslandInterface and IslandEventSubscriberInterface, with IslandWithFormInterface, IslandConfigurationFormInterface, RenderableAltererInterface and ThirdPartySettingsInterface. Extended through IslandPluginBase and its traits.
  • Buildables: DisplayBuildableInterface and DisplayBuildableOverrideInterface. Extended through DisplayBuildablePluginBase, or ConfigBuildablePluginBase for a buildable storing its sources in a config entity.
  • Instances: InstanceInterface, the HistoryInterface and PublishableInterface it extends, the ProfileInterface and PublicationStatus they return.
  • Sources: SourceWithSlotsInterface, SourceProcessingDataInterface and EmptyPlaceholderHelpInterface, implemented by the source plugins that take part in the builder.

Three hooks are part of the same contract: hook_island_info_alter(), hook_display_buildable_info_alter(), and hook_display_builder_page_displays() with its PageDisplays. So are the DisplayBuilderEvents.

Once RC1 freezes the API, every flaw below becomes a BC break to fix. Nothing marks what is API and what is internal, and the instance contract is not consistent.

Each finding with a child issue is detailed there. The ones without one yet:

DisplayBuildableInterface

  • 29 methods of its own, 4 of them static: checkAccess(), checkInstanceId(), getUrlFromInstanceId(), getDisplayUrlFromInstanceId(). #3616313: Check static methods on DisplayBuildableInterface owns them, and #3573905: Simplify DisplayBuildableInterface owns checkAccess() as AccessibleInterface::access(). They exist because the instance ID carries meaning, and that meaning leaks: display_builder_ai parses instance ID prefixes by hand, gets the override one wrong, entity_view_override__ for entity_override__, and does not know pattern_preset__.
  • The prefix is read from the attribute by reflection in getPrefix(), and from the altered definition everywhere else, so hook_display_buildable_info_alter() cannot change it consistently.
  • Nothing public leads from a display entity to its buildable. display_builder_ai guesses the plugin ID from the entity type and calls getInstanceId() and initInstanceIfMissing() behind method_exists(). Per #3573905-30: Simplify DisplayBuildableInterface, initInstanceIfMissing() stays, and getInstanceId() goes with #3616313: Check static methods on DisplayBuildableInterface, whose summary does not list it yet. Removing it would switch that code off without an error.
  • buildInstanceForm(bool $mandatory, ?TranslatableMarkup $title, bool $link) grows one boolean flag per need, and its three callers pass them by position.
  • getTranslationLanguages($include_default = TRUE) is an untyped flag with no default in DisplayBuildablePluginBase: since beta8, a contrib buildable extending that base fails to load. isTranslationSynchronized() has no caller, and its docblock names a data field that is sources.
  • DisplayBuildablePluginManager is final and has no interface. The islands manager has one.
  • No page of the docs explains how to write a buildable.

Array shapes

  • getNode(), getSources(), getPathIndex(), getPast() and getFuture() return arrays of undocumented shape, and so do the $data and $third_party_settings arguments. getUsers() describes its keys in prose only.
  • DisplayBuilderEvent documents string for three getters returning ?string, and two of its getters dereference a nullable profile.

Proposed resolution

One child issue per numbered item, in this order. Rules for all of them:

  • A new public method needs a known caller: a submodule or a known contrib module doing it by hand today. Adding one after RC1 is BC-safe, removing one is not. Callers may walk getSources(), whose node shape item 8 documents.
  • Break before RC1, with a change record, where a deprecation cannot express the change: a return type, a thrown exception. Deprecate everywhere else, @deprecated in display_builder:X.Y.Z and is removed from display_builder:X+1.0.0 plus @trigger_error().
  • Queries: has*() and is*() answer FALSE for an unknown node. get*() returns NULL when the value is absent. Where NULL already means something, as getParentId() for a root node, an unknown node throws.
  • Mutators throw InvalidNodeException for an unknown or invalid node and \OutOfRangeException for a full root or slot, both in @throws, and return static unless they create something.
  • No new static method on a contract, no new boolean flag, every array with an array{...} shape, one name per concept.
  • Renaming a method parameter is free: named arguments are not API. Renaming a constructor parameter of an attribute or a value object is not, since callers use them by name: #[Island] renamed default_region to region in beta7.
  • A new method on an extended contract ships with a default in its base class.
  1. #3627027: InstanceInterface: list the nodes of a slot
  2. #3627028: InstanceInterface: one failure style for the tree methods
  3. #3627285: Profile, page layout, override and display interfaces: setters return static, revert() split, docblocks and #3627284: InstanceInterface: setSources() instead of setNewPresent(), one save per change, the hashes
  4. Dropped: InstanceStorageInterface, no caller outside Display Builder.
  5. #3627256: IslandInterface: name the instance $instance, and settle the island API before RC1
  6. #3627260: Island forms: one pattern, through PluginWithFormsInterface, with one build info
  7. DisplayBuildableInterface. Replace the buildInstanceForm() flags with a documented $options array. Give getTranslationLanguages() a default in the base class and a typed flag, and drop isTranslationSynchronized() unless something calls it. Give DisplayBuildablePluginManager an interface, where #3616313: Check static methods on DisplayBuildableInterface adds its resolver, and read the prefix from the definition. Document how to write a buildable, on either base class. The statics stay with #3616313: Check static methods on DisplayBuildableInterface and #3573905: Simplify DisplayBuildableInterface.
  8. Array shapes. Document every array left in the contracts, the node shape once, the island build() arguments and configuration keys included. Fix the event docblocks. After 1 to 7, so nothing is documented twice.
  9. #3627266: Mark the API boundary: @api on what contrib extends, @internal on the rest

Also before RC1: #3627272: Rename the island alter hook and the service IDs before RC1, #3627279: Relay every change of an instance to open builders, not only the builder's own.

Fixed on the way: #3626658: An instance translation can save the default language's tree over its own, #3626659: Paste, duplicate and preset insert end in a server error when the target is full or gone, #3626660: Inserting a preset makes one undo step per island setting, #3626661: Reading the group of a pattern preset saves the preset. Landed since the first summary: #3623326: Consistent display lists: operations, publication state, columns, sorting, #3555110: Symmetric translation, #3617547: Make pattern presets buildable.

Follow-ups, separate issues, fine after RC1:

  • Profile with EntityWithPluginCollectionInterface, so profiles depend on the modules providing their islands. PageLayout already does it for its conditions.
  • ContextProviderInterface: InstanceInterface and DisplayBuildableInterface extend it, are registered as no provider, and ignore $unqualified_context_ids. Document the deviation, or replace it with a plain getContexts().
  • IslandType: instance methods instead of statics taking a string, and IslandInterface::getType() returning the enum.

Remaining tasks

User interface changes

None.

API changes

In each child issue. Item 7 deprecates the buildInstanceForm() flags and gives DisplayBuildablePluginManager an interface.

Data model changes

None.

Comments

mogtofu33 created an issue. See original summary.

mogtofu33’s picture

Issue summary: View changes
mogtofu33’s picture

Issue summary: View changes

Updated the summary after a second pass and an adversarial last pass. Every claim was checked against 1.0.x and against the translation branch, and the paste, preset, cardinality and getParentId() problems were reproduced in kernel tests.

Main changes:

Turning this into a plan with nine child issues, plus three bug issues for what the tests found. The order and the rules are in the summary.

mogtofu33’s picture

Category: Task » Plan
mogtofu33’s picture

Assigned: Unassigned » pdureau
Status: Active » Needs review

No code but in review just for a global agreement or not on this plan and decide if it is sound.

mogtofu33’s picture

Issue tags: +AI-accelerated
mogtofu33’s picture

Title: Harden InstanceInterface, IslandInterface and DisplayBuildableInterface as public API before RC1 » Plan: Harden InstanceInterface, IslandInterface and DisplayBuildableInterface as public API before RC1
mogtofu33’s picture

mogtofu33’s picture

Issue summary: View changes
mogtofu33’s picture

Issue summary: View changes
mogtofu33’s picture

Issue summary: View changes
mogtofu33’s picture

Priority: Normal » Major
mogtofu33’s picture

Issue summary: View changes
mogtofu33’s picture

Issue summary: View changes