Commit 536319a8 authored by catch's avatar catch

Issue #1757452 by amateescu, Xano, chx: Support config entities in entity reference fields.

parent 9c54978d
......@@ -610,4 +610,19 @@ public function getAllBundleInfo() {
return $this->bundleInfo;
}
/**
* Builds a list of entity type labels suitable for a Form API options list.
*
* @return array
* An array of entity type labels, keyed by entity type name.
*/
public function getEntityTypeLabels() {
$options = array();
foreach ($this->getDefinitions() as $entity_type => $definition) {
$options[$entity_type] = $definition['label'];
}
return $options;
}
}
......@@ -10,7 +10,6 @@
use Drupal\Core\TypedData\Annotation\DataType;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Entity\Field\FieldItemBase;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* Defines the 'entity_reference' entity field type.
......@@ -44,19 +43,32 @@ class EntityReferenceItem extends FieldItemBase {
* Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinitions().
*/
public function getPropertyDefinitions() {
$target_type = $this->definition['settings']['target_type'];
// Definitions vary by entity type and bundle, so key them accordingly.
$key = $this->definition['settings']['target_type'] . ':';
$key = $target_type . ':';
$key .= isset($this->definition['settings']['target_bundle']) ? $this->definition['settings']['target_bundle'] : '';
if (!isset(static::$propertyDefinitions[$key])) {
static::$propertyDefinitions[$key]['target_id'] = array(
// @todo: Lookup the entity type's ID data type and use it here.
'type' => 'integer',
'label' => t('Entity ID'),
'constraints' => array(
'Range' => array('min' => 0),
),
);
$target_type_info = \Drupal::entityManager()->getDefinition($target_type);
if (is_subclass_of($target_type_info['class'], '\Drupal\Core\Entity\ContentEntityInterface')) {
static::$propertyDefinitions[$key]['target_id'] = array(
// @todo: Lookup the entity type's ID data type and use it here.
// https://drupal.org/node/2107249
'type' => 'integer',
'label' => t('Entity ID'),
'constraints' => array(
'Range' => array('min' => 0),
),
);
}
else {
static::$propertyDefinitions[$key]['target_id'] = array(
'type' => 'string',
'label' => t('Entity ID'),
);
}
static::$propertyDefinitions[$key]['entity'] = array(
'type' => 'entity_reference',
'constraints' => array(
......
......@@ -7,9 +7,9 @@
namespace Drupal\entity_reference;
use Drupal\field\FieldInterface;
use Drupal\field\Plugin\Type\FieldType\ConfigEntityReferenceItemBase;
use Drupal\field\Plugin\Type\FieldType\ConfigFieldItemInterface;
use Drupal\field\FieldInterface;
/**
* Alternative plugin implementation of the 'entity_reference' field type.
......@@ -26,8 +26,11 @@ class ConfigurableEntityReferenceItem extends ConfigEntityReferenceItemBase impl
* {@inheritdoc}
*/
public static function schema(FieldInterface $field) {
$schema = array(
'columns' => array(
$target_type = $field->getFieldSetting('target_type');
$target_type_info = \Drupal::entityManager()->getDefinition($target_type);
if (is_subclass_of($target_type_info['class'], '\Drupal\Core\Entity\ContentEntityInterface')) {
$columns = array(
'target_id' => array(
'description' => 'The ID of the target entity.',
'type' => 'int',
......@@ -40,24 +43,25 @@ public static function schema(FieldInterface $field) {
'unsigned' => TRUE,
'not null' => FALSE,
),
),
);
}
else {
$columns = array(
'target_id' => array(
'description' => 'The ID of the target entity.',
'type' => 'varchar',
'length' => '255',
),
);
}
$schema = array(
'columns' => $columns,
'indexes' => array(
'target_id' => array('target_id'),
),
);
// Create a foreign key to the target entity type base type.
$entity_manager = \Drupal::service('entity.manager');
$target_type = $field->getFieldSetting('target_type');
if (is_subclass_of($entity_manager->getControllerClass($target_type, 'storage'), 'Drupal\Core\Entity\FieldableDatabaseStorageController')) {
$entity_info = $entity_manager->getDefinition($target_type);
$base_table = $entity_info['base_table'];
$schema['foreign keys'][$base_table] = array(
'table' => $base_table,
'columns' => array('target_id' => $entity_info['entity_keys']['id']),
);
}
return $schema;
}
......@@ -78,21 +82,10 @@ public function preSave() {
* {@inheritdoc}
*/
public function settingsForm(array $form, array &$form_state, $has_data) {
// Select the target entity type.
$entity_type_options = array();
foreach (\Drupal::entityManager()->getDefinitions() as $entity_type => $entity_info) {
// @todo As the database schema can currently only store numeric IDs of
// referenced entities and configuration entities have string IDs, prevent
// configuration entities from being referenced.
if (!is_subclass_of($entity_info['class'], '\Drupal\Core\Config\Entity\ConfigEntityInterface')) {
$entity_type_options[$entity_type] = $entity_info['label'];
}
}
$element['target_type'] = array(
'#type' => 'select',
'#title' => t('Type of item to reference'),
'#options' => $entity_type_options,
'#options' => \Drupal::entityManager()->getEntityTypeLabels(),
'#default_value' => $this->getFieldSetting('target_type'),
'#required' => TRUE,
'#disabled' => $has_data,
......
......@@ -102,50 +102,54 @@ public static function settingsForm(FieldDefinitionInterface $field_definition)
);
}
// @todo Use Entity::getPropertyDefinitions() when all entity types are
// converted to the new Field API.
$fields = drupal_map_assoc(drupal_schema_fields_sql($entity_info['base_table']));
foreach (field_info_instances($target_type) as $bundle_instances) {
foreach ($bundle_instances as $instance_name => $instance) {
foreach ($instance->getField()->getColumns() as $column_name => $column_info) {
$fields[$instance_name . '.' . $column_name] = t('@label (@column)', array('@label' => $instance->getFieldLabel(), '@column' => $column_name));
$target_type_info = \Drupal::entityManager()->getDefinition($target_type);
if (is_subclass_of($target_type_info['class'], '\Drupal\Core\Entity\ContentEntityInterface')) {
// @todo Use Entity::getPropertyDefinitions() when all entity types are
// converted to the new Field API.
$fields = drupal_map_assoc(drupal_schema_fields_sql($entity_info['base_table']));
foreach (field_info_instances($target_type) as $bundle_instances) {
foreach ($bundle_instances as $instance_name => $instance) {
foreach ($instance->getField()->getColumns() as $column_name => $column_info) {
$fields[$instance_name . '.' . $column_name] = t('@label (@column)', array('@label' => $instance->getFieldLabel(), '@column' => $column_name));
}
}
}
}
$form['sort']['field'] = array(
'#type' => 'select',
'#title' => t('Sort by'),
'#options' => array(
'_none' => t('- None -'),
) + $fields,
'#ajax' => TRUE,
'#limit_validation_errors' => array(),
'#default_value' => $selection_handler_settings['sort']['field'],
);
$form['sort']['settings'] = array(
'#type' => 'container',
'#attributes' => array('class' => array('entity_reference-settings')),
'#process' => array('_entity_reference_form_process_merge_parent'),
);
if ($selection_handler_settings['sort']['field'] != '_none') {
// Merge-in default values.
$selection_handler_settings['sort'] += array(
'direction' => 'ASC',
);
$form['sort']['settings']['direction'] = array(
$form['sort']['field'] = array(
'#type' => 'select',
'#title' => t('Sort direction'),
'#required' => TRUE,
'#title' => t('Sort by'),
'#options' => array(
'ASC' => t('Ascending'),
'DESC' => t('Descending'),
),
'#default_value' => $selection_handler_settings['sort']['direction'],
'_none' => t('- None -'),
) + $fields,
'#ajax' => TRUE,
'#limit_validation_errors' => array(),
'#default_value' => $selection_handler_settings['sort']['field'],
);
$form['sort']['settings'] = array(
'#type' => 'container',
'#attributes' => array('class' => array('entity_reference-settings')),
'#process' => array('_entity_reference_form_process_merge_parent'),
);
if ($selection_handler_settings['sort']['field'] != '_none') {
// Merge-in default values.
$selection_handler_settings['sort'] += array(
'direction' => 'ASC',
);
$form['sort']['settings']['direction'] = array(
'#type' => 'select',
'#title' => t('Sort direction'),
'#required' => TRUE,
'#options' => array(
'ASC' => t('Ascending'),
'DESC' => t('Descending'),
),
'#default_value' => $selection_handler_settings['sort']['direction'],
);
}
}
return $form;
......
......@@ -61,10 +61,15 @@ public function elementValidate($element, &$form_state, $form) {
$value = '';
if (!empty($element['#value'])) {
// Take "label (entity id)', match the id from parenthesis.
if (preg_match("/.+\((\d+)\)/", $element['#value'], $matches)) {
// @todo: Lookup the entity type's ID data type and use it here.
// https://drupal.org/node/2107249
if ($this->isContentReferenced() && preg_match("/.+\((\d+)\)/", $element['#value'], $matches)) {
$value = $matches[1];
}
else {
elseif (preg_match("/.+\(([\w.]+)\)/", $element['#value'], $matches)) {
$value = $matches[1];
}
if (!$value) {
// Try to get a match from the input string when the user didn't use the
// autocomplete but filled in a value manually.
$handler = \Drupal::service('plugin.manager.entity_reference.selection')->getSelectionHandler($this->fieldDefinition);
......
......@@ -193,4 +193,15 @@ protected function getSelectionHandlerSetting($setting_name) {
return isset($settings[$setting_name]) ? $settings[$setting_name] : NULL;
}
/**
* Checks whether a content entity is referenced.
*
* @return bool
*/
protected function isContentReferenced() {
$target_type = $this->getFieldSetting('target_type');
$target_type_info = \Drupal::entityManager()->getDefinition($target_type);
return is_subclass_of($target_type_info['class'], '\Drupal\Core\Entity\ContentEntityInterface');
}
}
......@@ -15,7 +15,7 @@
class EntityReferenceAdminTest extends WebTestBase {
public static function getInfo() {
return array(
'name' => 'Entity Reference UI',
'name' => 'Entity Reference admin UI',
'description' => 'Tests for the administrative UI.',
'group' => 'Entity Reference',
);
......@@ -75,6 +75,11 @@ public function testFieldAdminHandler() {
// Node should be selected by default.
$this->assertFieldByName('field[settings][target_type]', 'node');
// Check that all entity types can be referenced.
foreach (\Drupal::entityManager()->getDefinitions() as $entity_type => $entity_info) {
$this->assertFieldByXPath("//select[@name='field[settings][target_type]']/option[@value='" . $entity_type . "']");
}
// Second step: 'Instance settings' form.
$this->drupalPostForm(NULL, array(), t('Save field settings'));
......
<?php
/**
* @file
* Contains \Drupal\entity_reference\Tests\EntityReferenceIntegrationTest.
*/
namespace Drupal\entity_reference\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests various Entity reference UI components.
*/
class EntityReferenceIntegrationTest extends WebTestBase {
/**
* The entity type used in this test.
*
* @var string
*/
protected $entityType = 'entity_test';
/**
* The bundle used in this test.
*
* @var string
*/
protected $bundle = 'entity_test';
/**
* The name of the field used in this test.
*
* @var string
*/
protected $fieldName = 'field_test';
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('config_test', 'entity_test', 'entity_reference');
public static function getInfo() {
return array(
'name' => 'Entity reference components (widgets, formatters, etc.)',
'description' => 'Tests for various Entity reference components.',
'group' => 'Entity Reference',
);
}
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Create a test user.
$web_user = $this->drupalCreateUser(array('administer entity_test content'));
$this->drupalLogin($web_user);
}
/**
* Tests the autocomplete widget when targeting a config entity type.
*/
public function testConfigAutocompleteWidget() {
// Create an Entity reference field targeting a config entity type.
entity_reference_create_instance($this->entityType, $this->bundle, $this->fieldName, 'Field test', 'config_test');
// Add the field to the default form mode.
entity_get_form_display($this->entityType, $this->bundle, 'default')->setComponent($this->fieldName)->save();
// Create a test config entity.
$config_entity_id = $this->randomName();
$config_entity_label = $this->randomString();
$config_entity = entity_create('config_test', array('id' => $config_entity_id, 'label' => $config_entity_label));
$config_entity->save();
$entity_name = $this->randomName();
$edit = array(
'name' => $entity_name,
'user_id' => mt_rand(0, 128),
$this->fieldName . '[0][target_id]' => $config_entity_label . ' (' . $config_entity_id . ')',
);
$this->drupalPostForm($this->entityType . '/add', $edit, t('Save'));
$entity = current(entity_load_multiple_by_properties($this->entityType, array('name' => $entity_name)));
$this->assertTrue($entity, format_string('%entity_type: Entity found in the database.', array('%entity_type' => $this->entityType)));
$this->assertEqual($entity->{$this->fieldName}->target_id, $config_entity_id);
$this->assertEqual($entity->{$this->fieldName}->entity->id(), $config_entity_id);
$this->assertEqual($entity->{$this->fieldName}->entity->label(), $config_entity_label);
}
}
......@@ -7,7 +7,6 @@
namespace Drupal\entity_reference\Tests;
use Drupal\Core\Entity\FieldableDatabaseStorageController;
use Drupal\Core\Entity\Field\FieldItemListInterface;
use Drupal\Core\Entity\Field\FieldItemInterface;
use Drupal\Core\Language\Language;
......@@ -25,6 +24,20 @@ class EntityReferenceItemTest extends FieldUnitTestBase {
*/
public static $modules = array('entity_reference', 'taxonomy', 'options');
/**
* The taxonomy vocabulary to test with.
*
* @var \Drupal\taxonomy\VocabularyInterface
*/
protected $vocabulary;
/**
* The taxonomy term to test with.
*
* @var \Drupal\taxonomy\TermInterface
*/
protected $term;
public static function getInfo() {
return array(
'name' => 'Entity Reference field item',
......@@ -42,48 +55,49 @@ public function setUp() {
$this->installSchema('taxonomy', 'taxonomy_term_data');
$this->installSchema('taxonomy', 'taxonomy_term_hierarchy');
$vocabulary = entity_create('taxonomy_vocabulary', array(
$this->vocabulary = entity_create('taxonomy_vocabulary', array(
'name' => $this->randomName(),
'vid' => drupal_strtolower($this->randomName()),
'langcode' => Language::LANGCODE_NOT_SPECIFIED,
));
$vocabulary->save();
$this->vocabulary->save();
$this->term = entity_create('taxonomy_term', array(
'name' => $this->randomName(),
'vid' => $vocabulary->id(),
'vid' => $this->vocabulary->id(),
'langcode' => Language::LANGCODE_NOT_SPECIFIED,
));
$this->term->save();
// Use the util to create an instance.
entity_reference_create_instance('entity_test', 'entity_test', 'field_test_taxonomy', 'Test entity reference', 'taxonomy_term');
entity_reference_create_instance('entity_test', 'entity_test', 'field_test_taxonomy_term', 'Test content entity reference', 'taxonomy_term');
entity_reference_create_instance('entity_test', 'entity_test', 'field_test_taxonomy_vocabulary', 'Test config entity reference', 'taxonomy_vocabulary');
}
/**
* Tests using entity fields of the entity reference field type.
* Tests the entity reference field type for referencing content entities.
*/
public function testEntityReferenceItem() {
public function testContentEntityReferenceItem() {
$tid = $this->term->id();
// Just being able to create the entity like this verifies a lot of code.
$entity = entity_create('entity_test', array());
$entity->field_test_taxonomy->target_id = $tid;
$entity->field_test_taxonomy_term->target_id = $tid;
$entity->name->value = $this->randomName();
$entity->save();
$entity = entity_load('entity_test', $entity->id());
$this->assertTrue($entity->field_test_taxonomy instanceof FieldItemListInterface, 'Field implements interface.');
$this->assertTrue($entity->field_test_taxonomy[0] instanceof FieldItemInterface, 'Field item implements interface.');
$this->assertEqual($entity->field_test_taxonomy->target_id, $tid);
$this->assertEqual($entity->field_test_taxonomy->entity->name->value, $this->term->name->value);
$this->assertEqual($entity->field_test_taxonomy->entity->id(), $tid);
$this->assertEqual($entity->field_test_taxonomy->entity->uuid(), $this->term->uuid());
$this->assertTrue($entity->field_test_taxonomy_term instanceof FieldItemListInterface, 'Field implements interface.');
$this->assertTrue($entity->field_test_taxonomy_term[0] instanceof FieldItemInterface, 'Field item implements interface.');
$this->assertEqual($entity->field_test_taxonomy_term->target_id, $tid);
$this->assertEqual($entity->field_test_taxonomy_term->entity->name->value, $this->term->name->value);
$this->assertEqual($entity->field_test_taxonomy_term->entity->id(), $tid);
$this->assertEqual($entity->field_test_taxonomy_term->entity->uuid(), $this->term->uuid());
// Change the name of the term via the reference.
$new_name = $this->randomName();
$entity->field_test_taxonomy->entity->name = $new_name;
$entity->field_test_taxonomy->entity->save();
$entity->field_test_taxonomy_term->entity->name = $new_name;
$entity->field_test_taxonomy_term->entity->save();
// Verify it is the correct name.
$term = entity_load('taxonomy_term', $tid);
$this->assertEqual($term->name->value, $new_name);
......@@ -96,9 +110,9 @@ public function testEntityReferenceItem() {
));
$term2->save();
$entity->field_test_taxonomy->target_id = $term2->id();
$this->assertEqual($entity->field_test_taxonomy->entity->id(), $term2->id());
$this->assertEqual($entity->field_test_taxonomy->entity->name->value, $term2->name->value);
$entity->field_test_taxonomy_term->target_id = $term2->id();
$this->assertEqual($entity->field_test_taxonomy_term->entity->id(), $term2->id());
$this->assertEqual($entity->field_test_taxonomy_term->entity->name->value, $term2->name->value);
// Delete terms so we have nothing to reference and try again
$term->delete();
......@@ -108,30 +122,50 @@ public function testEntityReferenceItem() {
}
/**
* Tests foreign key support.
* Tests the entity reference field type for referencing config entities.
*/
public function testEntityReferenceFieldSchema() {
$field = field_info_field('entity_test', 'field_test_taxonomy');
$foreign_key_column_name = 'target_id';
// Grab the SQL schema and verify that the 'foreign keys' are present.
$schemas = FieldableDatabaseStorageController::_fieldSqlSchema($field);
$schema = $schemas[FieldableDatabaseStorageController::_fieldTableName($field)];
$this->assertEqual(count($schema['foreign keys']), 1, 'There is 1 foreign key in the schema.');
$foreign_key = reset($schema['foreign keys']);
$foreign_key_column = FieldableDatabaseStorageController::_fieldColumnName($field, $foreign_key_column_name);
$this->assertEqual($foreign_key['table'], 'taxonomy_term_data', 'Foreign key table name preserved in the schema.');
$this->assertEqual($foreign_key['columns'][$foreign_key_column], 'tid', 'Foreign key column name preserved in the schema.');
// Create a field that references a config entity type and check that no
// foreign key is present.
$field_name = 'field_test_vocabulary';
entity_reference_create_instance('entity_test', 'entity_test', $field_name, 'Test vocabulary reference', 'taxonomy_vocabulary');
$field = field_info_field('entity_test', $field_name);
$schemas = FieldableDatabaseStorageController::_fieldSqlSchema($field);
$schema = $schemas[FieldableDatabaseStorageController::_fieldTableName($field)];
$this->assertFalse(isset($schema['foreign keys']), 'There is no foreign key in the schema.');
public function testConfigEntityReferenceItem() {
$referenced_entity_id = $this->vocabulary->id();
// Just being able to create the entity like this verifies a lot of code.
$entity = entity_create('entity_test', array());
$entity->field_test_taxonomy_vocabulary->target_id = $referenced_entity_id;
$entity->name->value = $this->randomName();
$entity->save();
$entity = entity_load('entity_test', $entity->id());
$this->assertTrue($entity->field_test_taxonomy_vocabulary instanceof FieldItemListInterface, 'Field implements interface.');
$this->assertTrue($entity->field_test_taxonomy_vocabulary[0] instanceof FieldItemInterface, 'Field item implements interface.');
$this->assertEqual($entity->field_test_taxonomy_vocabulary->target_id, $referenced_entity_id);
$this->assertEqual($entity->field_test_taxonomy_vocabulary->entity->name, $this->vocabulary->name);
$this->assertEqual($entity->field_test_taxonomy_vocabulary->entity->id(), $referenced_entity_id);
$this->assertEqual($entity->field_test_taxonomy_vocabulary->entity->uuid(), $this->vocabulary->uuid());
// Change the name of the term via the reference.
$new_name = $this->randomName();
$entity->field_test_taxonomy_vocabulary->entity->name = $new_name;
$entity->field_test_taxonomy_vocabulary->entity->save();
// Verify it is the correct name.
$term = entity_load('taxonomy_vocabulary', $referenced_entity_id);
$this->assertEqual($term->name, $new_name);
// Make sure the computed term reflects updates to the term id.
$vocabulary2 = entity_create('taxonomy_vocabulary', array(
'name' => $this->randomName(),
'vid' => drupal_strtolower($this->randomName()),
'langcode' => Language::LANGCODE_NOT_SPECIFIED,
));
$vocabulary2->save();
$entity->field_test_taxonomy_vocabulary->target_id = $vocabulary2->id();
$this->assertEqual($entity->field_test_taxonomy_vocabulary->entity->id(), $vocabulary2->id());
$this->assertEqual($entity->field_test_taxonomy_vocabulary->entity->name, $vocabulary2->name);
// Delete terms so we have nothing to reference and try again
$this->vocabulary->delete();
$vocabulary2->delete();
$entity = entity_create('entity_test', array('name' => $this->randomName()));
$entity->save();
}
}
......@@ -8,9 +8,9 @@
namespace Drupal\field\Plugin\Type\FieldType;
use Drupal\Core\Entity\Plugin\field\field_type\EntityReferenceItem;
use Drupal\field\Plugin\Type\FieldType\ConfigFieldItemInterface;
use Drupal\field\FieldInterface;
use Drupal\field\FieldInstanceInterface;
use Drupal\field\FieldInterface;
use Drupal\field\Plugin\Type\FieldType\ConfigFieldItemInterface;
/**
* A common base class for configurable entity reference fields.
......@@ -36,22 +36,29 @@ class ConfigEntityReferenceItemBase extends EntityReferenceItem implements Confi
* {@inheritdoc}
*/
public function getPropertyDefinitions() {
$target_type = $this->definition['settings']['target_type'];
// Definitions vary by entity type and bundle, so key them accordingly.
$key = $this->definition['settings']['target_type'] . ':';
$key = $target_type . ':';
$key .= isset($this->definition['settings']['target_bundle']) ? $this->definition['settings']['target_bundle'] : '';
if (!isset(static::$propertyDefinitions[$key])) {
// Call the parent to define the target_id and entity properties.
parent::getPropertyDefinitions();
static::$propertyDefinitions[$key]['revision_id'] = array(
// @todo: Lookup the entity type's ID data type and use it here.
'type' => 'integer',
'label' => t('Revision ID'),
'constraints' => array(
'Range' => array('min' => 0),
),
);
// Only add the revision ID property if the target entity type supports