Skip to content

Architecture Overview#

Entity Builder implements two core architectural principles:

  1. EbDefinition-Centric Workflow - All entry points save data to a configuration entity before execution
  2. Field-Centric Architecture - Each field is the single source of truth for its configuration

Design Principles#

1. Field-Centric Architecture#

The YAML format and UI are built around the concept that each field knows its own configuration:

field_definitions:
  - entity_type: node
    bundle: article
    field_name: field_body
    field_type: text_long
    label: Body
    required: true
    # Display configuration for default mode
    widget: text_textarea
    widget_settings:
      rows: 10
    formatter: text_default
    # Field group assignment
    form_group: group_content
    view_group: group_main
    weight: 0

Key Principles:

Principle Description
Single Source of Truth Fields define their own widget, formatter, and group assignments
Default Mode Focus field_definitions configure the default display mode
Non-Default Modes Use display_field_definitions only for teaser, card, or custom view modes
Extension Integration Extensions add properties to existing definitions (e.g., form_group, view_group)

2. EbDefinition-Centric Workflow#

All entry points first save data to the EbDefinition config entity, then the apply workflow reads from it:

flowchart TB
    subgraph Phase1["Phase 1: Data Entry"]
        UI[Entity Builder UI<br/>AG Grid]
        YAML[YAML Import<br/>drush eb:import]

        UI -->|Grid Data JSON| Save[Save to Definition]
        YAML -->|File Content| Parse[YamlParser]
        Parse --> Validate[Validate]
        Validate --> Save

        Save --> DEF[(EbDefinition<br/>Config Entity)]
    end

    subgraph Phase2["Phase 2: Apply Workflow"]
        DEF -->|User clicks Apply| ODB[OperationDataBuilder]
        ODB --> DR[DependencyResolver<br/>Topological Sort]
        DR --> CD[ChangeDetector<br/>Sync Mode]
        CD --> VM[ValidationManager<br/>Two-Stage]
        VM --> OP[OperationProcessor]
        OP --> RM[RollbackManager]
        RM --> LOG[EbLog + Watchdog]
    end

    OP -->|Creates| Drupal[(Drupal Entities<br/>Bundles, Fields, Displays)]

High-Level System Architecture#

flowchart TB
    subgraph Input["Input Layer"]
        UI[Web UI<br/>AG Grid]
        CLI[Drush CLI]
        API[PHP API]
    end

    subgraph Core["Core Services"]
        ODB[OperationDataBuilder]
        OB[OperationBuilder]
        DR[DependencyResolver]
        CD[ChangeDetector]
        VM[ValidationManager]
        PG[PreviewGenerator]
        OP[OperationProcessor]
    end

    subgraph Storage["Storage Layer"]
        DEF[(EbDefinition<br/>Config Entity)]
        RB[(EbRollback<br/>Content Entity)]
        LOG[(EbLog<br/>Content Entity)]
        WD[(Watchdog)]
    end

    subgraph Extensions["Extension Plugins"]
        FG[eb_field_group]
        PA[eb_pathauto]
        AEL[eb_auto_entitylabel]
    end

    UI --> ODB
    CLI --> ODB
    API --> ODB

    ODB --> DEF
    ODB --> OB
    OB --> DR
    DR --> CD
    CD --> VM
    VM --> PG
    VM --> OP

    OP --> RB
    OP --> LOG
    LOG --> WD

    Extensions -.->|buildOperations| ODB
    Extensions -.->|getOperationDependencies| DR
    Extensions -.->|detectChanges| CD

Data Flow Pipeline#

sequenceDiagram
    participant User
    participant UI as Entity Builder UI
    participant DEF as EbDefinition
    participant ODB as OperationDataBuilder
    participant DR as DependencyResolver
    participant CD as ChangeDetector
    participant VM as ValidationManager
    participant OP as OperationProcessor
    participant Drupal as Drupal Entities

    User->>UI: Edit definition
    UI->>DEF: Save grid data
    User->>UI: Click Apply
    UI->>ODB: Build operations
    ODB->>DR: Resolve dependencies
    DR->>CD: Detect changes
    CD->>VM: Validate batch
    VM->>OP: Execute operations
    OP->>Drupal: Create/Update entities
    OP-->>User: Success + Rollback available

Definition Keys#

