Problem/Motivation
Note: AI assisted issue, but human investigation and fix tested within Display builder.
When a Drupal entity with a ui_patterns_source field is created, the TypedData prototype mechanism injects a null placeholder item into the field list. FieldItemList::filterEmptyItems() is supposed to remove it (called from applyDefaultValue() and preSave()), but it doesn't, because SourceValueItem does not override isEmpty().
The inherited Map::isEmpty() checks $property->getValue() !== NULL for each sub-property. The source and third_party_settings sub-properties are MapDataDefinition instances whose getValue() returns [] (empty array), not NULL. Since [] !== NULL, isEmpty() returns FALSE for a fully null/unset item, and the phantom survives.
Additionally, setValue() silently bails when source_id is empty, so calling $entity->set('field', []) does not clear the phantom — it just no-ops.
Steps to reproduce
$entity = MyEntity::create([]);
$values = $entity->get('my_ui_patterns_source_field')->getValue();
// Expected: []
// Actual: [['node_id' => NULL, 'source_id' => NULL, 'source' => [], 'third_party_settings' => []]]
Proposed resolution
Override isEmpty() in SourceValueItem to delegate to source_id, the only property that makes an item valid. Read $this->values/$this->properties directly rather than calling $this->get('source_id') to avoid the side effect of materializing the property object, which changes the key order returned by getValue().
Remaining tasks
User interface changes
API changes
Data model changes
Issue fork ui_patterns-3604317
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
Comment #3
pdureau commentedThe previous implementation (until #3584856: Source field model and storage) of
ComplexDataInterface::isEmpty()for SourceValueItem was:It was working well with Display Builder, and still is, and its simpler than the proposal in the MR. What do you think, Jean?
Comment #4
mogtofu33 commentedThis was not enough when testing with Display Builder, did you test with DB?
Comment #5
mogtofu33 commentedMap::setValue() contains this:
When setValue() is called on an item where source_id has already been materialized into $this->properties (by a previous $this->get('source_id') call anywhere in the call stack), source_id is set on the property object and then deleted from $this->values. After that, $this->values['source_id'] is undefined.
So empty($this->values['source_id']) would return TRUE — incorrectly marking ailterEmptyItems() would then delete it during save.
The array_key_exists check distinguishes two reasons source_id might be absent from $this->values:
Without the guard, the phantom case and the "moved to properties" case are indistinguishable by $this->values alone, and a valid item gets deleted.
Comment #6
pdureau commentedYes, it was looking enough on my local environment. But it was just a proposal.