Architecture Overview#
Entity Builder implements two core architectural principles:
- EbDefinition-Centric Workflow - All entry points save data to a configuration entity before execution
- 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:
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:
Why Field-Centric?#
- Maps to UI: Each row in the Fields grid contains all field configuration
- Spreadsheet Export: Flat structure exports cleanly to Excel/Google Sheets
- Single Edit Point: Change widget/formatter/group in one place
- 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):
Output (Operation Format):
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]
- Stage 1: Operations validate their specific requirements
CreateFieldOperation: Validate field type exists-
CreateBundleOperation: Validate entity type supports bundles -
Stage 2: Validator plugins check cross-cutting concerns
DependencyValidator: Entity/bundle/field existenceWidgetCompatibilityValidator: Widget supports field typeCircularDependencyValidator: 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:
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 |