Core Definition Keys#

Key Purpose UI Tab Operations Generated
bundle_definitions Content types, vocabularies, media types Bundles create_bundle
field_definitions Fields with default mode display config Fields create_field, configure_form_mode, configure_view_mode
display_field_definitions Non-default display mode overrides - configure_form_mode, configure_view_mode
menu_definitions Custom menus - create_menu

Extension Definition Keys#

Extension Key UI Tab Operations Generated
eb_field_group field_group_definitions Field Groups create_field_group
eb_pathauto (bundle column: pathauto_pattern) Bundles create_pathauto_pattern
eb_auto_entitylabel (bundle columns: auto_entitylabel_*) Bundles configure_auto_entitylabel

Field Definition Structure#

Each field definition contains everything needed to configure that field:

erDiagram
    FIELD_DEFINITION {
        string entity_type "node, taxonomy_term, media"
        string bundle "Target bundle"
        string field_name "field_* machine name"
        string field_type "text_long, entity_reference, etc"
        string label "Human-readable label"
        boolean required "Is field required"
        int cardinality "Number of values"
    }

    FIELD_DEFINITION ||--o| STORAGE_SETTINGS : has
    STORAGE_SETTINGS {
        string target_type "For entity_reference"
        int max_length "For string fields"
        int precision "For decimal fields"
    }

    FIELD_DEFINITION ||--o| CONFIG_SETTINGS : has
    CONFIG_SETTINGS {
        object handler_settings "For entity_reference"
        string min "For number fields"
        string max "For number fields"
    }

    FIELD_DEFINITION ||--o| FORM_DISPLAY : has
    FORM_DISPLAY {
        string widget "Widget plugin ID"
        object widget_settings "Widget configuration"
        string form_group "Field group assignment"
        int weight "Display order"
    }

    FIELD_DEFINITION ||--o| VIEW_DISPLAY : has
    VIEW_DISPLAY {
        string formatter "Formatter plugin ID"
        object formatter_settings "Formatter configuration"
        string label_display "above, inline, hidden"
        string view_group "Field group assignment"
    }

Example YAML:

field_definitions:
  - # Identity
    entity_type: node
    bundle: article
    field_name: field_category

    # Field Type
    field_type: entity_reference
    label: Category
    description: Select a category for this article.
    required: true
    cardinality: 1

    # Storage Settings (shared across all instances)
    field_storage_settings:
      target_type: taxonomy_term

    # Instance Settings (specific to this bundle)
    field_config_settings:
      handler_settings:
        target_bundles:
          categories: categories

    # Form Display (default mode)
    widget: options_select
    widget_settings: {}

    # View Display (default mode)
    formatter: entity_reference_label
    formatter_settings:
      link: true
    label_display: above

    # Field Group Assignment (eb_field_group)
    form_group: group_classification
    view_group: group_meta

    # Weight (display order)
    weight: 5

Why Field-Centric?#

  1. Maps to UI: Each row in the Fields grid contains all field configuration
  2. Spreadsheet Export: Flat structure exports cleanly to Excel/Google Sheets
  3. Single Edit Point: Change widget/formatter/group in one place
  4. Reduces Redundancy: No separate display_field_definitions for default mode

Flat Operations Pattern#

All definition data is converted to flat operation arrays before processing:

flowchart LR
    subgraph Input["Definition Format"]
        BD[bundle_definitions]
        FD[field_definitions]
        FGD[field_group_definitions]
        DFD[display_field_definitions]
    end

    subgraph Builder["OperationDataBuilder"]
        B1[buildBundleOperations]
        B2[buildFieldOperations]
        B3[buildDisplayOperations]
        EXT[extension.buildOperations]
    end

    subgraph Output["Flat Operations"]
        O1["create_bundle"]
        O2["create_field"]
        O3["configure_form_mode"]
        O4["configure_view_mode"]
        O5["create_field_group"]
    end

    BD --> B1 --> O1
    FD --> B2 --> O2
    FD --> B3 --> O3
    FD --> B3 --> O4
    FGD --> EXT --> O5
    DFD --> B3

Input (Definition Format):

bundle_definitions:
  - entity_type: node
    bundle_id: article
    label: Article

field_definitions:
  - entity_type: node
    bundle: article
    field_name: field_body
    field_type: text_long
    widget: text_textarea

