Skip to content

Define and use a compound

A compound entity carries only its identity: id, label, description and includes. Saving it registers its derived entity type cfi_<id> (see compound items as entities). Each sub-field is then ordinary FieldStorageConfig and FieldConfig on that type and bundle, created as on any other content entity type. Run this once in a bootstrapped development environment with Compound Field enabled:

use Drupal\compound_field\Entity\CompoundField;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;

$compound = CompoundField::create([
  'id' => 'speaker',
  'label' => 'Speaker card',
]);
$compound->save();

FieldStorageConfig::create([
  'field_name' => 'name',
  'entity_type' => 'cfi_speaker',
  'type' => 'string',
])->save();
FieldConfig::create([
  'field_name' => 'name',
  'entity_type' => 'cfi_speaker',
  'bundle' => 'cfi_speaker',
  'label' => 'Name',
])->save();

FieldStorageConfig::create([
  'field_name' => 'topic',
  'entity_type' => 'cfi_speaker',
  'type' => 'string',
  'cardinality' => 3,
])->save();
FieldConfig::create([
  'field_name' => 'topic',
  'entity_type' => 'cfi_speaker',
  'bundle' => 'cfi_speaker',
  'label' => 'Topics',
])->save();

\Drupal\compound_field\CompoundFieldCanonicalSubFields::write($compound_id, $name, $definition) does the same in one call, taking storage_settings and settings in the runtime shape FieldStorageConfig::getSettings() answers. Field UI, composition mirroring and the structural Drush commands all use it, so a sub-field written by any of them is spelled identically; use it when scripting several sub-fields from one array. Field UI remains the interactive way, at /admin/structure/compound-field/<id>/fields/....

Attach compound_field:speaker to a content type through normal FieldStorageConfig and FieldConfig on the host entity type, or through Field UI. The host storage's cardinality can be unlimited. The sub-field caps are separate: each is the cardinality on that sub-field's own storage configuration.

Read, validate and save through the host

With a loaded Conference node and its field_speakers field:

$node->set('field_speakers', [
  [
    'name' => 'Ada Lovelace',
    'topic' => [
      ['value' => 'Computing history'],
      ['value' => 'Analytical engines'],
    ],
  ],
]);
$violations = $node->validate();
if ($violations->count() !== 0) {
  throw new \RuntimeException('Conference values failed validation.');
}
$node->save();

$name = $node->get('field_speakers')->first()->get('name')->get('value')->getValue();

The entity API does not validate every programmatic save. Handle violations before persisting user-supplied values, and do not expose internal exceptions to an untrusted client. To read nested values by path, or build tokens, see nested values, tokens and Search API. To load, query and save one item through its own storage, see compound items as entities.

Storage settings

storage_settings belong to a sub-field's storage configuration, such as a string's maximum length or a reference's target entity type; settings are field settings, such as a reference handler and its allowed bundles. Defaults come from the field type. Both take the runtime shape FieldStorageConfig::getSettings() answers for any field, for example an Options field's allowed_values as the map ['red' => 'Red'], not the configuration-export list of pairs. Core converts between the two on save and export, exactly as for a field created through Field UI; Compound Field keeps no settings shape of its own.

Changing datetime_type on an in-use sub-field is refused even when its column specification is unchanged, because it changes how stored values are read. Retiring populated allowed-value keys needs explicit consent.

Include another compound's fields

Given a person compound, this export makes employee include its sub-fields:

id: employee
label: Employee
includes:
  - person

employee's own sub-fields, such as cv, are separate field.storage.cfi_employee.cv / field.field.cfi_employee.cfi_employee.cv configuration, created like any other sub-field. Composition mirrors each included compound's sub-fields onto every dependent's derived type, marked with the compound_field.inherited_from third-party setting so they are never mistaken for the dependent's own declarations.

Use the compound's Composition tab or set includes in configuration. getDeclaredFields() returns only local definitions; getFields() resolves included fields first, transitively, then local fields, and a repeated base contributes once. Duplicate sub-field names, missing bases and cycles are refused.

An inclusion adds no value nesting: a sub-field of type compound_field:person is what holds a nested person record. Both count toward the expanded column budget. Edit inherited fields on their declaring compound; the change reaches every dependent host storage. An in-use dependent prevents deleting its base, and deleting an unused base can cascade to unused dependents.

Change an in-use definition

There is no setFields() mutator. Sub-fields are field configuration, so a structural change is planned against a candidate snapshot and applied through the structural pipeline, never by saving the definition directly:

use Drupal\compound_field\StructuralChange\CompoundFieldDefinitionSet;

$fields = $compound->getDeclaredFields();
$fields['phone'] = ['field_type' => 'telephone', 'label' => 'Phone'];
unset($fields['fax']);
$candidate = CompoundFieldDefinitionSet::fromCompound($compound, $fields);
$request = \Drupal\compound_field\StructuralChange\CompoundFieldStructuralChangeRequest::create()
  ->acknowledgeRemoval('fax');
$pipeline = \Drupal::service('compound_field.structural_change_pipeline');
$plan = $pipeline->plan($candidate, $compound, $request);
// Inspect $plan->describe() before applying.
$pipeline->apply($candidate, $request);

Changes to includes use this pipeline too; removing an inclusion requires acknowledging the sub-fields it removes. See the deployment guide for imports and recovery.