Issue #2370703 by amateescu, yched: Fixed ER's "autocreate" feature is mostly broken (and untested).

8.0.x
Alex Pott 2014-11-17 12:05:26 +00:00
parent b733559eb7
commit c18ba6aad5
10 changed files with 204 additions and 137 deletions

View File

@ -23,27 +23,29 @@ class EntityReferenceFieldItemList extends FieldItemList implements EntityRefere
return array();
}
// Get a list of items having non-empty target ids.
$list = array_filter($this->list, function($item) {
return (bool) $item->target_id;
});
$ids = array();
foreach ($list as $delta => $item) {
$ids[$delta] = $item->target_id;
}
if (empty($ids)) {
return array();
}
$target_type = $this->getFieldDefinition()->getSetting('target_type');
$entities = \Drupal::entityManager()->getStorage($target_type)->loadMultiple($ids);
$target_entities = array();
foreach ($ids as $delta => $target_id) {
if (isset($entities[$target_id])) {
$target_entities[$delta] = $entities[$target_id];
// Collect the IDs of existing entities to load, and directly grab the
// "autocreate" entities that are already populated in $item->entity.
$target_entities = $ids = array();
foreach ($this->list as $delta => $item) {
if ($item->target_id !== NULL) {
$ids[$delta] = $item->target_id;
}
elseif ($item->hasNewEntity()) {
$target_entities[$delta] = $item->entity;
}
}
// Load and add the existing entities.
if ($ids) {
$target_type = $this->getFieldDefinition()->getSetting('target_type');
$entities = \Drupal::entityManager()->getStorage($target_type)->loadMultiple($ids);
foreach ($ids as $delta => $target_id) {
if (isset($entities[$target_id])) {
$target_entities[$delta] = $entities[$target_id];
}
}
// Ensure the returned array is ordered by deltas.
ksort($target_entities);
}
return $target_entities;

View File

@ -8,7 +8,7 @@
namespace Drupal\Core\Field\Plugin\Field\FieldType;
use Drupal\Core\Config\Entity\ConfigEntityType;
use Drupal\Core\Entity\Entity;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\TypedData\EntityDataDefinition;
use Drupal\Core\Field\FieldDefinitionInterface;
@ -166,7 +166,7 @@ class EntityReferenceItem extends FieldItemBase {
// If there is an unsaved entity, return it as part of the field item values
// to ensure idempotency of getValue() / setValue().
if ($this->hasUnsavedEntity()) {
if ($this->hasNewEntity()) {
$values['entity'] = $this->entity;
}
return $values;
@ -191,11 +191,10 @@ class EntityReferenceItem extends FieldItemBase {
*/
public function isEmpty() {
// Avoid loading the entity by first checking the 'target_id'.
$target_id = $this->target_id;
if ($target_id !== NULL) {
if ($this->target_id !== NULL) {
return FALSE;
}
if ($this->entity && $this->entity instanceof Entity) {
if ($this->entity && $this->entity instanceof EntityInterface) {
return FALSE;
}
return TRUE;
@ -205,12 +204,12 @@ class EntityReferenceItem extends FieldItemBase {
* {@inheritdoc}
*/
public function preSave() {
if ($this->hasUnsavedEntity()) {
if ($this->hasNewEntity()) {
$this->entity->save();
}
// Handle the case where an unsaved entity was directly set using the public
// 'entity' property and then saved before this entity. In this case
// ::hasUnsavedEntity() will return FALSE but $this->target_id will still be
// ::hasNewEntity() will return FALSE but $this->target_id will still be
// empty.
if (empty($this->target_id) && $this->entity) {
$this->target_id = $this->entity->id();
@ -239,7 +238,7 @@ class EntityReferenceItem extends FieldItemBase {
* @return bool
* TRUE if the item holds an unsaved entity.
*/
public function hasUnsavedEntity() {
public function hasNewEntity() {
return $this->target_id === NULL && ($entity = $this->entity) && $entity->isNew();
}

View File

@ -30,6 +30,14 @@ abstract class EntityReferenceFormatterBase extends FormatterBase {
$parent_entity_langcode = $items->getEntity()->language()->getId();
foreach ($items as $delta => $item) {
// The "originalEntity" property is assigned in self::prepareView() and
// its absence means that the referenced entity was neither found in the
// persistent storage nor is it a new entity (e.g. from "autocreate").
if (!isset($item->originalEntity)) {
$item->access = FALSE;
continue;
}
if ($item->originalEntity instanceof TranslatableInterface && $item->originalEntity->hasTranslation($parent_entity_langcode)) {
$entity = $item->originalEntity->getTranslation($parent_entity_langcode);
}
@ -51,47 +59,38 @@ abstract class EntityReferenceFormatterBase extends FormatterBase {
/**
* {@inheritdoc}
*
* Mark the accessible IDs a user can see. We do not unset unaccessible
* values, as other may want to act on those values, even if they can
* not be accessed.
* Loads the entities referenced in that field across all the entities being
* viewed, and places them in a custom item property for getEntitiesToView().
*/
public function prepareView(array $entities_items) {
$target_ids = array();
// Collect every possible entity attached to any of the entities.
// Load the existing (non-autocreate) entities. For performance, we want to
// use a single "multiple entity load" to load all the entities for the
// multiple "entity reference item lists" that are being displayed. We thus
// cannot use
// \Drupal\Core\Field\EntityReferenceFieldItemList::referencedEntities().
$ids = array();
foreach ($entities_items as $items) {
foreach ($items as $item) {
if (!empty($item->target_id)) {
$target_ids[] = $item->target_id;
if ($item->target_id !== NULL) {
$ids[] = $item->target_id;
}
}
}
$target_type = $this->getFieldSetting('target_type');
$target_entities = array();
if ($target_ids) {
$target_entities = entity_load_multiple($target_type, $target_ids);
if ($ids) {
$target_type = $this->getFieldSetting('target_type');
$target_entities = \Drupal::entityManager()->getStorage($target_type)->loadMultiple($ids);
}
// Iterate through the fieldable entities again to attach the loaded data.
// For each item, place the referenced entity where getEntitiesToView()
// reads it.
foreach ($entities_items as $items) {
$rekey = FALSE;
foreach ($items as $item) {
if ($item->target_id !== 0 && !isset($target_entities[$item->target_id])) {
// The entity no longer exists, so empty the item.
$item->setValue(NULL);
$rekey = TRUE;
continue;
if (isset($target_entities[$item->target_id])) {
$item->originalEntity = $target_entities[$item->target_id];
}
elseif ($item->hasNewEntity()) {
$item->originalEntity = $item->entity;
}
$item->originalEntity = $target_entities[$item->target_id];
}
// Re-key the items array if needed.
if ($rekey) {
$items->filterEmptyItems();
}
}
}

View File

@ -66,7 +66,7 @@ class EntityReferenceLabelFormatter extends EntityReferenceFormatterBase {
$label = $entity->label();
// If the link is to be displayed and the entity has a uri, display a
// link.
if ($this->getSetting('link') && $uri = $entity->urlInfo()) {
if ($this->getSetting('link') && !$entity->isNew() && $uri = $entity->urlInfo()) {
$elements[$delta] = [
'#type' => 'link',
'#title' => $label,

View File

@ -7,7 +7,6 @@
namespace Drupal\entity_reference\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
/**
@ -43,18 +42,6 @@ class AutocompleteWidget extends AutocompleteWidgetBase {
) + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
protected function getEntityIds(FieldItemListInterface $items, $delta) {
// The autocomplete widget outputs one entity label per form element.
if (isset($items[$delta])) {
return array($items[$delta]->target_id);
}
return array();
}
/**
* {@inheritdoc}
*/

View File

@ -8,6 +8,7 @@
namespace Drupal\entity_reference\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
@ -116,47 +117,32 @@ abstract class AutocompleteWidgetBase extends WidgetBase {
/**
* Gets the entity labels.
*/
protected function getLabels(FieldItemListInterface $items, $delta) {
protected function getLabels(EntityReferenceFieldItemListInterface $items, $delta) {
if ($items->isEmpty()) {
return array();
}
$entity_labels = array();
$handles_multiple_values = $this->handlesMultipleValues();
foreach ($items->referencedEntities() as $referenced_delta => $referenced_entity) {
// The autocomplete widget outputs one entity label per form element.
if (!$handles_multiple_values && $referenced_delta != $delta) {
continue;
}
// Load those entities and loop through them to extract their labels.
$entities = entity_load_multiple($this->getFieldSetting('target_type'), $this->getEntityIds($items, $delta));
$key = $referenced_entity->label();
// Take into account "autocreate" items.
if (!$referenced_entity->isNew()) {
$key .= ' (' . $referenced_entity->id() . ')';
}
foreach ($entities as $entity_id => $entity_item) {
$label = $entity_item->label();
$key = "$label ($entity_id)";
// Labels containing commas or quotes must be wrapped in quotes.
$key = Tags::encode($key);
$entity_labels[] = $key;
$entity_labels[] = Tags::encode($key);
}
return $entity_labels;
}
/**
* Builds an array of entity IDs for which to get the entity labels.
*
* @param \Drupal\Core\Field\FieldItemListInterface $items
* Array of default values for this field.
* @param int $delta
* The order of a field item in the array of subelements (0, 1, 2, etc).
*
* @return array
* An array of entity IDs.
*/
protected function getEntityIds(FieldItemListInterface $items, $delta) {
$entity_ids = array();
foreach ($items as $item) {
$entity_ids[] = $item->target_id;
}
return $entity_ids;
}
/**
* Creates a new entity from a label entered in the autocomplete input.
*

View File

@ -127,6 +127,12 @@ class EntityReferenceFieldTest extends EntityUnitTestBase {
$reference_field[5] = $reference_field[0];
$target_entities[5] = $target_entities[0];
// Create a new target entity that is not saved, thus testing the
// "autocreate" feature.
$target_entity_unsaved = entity_create($this->referencedEntityType, array('type' => $this->bundle, 'name' => $this->randomString()));
$reference_field[6]['entity'] = $target_entity_unsaved;
$target_entities[6] = $target_entity_unsaved;
// Set the field value.
$entity->{$this->fieldName}->setValue($reference_field);
@ -138,9 +144,16 @@ class EntityReferenceFieldTest extends EntityUnitTestBase {
// - Non-existent entities must not be retrieved in target entities result.
foreach ($target_entities as $delta => $target_entity) {
if (!empty($target_entity)) {
// There must be an entity in the loaded set having the same id for the
// same delta.
$this->assertEqual($target_entity->id(), $entities[$delta]->id());
if (!$target_entity->isNew()) {
// There must be an entity in the loaded set having the same id for
// the same delta.
$this->assertEqual($target_entity->id(), $entities[$delta]->id());
}
else {
// For entities that were not yet saved, there must an entity in the
// loaded set having the same label for the same delta.
$this->assertEqual($target_entity->label(), $entities[$delta]->label());
}
}
else {
// A non-existent or NULL entity target id must not return any item in

View File

@ -8,6 +8,7 @@
namespace Drupal\entity_reference\Tests;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\filter\Entity\FilterFormat;
use Drupal\system\Tests\Entity\EntityUnitTestBase;
@ -44,7 +45,15 @@ class EntityReferenceFormatterTest extends EntityUnitTestBase {
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $referencedEntity = NULL;
protected $referencedEntity;
/**
* The entity that is not yet saved to its persistent storage to be referenced
* in this test.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $unsavedReferencedEntity;
/**
* Modules to install.
@ -56,7 +65,11 @@ class EntityReferenceFormatterTest extends EntityUnitTestBase {
protected function setUp() {
parent::setUp();
entity_reference_create_field($this->entityType, $this->bundle, $this->fieldName, 'Field test', $this->entityType);
// The label formatter rendering generates links, so build the router.
$this->installSchema('system', 'router');
$this->container->get('router.builder')->rebuild();
entity_reference_create_field($this->entityType, $this->bundle, $this->fieldName, 'Field test', $this->entityType, 'default', array(), FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
// Set up a field, so that the entity that'll be referenced bubbles up a
// cache tag when rendering it entirely.
@ -91,6 +104,13 @@ class EntityReferenceFormatterTest extends EntityUnitTestBase {
'format' => 'full_html',
);
$this->referencedEntity->save();
// Create another entity to be referenced but do not save it.
$this->unsavedReferencedEntity = entity_create($this->entityType, array('name' => $this->randomMachineName()));
$this->unsavedReferencedEntity->body = array(
'value' => '<p>Hello, unsaved world!</p>',
'format' => 'full_html',
);
}
/**
@ -130,21 +150,11 @@ class EntityReferenceFormatterTest extends EntityUnitTestBase {
*/
public function testIdFormatter() {
$formatter = 'entity_reference_entity_id';
$field_name = $this->fieldName;
// Create the entity that will have the entity reference field.
$referencing_entity = entity_create($this->entityType, array('name' => $this->randomMachineName()));
$referencing_entity->save();
$referencing_entity->{$field_name}->entity = $this->referencedEntity;
$referencing_entity->{$field_name}->access = TRUE;
// Build the renderable array for the entity reference field.
$items = $referencing_entity->get($field_name);
$build = $items->view(array('type' => $formatter));
$this->assertEqual($build[0]['#markup'], $this->referencedEntity->id(), format_string('The markup returned by the @formatter formatter is correct.', array('@formatter' => $formatter)));
$this->assertEqual($build[0]['#cache']['tags'], $this->referencedEntity->getCacheTags(), format_string('The @formatter formatter has the expected cache tags.', array('@formatter' => $formatter)));
$build = $this->buildRenderArray([$this->referencedEntity, $this->unsavedReferencedEntity], $formatter);
$this->assertEqual($build[0]['#markup'], $this->referencedEntity->id(), sprintf('The markup returned by the %s formatter is correct for an item with a saved entity.', $formatter));
$this->assertEqual($build[0]['#cache']['tags'], $this->referencedEntity->getCacheTags(), sprintf('The %s formatter has the expected cache tags.', $formatter));
$this->assertTrue(!isset($build[1]), sprintf('The markup returned by the %s formatter is correct for an item with a unsaved entity.', $formatter));
}
/**
@ -152,25 +162,16 @@ class EntityReferenceFormatterTest extends EntityUnitTestBase {
*/
public function testEntityFormatter() {
$formatter = 'entity_reference_entity_view';
$field_name = $this->fieldName;
$build = $this->buildRenderArray([$this->referencedEntity, $this->unsavedReferencedEntity], $formatter);
// Create the entity that will have the entity reference field.
$referencing_entity = entity_create($this->entityType, array('name' => $this->randomMachineName()));
$referencing_entity->save();
$referencing_entity->{$field_name}->entity = $this->referencedEntity;
$referencing_entity->{$field_name}->access = TRUE;
// Build the renderable array for the entity reference field.
$items = $referencing_entity->get($field_name);
$build = $items->view(array('type' => $formatter));
$expected_rendered_name_field = '<div class="field field-entity-test--name field-name-name field-type-string field-label-hidden">
// Test the first field item.
$expected_rendered_name_field_1 = '<div class="field field-entity-test--name field-name-name field-type-string field-label-hidden">
<div class="field-items">
<div class="field-item">' . $this->referencedEntity->label() . '</div>
</div>
</div>
';
$expected_rendered_body_field = '<div class="field field-entity-test--body field-name-body field-type-text field-label-above">
$expected_rendered_body_field_1 = '<div class="field field-entity-test--body field-name-body field-type-text field-label-above">
<div class="field-label">Body</div>
<div class="field-items">
<div class="field-item"><p>Hello, world!</p></div>
@ -178,13 +179,84 @@ class EntityReferenceFormatterTest extends EntityUnitTestBase {
</div>
';
drupal_render($build[0]);
$this->assertEqual($build[0]['#markup'], 'default | ' . $this->referencedEntity->label() . $expected_rendered_name_field . $expected_rendered_body_field, format_string('The markup returned by the @formatter formatter is correct.', array('@formatter' => $formatter)));
$this->assertEqual($build[0]['#markup'], 'default | ' . $this->referencedEntity->label() . $expected_rendered_name_field_1 . $expected_rendered_body_field_1, sprintf('The markup returned by the %s formatter is correct for an item with a saved entity.', $formatter));
$expected_cache_tags = Cache::mergeTags(
\Drupal::entityManager()->getViewBuilder($this->entityType)->getCacheTags(),
$this->referencedEntity->getCacheTags(),
FilterFormat::load('full_html')->getCacheTags()
);
$this->assertEqual($build[0]['#cache']['tags'], $expected_cache_tags, format_string('The @formatter formatter has the expected cache tags.', array('@formatter' => $formatter)));
// Test the second field item.
drupal_render($build[1]);
$this->assertEqual($build[1]['#markup'], $this->unsavedReferencedEntity->label(), sprintf('The markup returned by the %s formatter is correct for an item with a unsaved entity.', $formatter));
}
/**
* Tests the label formatter.
*/
public function testLabelFormatter() {
$formatter = 'entity_reference_label';
// The 'link' settings is TRUE by default.
$build = $this->buildRenderArray([$this->referencedEntity, $this->unsavedReferencedEntity], $formatter);
$expected_item_1 = array(
'#type' => 'link',
'#title' => $this->referencedEntity->label(),
'#url' => $this->referencedEntity->urlInfo(),
'#options' => $this->referencedEntity->urlInfo()->getOptions(),
'#cache' => array(
'tags' => $this->referencedEntity->getCacheTags(),
),
);
$this->assertEqual(drupal_render($build[0]), drupal_render($expected_item_1), sprintf('The markup returned by the %s formatter is correct for an item with a saved entity.', $formatter));
// The second referenced entity is "autocreated", therefore not saved and
// lacking any URL info.
$expected_item_2 = array(
'#markup' => $this->unsavedReferencedEntity->label(),
'#cache' => array(
'tags' => $this->unsavedReferencedEntity->getCacheTags(),
),
);
$this->assertEqual($build[1], $expected_item_2, sprintf('The markup returned by the %s formatter is correct for an item with a unsaved entity.', $formatter));
// Test with the 'link' setting set to FALSE.
$build = $this->buildRenderArray([$this->referencedEntity, $this->unsavedReferencedEntity], $formatter, array('link' => FALSE));
$this->assertEqual($build[0]['#markup'], $this->referencedEntity->label(), sprintf('The markup returned by the %s formatter is correct for an item with a saved entity.', $formatter));
$this->assertEqual($build[1]['#markup'], $this->unsavedReferencedEntity->label(), sprintf('The markup returned by the %s formatter is correct for an item with a unsaved entity.', $formatter));
}
/**
* Sets field values and returns a render array as built by
* \Drupal\Core\Field\FieldItemListInterface::view().
*
* @param \Drupal\Core\Entity\EntityInterface[] $referenced_entities
* An array of entity objects that will be referenced.
* @param string $formatter
* The formatted plugin that will be used for building the render array.
* @param array $formatter_options
* Settings specific to the formatter. Defaults to the formatter's default
* settings.
*
* @return array
* A render array.
*/
protected function buildRenderArray(array $referenced_entities, $formatter, $formatter_options = array()) {
// Create the entity that will have the entity reference field.
$referencing_entity = entity_create($this->entityType, array('name' => $this->randomMachineName()));
$delta = 0;
foreach ($referenced_entities as $referenced_entity) {
$referencing_entity->{$this->fieldName}[$delta]->entity = $referenced_entity;
$referencing_entity->{$this->fieldName}[$delta++]->access = TRUE;
}
// Build the renderable array for the entity reference field.
$items = $referencing_entity->get($this->fieldName);
return $items->view(array('type' => $formatter, 'settings' => $formatter_options));
}
}

View File

@ -85,7 +85,11 @@ class EntityReferenceIntegrationTest extends WebTestBase {
// Try to post the form again with no modification and check if the field
// values remain the same.
$entity = current(entity_load_multiple_by_properties($this->entityType, array('name' => $entity_name)));
$this->drupalPostForm($this->entityType . '/manage/' . $entity->id(), array(), t('Save'));
$this->drupalGet($this->entityType . '/manage/' . $entity->id());
$this->assertFieldByName($this->fieldName . '[0][target_id]', $referenced_entities[0]->label() . ' (' . $referenced_entities[0]->id() . ')');
$this->assertFieldByName($this->fieldName . '[1][target_id]', $referenced_entities[1]->label() . ' (' . $referenced_entities[1]->id() . ')');
$this->drupalPostForm(NULL, array(), t('Save'));
$this->assertFieldValues($entity_name, $referenced_entities);
// Test the 'entity_reference_autocomplete_tags' widget.
@ -107,7 +111,10 @@ class EntityReferenceIntegrationTest extends WebTestBase {
// Try to post the form again with no modification and check if the field
// values remain the same.
$entity = current(entity_load_multiple_by_properties($this->entityType, array('name' => $entity_name)));
$this->drupalPostForm($this->entityType . '/manage/' . $entity->id(), array(), t('Save'));
$this->drupalGet($this->entityType . '/manage/' . $entity->id());
$this->assertFieldByName($this->fieldName . '[target_id]', $target_id . ' (' . $referenced_entities[1]->id() . ')');
$this->drupalPostForm(NULL, array(), t('Save'));
$this->assertFieldValues($entity_name, $referenced_entities);
// Test all the other widgets supported by the entity reference field.

View File

@ -37,7 +37,9 @@ abstract class TaxonomyFormatterBase extends FormatterBase {
$term = $translated_term;
}
}
$terms[$term->id()] = $term;
if (!$term->isNew()) {
$terms[$term->id()] = $term;
}
}
}
if ($terms) {
@ -53,7 +55,7 @@ abstract class TaxonomyFormatterBase extends FormatterBase {
$item->entity = $terms[$item->target_id];
}
// Terms to be created are not in $terms, but are still legitimate.
elseif ($item->hasUnsavedEntity()) {
elseif ($item->hasNewEntity()) {
// Leave the item in place.
}
// Otherwise, unset the instance value, since the term does not exist.