Output (Operation Format):

1
2
3
4
5
6
[
    ['operation' => 'create_bundle', 'entity_type' => 'node', 'bundle_id' => 'article', ...],
    ['operation' => 'create_field', 'entity_type' => 'node', 'bundle' => 'article', ...],
    ['operation' => 'configure_form_mode', 'widget' => 'text_textarea', ...],
    ['operation' => 'configure_view_mode', 'formatter' => 'text_default', ...],
]

Benefits of Flat Operations#

Benefit Description
Unified Processing All operations go through the same pipeline
Explicit Dependencies Dependencies calculated from operation data, not nesting
Extension Simplicity Extensions just return operation arrays
Batch Context Validators see what will be created in the same batch
Testability Operations are plain arrays easy to test

Dependency Resolution#

Dependencies are automatically detected and operations ordered using Kahn's topological sort:

flowchart TD
    subgraph "Dependency Graph Example"
        CB1[create_bundle<br/>taxonomy_term:categories]
        CB2[create_bundle<br/>node:article]

        CF1[create_field<br/>field_category]
        CF2[create_field<br/>field_body]

        CFM1[configure_form_mode<br/>field_category]
        CFM2[configure_form_mode<br/>field_body]

        CVM1[configure_view_mode<br/>field_category]
        CVM2[configure_view_mode<br/>field_body]

        CFG[create_field_group<br/>group_content]

        CB1 --> CF1
        CB2 --> CF1
        CB2 --> CF2

        CF1 --> CFM1
        CF1 --> CVM1
        CF2 --> CFM2
        CF2 --> CVM2

        CF1 --> CFG
        CF2 --> CFG
    end
