Issue #2232477 by plach, yched, tstoeckler, amateescu: Fatal when adding new fields with NOT NULL constraints in a base table that contains existing entities

8.0.x
Alex Pott 2015-01-02 21:39:42 +00:00
parent fb4f25aac7
commit 057b0cabaf
54 changed files with 755 additions and 335 deletions

View File

@ -55,6 +55,17 @@ class EntityReference extends DataReferenceBase {
return $this->definition->getTargetDefinition();
}
/**
* Checks whether the target entity has not been saved yet.
*
* @return bool
* TRUE if the entity is new, FALSE otherwise.
*/
public function isTargetNew() {
// If only an ID is given, the reference cannot be a new entity.
return !isset($this->id) && isset($this->target) && $this->target->getValue()->isNew();
}
/**
* {@inheritdoc}
*/

View File

@ -1019,6 +1019,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
if ($schema['fields'][$key]['type'] == 'int') {
$schema['fields'][$key]['type'] = 'serial';
}
$schema['fields'][$key]['not null'] = TRUE;
unset($schema['fields'][$key]['default']);
}
@ -1386,27 +1387,36 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
$field_name = $storage_definition->getName();
$field_description = $storage_definition->getDescription();
$base_table = $this->storage->getBaseTable();
// A shared table contains rows for entities where the field is empty
// (since other fields stored in the same table might not be empty), thus
// the only columns that can be 'not null' are those for required
// properties of required fields. However, even those would break in the
// case where a new field is added to a table that contains existing rows.
// For now, we only hardcode 'not null' to a couple "entity keys", in order
// to keep their indexes optimized.
// @todo Revisit once we have support for 'initial' in
// https://www.drupal.org/node/2346019.
$not_null_keys = $this->entityType->getKeys();
// Label fields are not necessarily required.
unset($not_null_keys['label']);
// Because entity ID and revision ID are both serial fields in the base and
// revision table respectively, the revision ID is not known yet, when
// inserting data into the base table. Instead the revision ID in the base
// table is updated after the data has been inserted into the revision
// table. For this reason the revision ID field cannot be marked as NOT
// NULL.
if ($table_name == $base_table) {
unset($not_null_keys['revision']);
}
foreach ($column_mapping as $field_column_name => $schema_field_name) {
$column_schema = $field_schema['columns'][$field_column_name];
$schema['fields'][$schema_field_name] = $column_schema;
$schema['fields'][$schema_field_name]['description'] = $field_description;
// Only entity keys are required.
$keys = $this->entityType->getKeys();
// The label is an entity key, but label fields are not necessarily
// required.
// Because entity ID and revision ID are both serial fields in the base
// and revision table respectively, the revision ID is not known yet, when
// inserting data into the base table. Instead the revision ID in the base
// table is updated after the data has been inserted into the revision
// table. For this reason the revision ID field cannot be marked as NOT
// NULL.
unset($keys['label'], $keys['revision']);
// Key fields may not be NULL.
if (in_array($field_name, $keys)) {
$schema['fields'][$schema_field_name]['not null'] = TRUE;
}
$schema['fields'][$schema_field_name]['not null'] = in_array($field_name, $not_null_keys);
}
if (!empty($field_schema['indexes'])) {
@ -1596,6 +1606,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
// Check that the schema does not include forbidden column names.
$schema = $storage_definition->getSchema();
$properties = $storage_definition->getPropertyDefinitions();
$table_mapping = $this->storage->getTableMapping();
if (array_intersect(array_keys($schema['columns']), $table_mapping->getReservedColumns())) {
throw new FieldException(format_string('Illegal field column names on @field_name', array('@field_name' => $storage_definition->getName())));
@ -1605,6 +1616,10 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
foreach ($schema['columns'] as $column_name => $attributes) {
$real_name = $table_mapping->getFieldColumnName($storage_definition, $column_name);
$data_schema['fields'][$real_name] = $attributes;
// A dedicated table only contain rows for actual field values, and no
// rows for entities where the field is empty. Thus, we can safely
// enforce 'not null' on the columns for the field's required properties.
$data_schema['fields'][$real_name]['not null'] = $properties[$column_name]->isRequired();
}
// Add indexes.

View File

@ -27,12 +27,12 @@ class EntityReferenceFieldItemList extends FieldItemList implements EntityRefere
// "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()) {
if ($item->hasNewEntity()) {
$target_entities[$delta] = $item->entity;
}
elseif ($item->target_id !== NULL) {
$ids[$delta] = $item->target_id;
}
}
// Load and add the existing entities.

View File

@ -159,13 +159,21 @@ interface FieldDefinitionInterface extends ListDataDefinitionInterface {
public function getDisplayOptions($display_context);
/**
* Returns whether at least one non-empty item is required for this field.
* Returns whether the field can be empty.
*
* Currently, required-ness is only enforced at the Form API level in entity
* edit forms, not during direct API saves.
* If a field is required, an entity needs to have at least a valid,
* non-empty item in that field's FieldItemList in order to pass validation.
*
* An item is considered empty if its isEmpty() method returns TRUE.
* Typically, that is if at least one of its required properties is empty.
*
* @return bool
* TRUE if the field is required.
*
* @see \Drupal\Core\TypedData\Plugin\DataType\ItemList::isEmpty()
* @see \Drupal\Core\Field\FieldItemInterface::isEmpty()
* @see \Drupal\Core\TypedData\DataDefinitionInterface:isRequired()
* @see \Drupal\Core\TypedData\TypedDataManager::getDefaultConstraints()
*/
public function isRequired();

View File

@ -28,6 +28,9 @@ interface FieldItemInterface extends ComplexDataInterface {
/**
* Defines field item properties.
*
* Properties that are required to constitute a valid, non-empty item should
* be denoted with \Drupal\Core\TypedData\DataDefinition::setRequired().
*
* @return \Drupal\Core\TypedData\DataDefinitionInterface[]
* An array of property definitions of contained properties, keyed by
* property name.
@ -67,10 +70,12 @@ interface FieldItemInterface extends ComplexDataInterface {
* following key/value pairs:
* - columns: An array of Schema API column specifications, keyed by column
* name. The columns need to be a subset of the properties defined in
* propertyDefinitions(). It is recommended to avoid having the column
* definitions depend on field settings when possible. No assumptions
* should be made on how storage engines internally use the original
* column name to structure their storage.
* propertyDefinitions(). The 'not null' property is ignored if present,
* as it is determined automatically by the storage controller depending
* on the table layout and the property definitions. It is recommended to
* avoid having the column definitions depend on field settings when
* possible. No assumptions should be made on how storage engines
* internally use the original column name to structure their storage.
* - unique keys: (optional) An array of Schema API unique key definitions.
* Only columns that appear in the 'columns' array are allowed.
* - indexes: (optional) An array of Schema API index definitions. Only

View File

@ -43,7 +43,8 @@ class BooleanItem extends FieldItemBase implements OptionsProviderInterface {
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('boolean')
->setLabel(t('Boolean value'));
->setLabel(t('Boolean value'))
->setRequired(TRUE);
return $properties;
}
@ -57,7 +58,6 @@ class BooleanItem extends FieldItemBase implements OptionsProviderInterface {
'value' => array(
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
),
),
);

View File

@ -41,7 +41,8 @@ class DecimalItem extends NumericItemBase {
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('string')
->setLabel(t('Decimal value'));
->setLabel(t('Decimal value'))
->setRequired(TRUE);
return $properties;
}
@ -56,7 +57,6 @@ class DecimalItem extends NumericItemBase {
'type' => 'numeric',
'precision' => $field_definition->getSetting('precision'),
'scale' => $field_definition->getSetting('scale'),
'not null' => FALSE
)
),
);

View File

@ -32,7 +32,8 @@ class EmailItem extends FieldItemBase {
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('email')
->setLabel(t('E-mail'));
->setLabel(t('E-mail'))
->setRequired(TRUE);
return $properties;
}
@ -46,7 +47,6 @@ class EmailItem extends FieldItemBase {
'value' => array(
'type' => 'varchar',
'length' => Email::EMAIL_MAX_LENGTH,
'not null' => FALSE,
),
),
);

View File

@ -11,8 +11,8 @@ use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\TypedData\EntityDataDefinition;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\DataReferenceDefinition;
@ -36,6 +36,13 @@ use Drupal\Core\TypedData\DataReferenceDefinition;
*/
class EntityReferenceItem extends FieldItemBase {
/**
* Marker value to identify a newly created entity.
*
* @var int
*/
protected static $NEW_ENTITY_MARKER = -1;
/**
* {@inheritdoc}
*/
@ -73,7 +80,9 @@ class EntityReferenceItem extends FieldItemBase {
$target_id_definition = DataDefinition::create('string')
->setLabel(t('@label ID', array($target_type_info->getLabel())));
}
$target_id_definition->setRequired(TRUE);
$properties['target_id'] = $target_id_definition;
$properties['entity'] = DataReferenceDefinition::create('entity')
->setLabel($target_type_info->getLabel())
->setDescription(t('The referenced entity'))
@ -109,7 +118,6 @@ class EntityReferenceItem extends FieldItemBase {
'description' => 'The ID of the target entity.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
);
}
@ -155,8 +163,12 @@ class EntityReferenceItem extends FieldItemBase {
$this->onChange('entity', FALSE);
}
elseif (isset($values['target_id']) && isset($values['entity'])) {
// If both properties are passed, verify the passed values match.
if ($this->get('entity')->getTargetIdentifier() != $values['target_id']) {
// If both properties are passed, verify the passed values match. The
// only exception we allow is when we have a new entity: in this case
// its actual id and target_id will be different, due to the new entity
// marker.
$entity_id = $this->get('entity')->getTargetIdentifier();
if ($entity_id != $values['target_id'] && ($values['target_id'] != static::$NEW_ENTITY_MARKER || !$this->entity->isNew())) {
throw new \InvalidArgumentException('The target id and entity passed to the entity reference item do not match.');
}
}
@ -187,11 +199,13 @@ class EntityReferenceItem extends FieldItemBase {
*/
public function onChange($property_name, $notify = TRUE) {
// Make sure that the target ID and the target property stay in sync.
if ($property_name == 'target_id') {
$this->writePropertyValue('entity', $this->target_id);
if ($property_name == 'entity') {
$property = $this->get('entity');
$target_id = $property->isTargetNew() ? static::$NEW_ENTITY_MARKER : $property->getTargetIdentifier();
$this->writePropertyValue('target_id', $target_id);
}
elseif ($property_name == 'entity') {
$this->writePropertyValue('target_id', $this->get('entity')->getTargetIdentifier());
elseif ($property_name == 'target_id' && $this->target_id != static::$NEW_ENTITY_MARKER) {
$this->writePropertyValue('entity', $this->target_id);
}
parent::onChange($property_name, $notify);
}
@ -215,13 +229,12 @@ class EntityReferenceItem extends FieldItemBase {
*/
public function preSave() {
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
// ::hasNewEntity() will return FALSE but $this->target_id will still be
// empty.
if (empty($this->target_id) && $this->entity) {
// Save the entity if it has not already been saved by some other code.
if ($this->entity->isNew()) {
$this->entity->save();
}
// Make sure the parent knows we are updating this property so it can
// react properly.
$this->target_id = $this->entity->id();
}
}
@ -249,7 +262,7 @@ class EntityReferenceItem extends FieldItemBase {
* TRUE if the item holds an unsaved entity.
*/
public function hasNewEntity() {
return $this->target_id === NULL && ($entity = $this->entity) && $entity->isNew();
return $this->target_id === static::$NEW_ENTITY_MARKER;
}
/**

View File

@ -29,7 +29,8 @@ class FloatItem extends NumericItemBase {
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('float')
->setLabel(t('Float'));
->setLabel(t('Float'))
->setRequired(TRUE);
return $properties;
}
@ -42,7 +43,6 @@ class FloatItem extends NumericItemBase {
'columns' => array(
'value' => array(
'type' => 'float',
'not null' => FALSE,
),
),
);

View File

@ -53,7 +53,8 @@ class IntegerItem extends NumericItemBase {
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('integer')
->setLabel(t('Integer value'));
->setLabel(t('Integer value'))
->setRequired(TRUE);
return $properties;
}
@ -92,7 +93,6 @@ class IntegerItem extends NumericItemBase {
'columns' => array(
'value' => array(
'type' => 'int',
'not null' => FALSE,
// Expose the 'unsigned' setting in the field item schema.
'unsigned' => $field_definition->getSetting('unsigned'),
// Expose the 'size' setting in the field item schema. For instance,

View File

@ -37,7 +37,8 @@ class LanguageItem extends FieldItemBase {
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('string')
->setLabel(t('Language code'));
->setLabel(t('Language code'))
->setRequired(TRUE);
$properties['language'] = DataReferenceDefinition::create('language')
->setLabel(t('Language object'))
@ -58,7 +59,6 @@ class LanguageItem extends FieldItemBase {
'value' => array(
'type' => 'varchar',
'length' => 12,
'not null' => FALSE,
),
),
);

View File

@ -43,7 +43,6 @@ class StringItem extends StringItemBase {
'value' => array(
'type' => 'varchar',
'length' => (int) $field_definition->getSetting('max_length'),
'not null' => FALSE,
'binary' => $field_definition->getSetting('case_sensitive'),
),
),

View File

@ -34,7 +34,8 @@ abstract class StringItemBase extends FieldItemBase {
// early t() calls by using the TranslationWrapper.
$properties['value'] = DataDefinition::create('string')
->setLabel(new TranslationWrapper('Text value'))
->setSetting('case_sensitive', $field_definition->getSetting('case_sensitive'));
->setSetting('case_sensitive', $field_definition->getSetting('case_sensitive'))
->setRequired(TRUE);
return $properties;
}

View File

@ -40,7 +40,8 @@ class TimestampItem extends FieldItemBase {
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('timestamp')
->setLabel(t('Timestamp value'));
->setLabel(t('Timestamp value'))
->setRequired(TRUE);
return $properties;
}
@ -52,7 +53,6 @@ class TimestampItem extends FieldItemBase {
'columns' => array(
'value' => array(
'type' => 'int',
'not null' => FALSE,
),
),
);

View File

@ -42,7 +42,8 @@ class UriItem extends StringItem {
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('uri')
->setLabel(t('URI value'))
->setSetting('case_sensitive', $field_definition->getSetting('case_sensitive'));
->setSetting('case_sensitive', $field_definition->getSetting('case_sensitive'))
->setRequired(TRUE);
return $properties;
}
@ -56,7 +57,6 @@ class UriItem extends StringItem {
'value' => array(
'type' => 'varchar',
'length' => (int) $field_definition->getSetting('max_length'),
'not null' => TRUE,
'binary' => $field_definition->getSetting('case_sensitive'),
),
),

View File

@ -61,4 +61,5 @@ abstract class DataReferenceBase extends TypedData implements DataReferenceInter
public function getString() {
return (string) $this->getType() . ':' . $this->getTargetIdentifier();
}
}

View File

@ -55,7 +55,8 @@ class CommentItem extends FieldItemBase implements CommentItemInterface {
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['status'] = DataDefinition::create('integer')
->setLabel(t('Comment status'));
->setLabel(t('Comment status'))
->setRequired(TRUE);
$properties['cid'] = DataDefinition::create('integer')
->setLabel(t('Last comment ID'));
@ -87,7 +88,6 @@ class CommentItem extends FieldItemBase implements CommentItemInterface {
'status' => array(
'description' => 'Whether comments are allowed on this entity: 0 = no, 1 = closed (read only), 2 = open (read/write).',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),

View File

@ -51,7 +51,8 @@ class DateTimeItem extends FieldItemBase {
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('datetime_iso8601')
->setLabel(t('Date value'));
->setLabel(t('Date value'))
->setRequired(TRUE);
$properties['date'] = DataDefinition::create('any')
->setLabel(t('Computed date'))
@ -73,7 +74,6 @@ class DateTimeItem extends FieldItemBase {
'description' => 'The date value.',
'type' => 'varchar',
'length' => 20,
'not null' => FALSE,
),
),
'indexes' => array(

View File

@ -8,6 +8,7 @@
namespace Drupal\entity_reference\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Form\FormStateInterface;
/**
@ -73,10 +74,7 @@ class AutocompleteTagsWidget extends AutocompleteWidgetBase {
elseif ($auto_create && (count($this->getSelectionHandlerSetting('target_bundles')) == 1 || count($bundles) == 1)) {
// Auto-create item. See
// \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::presave().
$value[] = array(
'target_id' => NULL,
'entity' => $this->createNewEntity($input, $element['#autocreate_uid']),
);
$value[] = array('entity' => $this->createNewEntity($input, $element['#autocreate_uid']));
}
}
};

View File

@ -7,6 +7,7 @@
namespace Drupal\entity_reference\Plugin\Field\FieldWidget;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Form\FormStateInterface;
/**
@ -71,7 +72,6 @@ class AutocompleteWidget extends AutocompleteWidgetBase {
// Auto-create item. See
// \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::presave().
$value = array(
'target_id' => NULL,
'entity' => $this->createNewEntity($element['#value'], $element['#autocreate_uid']),
// Keep the weight property.
'_weight' => $element['#weight'],

View File

@ -1,209 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\entity_reference\Tests\EntityReferenceFieldTest.
*/
namespace Drupal\entity_reference\Tests;
use Drupal\config\Tests\SchemaCheckTestTrait;
use Drupal\field\Entity\FieldConfig;
use Drupal\system\Tests\Entity\EntityUnitTestBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Tests for the entity reference field.
*
* @group entity_reference
*/
class EntityReferenceFieldTest extends EntityUnitTestBase {
use SchemaCheckTestTrait;
/**
* The entity type used in this test.
*
* @var string
*/
protected $entityType = 'entity_test';
/**
* The entity type that is being referenced.
*
* @var string
*/
protected $referencedEntityType = 'entity_test_rev';
/**
* 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 install.
*
* @var array
*/
public static $modules = array('entity_reference');
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test_rev');
// Create a field.
entity_reference_create_field(
$this->entityType,
$this->bundle,
$this->fieldName,
'Field test',
$this->referencedEntityType,
'default',
array('target_bundles' => array($this->bundle)),
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
);
}
/**
* Tests reference field validation.
*/
public function testEntityReferenceFieldValidation() {
// Test a valid reference.
$referenced_entity = entity_create($this->referencedEntityType, array('type' => $this->bundle));
$referenced_entity->save();
$entity = entity_create($this->entityType, array('type' => $this->bundle));
$entity->{$this->fieldName}->target_id = $referenced_entity->id();
$violations = $entity->{$this->fieldName}->validate();
$this->assertEqual($violations->count(), 0, 'Validation passes.');
// Test an invalid reference.
$entity->{$this->fieldName}->target_id = 9999;
$violations = $entity->{$this->fieldName}->validate();
$this->assertEqual($violations->count(), 1, 'Validation throws a violation.');
$this->assertEqual($violations[0]->getMessage(), t('The referenced entity (%type: %id) does not exist.', array('%type' => $this->referencedEntityType, '%id' => 9999)));
// @todo Implement a test case for invalid bundle references after
// https://drupal.org/node/2064191 is fixed
}
/**
* Tests the multiple target entities loader.
*/
public function testReferencedEntitiesMultipleLoad() {
// Create the parent entity.
$entity = entity_create($this->entityType, array('type' => $this->bundle));
// Create three target entities and attach them to parent field.
$target_entities = array();
$reference_field = array();
for ($i = 0; $i < 3; $i++) {
$target_entity = entity_create($this->referencedEntityType, array('type' => $this->bundle));
$target_entity->save();
$target_entities[] = $target_entity;
$reference_field[]['target_id'] = $target_entity->id();
}
// Also attach a non-existent entity and a NULL target id.
$reference_field[3]['target_id'] = 99999;
$target_entities[3] = NULL;
$reference_field[4]['target_id'] = NULL;
$target_entities[4] = NULL;
// Attach the first created target entity as the sixth item ($delta == 5) of
// the parent entity field. We want to test the case when the same target
// entity is referenced twice (or more times) in the same entity reference
// field.
$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);
// Load the target entities using EntityReferenceField::referencedEntities().
$entities = $entity->{$this->fieldName}->referencedEntities();
// Test returned entities:
// - Deltas must be preserved.
// - Non-existent entities must not be retrieved in target entities result.
foreach ($target_entities as $delta => $target_entity) {
if (!empty($target_entity)) {
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
// the target entities set.
$this->assertFalse(isset($loaded_entities[$delta]));
}
}
}
/**
* Tests referencing entities with string IDs.
*/
public function testReferencedEntitiesStringId() {
$field_name = 'entity_reference_string_id';
$this->installEntitySchema('entity_test_string_id');
entity_reference_create_field(
$this->entityType,
$this->bundle,
$field_name,
'Field test',
'entity_test_string_id',
'default',
array('target_bundles' => array($this->bundle)),
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
);
// Create the parent entity.
$entity = entity_create($this->entityType, array('type' => $this->bundle));
// Create the default target entity.
$target_entity = entity_create('entity_test_string_id', array('id' => $this->randomString(), 'type' => $this->bundle));
$target_entity->save();
// Set the field value.
$entity->{$field_name}->setValue(array(array('target_id' => $target_entity->id())));
// Load the target entities using EntityReferenceField::referencedEntities().
$entities = $entity->{$field_name}->referencedEntities();
$this->assertEqual($entities[0]->id(), $target_entity->id());
// Test that a string ID works as a default value and the field's config
// schema is correct.
$field = FieldConfig::loadByName($this->entityType, $this->bundle, $field_name);
$field->setDefaultValue($target_entity->id());
$field->save();
$this->assertConfigSchema(\Drupal::service('config.typed'), 'field.field.' . $field->id(), $field->toArray());
// Test that the default value works.
$entity = entity_create($this->entityType, array('type' => $this->bundle));
$entities = $entity->{$field_name}->referencedEntities();
$this->assertEqual($entities[0]->id(), $target_entity->id());
}
}

View File

@ -6,6 +6,24 @@
*/
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Implements hook_entity_base_field_info().
*/
function entity_reference_test_entity_base_field_info(EntityTypeInterface $entity_type) {
$fields = array();
if ($entity_type->id() === 'entity_test') {
$fields['user_role'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('User role'))
->setDescription(t('The role of the associated user.'))
->setSetting('target_type', 'user_role')
->setSetting('handler', 'default');
}
return $fields;
}
/**
* Implements hook_entity_base_field_info_alter().

View File

@ -169,7 +169,6 @@ class NumberFieldTest extends WebTestBase {
'columns' => array(
'value' => array(
'type' => 'int',
'not null' => FALSE,
'unsigned' => '',
'size' => 'normal'
),

View File

@ -83,7 +83,6 @@ class TestItemTest extends FieldUnitTestBase {
'value' => array(
'type' => 'int',
'size' => 'medium',
'not null' => FALSE,
),
),
'unique keys' => array(),

View File

@ -24,14 +24,4 @@ use Drupal\Core\TypedData\DataDefinition;
*/
class HiddenTestItem extends TestItem {
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('integer')
->setLabel(t('Test integer value'));
return $properties;
}
}

View File

@ -50,7 +50,8 @@ class TestItem extends FieldItemBase {
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('integer')
->setLabel(t('Test integer value'));
->setLabel(t('Test integer value'))
->setRequired(TRUE);
return $properties;
}
@ -64,7 +65,6 @@ class TestItem extends FieldItemBase {
'value' => array(
'type' => 'int',
'size' => 'medium',
'not null' => FALSE,
),
),
'indexes' => array(

View File

@ -265,7 +265,8 @@ class File extends ContentEntityBase implements FileInterface {
$fields['status'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Status'))
->setDescription(t('The status of the file, temporary (FALSE) and permanent (TRUE).'));
->setDescription(t('The status of the file, temporary (FALSE) and permanent (TRUE).'))
->setDefaultValue(FALSE);
$fields['created'] = BaseFieldDefinition::create('created')
->setLabel(t('Created'))

View File

@ -64,7 +64,6 @@ class FileItem extends EntityReferenceItem {
'target_id' => array(
'description' => 'The ID of the file entity.',
'type' => 'int',
'not null' => TRUE,
'unsigned' => TRUE,
),
'display' => array(
@ -72,13 +71,11 @@ class FileItem extends EntityReferenceItem {
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 1,
),
'description' => array(
'description' => 'A description of the file.',
'type' => 'text',
'not null' => FALSE,
),
),
'indexes' => array(

View File

@ -96,20 +96,17 @@ class ImageItem extends FileItem {
'target_id' => array(
'description' => 'The ID of the file entity.',
'type' => 'int',
'not null' => TRUE,
'unsigned' => TRUE,
),
'alt' => array(
'description' => "Alternative image text, for the image's 'alt' attribute.",
'type' => 'varchar',
'length' => 512,
'not null' => FALSE,
),
'title' => array(
'description' => "Image title text, for the image's 'title' attribute.",
'type' => 'varchar',
'length' => 1024,
'not null' => FALSE,
),
'width' => array(
'description' => 'The width of the image in pixels.',

View File

@ -31,6 +31,8 @@ class EntityDefaultLanguageTest extends KernelTestBase {
public function setUp() {
parent::setUp();
$this->installEntitySchema('user');
// Activate Spanish language, so there are two languages activated.
$language = $this->container->get('entity.manager')->getStorage('configurable_language')->create(array(
'id' => 'es',

View File

@ -72,32 +72,27 @@ class LinkItem extends FieldItemBase implements LinkItemInterface {
'description' => 'The URL of the link.',
'type' => 'varchar',
'length' => 2048,
'not null' => FALSE,
),
'title' => array(
'description' => 'The link text.',
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
),
'route_name' => array(
'description' => 'The machine name of a defined Route this link represents.',
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
),
'route_parameters' => array(
'description' => 'Serialized array of route parameters of the link.',
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
'serialize' => TRUE,
),
'options' => array(
'description' => 'Serialized array of options for the link.',
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
'serialize' => TRUE,
),
),

View File

@ -452,6 +452,7 @@ class Node extends ContentEntityBase implements NodeInterface {
->setDescription(t('A boolean indicating whether the node should be displayed at the top of lists in which it appears.'))
->setRevisionable(TRUE)
->setTranslatable(TRUE)
->setDefaultValue(FALSE)
->setDisplayOptions('form', array(
'type' => 'boolean_checkbox',
'settings' => array(

View File

@ -28,7 +28,8 @@ class ListFloatItem extends ListItemBase {
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('float')
->setLabel(t('Float value'));
->setLabel(t('Float value'))
->setRequired(TRUE);
return $properties;
}
@ -41,7 +42,6 @@ class ListFloatItem extends ListItemBase {
'columns' => array(
'value' => array(
'type' => 'float',
'not null' => FALSE,
),
),
'indexes' => array(

View File

@ -28,7 +28,8 @@ class ListIntegerItem extends ListItemBase {
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('integer')
->setLabel(t('Integer value'));
->setLabel(t('Integer value'))
->setRequired(TRUE);
return $properties;
}
@ -41,7 +42,6 @@ class ListIntegerItem extends ListItemBase {
'columns' => array(
'value' => array(
'type' => 'int',
'not null' => FALSE,
),
),
'indexes' => array(

View File

@ -30,7 +30,8 @@ class ListStringItem extends ListItemBase {
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('string')
->setLabel(t('Text value'))
->addConstraint('Length', array('max' => 255));
->addConstraint('Length', array('max' => 255))
->setRequired(TRUE);
return $properties;
}
@ -44,7 +45,6 @@ class ListStringItem extends ListItemBase {
'value' => array(
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
),
),
'indexes' => array(

View File

@ -23,7 +23,8 @@ class ShortcutPathItem extends StringItem {
$properties['value'] = DataDefinition::create('string')
->setLabel(t('String value'))
->setComputed(TRUE)
->setClass('\Drupal\shortcut\ShortcutPathValue');
->setClass('\Drupal\shortcut\ShortcutPathValue')
->setRequired(TRUE);
return $properties;
}

View File

@ -29,6 +29,7 @@ class BundleConstraintValidatorTest extends KernelTestBase {
protected function setUp() {
parent::setUp();
$this->installEntitySchema('user');
$this->typedData = $this->container->get('typed_data_manager');
}

View File

@ -7,11 +7,13 @@
namespace Drupal\system\Tests\Entity;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeEvents;
use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldStorageDefinitionEvents;
use Drupal\Core\Language\LanguageInterface;
use Drupal\entity_test\FieldStorageDefinition;
/**
@ -236,14 +238,16 @@ class EntityDefinitionUpdateTest extends EntityUnitTestBase {
public function testBaseFieldCreateDeleteWithExistingEntities() {
// Save an entity.
$name = $this->randomString();
$entity = $this->entityManager->getStorage('entity_test_update')->create(array('name' => $name));
$storage = $this->entityManager->getStorage('entity_test_update');
$entity = $storage->create(array('name' => $name));
$entity->save();
// Add a base field and run the update. Ensure the base field's column is
// created and the prior saved entity data is still there.
$this->addBaseField();
$this->entityDefinitionUpdateManager->applyUpdates();
$this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.');
$schema_handler = $this->database->schema();
$this->assertTrue($schema_handler->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.');
$entity = $this->entityManager->getStorage('entity_test_update')->load($entity->id());
$this->assertIdentical($entity->name->value, $name, 'Entity data preserved during field creation.');
@ -251,9 +255,33 @@ class EntityDefinitionUpdateTest extends EntityUnitTestBase {
// is deleted and the prior saved entity data is still there.
$this->removeBaseField();
$this->entityDefinitionUpdateManager->applyUpdates();
$this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Column deleted from shared table for new_base_field.');
$this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field'), 'Column deleted from shared table for new_base_field.');
$entity = $this->entityManager->getStorage('entity_test_update')->load($entity->id());
$this->assertIdentical($entity->name->value, $name, 'Entity data preserved during field deletion.');
// Add a base field with a required property and run the update. Ensure
// 'not null' is not applied and thus no exception is thrown.
$this->addBaseField('shape_required');
$this->entityDefinitionUpdateManager->applyUpdates();
$assert = $schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && $schema_handler->fieldExists('entity_test_update', 'new_base_field__color');
$this->assertTrue($assert, 'Columns created in shared table for new_base_field.');
// Recreate the field after emptying the base table and check that its
// columns are not 'not null'.
// @todo Revisit this test when allowing for required storage field
// definitions. See https://www.drupal.org/node/2390495.
$entity->delete();
$this->removeBaseField();
$this->entityDefinitionUpdateManager->applyUpdates();
$assert = !$schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && !$schema_handler->fieldExists('entity_test_update', 'new_base_field__color');
$this->assert($assert, 'Columns removed from the shared table for new_base_field.');
$this->addBaseField('shape_required');
$this->entityDefinitionUpdateManager->applyUpdates();
$assert = $schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && $schema_handler->fieldExists('entity_test_update', 'new_base_field__color');
$this->assertTrue($assert, 'Columns created again in shared table for new_base_field.');
$entity = $storage->create(array('name' => $name));
$entity->save();
$this->pass('The new_base_field columns are still nullable');
}
/**
@ -267,14 +295,16 @@ class EntityDefinitionUpdateTest extends EntityUnitTestBase {
public function testBundleFieldCreateDeleteWithExistingEntities() {
// Save an entity.
$name = $this->randomString();
$entity = $this->entityManager->getStorage('entity_test_update')->create(array('name' => $name));
$storage = $this->entityManager->getStorage('entity_test_update');
$entity = $storage->create(array('name' => $name));
$entity->save();
// Add a bundle field and run the update. Ensure the bundle field's table
// is created and the prior saved entity data is still there.
$this->addBundleField();
$this->entityDefinitionUpdateManager->applyUpdates();
$this->assertTrue($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.');
$schema_handler = $this->database->schema();
$this->assertTrue($schema_handler->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.');
$entity = $this->entityManager->getStorage('entity_test_update')->load($entity->id());
$this->assertIdentical($entity->name->value, $name, 'Entity data preserved during field creation.');
@ -282,9 +312,40 @@ class EntityDefinitionUpdateTest extends EntityUnitTestBase {
// table is deleted and the prior saved entity data is still there.
$this->removeBundleField();
$this->entityDefinitionUpdateManager->applyUpdates();
$this->assertFalse($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.');
$this->assertFalse($schema_handler->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.');
$entity = $this->entityManager->getStorage('entity_test_update')->load($entity->id());
$this->assertIdentical($entity->name->value, $name, 'Entity data preserved during field deletion.');
// Test that required columns are created as 'not null'.
$this->addBundleField('shape_required');
$this->entityDefinitionUpdateManager->applyUpdates();
$message = 'The new_bundle_field_shape column is not nullable.';
$values = array(
'bundle' => $entity->bundle(),
'deleted'=> 0,
'entity_id' => $entity->id(),
'revision_id' => $entity->id(),
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'delta' => 0,
'new_bundle_field_color' => $this->randomString(),
);
try {
// Try to insert a record without providing a value for the 'not null'
// column. This should fail.
$this->database->insert('entity_test_update__new_bundle_field')
->fields($values)
->execute();
$this->fail($message);
}
catch (DatabaseExceptionWrapper $e) {
// Now provide a value for the 'not null' column. This is expected to
// succeed.
$values['new_bundle_field_shape'] = $this->randomString();
$this->database->insert('entity_test_update__new_bundle_field')
->fields($values)
->execute();
$this->pass($message);
}
}
/**

View File

@ -0,0 +1,402 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Entity\EntityReferenceFieldTest.
*/
namespace Drupal\system\Tests\Entity;
use Drupal\config\Tests\SchemaCheckTestTrait;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Drupal\user\RoleInterface;
use Drupal\user\UserInterface;
/**
* Tests for the entity reference field.
*
* @group Entity
*/
class EntityReferenceFieldTest extends EntityUnitTestBase {
use SchemaCheckTestTrait;
/**
* The entity type used in this test.
*
* @var string
*/
protected $entityType = 'entity_test';
/**
* The entity type that is being referenced.
*
* @var string
*/
protected $referencedEntityType = 'entity_test_rev';
/**
* 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 install.
*
* @var array
*/
public static $modules = array('entity_reference_test');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test_rev');
// Create a field.
entity_reference_create_field(
$this->entityType,
$this->bundle,
$this->fieldName,
'Field test',
$this->referencedEntityType,
'default',
array('target_bundles' => array($this->bundle)),
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
);
}
/**
* Tests reference field validation.
*/
public function testEntityReferenceFieldValidation() {
// Test a valid reference.
$referenced_entity = entity_create($this->referencedEntityType, array('type' => $this->bundle));
$referenced_entity->save();
$entity = entity_create($this->entityType, array('type' => $this->bundle));
$entity->{$this->fieldName}->target_id = $referenced_entity->id();
$violations = $entity->{$this->fieldName}->validate();
$this->assertEqual($violations->count(), 0, 'Validation passes.');
// Test an invalid reference.
$entity->{$this->fieldName}->target_id = 9999;
$violations = $entity->{$this->fieldName}->validate();
$this->assertEqual($violations->count(), 1, 'Validation throws a violation.');
$this->assertEqual($violations[0]->getMessage(), t('The referenced entity (%type: %id) does not exist.', array('%type' => $this->referencedEntityType, '%id' => 9999)));
// @todo Implement a test case for invalid bundle references after
// https://drupal.org/node/2064191 is fixed
}
/**
* Tests the multiple target entities loader.
*/
public function testReferencedEntitiesMultipleLoad() {
// Create the parent entity.
$entity = entity_create($this->entityType, array('type' => $this->bundle));
// Create three target entities and attach them to parent field.
$target_entities = array();
$reference_field = array();
for ($i = 0; $i < 3; $i++) {
$target_entity = entity_create($this->referencedEntityType, array('type' => $this->bundle));
$target_entity->save();
$target_entities[] = $target_entity;
$reference_field[]['target_id'] = $target_entity->id();
}
// Also attach a non-existent entity and a NULL target id.
$reference_field[3]['target_id'] = 99999;
$target_entities[3] = NULL;
$reference_field[4]['target_id'] = NULL;
$target_entities[4] = NULL;
// Attach the first created target entity as the sixth item ($delta == 5) of
// the parent entity field. We want to test the case when the same target
// entity is referenced twice (or more times) in the same entity reference
// field.
$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);
// Load the target entities using EntityReferenceField::referencedEntities().
$entities = $entity->{$this->fieldName}->referencedEntities();
// Test returned entities:
// - Deltas must be preserved.
// - Non-existent entities must not be retrieved in target entities result.
foreach ($target_entities as $delta => $target_entity) {
if (!empty($target_entity)) {
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
// the target entities set.
$this->assertFalse(isset($loaded_entities[$delta]));
}
}
}
/**
* Tests referencing entities with string IDs.
*/
public function testReferencedEntitiesStringId() {
$field_name = 'entity_reference_string_id';
$this->installEntitySchema('entity_test_string_id');
entity_reference_create_field(
$this->entityType,
$this->bundle,
$field_name,
'Field test',
'entity_test_string_id',
'default',
array('target_bundles' => array($this->bundle)),
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
);
// Create the parent entity.
$entity = entity_create($this->entityType, array('type' => $this->bundle));
// Create the default target entity.
$target_entity = entity_create('entity_test_string_id', array('id' => $this->randomString(), 'type' => $this->bundle));
$target_entity->save();
// Set the field value.
$entity->{$field_name}->setValue(array(array('target_id' => $target_entity->id())));
// Load the target entities using EntityReferenceField::referencedEntities().
$entities = $entity->{$field_name}->referencedEntities();
$this->assertEqual($entities[0]->id(), $target_entity->id());
// Test that a string ID works as a default value and the field's config
// schema is correct.
$field = FieldConfig::loadByName($this->entityType, $this->bundle, $field_name);
$field->setDefaultValue($target_entity->id());
$field->save();
$this->assertConfigSchema(\Drupal::service('config.typed'), 'field.field.' . $field->id(), $field->toArray());
// Test that the default value works.
$entity = entity_create($this->entityType, array('type' => $this->bundle));
$entities = $entity->{$field_name}->referencedEntities();
$this->assertEqual($entities[0]->id(), $target_entity->id());
}
/**
* Tests all the possible ways to autocreate an entity via the API.
*/
function testAutocreateApi() {
$entity = $this->entityManager
->getStorage($this->entityType)
->create(array('name' => $this->randomString()));
// Test content entity autocreation.
$this->assertUserAutocreate($entity, function(EntityInterface $entity, UserInterface $user) {
$entity->set('user_id', $user);
});
$this->assertUserAutocreate($entity, function(EntityInterface $entity, UserInterface $user) {
$entity->set('user_id', $user, FALSE);
});
$this->assertUserAutocreate($entity, function(EntityInterface $entity, UserInterface $user) {
$entity->user_id->setValue($user);
});
$this->assertUserAutocreate($entity, function(EntityInterface $entity, UserInterface $user) {
$entity->user_id[0]->get('entity')->setValue($user);
$entity->user_id[0]->get('target_id')->setValue(-1);
});
$this->assertUserAutocreate($entity, function(EntityInterface $entity, UserInterface $user) {
$entity->user_id->setValue(array('entity' => $user, 'target_id' => -1));
});
try {
$message = 'Setting both the entity and an invalid target_id property fails.';
$this->assertUserAutocreate($entity, function(EntityInterface $entity, UserInterface $user) {
$user->save();
$entity->user_id->setValue(array('entity' => $user, 'target_id' => -1));
});
$this->fail($message);
}
catch (\InvalidArgumentException $e) {
$this->pass($message);
}
$this->assertUserAutocreate($entity, function(EntityInterface $entity, UserInterface $user) {
$entity->user_id = $user;
});
$this->assertUserAutocreate($entity, function(EntityInterface $entity, UserInterface $user) {
$entity->user_id->entity = $user;
});
// Test config entity autocreation.
$this->assertUserRoleAutocreate($entity, function(EntityInterface $entity, RoleInterface $role) {
$entity->set('user_role', $role);
});
$this->assertUserRoleAutocreate($entity, function(EntityInterface $entity, RoleInterface $role) {
$entity->set('user_role', $role, FALSE);
});
$this->assertUserRoleAutocreate($entity, function(EntityInterface $entity, RoleInterface $role) {
$entity->user_role->setValue($role);
});
$this->assertUserRoleAutocreate($entity, function(EntityInterface $entity, RoleInterface $role) {
$entity->user_role[0]->get('entity')->setValue($role);
$entity->user_role[0]->get('target_id')->setValue(-1);
});
$this->assertUserRoleAutocreate($entity, function(EntityInterface $entity, RoleInterface $role) {
$entity->user_role->setValue(array('entity' => $role, 'target_id' => -1));
});
try {
$message = 'Setting both the entity and an invalid target_id property fails.';
$this->assertUserRoleAutocreate($entity, function(EntityInterface $entity, RoleInterface $role) {
$role->save();
$entity->user_role->setValue(array('entity' => $role, 'target_id' => -1));
});
$this->fail($message);
}
catch (\InvalidArgumentException $e) {
$this->pass($message);
}
$this->assertUserRoleAutocreate($entity, function(EntityInterface $entity, RoleInterface $role) {
$entity->user_role = $role;
});
$this->assertUserRoleAutocreate($entity, function(EntityInterface $entity, RoleInterface $role) {
$entity->user_role->entity = $role;
});
// Test target entity saving after setting it as new.
$storage = $this->entityManager->getStorage('user');
$user_id = $this->generateRandomEntityId();
$user = $storage->create(array('uid' => $user_id, 'name' => $this->randomString()));
$entity->user_id = $user;
$user->save();
$entity->save();
$this->assertEqual($entity->user_id->target_id, $user->id());
}
/**
* Asserts that the setter callback performs autocreation for users.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The referencing entity.
* @param $setter_callback
* A callback setting the target entity on the referencing entity.
*
* @return bool
* TRUE if the user was autocreated, FALSE otherwise.
*/
protected function assertUserAutocreate(EntityInterface $entity, $setter_callback) {
$storage = $this->entityManager->getStorage('user');
$user_id = $this->generateRandomEntityId();
$user = $storage->create(array('uid' => $user_id, 'name' => $this->randomString()));
$setter_callback($entity, $user);
$entity->save();
$storage->resetCache();
$user = User::load($user_id);
return $this->assertEqual($entity->user_id->target_id, $user->id());
}
/**
* Asserts that the setter callback performs autocreation for user roles.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The referencing entity.
* @param $setter_callback
* A callback setting the target entity on the referencing entity.
*
* @return bool
* TRUE if the user was autocreated, FALSE otherwise.
*/
protected function assertUserRoleAutocreate(EntityInterface $entity, $setter_callback) {
$storage = $this->entityManager->getStorage('user_role');
$role_id = $this->generateRandomEntityId(TRUE);
$role = $storage->create(array('id' => $role_id, 'label' => $this->randomString()));
$setter_callback($entity, $role);
$entity->save();
$storage->resetCache();
$role = Role::load($role_id);
return $this->assertEqual($entity->user_role->target_id, $role->id());
}
/**
* Tests that the target entity is not unnecessarily loaded.
*/
public function testTargetEntityNoLoad() {
// Setup a test entity type with an entity reference field to itself. We use
// a special storage class throwing exceptions when a load operation is
// triggered to be able to detect them.
$entity_type = clone $this->entityManager->getDefinition('entity_test_update');
$entity_type->setHandlerClass('storage', '\Drupal\entity_test\EntityTestNoLoadStorage');
$this->state->set('entity_test_update.entity_type', $entity_type);
$definitions = array(
'target_reference' => BaseFieldDefinition::create('entity_reference')
->setSetting('target_type', $entity_type->id())
->setSetting('handler', 'default')
);
$this->state->set('entity_test_update.additional_base_field_definitions', $definitions);
$this->entityManager->clearCachedDefinitions();
$this->installEntitySchema($entity_type->id());
// Create the target entity.
$storage = $this->entityManager->getStorage($entity_type->id());
$target_id = $this->generateRandomEntityId();
$target = $storage->create(array('id' => $target_id, 'name' => $this->randomString()));
$target->save();
$this->assertEqual($target_id, $target->id(), 'The target entity has a random identifier.');
// Check that populating the reference with an existing target id does not
// trigger a load operation.
$message = 'The target entity was not loaded.';
try {
$entity = $this->entityManager
->getStorage($entity_type->id())
->create(array('name' => $this->randomString()));
$entity->target_reference = $target_id;
$this->pass($message);
}
catch (EntityStorageException $e) {
$this->fail($message);
}
// Check that the storage actually triggers the expected exception when
// trying to load the target entity.
$message = 'An exception is thrown when trying to load the target entity';
try {
$storage->load($target_id);
$this->fail($message);
}
catch (EntityStorageException $e) {
$this->pass($message);
}
}
}

View File

@ -29,6 +29,13 @@ abstract class EntityUnitTestBase extends KernelTestBase {
*/
protected $entityManager;
/**
* A list of generated identifiers.
*
* @var array
*/
protected $generatedIds = array();
/**
* The state service.
*
@ -160,4 +167,23 @@ abstract class EntityUnitTestBase extends KernelTestBase {
$this->state = $this->container->get('state');
}
/**
* Generates a random ID avoiding collisions.
*
* @param bool $string
* (optional) Whether the id should have string type. Defaults to FALSE.
*
* @return int|string
* The entity identifier.
*/
protected function generateRandomEntityId($string = FALSE) {
srand(time());
do {
$id = $string ? $this->randomMachineName() : mt_rand(1, 0xFFFFFFFF);
}
while (isset($this->generatedIds[$id]));
$this->generatedIds[$id] = $id;
return $id;
}
}

View File

@ -25,6 +25,8 @@ class ContextPluginTest extends KernelTestBase {
* Tests basic context definition and value getters and setters.
*/
function testContext() {
$this->installEntitySchema('user');
$name = $this->randomMachineName();
$manager = new MockBlockManager();
$plugin = $manager->createInstance('user_name');

View File

@ -0,0 +1,25 @@
<?php
/**
* @file
* Contains \Drupal\entity_test\EntityTestNoLoadStorage.
*/
namespace Drupal\entity_test;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
/**
* Test storage class used to verify that no load operation is triggered.
*/
class EntityTestNoLoadStorage extends SqlContentEntityStorage {
/**
* {@inheritdoc}
*/
public function load($id) {
throw new EntityStorageException('No load operation is supposed to happen.');
}
}

View File

@ -65,12 +65,10 @@ class ShapeItem extends FieldItemBase {
'shape' => array(
'type' => 'varchar',
'length' => 32,
'not null' => FALSE,
),
'color' => array(
'type' => 'varchar',
'length' => 32,
'not null' => FALSE,
),
),
) + $foreign_keys;

View File

@ -0,0 +1,32 @@
<?php
/**
* @file
* Contains \Drupal\entity_test\Plugin\Field\FieldType\ShapeItemRequired.
*/
namespace Drupal\entity_test\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Defines the 'shape_required' field type.
*
* @FieldType(
* id = "shape_required",
* label = @Translation("Shape (required)"),
* description = @Translation("Yet another dummy field type."),
* )
*/
class ShapeItemRequired extends ShapeItem {
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties = parent::propertyDefinitions($field_definition);
$properties['shape']->setRequired(TRUE);
return $properties;
}
}

View File

@ -9,6 +9,7 @@ namespace Drupal\taxonomy\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Component\Utility\String;
use Drupal\taxonomy\Plugin\Field\FieldType\TaxonomyTermReferenceItem;
/**
* Plugin implementation of the 'taxonomy_term_reference_link' formatter.
@ -31,7 +32,7 @@ class LinkFormatter extends TaxonomyFormatterBase {
// Terms without target_id do not exist yet, theme such terms as just their
// name.
foreach ($items as $delta => $item) {
if (!$item->target_id) {
if ($item->hasNewEntity()) {
$elements[$delta] = array(
'#markup' => String::checkPlain($item->entity->label()),
);

View File

@ -68,8 +68,9 @@ class TaxonomyTermReferenceItem extends EntityReferenceItem implements OptionsPr
public function getSettableValues(AccountInterface $account = NULL) {
// Flatten options firstly, because Settable Options may contain group
// arrays.
$flatten_options = OptGroup::flattenOptions($this->getSettableOptions($account));
return array_keys($flatten_options);
$values = array_keys(OptGroup::flattenOptions($this->getSettableOptions($account)));
$values[] = static::$NEW_ENTITY_MARKER;
return $values;
}
/**
@ -103,7 +104,6 @@ class TaxonomyTermReferenceItem extends EntityReferenceItem implements OptionsPr
'target_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => FALSE,
),
),
'indexes' => array(

View File

@ -14,6 +14,7 @@ use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Plugin\Field\FieldType\TaxonomyTermReferenceItem;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@ -156,7 +157,7 @@ class TaxonomyAutocompleteWidget extends WidgetBase implements ContainerFactoryP
'vid' => $vocabulary->id(),
'name' => $value,
));
$item = array('target_id' => NULL, 'entity' => $term);
$item = array('entity' => $term);
}
$items[] = $item;
}

View File

@ -34,7 +34,6 @@ class TelephoneItem extends FieldItemBase {
'value' => array(
'type' => 'varchar',
'length' => 256,
'not null' => FALSE,
),
),
);
@ -45,7 +44,8 @@ class TelephoneItem extends FieldItemBase {
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('string')
->setLabel(t('Telephone number'));
->setLabel(t('Telephone number'))
->setRequired(TRUE);
return $properties;
}

View File

@ -41,12 +41,10 @@ class TextItem extends TextItemBase {
'value' => array(
'type' => 'varchar',
'length' => $field_definition->getSetting('max_length'),
'not null' => FALSE,
),
'format' => array(
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
),
),
'indexes' => array(

View File

@ -23,7 +23,8 @@ abstract class TextItemBase extends FieldItemBase {
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('string')
->setLabel(t('Text'));
->setLabel(t('Text'))
->setRequired(TRUE);
$properties['format'] = DataDefinition::create('filter_format')
->setLabel(t('Text format'));

View File

@ -31,12 +31,10 @@ class TextLongItem extends TextItemBase {
'value' => array(
'type' => 'text',
'size' => 'big',
'not null' => FALSE,
),
'format' => array(
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
),
),
'indexes' => array(

View File

@ -61,17 +61,14 @@ class TextWithSummaryItem extends TextItemBase {
'value' => array(
'type' => 'text',
'size' => 'big',
'not null' => FALSE,
),
'summary' => array(
'type' => 'text',
'size' => 'big',
'not null' => FALSE,
),
'format' => array(
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
),
),
'indexes' => array(

View File

@ -9,7 +9,6 @@ namespace Drupal\Tests\Core\Entity\Sql;
use Drupal\Core\Entity\ContentEntityType;
use Drupal\Core\Entity\Sql\DefaultTableMapping;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
use Drupal\Tests\UnitTestCase;
/**
@ -257,72 +256,89 @@ class SqlContentEntityStorageSchemaTest extends UnitTestCase {
'description' => 'The name field.',
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
),
'description__value' => array(
'description' => 'The description field.',
'type' => 'text',
'not null' => FALSE,
),
'description__format' => array(
'description' => 'The description field.',
'type' => 'varchar',
'not null' => FALSE,
),
'uuid' => array(
'description' => 'The uuid field.',
'type' => 'varchar',
'length' => 128,
'not null' => FALSE,
),
'hash' => array(
'description' => 'The hash field.',
'type' => 'varchar',
'length' => 20,
'not null' => FALSE,
),
'email__username' => array(
'description' => 'The email field.',
'type' => 'varchar',
'not null' => FALSE,
),
'email__hostname' => array(
'description' => 'The email field.',
'type' => 'varchar',
'not null' => FALSE,
),
'email__domain' => array(
'description' => 'The email field.',
'type' => 'varchar',
'not null' => FALSE,
),
'owner' => array(
'description' => 'The owner field.',
'type' => 'int',
'not null' => FALSE,
),
'translator' => array(
'description' => 'The translator field.',
'type' => 'int',
'not null' => FALSE,
),
'location__country' => array(
'description' => 'The location field.',
'type' => 'varchar',
'not null' => FALSE,
),
'location__state' => array(
'description' => 'The location field.',
'type' => 'varchar',
'not null' => FALSE,
),
'location__city' => array(
'description' => 'The location field.',
'type' => 'varchar',
'not null' => FALSE,
),
'editor' => array(
'description' => 'The editor field.',
'type' => 'int',
'not null' => FALSE,
),
'editor_revision__target_id' => array(
'description' => 'The editor_revision field.',
'type' => 'int',
'not null' => FALSE,
),
'editor_revision__target_revision_id' => array(
'description' => 'The editor_revision field.',
'type' => 'int',
'not null' => FALSE,
),
'long_index_name' => array(
'description' => 'The long_index_name field.',
'type' => 'int',
'not null' => FALSE,
),
'default_langcode' => array(
'description' => 'Boolean indicating whether field values are in the default entity language.',
@ -430,6 +446,7 @@ class SqlContentEntityStorageSchemaTest extends UnitTestCase {
'revision_id' => array(
'description' => 'The revision_id field.',
'type' => 'int',
'not null' => FALSE,
)
),
'primary key' => array('id'),
@ -455,6 +472,7 @@ class SqlContentEntityStorageSchemaTest extends UnitTestCase {
'revision_id' => array(
'description' => 'The revision_id field.',
'type' => 'serial',
'not null' => TRUE,
),
),
'primary key' => array('revision_id'),
@ -634,6 +652,7 @@ class SqlContentEntityStorageSchemaTest extends UnitTestCase {
'revision_id' => array(
'description' => 'The revision_id field.',
'type' => 'int',
'not null' => FALSE,
),
'langcode' => array(
'description' => 'The langcode field.',
@ -664,6 +683,7 @@ class SqlContentEntityStorageSchemaTest extends UnitTestCase {
'revision_id' => array(
'description' => 'The revision_id field.',
'type' => 'serial',
'not null' => TRUE,
),
'langcode' => array(
'description' => 'The langcode field.',
@ -694,6 +714,7 @@ class SqlContentEntityStorageSchemaTest extends UnitTestCase {
'revision_id' => array(
'description' => 'The revision_id field.',
'type' => 'int',
'not null' => TRUE,
),
'langcode' => array(
'description' => 'The langcode field.',
@ -724,6 +745,7 @@ class SqlContentEntityStorageSchemaTest extends UnitTestCase {
'revision_id' => array(
'description' => 'The revision_id field.',
'type' => 'int',
'not null' => TRUE,
),
'langcode' => array(
'description' => 'The langcode field.',
@ -1231,6 +1253,19 @@ class SqlContentEntityStorageSchemaTest extends UnitTestCase {
$this->storageDefinitions[$field_name]->expects($this->any())
->method('getColumns')
->will($this->returnValue($schema['columns']));
// Add property definitions.
if (!empty($schema['columns'])) {
$property_definitions = array();
foreach ($schema['columns'] as $column => $info) {
$property_definitions[$column] = $this->getMock('Drupal\Core\TypedData\DataDefinitionInterface');
$property_definitions[$column]->expects($this->any())
->method('isRequired')
->will($this->returnValue(!empty($info['not null'])));
}
$this->storageDefinitions[$field_name]->expects($this->any())
->method('getPropertyDefinitions')
->will($this->returnValue($property_definitions));
}
}
}