Commit 0509fbe7 authored by alexpott's avatar alexpott

Issue #2164601 by yched, jibran, amateescu, pfrenssen: Stop auto-creating...

Issue #2164601 by yched, jibran, amateescu, pfrenssen: Stop auto-creating FieldItems on mere reading of $entity->field[N]
parent 67cc654b
......@@ -84,11 +84,28 @@ public function first() {
/**
* {@inheritdoc}
*/
public function set($index, $item) {
$this->offsetSet($index, $item);
public function set($index, $value) {
$this->offsetSet($index, $value);
return $this;
}
/**
* {@inheritdoc}
*/
public function removeItem($index) {
$this->offsetUnset($index);
return $this;
}
/**
* {@inheritdoc}
*/
public function appendItem($value = NULL) {
$offset = $this->count();
$this->offsetSet($offset, $value);
return $this->offsetGet($offset);
}
/**
* {@inheritdoc}
*/
......
......@@ -1090,7 +1090,7 @@ protected function mapToStorageRecord(ContentEntityInterface $entity, $table_nam
// @todo Give field types more control over this behavior in
// https://drupal.org/node/2232427.
if (!$definition->getMainPropertyName() && count($columns) == 1) {
$value = $entity->$field_name->first()->getValue();
$value = ($item = $entity->$field_name->first()) ? $item->getValue() : array();
}
else {
$value = isset($entity->$field_name->$column_name) ? $entity->$field_name->$column_name : NULL;
......@@ -1300,7 +1300,7 @@ protected function loadFieldItems(array $entities) {
}
// Add the item to the field values for the entity.
$entities[$row->entity_id]->getTranslation($row->langcode)->{$field_name}[$delta_count[$row->entity_id][$row->langcode]] = $item;
$entities[$row->entity_id]->getTranslation($row->langcode)->{$field_name}->appendItem($item);
$delta_count[$row->entity_id][$row->langcode]++;
}
}
......
......@@ -510,10 +510,12 @@ public function setDefaultValue($value) {
* {@inheritdoc}
*/
public function getOptionsProvider($property_name, FieldableEntityInterface $entity) {
// If the field item class implements the interface, proxy it through.
$item = $entity->get($this->getName())->first();
if ($item instanceof OptionsProviderInterface) {
return $item;
// If the field item class implements the interface, create an orphaned
// runtime item object, so that it can be used as the options provider
// without modifying the entity being worked on.
if (is_subclass_of($this->getFieldItemClass(), '\Drupal\Core\TypedData\OptionsProviderInterface')) {
$items = $entity->get($this->getName());
return \Drupal::typedDataManager()->getPropertyInstance($items, 0);
}
// @todo: Allow setting custom options provider, see
// https://www.drupal.org/node/2002138.
......
......@@ -40,18 +40,6 @@ class FieldItemList extends ItemList implements FieldItemListInterface {
*/
protected $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
/**
* {@inheritdoc}
*/
public function __construct(DataDefinitionInterface $definition, $name = NULL, TypedDataInterface $parent = NULL) {
parent::__construct($definition, $name, $parent);
// Always initialize one empty item as most times a value for at least one
// item will be present. That way prototypes created by
// \Drupal\Core\TypedData\TypedDataManager::getPropertyInstance() will
// already have this field item ready for use after cloning.
$this->list[0] = $this->createItem(0);
}
/**
* {@inheritdoc}
*/
......@@ -133,28 +121,39 @@ public function setValue($values, $notify = TRUE) {
* {@inheritdoc}
*/
public function __get($property_name) {
return $this->first()->__get($property_name);
// For empty fields, $entity->field->property is NULL.
if ($item = $this->first()) {
return $item->__get($property_name);
}
}
/**
* {@inheritdoc}
*/
public function __set($property_name, $value) {
$this->first()->__set($property_name, $value);
// For empty fields, $entity->field->property = $value automatically
// creates the item before assigning the value.
$item = $this->first() ?: $this->appendItem();
$item->__set($property_name, $value);
}
/**
* {@inheritdoc}
*/
public function __isset($property_name) {
return $this->first()->__isset($property_name);
if ($item = $this->first()) {
return $item->__isset($property_name);
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function __unset($property_name) {
return $this->first()->__unset($property_name);
if ($item = $this->first()) {
$item->__unset($property_name);
}
}
/**
......@@ -177,16 +176,17 @@ public function defaultAccess($operation = 'view', AccountInterface $account = N
* {@inheritdoc}
*/
public function applyDefaultValue($notify = TRUE) {
$value = $this->getFieldDefinition()->getDefaultValue($this->getEntity());
// NULL or array() mean "no default value", but 0, '0' and the empty string
// are valid default values.
if (!isset($value) || (is_array($value) && empty($value))) {
// Create one field item and apply defaults.
$this->first()->applyDefaultValue(FALSE);
if ($value = $this->getFieldDefinition()->getDefaultValue($this->getEntity())) {
$this->setValue($value, $notify);
}
else {
$this->setValue($value, $notify);
// Create one field item and give it a chance to apply its defaults.
// Remove it if this ended up doing nothing.
// @todo Having to create an item in case it wants to set a value is
// absurd. Remove that in https://www.drupal.org/node/2356623.
$item = $this->first() ?: $this->appendItem();
$item->applyDefaultValue(FALSE);
$this->filterEmptyItems();
}
return $this;
}
......
......@@ -169,6 +169,11 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f
$elements = array();
for ($delta = 0; $delta <= $max; $delta++) {
// Add a new empty item if it doesn't exist yet at this delta.
if (!isset($items[$delta])) {
$items->appendItem();
}
// For multiple fields, title and description are handled by the wrapping
// table.
$element = array(
......
......@@ -44,9 +44,9 @@ public function getItemDefinition();
* @param int $index
* Index of the item to return.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The item at the specified position in this list. An empty item is created
* if it does not exist yet.
* @return \Drupal\Core\TypedData\TypedDataInterface|null
* The item at the specified position in this list, or NULL if no item
* exists at that position.
*
* @throws \Drupal\Core\TypedData\Exception\MissingDataException
* If the complex data structure is unset and no item can be created.
......@@ -54,20 +54,28 @@ public function getItemDefinition();
public function get($index);
/**
* Replaces the item at the specified position in this list.
* Sets the value of the item at a given position in the list.
*
* @param int $index
* Index of the item to replace.
* @param mixed
* Item to be stored at the specified position.
* The position of the item in the list. Since a List only contains
* sequential, 0-based indexes, $index has to be:
* - Either the position of an existing item in the list. This updates the
* item value.
* - Or the next available position in the sequence of the current list
* indexes. This appends a new item with the provided value at the end of
* the list.
* @param mixed $value
* The value of the item to be stored at the specified position.
*
* @return static
* Returns the list.
* @return $this
*
* @throws \InvalidArgumentException
* If the $index is invalid (non-numeric, or pointing to an invalid
* position in the list).
* @throws \Drupal\Core\TypedData\Exception\MissingDataException
* If the complex data structure is unset and no item can be set.
*/
public function set($index, $item);
public function set($index, $value);
/**
* Returns the first item in this list.
......@@ -80,6 +88,27 @@ public function set($index, $item);
*/
public function first();
/**
* Appends a new item to the list.
*
* @param mixed $value
* The value of the new item.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The item that was appended.
*/
public function appendItem($value = NULL);
/**
* Removes the item at the specified position.
*
* @param int $index
* Index of the item to remove.
*
* @return $this
*/
public function removeItem($index);
/**
* Filters the items in the list using a custom callback.
*
......
......@@ -59,25 +59,23 @@ public function setValue($values, $notify = TRUE) {
$this->list = array();
}
else {
// Only arrays with numeric keys are supported.
if (!is_array($values)) {
throw new \InvalidArgumentException('Cannot set a list with a non-array value.');
}
// Clear the values of properties for which no value has been passed.
$this->list = array_intersect_key($this->list, $values);
// Set the values.
foreach ($values as $delta => $value) {
if (!is_numeric($delta)) {
throw new \InvalidArgumentException('Unable to set a value with a non-numeric delta in a list.');
}
elseif (!isset($this->list[$delta])) {
// Assign incoming values. Keys are renumbered to ensure 0-based
// sequential deltas. If possible, reuse existing items rather than
// creating new ones.
foreach (array_values($values) as $delta => $value) {
if (!isset($this->list[$delta])) {
$this->list[$delta] = $this->createItem($delta, $value);
}
else {
$this->list[$delta]->setValue($value, FALSE);
}
}
// Truncate extraneous pre-existing values.
$this->list = array_slice($this->list, 0, count($values));
}
// Notify the parent of any changes.
if ($notify && isset($this->parent)) {
......@@ -104,28 +102,65 @@ public function get($index) {
if (!is_numeric($index)) {
throw new \InvalidArgumentException('Unable to get a value with a non-numeric delta in a list.');
}
// Allow getting not yet existing items as well.
// @todo: Maybe add a public createItem() method in addition?
elseif (!isset($this->list[$index])) {
$this->list[$index] = $this->createItem($index);
// Automatically create the first item for computed fields.
if ($index == 0 && !isset($this->list[0]) && $this->definition->isComputed()) {
$this->list[0] = $this->createItem(0);
}
return $this->list[$index];
return isset($this->list[$index]) ? $this->list[$index] : NULL;
}
/**
* {@inheritdoc}
*/
public function set($index, $item) {
if (is_numeric($index)) {
// Support setting values via typed data objects.
if ($item instanceof TypedDataInterface) {
$item = $item->getValue();
}
$this->get($index)->setValue($item);
return $this;
public function set($index, $value) {
if (!is_numeric($index)) {
throw new \InvalidArgumentException('Unable to set a value with a non-numeric delta in a list.');
}
// Ensure indexes stay sequential. We allow assigning an item at an existing
// index, or at the next index available.
if ($index < 0 || $index > count($this->list)) {
throw new \InvalidArgumentException('Unable to set a value to a non-subsequent delta in a list.');
}
// Support setting values via typed data objects.
if ($value instanceof TypedDataInterface) {
$value = $value->getValue();
}
// If needed, create the item at the next position.
$item = isset($this->list[$index]) ? $this->list[$index] : $this->appendItem();
$item->setValue($value);
return $this;
}
/**
* {@inheritdoc}
*/
public function removeItem($index) {
if (isset($this->list) && array_key_exists($index, $this->list)) {
// Remove the item, and reassign deltas.
unset($this->list[$index]);
$this->rekey($index);
}
else {
throw new \InvalidArgumentException('Unable to set a value with a non-numeric delta in a list.');
throw new \InvalidArgumentException('Unable to remove item at non-existing index.');
}
return $this;
}
/**
* Renumbers the items in the list.
*
* @param int $from_index
* Optionally, the index at which to start the renumbering, if it is known
* that items before that can safely be skipped (for example, when removing
* an item at a given index).
*/
protected function rekey($from_index = 0) {
// Re-key the list to maintain consecutive indexes.
$this->list = array_values($this->list);
// Each item holds its own index as a "name", it needs to be updated
// according to the new list indexes.
for ($i = $from_index; $i < count($this->list); $i++) {
$this->list[$i]->setContext($i, $this);
}
}
......@@ -140,14 +175,15 @@ public function first() {
* Implements \ArrayAccess::offsetExists().
*/
public function offsetExists($offset) {
return array_key_exists($offset, $this->list) && $this->get($offset)->getValue() !== NULL;
// We do not want to throw exceptions here, so we do not use get().
return isset($this->list[$offset]);
}
/**
* Implements \ArrayAccess::offsetUnset().
*/
public function offsetUnset($offset) {
unset($this->list[$offset]);
$this->removeItem($offset);
}
/**
......@@ -157,6 +193,29 @@ public function offsetGet($offset) {
return $this->get($offset);
}
/**
* {@inheritdoc}
*/
public function offsetSet($offset, $value) {
if (!isset($offset)) {
// The [] operator has been used.
$this->appendItem($value);
}
else {
$this->set($offset, $value);
}
}
/**
* {@inheritdoc}
*/
public function appendItem($value = NULL) {
$offset = count($this->list);
$item = $this->createItem($offset, $value);
$this->list[$offset] = $item;
return $item;
}
/**
* Helper for creating a list item object.
*
......@@ -173,17 +232,6 @@ public function getItemDefinition() {
return $this->definition->getItemDefinition();
}
/**
* Implements \ArrayAccess::offsetSet().
*/
public function offsetSet($offset, $value) {
if (!isset($offset)) {
// The [] operator has been used so point at a new entry.
$offset = $this->list ? max(array_keys($this->list)) + 1 : 0;
}
$this->set($offset, $value);
}
/**
* Implements \IteratorAggregate::getIterator().
*/
......@@ -220,22 +268,19 @@ public function isEmpty() {
* {@inheritdoc}
*/
public function filter($callback) {
$removed = FALSE;
// Apply the filter, detecting if some items were actually removed.
$this->list = array_filter($this->list, function ($item) use ($callback, &$removed) {
if (call_user_func($callback, $item)) {
return TRUE;
}
else {
$removed = TRUE;
}
});
if ($removed) {
// Rekey the array using array_values().
$this->list = array_values($this->list);
// Manually update each item's delta.
foreach ($this->list as $delta => $item) {
$item->setContext($delta, $this);
if (isset($this->list)) {
$removed = FALSE;
// Apply the filter, detecting if some items were actually removed.
$this->list = array_filter($this->list, function ($item) use ($callback, &$removed) {
if (call_user_func($callback, $item)) {
return TRUE;
}
else {
$removed = TRUE;
}
});
if ($removed) {
$this->rekey();
}
}
return $this;
......
<?php
/**
* @file
* Contains \Drupal\comment\CommentFieldItemList.
*/
namespace Drupal\comment;
use Drupal\Core\Field\FieldItemList;
/**
* Defines a item list class for comment fields.
*/
class CommentFieldItemList extends FieldItemList {
/**
* {@inheritdoc}
*/
public function get($index) {
// The Field API only applies the "field default value" to newly created
// entities. In the specific case of the "comment status", though, we need
// this default value to be also applied for existing entities created
// before the comment field was added, which have no value stored for the
// field.
if ($index == 0 && empty($this->list)) {
$field_default_value = $this->getFieldDefinition()->getDefaultValue($this->getEntity());
return $this->appendItem($field_default_value[0]);
}
return parent::get($index);
}
/**
* {@inheritdoc}
*/
public function offsetExists($offset) {
// For consistency with what happens in get(), we force offsetExists() to
// be TRUE for delta 0.
if ($offset === 0) {
return TRUE;
}
return parent::offsetExists($offset);
}
}
......@@ -23,6 +23,7 @@
* id = "comment",
* label = @Translation("Comments"),
* description = @Translation("This field manages configuration and presentation of comments on an entity."),
* list_class = "\Drupal\comment\CommentFieldItemList",
* default_widget = "comment_default",
* default_formatter = "comment_default"
* )
......@@ -152,20 +153,6 @@ public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
return $element;
}
/**
* {@inheritdoc}
*/
public function __get($name) {
if ($name == 'status' && !isset($this->values[$name])) {
// Get default value from the field when no data saved in entity.
$field_default_values = $this->getFieldDefinition()->getDefaultValue($this->getEntity());
return $field_default_values[0]['status'];
}
else {
return parent::__get($name);
}
}
/**
* {@inheritdoc}
*/
......
......@@ -149,7 +149,7 @@ function testImageFieldSync() {
'alt' => $default_langcode . '_' . $fid . '_' . $this->randomMachineName(),
'title' => $default_langcode . '_' . $fid . '_' . $this->randomMachineName(),
);
$entity->{$this->fieldName}->get($delta)->setValue($item);
$entity->{$this->fieldName}[] = $item;
// Store the generated values keying them by fid for easier lookup.
$values[$default_langcode][$fid] = $item;
......@@ -175,7 +175,7 @@ function testImageFieldSync() {
'alt' => $langcode . '_' . $fid . '_' . $this->randomMachineName(),
'title' => $langcode . '_' . $fid . '_' . $this->randomMachineName(),
);
$translation->{$this->fieldName}->get($delta)->setValue($item);
$translation->{$this->fieldName}[] = $item;
// Again store the generated values keying them by fid for easier lookup.
$values[$langcode][$fid] = $item;
......
......@@ -384,7 +384,7 @@ function testDefaultValue() {
// Create a new node to check that datetime field default value is not set.
$new_node = entity_create('node', array('type' => 'date_content'));
$this->assertNull($new_node->get($field_name)->offsetGet(0)->value, 'Default value is not set');
$this->assertNull($new_node->get($field_name)->value, 'Default value is not set');
}
/**
......
......@@ -592,10 +592,12 @@ public function getCardinality() {
* {@inheritdoc}
*/
public function getOptionsProvider($property_name, FieldableEntityInterface $entity) {
// If the field item class implements the interface, proxy it through.
$item = $entity->get($this->getName())->first();
if ($item instanceof OptionsProviderInterface) {
return $item;
// If the field item class implements the interface, create an orphaned
// runtime item object, so that it can be used as the options provider
// without modifying the entity being worked on.
if (is_subclass_of($this->getFieldItemClass(), '\Drupal\Core\TypedData\OptionsProviderInterface')) {
$items = $entity->get($this->getName());
return \Drupal::typedDataManager()->getPropertyInstance($items, 0);
}
// @todo: Allow setting custom options provider, see
// https://www.drupal.org/node/2002138.
......
......@@ -275,14 +275,14 @@ protected function buildRenderArray(array $referenced_entities, $formatter, $for
// Create the entity that will have the entity reference field.
$referencing_entity = entity_create($this->entityType, array('name' => $this->randomMachineName()));
$delta = 0;
$items = $referencing_entity->get($this->fieldName);
// Assign the referenced entities.
foreach ($referenced_entities as $referenced_entity) {
$referencing_entity->{$this->fieldName}[$delta++]->entity = $referenced_entity;
$items[] = ['entity' => $referenced_entity];
}
// Build the renderable array for the entity reference field.
$items = $referencing_entity->get($this->fieldName);
// Build the renderable array for the field.
return $items->view(array('type' => $formatter, 'settings' => $formatter_options));
}
......
......@@ -72,11 +72,9 @@ public function testEntityCountAndHasData() {
// Create 12 entities to ensure that the purging works as expected.
for ($i=0; $i < 12; $i++) {
$entity = entity_create('entity_test');
$value = mt_rand(1,99);
$value2 = mt_rand(1,99);
$entity->field_int[0]->value = $value;
$entity->field_int[1]->value = $value2;
$entity->name->value = $this->randomMachineName();
$entity->field_int[] = mt_rand(1,99);
$entity->field_int[] = mt_rand(1,99);
$entity->name[] = $this->randomMachineName();
$entity->save();
}
......
......@@ -399,7 +399,7 @@ function testUpdate() {
// Fill in the entity with more values than $cardinality.
for ($i = 0; $i < 20; $i++) {
// We can not use $i here because 0 values are filtered out.
$entity->field_update[$i]->value = $i + 1;
$entity->field_update[] = $i + 1;
}
// Load back and assert there are $cardinality number of values.
$entity = $this->entitySaveReload($entity);
......
......@@ -52,7 +52,7 @@ function testCardinalityConstraint() {
$entity = $this->entity;
for ($delta = 0; $delta < $cardinality + 1; $delta++) {
$entity->{$this->fieldTestData->field_name}->get($delta)->set('value', 1);
$entity->{$this->fieldTestData->field_name}[] = array('value' => 1);
}
// Validate the field.
......@@ -85,7 +85,7 @@ function testFieldConstraints() {
$value = -1;
$expected_violations[$delta . '.value'][] = t('%name does not accept the value -1.', array('%name' => $this->fieldTestData->field->getLabel()));
}
$entity->{$this->fieldTestData->field_name}->get($delta)->set('value', $value);
$entity->{$this->fieldTestData->field_name}[] = $value;
}
// Validate the field.
......
......@@ -86,6 +86,7 @@ public function buildForm(array $form, FormStateInterface $form_state, FieldConf
$ids = (object) array('entity_type' => $this->field->entity_type, 'bundle' => $this->field->bundle, 'entity_id' => NULL);
$form['#entity'] = _field_create_entity_from_ids($ids);
$items = $form['#entity']->get($this->field->getName());
$item = $items->first() ?: $items->appendItem();
if (!empty($field_storage->locked)) {
$form['locked'] = array(
......@@ -140,7 +141,7 @@ public function buildForm(array $form, FormStateInterface $form_state, FieldConf
// Add field settings for the field type and a container for third party
// settings that modules can add to via hook_form_FORM_ID_alter().
$form['field']['settings'] = $items[0]->fieldSettingsForm($form, $form_state);
$form['field']['settings'] = $item->fieldSettingsForm($form, $form_state);
$form['field']['settings']['#weight'] = 10;
$form['field']['third_party_settings'] = array();
$form['field']['third_party_settings']['#weight'] = 11;
......
......@@ -108,7 +108,9 @@ public function buildForm(array $form, FormStateInterface $form_state, FieldConf
// FieldItem.
$ids = (object) array('entity_type' => $this->field->entity_type, 'bundle' => $this->field->bundle, 'entity_id' => NULL);
$entity = _field_create_entity_from_ids($ids);
$form['field_storage']['settings'] += $entity->get($field_storage->getName())->first()->storageSettingsForm($form, $form_state, $field_storage->hasData());
$items = $entity->get($field_storage->getName());
$item = $items->first() ?: $items->appendItem();
$form['field_storage']['settings'] += $item->storageSettingsForm($form, $form_state, $field_storage->hasData());
// Build the configurable field values.
$cardinality = $field_storage->getCardinality();
......
......@@ -139,6 +139,8 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f
// Add one more empty row for new uploads except when this is a programmed
// multiple form as it is not necessary.
if ($empty_single_allowed || $empty_multiple_allowed) {
// Create a new empty item.
$items->appendItem();
$element = array(
'#title' => $title,
'#description' => $description,
......
......@@ -177,20 +177,16 @@ public function denormalize($data, $class, $format = NULL, array $context = arra
// Iterate through remaining items in data array. These should all
// correspond to fields.
foreach ($data as $field_name => $field_data) {
$items = $entity->get($field_name);
// Remove any values that were set as a part of entity creation (e.g
// uuid). If this field is set to an empty array in the data, this will