Operation Depends On
create_field create_bundle (for the field's bundle)
create_field (entity_reference) Target bundle
configure_form_mode create_field
configure_view_mode create_field
create_field_group Bundle, display, child fields, parent group
create_pathauto_pattern create_bundle

Smart Sync Mode#

Change detection compares definitions with current Drupal state:

flowchart TD
    Start[Operation Data] --> Exists{Entity<br/>Exists?}

    Exists -->|No| Create[create_* operation]
    Exists -->|Yes| Changed{Has<br/>Changes?}

    Changed -->|Yes| Update[update_* operation]
    Changed -->|No| Skip[Skip operation]

    Create --> Execute[Execute]
    Update --> Execute
    Skip --> Next[Next Operation]
Entity Exists Has Changes Result
No N/A create_*
Yes Yes update_*
Yes No skip

Two-Stage Validation#

flowchart TB
    subgraph Stage1["Stage 1: Operation Validation"]
        O1[CreateBundleOperation<br/>validate entity type]
        O2[CreateFieldOperation<br/>validate field type]
        O3[ConfigureFormModeOperation<br/>validate widget]
    end

    subgraph Stage2["Stage 2: Validator Plugins"]
        V1[DependencyValidator<br/>entity/bundle/field existence]
        V2[WidgetCompatibilityValidator<br/>widget supports field type]
        V3[FormatterCompatibilityValidator<br/>formatter supports field type]
        V4[CircularDependencyValidator<br/>no circular dependencies]
        V5[RequiredFieldsValidator<br/>required properties present]
    end

    Operations[Operations Array] --> Stage1
    Stage1 --> Stage2
    Stage2 --> Result{Valid?}
    Result -->|Yes| Execute[Execute Operations]
    Result -->|No| Errors[Return Errors]
  1. Stage 1: Operations validate their specific requirements
  2. CreateFieldOperation: Validate field type exists
  3. CreateBundleOperation: Validate entity type supports bundles

  4. Stage 2: Validator plugins check cross-cutting concerns

  5. DependencyValidator: Entity/bundle/field existence
  6. WidgetCompatibilityValidator: Widget supports field type
  7. CircularDependencyValidator: No circular dependencies

Entity Types#

erDiagram
    EbDefinition ||--o{ EbRollback : "has rollbacks"
    EbDefinition ||--o{ EbLog : "has logs"
    EbRollback ||--|{ EbRollbackOperation : "contains"

    EbDefinition {
        string id PK "Machine name"
        string label "Human-readable"
        array bundle_definitions "Bundle configs"
        array field_definitions "Field configs"
        array field_group_definitions "Group configs"
        string application_status "applied, pending"
    }

    EbRollback {
        int id PK "Auto-increment"
        string definition_id FK "Source definition"
        string status "pending, completed, failed"
        int operation_count "Number of operations"
        timestamp created "When apply started"
    }

    EbRollbackOperation {
        int id PK "Auto-increment"
        int rollback_id FK "Parent rollback"
        string operation_type "Plugin ID"
        map original_data "Serialized undo data"
        int sequence "Execution order"
    }

    EbLog {
        int id PK "Auto-increment"
        string definition_id FK "Source definition"
        string action "apply, rollback, import"
        string status "pending, success, partial, failed"
        timestamp started "Action start time"
        timestamp completed "Action end time"
    }

ConfigEntity: EbDefinition#

Stores the YAML definition data for import/export and application.

Key Properties: - id - Machine name identifier - label - Human-readable label - bundle_definitions - Array of bundle configurations - field_definitions - Array of field configurations with display settings - field_group_definitions - Array of field groups (eb_field_group) - display_field_definitions - Non-default mode overrides

Storage: Config sync directory

ContentEntity: EbRollback / EbRollbackOperation#

Rollback storage for undo operations.

Storage: Database only (NOT exported with config)

ContentEntity: EbLog#

Tracks apply/rollback/import sessions with timestamps for watchdog queries.

Storage: Database only

Extension Plugin System#

flowchart TB
    subgraph Manager["EbExtensionPluginManager"]
        GM[getExtensions]
        GEO[getExtensionsForOperation]
        GEK[getExtensionsForYamlKey]
    end

    subgraph Extensions["Extension Plugins"]
        FG[FieldGroupExtension<br/>yaml_keys: field_group_definitions]
        PA[PathautoExtension<br/>bundle column: pathauto_pattern]
        AEL[AutoEntityLabelExtension<br/>bundle columns: auto_entitylabel_*]
    end

    subgraph Interface["EbExtensionInterface"]
        BO[buildOperations]
        GOD[getOperationDependencies]
        DC[detectChanges]
        EC[extractConfig]
    end

    Manager --> Extensions
    Extensions --> Interface

Available Extensions#

Extension YAML Key Columns Added Operations
eb_field_group field_group_definitions form_group, view_group on fields create_field_group
eb_pathauto - pathauto_pattern on bundles create_pathauto_pattern
eb_auto_entitylabel - auto_entitylabel_* on bundles configure_auto_entitylabel

Creating Extensions#

Extensions implement the EbExtensionInterface:

#[EbExtension(
  id: 'my_extension',
  label: new TranslatableMarkup('My Extension'),
  yaml_keys: ['my_definitions'],
  operations: ['create_my_entity'],
)]
class MyExtension extends EbExtensionBase {

  public function buildOperations(array $data): array {
    // Convert my_definitions to operations
  }

}

Rollback Flow#

sequenceDiagram
    participant User
    participant RM as RollbackManager
    participant RB as EbRollback
    participant RBO as EbRollbackOperation
    participant OP as OperationProcessor
    participant Drupal as Drupal Entities

    Note over User,Drupal: Apply Definition
    User->>RM: startRollback(definitionId)
    RM->>RB: Create (status: pending)

    loop For each operation
        OP->>Drupal: Execute operation
        OP->>RM: storeRollbackData(result)
        RM->>RBO: Create with undo data
    end

    RM->>RB: Update operation_count

    Note over User,Drupal: Later: Execute Rollback
    User->>RM: executeRollback(rollbackId)
    RM->>RBO: Load all (ORDER BY sequence DESC)

    loop For each operation (reverse order)
        RM->>OP: Execute rollback
        OP->>Drupal: Delete/Restore entity
    end

    RM->>RB: Update status: completed

Key Design Decisions#

Decision Rationale
Field-centric over display-centric Single source of truth, maps to UI grid, cleaner exports
Flat operations over nested structure Unified processing, explicit dependencies, testability
OperationDataBuilder as single source Consistency across UI, CLI, and API
Two-stage validation Separation of concerns, extensibility
Kahn's algorithm O(V + E) efficiency, mathematical correctness
Sync mode as default Idempotency, efficiency, developer experience
ContentEntity for rollback Database-only storage, not exported with config