Commit 0f881613 authored by catch's avatar catch

Issue #2482295 by Berdir: Rebuilding field map with many bundles/fields is very slow

parent b480c525
......@@ -433,7 +433,7 @@ services:
arguments: ['@app.root', '@config.factory', '@module_handler', '@state', '@info_parser', '@logger.channel.default', '@asset.css.collection_optimizer', '@config.installer', '@config.manager', '@router.builder']
entity.manager:
class: Drupal\Core\Entity\EntityManager
arguments: ['@container.namespaces', '@module_handler', '@cache.discovery', '@language_manager', '@string_translation', '@class_resolver', '@typed_data_manager', '@entity.definitions.installed', '@event_dispatcher']
arguments: ['@container.namespaces', '@module_handler', '@cache.discovery', '@language_manager', '@string_translation', '@class_resolver', '@typed_data_manager', '@keyvalue', '@event_dispatcher']
parent: container.trait
tags:
- { name: plugin_manager_cache_clear }
......@@ -442,11 +442,6 @@ services:
arguments: ['@entity.manager']
tags:
- { name: event_subscriber }
entity.definitions.installed:
class: Drupal\Core\KeyValueStore\KeyValueStoreInterface
factory_method: get
factory_service: keyvalue
arguments: ['entity.definitions.installed']
entity.definition_update_manager:
class: Drupal\Core\Entity\EntityDefinitionUpdateManager
arguments: ['@entity.manager']
......
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldDefinitionListenerInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
......@@ -20,7 +21,7 @@
*
* For example, configurable fields defined and exposed by field.module.
*/
interface DynamicallyFieldableEntityStorageInterface extends FieldableEntityStorageInterface, FieldStorageDefinitionListenerInterface {
interface DynamicallyFieldableEntityStorageInterface extends FieldableEntityStorageInterface, FieldStorageDefinitionListenerInterface, FieldDefinitionListenerInterface {
/**
* Determines if the storage contains any data.
......@@ -30,37 +31,6 @@ interface DynamicallyFieldableEntityStorageInterface extends FieldableEntityStor
*/
public function hasData();
/**
* Reacts to the creation of a field.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition created.
*/
public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definition);
/**
* Reacts to the update of a field.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition being updated.
* @param \Drupal\Core\Field\FieldDefinitionInterface $original
* The original field definition; i.e., the definition before the update.
*/
public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original);
/**
* Reacts to the deletion of a field.
*
* Stored values should not be wiped at once, but marked as 'deleted' so that
* they can go through a proper purge process later on.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition being deleted.
*
* @see purgeFieldData()
*/
public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition);
/**
* Purges a batch of field data.
*
......
......@@ -18,10 +18,12 @@
use Drupal\Core\Entity\Exception\NoCorrespondingEntityClassException;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionEvent;
use Drupal\Core\Field\FieldStorageDefinitionEvents;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
......@@ -121,11 +123,11 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
protected $languageManager;
/**
* The keyvalue collection for tracking installed definitions.
* The keyvalue factory.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
* @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface
*/
protected $installedDefinitions;
protected $keyValueFactory;
/**
* The event dispatcher.
......@@ -193,12 +195,12 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
* The class resolver.
* @param \Drupal\Core\TypedData\TypedDataManager $typed_data_manager
* The typed data manager.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $installed_definitions
* The keyvalue collection for tracking installed definitions.
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
* The keyvalue factory.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
*/
public function __construct(\Traversable $namespaces, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, TranslationInterface $translation_manager, ClassResolverInterface $class_resolver, TypedDataManager $typed_data_manager, KeyValueStoreInterface $installed_definitions, EventDispatcherInterface $event_dispatcher) {
public function __construct(\Traversable $namespaces, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, TranslationInterface $translation_manager, ClassResolverInterface $class_resolver, TypedDataManager $typed_data_manager, KeyValueFactoryInterface $key_value_factory, EventDispatcherInterface $event_dispatcher) {
parent::__construct('Entity', $namespaces, $module_handler, 'Drupal\Core\Entity\EntityInterface');
$this->setCacheBackend($cache, 'entity_type', array('entity_types'));
......@@ -209,7 +211,7 @@ public function __construct(\Traversable $namespaces, ModuleHandlerInterface $mo
$this->translationManager = $translation_manager;
$this->classResolver = $class_resolver;
$this->typedDataManager = $typed_data_manager;
$this->installedDefinitions = $installed_definitions;
$this->keyValueFactory = $key_value_factory;
$this->eventDispatcher = $event_dispatcher;
}
......@@ -642,19 +644,43 @@ public function getFieldMap() {
$this->fieldMap = $cache->data;
}
else {
// Rebuild the definitions and put it into the cache.
// The field map is built in two steps. First, add all base fields, by
// looping over all fieldable entity types. They always exist for all
// bundles, and we do not expect to have so many different entity
// types for this to become a bottleneck.
foreach ($this->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) {
foreach ($this->getBundleInfo($entity_type_id) as $bundle => $bundle_info) {
foreach ($this->getFieldDefinitions($entity_type_id, $bundle) as $field_name => $field_definition) {
$this->fieldMap[$entity_type_id][$field_name]['type'] = $field_definition->getType();
$this->fieldMap[$entity_type_id][$field_name]['bundles'][] = $bundle;
}
$bundles = array_keys($this->getBundleInfo($entity_type_id));
foreach ($this->getBaseFieldDefinitions($entity_type_id) as $field_name => $base_field_definition) {
$this->fieldMap[$entity_type_id][$field_name] = [
'type' => $base_field_definition->getType(),
'bundles' => array_combine($bundles, $bundles),
];
}
}
}
// In the second step, the per-bundle fields are added, based on the
// persistent bundle field map stored in a key value collection. This
// data is managed in the EntityManager::onFieldDefinitionCreate()
// and EntityManager::onFieldDefinitionDelete() methods. Rebuilding this
// information in the same way as base fields would not scale, as the
// time to query would grow exponentially with more fields and bundles.
// A cache would be deleted during cache clears, which is the only time
// it is needed, so a key value collection is used.
$bundle_field_maps = $this->keyValueFactory->get('entity.definitions.bundle_field_map')->getAll();
foreach ($bundle_field_maps as $entity_type_id => $bundle_field_map) {
foreach ($bundle_field_map as $field_name => $map_entry) {
if (!isset($this->fieldMap[$entity_type_id][$field_name])) {
$this->fieldMap[$entity_type_id][$field_name] = $map_entry;
}
else {
$this->fieldMap[$entity_type_id][$field_name]['bundles'] += $map_entry['bundles'];
}
}
}
$this->cacheSet($cid, $this->fieldMap, Cache::PERMANENT, array('entity_types', 'entity_field_info'));
$this->cacheSet($cid, $this->fieldMap, Cache::PERMANENT, array('entity_types'));
}
}
return $this->fieldMap;
......@@ -679,6 +705,89 @@ public function getFieldMapByFieldType($field_type) {
return $this->fieldMapByFieldType[$field_type];
}
/**
* {@inheritdoc}
*/
public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definition) {
$entity_type_id = $field_definition->getTargetEntityTypeId();
$bundle = $field_definition->getTargetBundle();
$field_name = $field_definition->getName();
// Notify the storage about the new field.
$this->getStorage($entity_type_id)->onFieldDefinitionCreate($field_definition);
// Update the bundle field map key value collection, add the new field.
$bundle_field_map = $this->keyValueFactory->get('entity.definitions.bundle_field_map')->get($entity_type_id);
if (!isset($bundle_field_map[$field_name])) {
// This field did not exist yet, initialize it with the type and empty
// bundle list.
$bundle_field_map[$field_name] = [
'type' => $field_definition->getType(),
'bundles' => [],
];
}
$bundle_field_map[$field_name]['bundles'][$bundle] = $bundle;
$this->keyValueFactory->get('entity.definitions.bundle_field_map')->set($entity_type_id, $bundle_field_map);
// Delete the cache entry.
$this->cacheBackend->delete('entity_field_map');
// If the field map is initialized, update it as well, so that calls to it
// do not have to rebuild it again.
if ($this->fieldMap) {
if (!isset($this->fieldMap[$entity_type_id][$field_name])) {
// This field did not exist yet, initialize it with the type and empty
// bundle list.
$this->fieldMap[$entity_type_id][$field_name] = [
'type' => $field_definition->getType(),
'bundles' => [],
];
}
$this->fieldMap[$entity_type_id][$field_name]['bundles'][$bundle] = $bundle;
}
}
/**
* {@inheritdoc}
*/
public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original) {
// Notify the storage about the updated field.
$this->getStorage($field_definition->getTargetEntityTypeId())->onFieldDefinitionUpdate($field_definition, $original);
}
/**
* {@inheritdoc}
*/
public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) {
$entity_type_id = $field_definition->getTargetEntityTypeId();
$bundle = $field_definition->getTargetBundle();
$field_name = $field_definition->getName();
// Notify the storage about the field deletion.
$this->getStorage($entity_type_id)->onFieldDefinitionDelete($field_definition);
// Unset the bundle from the bundle field map key value collection.
$bundle_field_map = $this->keyValueFactory->get('entity.definitions.bundle_field_map')->get($entity_type_id);
unset($bundle_field_map[$field_name]['bundles'][$bundle]);
if (empty($bundle_field_map[$field_name]['bundles'])) {
// If there are no bundles left, remove the field from the map.
unset($bundle_field_map[$field_name]);
}
$this->keyValueFactory->get('entity.definitions.bundle_field_map')->set($entity_type_id, $bundle_field_map);
// Delete the cache entry.
$this->cacheBackend->delete('entity_field_map');
// If the field map is initialized, update it as well, so that calls to it
// do not have to rebuild it again.
if ($this->fieldMap) {
unset($this->fieldMap[$entity_type_id][$field_name]['bundles'][$bundle]);
if (empty($this->fieldMap[$entity_type_id][$field_name]['bundles'])) {
unset($this->fieldMap[$entity_type_id][$field_name]);
}
}
}
/**
* Builds field storage definitions for an entity type.
*
......@@ -1243,7 +1352,7 @@ public function onBundleDelete($bundle, $entity_type_id) {
* {@inheritdoc}
*/
public function getLastInstalledDefinition($entity_type_id) {
return $this->installedDefinitions->get($entity_type_id . '.entity_type');
return $this->keyValueFactory->get('entity.definitions.installed')->get($entity_type_id . '.entity_type');
}
/**
......@@ -1267,7 +1376,7 @@ public function useCaches($use_caches = FALSE) {
*/
protected function setLastInstalledDefinition(EntityTypeInterface $entity_type) {
$entity_type_id = $entity_type->id();
$this->installedDefinitions->set($entity_type_id . '.entity_type', $entity_type);
$this->keyValueFactory->get('entity.definitions.installed')->set($entity_type_id . '.entity_type', $entity_type);
}
/**
......@@ -1277,18 +1386,18 @@ protected function setLastInstalledDefinition(EntityTypeInterface $entity_type)
* The entity type definition identifier.
*/
protected function deleteLastInstalledDefinition($entity_type_id) {
$this->installedDefinitions->delete($entity_type_id . '.entity_type');
$this->keyValueFactory->get('entity.definitions.installed')->delete($entity_type_id . '.entity_type');
// Clean up field storage definitions as well. Even if the entity type
// isn't currently fieldable, there might be legacy definitions or an
// empty array stored from when it was.
$this->installedDefinitions->delete($entity_type_id . '.field_storage_definitions');
$this->keyValueFactory->get('entity.definitions.installed')->delete($entity_type_id . '.field_storage_definitions');
}
/**
* {@inheritdoc}
*/
public function getLastInstalledFieldStorageDefinitions($entity_type_id) {
return $this->installedDefinitions->get($entity_type_id . '.field_storage_definitions', array());
return $this->keyValueFactory->get('entity.definitions.installed')->get($entity_type_id . '.field_storage_definitions', array());
}
/**
......@@ -1300,7 +1409,7 @@ public function getLastInstalledFieldStorageDefinitions($entity_type_id) {
* An array of field storage definitions.
*/
protected function setLastInstalledFieldStorageDefinitions($entity_type_id, array $storage_definitions) {
$this->installedDefinitions->set($entity_type_id . '.field_storage_definitions', $storage_definitions);
$this->keyValueFactory->get('entity.definitions.installed')->set($entity_type_id . '.field_storage_definitions', $storage_definitions);
}
/**
......
......@@ -9,12 +9,13 @@
use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Field\FieldDefinitionListenerInterface;
use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
/**
* Provides an interface for entity type managers.
*/
interface EntityManagerInterface extends PluginManagerInterface, EntityTypeListenerInterface, EntityBundleListenerInterface, FieldStorageDefinitionListenerInterface, CachedDiscoveryInterface {
interface EntityManagerInterface extends PluginManagerInterface, EntityTypeListenerInterface, EntityBundleListenerInterface, FieldStorageDefinitionListenerInterface, FieldDefinitionListenerInterface, CachedDiscoveryInterface {
/**
* Builds a list of entity type labels suitable for a Form API options list.
......
<?php
/**
* @file
* Contains \Drupal\Core\Field\FieldDefinitionListenerInterface.
*/
namespace Drupal\Core\Field;
/**
* Defines an interface for reacting to field creation, deletion, and updates.
*/
interface FieldDefinitionListenerInterface {
/**
* Reacts to the creation of a field.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition created.
*/
public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definition);
/**
* Reacts to the update of a field.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition being updated.
* @param \Drupal\Core\Field\FieldDefinitionInterface $original
* The original field definition; i.e., the definition before the update.
*/
public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original);
/**
* Reacts to the deletion of a field.
*
* Stored values should not be wiped at once, but marked as 'deleted' so that
* they can go through a proper purge process later on.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition being deleted.
*/
public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition);
}
......@@ -160,7 +160,7 @@ public function preSave(EntityStorageInterface $storage) {
if ($this->isNew()) {
// Notify the entity storage.
$entity_manager->getStorage($this->entity_type)->onFieldDefinitionCreate($this);
$entity_manager->onFieldDefinitionCreate($this);
}
else {
// Some updates are always disallowed.
......@@ -174,7 +174,7 @@ public function preSave(EntityStorageInterface $storage) {
throw new FieldException("Cannot change an existing field's storage.");
}
// Notify the entity storage.
$entity_manager->getStorage($this->entity_type)->onFieldDefinitionUpdate($this, $this->original);
$entity_manager->onFieldDefinitionUpdate($this, $this->original);
}
parent::preSave($storage);
......@@ -221,7 +221,7 @@ public static function postDelete(EntityStorageInterface $storage, array $fields
// Notify the entity storage.
foreach ($fields as $field) {
if (!$field->deleted) {
\Drupal::entityManager()->getStorage($field->entity_type)->onFieldDefinitionDelete($field);
\Drupal::entityManager()->onFieldDefinitionDelete($field);
}
}
......
......@@ -66,6 +66,11 @@ public function testCustomBundleFieldUsage() {
'type' => 'custom',
]);
$this->assertTrue($entity->hasField('custom_bundle_field'));
// Ensure that the field exists in the field map.
$field_map = \Drupal::entityManager()->getFieldMap();
$this->assertEqual($field_map['entity_test']['custom_bundle_field'], ['type' => 'string', 'bundles' => ['custom' => 'custom']]);
$entity->custom_bundle_field->value = 'swanky';
$entity->save();
$storage->resetCache();
......@@ -102,6 +107,10 @@ public function testCustomBundleFieldUsage() {
->execute();
$this->assertEqual(1, $result->fetchField(), 'Field data has been deleted');
// Ensure that the field no longer exists in the field map.
$field_map = \Drupal::entityManager()->getFieldMap();
$this->assertFalse(isset($field_map['entity_test']['custom_bundle_field']));
// @todo Test field purge and table deletion once supported. See
// https://www.drupal.org/node/2282119.
// $this->assertFalse($this->database->schema()->tableExists($table), 'Custom field table was deleted');
......
......@@ -67,6 +67,21 @@ function entity_schema_test_entity_bundle_field_info(EntityTypeInterface $entity
}
}
/**
* Implements hook_entity_bundle_create().
*/
function entity_schema_test_entity_bundle_create($entity_type_id, $bundle) {
if ($entity_type_id == 'entity_test' && $bundle == 'custom') {
$entity_type = \Drupal::entityManager()->getDefinition($entity_type_id);
$field_definitions = entity_schema_test_entity_bundle_field_info($entity_type, $bundle);
$field_definitions['custom_bundle_field']
->setTargetEntityTypeId($entity_type_id)
->setTargetBundle($bundle);
// Notify the entity storage that we just created a new field.
\Drupal::entityManager()->onFieldDefinitionCreate($field_definitions['custom_bundle_field']);
}
}
/**
* Implements hook_entity_bundle_delete().
*/
......@@ -78,7 +93,6 @@ function entity_schema_test_entity_bundle_delete($entity_type_id, $bundle) {
->setTargetEntityTypeId($entity_type_id)
->setTargetBundle($bundle);
// Notify the entity storage that our field is gone.
\Drupal::entityManager()->getStorage($entity_type_id)
->onFieldDefinitionDelete($field_definitions['custom_bundle_field']);
\Drupal::entityManager()->onFieldDefinitionDelete($field_definitions['custom_bundle_field']);
}
}
......@@ -114,11 +114,11 @@ class EntityManagerTest extends UnitTestCase {
protected $typedDataManager;
/**
* The keyvalue collection for tracking installed definitions.
* The keyvalue factory.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface|\PHPUnit_Framework_MockObject_MockObject
* @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $installedDefinitions;
protected $keyValueFactory;
/**
* The event dispatcher.
......@@ -183,7 +183,7 @@ protected function setUp() {
$this->eventDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$this->installedDefinitions = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreInterface');
$this->keyValueFactory = $this->getMock('Drupal\Core\KeyValueStore\KeyValueFactoryInterface');
$this->container = $this->getContainerWithCacheTagsInvalidator($this->cacheTagsInvalidator);
$this->container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
......@@ -239,7 +239,7 @@ protected function setUpEntityManager($definitions = array()) {
->method('getDefinitions')
->will($this->returnValue($definitions));
$this->entityManager = new TestEntityManager(new \ArrayObject(), $this->moduleHandler, $this->cacheBackend, $this->languageManager, $this->translationManager, $this->getClassResolverStub(), $this->typedDataManager, $this->installedDefinitions, $this->eventDispatcher);
$this->entityManager = new TestEntityManager(new \ArrayObject(), $this->moduleHandler, $this->cacheBackend, $this->languageManager, $this->translationManager, $this->getClassResolverStub(), $this->typedDataManager, $this->keyValueFactory, $this->eventDispatcher);
$this->entityManager->setContainer($this->container);
$this->entityManager->setDiscovery($this->discovery);
}
......@@ -1250,7 +1250,7 @@ public function testGetFieldMap() {
$id_definition = $this->getMockBuilder('Drupal\Core\Field\BaseFieldDefinition')
->disableOriginalConstructor()
->getMock();
$id_definition->expects($this->exactly(2))
$id_definition->expects($this->once())
->method('getType')
->will($this->returnValue('integer'));
$base_field_definitions = array(
......@@ -1258,21 +1258,20 @@ public function testGetFieldMap() {
);
$entity_class::$baseFieldDefinitions = $base_field_definitions;
// Set up a by bundle field definition that only exists on one bundle.
$bundle_definition = $this->getMockBuilder('Drupal\Core\Field\BaseFieldDefinition')
->disableOriginalConstructor()
->getMock();
$bundle_definition->expects($this->once())
->method('getType')
->will($this->returnValue('string'));
$entity_class::$bundleFieldDefinitions = array(
'test_entity_type' => array(
'first_bundle' => array(),
'second_bundle' => array(
'by_bundle' => $bundle_definition,
),
),
);
// Set up the stored bundle field map.
$key_value_store = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreInterface');
$this->keyValueFactory->expects($this->once())
->method('get')
->with('entity.definitions.bundle_field_map')
->willReturn($key_value_store);
$key_value_store->expects($this->once())
->method('getAll')
->willReturn(['test_entity_type' => [
'by_bundle' => [
'type' => 'string',
'bundles' => ['second_bundle' => 'second_bundle'],
],
]]);
// Set up a non-content entity type.
$non_content_entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
......@@ -1303,11 +1302,11 @@ public function testGetFieldMap() {
'test_entity_type' => array(
'id' => array(
'type' => 'integer',
'bundles' => array('first_bundle', 'second_bundle'),
'bundles' => array('first_bundle' => 'first_bundle', 'second_bundle' => 'second_bundle'),
),
'by_bundle' => array(
'type' => 'string',
'bundles' => array('second_bundle'),
'bundles' => array('second_bundle' => 'second_bundle'),
),
)
);
......@@ -1322,11 +1321,11 @@ public function testGetFieldMapFromCache() {
'test_entity_type' => array(
'id' => array(
'type' => 'integer',
'bundles' => array('first_bundle', 'second_bundle'),
'bundles' => array('first_bundle' => 'first_bundle', 'second_bundle' => 'second_bundle'),
),
'by_bundle' => array(
'type' => 'string',
'bundles' => array('second_bundle'),
'bundles' => array('second_bundle' => 'second_bundle'),
),
)
);
......@@ -1393,7 +1392,7 @@ public function testGetFieldMapByFieldType() {
$id_definition = $this->getMockBuilder('Drupal\Core\Field\BaseFieldDefinition')
->disableOriginalConstructor()
->getMock();
$id_definition->expects($this->exactly(2))
$id_definition->expects($this->once())
->method('getType')
->will($this->returnValue('integer'));
$base_field_definitions = array(
......@@ -1401,21 +1400,20 @@ public function testGetFieldMapByFieldType() {
);
$entity_class::$baseFieldDefinitions = $base_field_definitions;
// Set up a by bundle field definition that only exists on one bundle.
$bundle_definition = $this->getMockBuilder('Drupal\Core\Field\BaseFieldDefinition')
->disableOriginalConstructor()
->getMock();
$bundle_definition->expects($this->once())
->method('getType')
->will($this->returnValue('string'));
$entity_class::$bundleFieldDefinitions = array(
'test_entity_type' => array(
'first_bundle' => array(),
'second_bundle' => array(
'by_bundle' => $bundle_definition,
),
),
);
// Set up the stored bundle field map.
$key_value_store = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreInterface');
$this->keyValueFactory->expects($this->once())
->method('get')
->with('entity.definitions.bundle_field_map')
->willReturn($key_value_store);
$key_value_store->expects($this->once())
->method('getAll')
->willReturn(['test_entity_type' => [
'by_bundle' => [
'type' => 'string',
'bundles' => ['second_bundle' => 'second_bundle'],
],
]]);
// Mock the base field definition override.
$override_entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
......@@ -1446,6 +1444,271 @@ public function testGetFieldMapByFieldType() {
$this->assertArrayNotHasKey('id', $stringFields['test_entity_type']);
}
/**
* @covers ::onFieldDefinitionCreate
*/
public function testonFieldDefinitionCreateNewField() {
$field_definition = $this->getMock('Drupal\Core\Field\FieldDefinitionInterface');
$field_definition->expects($this->atLeastOnce())
->method('getTargetEntityTypeId')
->willReturn('test_entity_type');
$field_definition->expects($this->atLeastOnce())
->method('getTargetBundle')
->willReturn('test_bundle');
$field_definition->expects($this->atLeastOnce())
->method('getName')
->willReturn('test_field');
$field_definition->expects($this->atLeastOnce())
->method('getType')
->willReturn('test_type');
$storage = $this->getMock('Drupal\Core\Entity\DynamicallyFieldableEntityStorageInterface');
$class = get_class($storage);
$entity = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
$entity->expects($this->once())
->method('getHandlerClass')
->with('storage')
->will($this->returnValue($class));
$this->setUpEntityManager(array('test_entity_type' => $entity));
// The entity manager will instantiate a new object with the given class
// name. Define the mock expectations on that.
$storage = $this->entityManager->getStorage('test_entity_type');
$storage->expects($this->once())
->method('onFieldDefinitionCreate')
->with($field_definition);
// Set up the stored bundle field map.
$key_value_store = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreInterface');
$this->keyValueFactory->expects($this->exactly(2))
->method('get')
->with('entity.definitions.bundle_field_map')
->willReturn($key_value_store);
$key_value_store->expects($this->once())
->method('get')
->with('test_entity_type')
->willReturn([]);
$key_value_store->expects($this->once())
->method('set')
->with('test_entity_type', [
'test_field' => [
'type' => 'test_type',
'bundles' => ['test_bundle' => 'test_bundle'],
],
]);
$this->entityManager->onFieldDefinitionCreate($field_definition);
}
/**
* @covers ::onFieldDefinitionCreate
*/
public function testonFieldDefinitionCreateExistingField() {
$field_definition = $this->getMock('Drupal\Core\Field\FieldDefinitionInterface');
$field_definition->expects($this->atLeastOnce())
->method('getTargetEntityTypeId')
->willReturn('test_entity_type');
$field_definition->expects($this->atLeastOnce())
->method('getTargetBundle')
->willReturn('test_bundle');
$field_definition->expects($this->atLeastOnce())
->method('getName')
->willReturn('test_field');
$storage = $this->getMock('Drupal\Core\Entity\DynamicallyFieldableEntityStorageInterface');
$class = get_class($storage);
$entity = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
$entity->expects($this->once())
->method('getHandlerClass')
->with('storage')
->will($this->returnValue($class));
$this->setUpEntityManager(array('test_entity_type' => $entity));
// The entity manager will instantiate a new object with the given class
// name. Define the mock expectations on that.
$storage = $this->entityManager->getStorage('test_entity_type');
$storage->expects($this->once())
->method('onFieldDefinitionCreate')
->with($field_definition);
// Set up the stored bundle field map.
$key_value_store = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreInterface');
$this->keyValueFactory->expects($this->exactly(2))