Commit c7994001 authored by catch's avatar catch

Issue #1497374 by yched, chx, amateescu, plach, Damien Tournoud, fago,...

Issue #1497374 by yched, chx, amateescu, plach, Damien Tournoud, fago, swentel: Switch from Field-based storage to Entity-based storage.
parent df5b3bc5
......@@ -102,6 +102,12 @@ function entity_get_bundles($entity_type = NULL) {
*/
function entity_invoke_bundle_hook($hook, $entity_type, $bundle, $bundle_new = NULL) {
entity_info_cache_clear();
// Notify the entity storage controller.
$method = 'onBundle' . ucfirst($hook);
Drupal::entityManager()->getStorageController($entity_type)->$method($bundle, $bundle_new);
// Invoke hook_entity_bundle_*() hooks.
Drupal::moduleHandler()->invokeAll('entity_bundle_' . $hook, array($entity_type, $bundle, $bundle_new));
}
......
......@@ -75,4 +75,14 @@ public function setStatus($status);
*/
public function status();
/**
* Retrieves the exportable properties of the entity.
*
* These are the values that get saved into config.
*
* @return array
* An array of exportable properties and their values.
*/
public function getExportProperties();
}
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Language\Language;
use Drupal\field\FieldInfo;
use PDO;
use Drupal\Core\Entity\Query\QueryInterface;
......@@ -53,8 +54,8 @@ class DatabaseStorageControllerNG extends DatabaseStorageController {
/**
* Overrides DatabaseStorageController::__construct().
*/
public function __construct($entity_type, array $entity_info, Connection $database) {
parent::__construct($entity_type,$entity_info, $database);
public function __construct($entity_type, array $entity_info, Connection $database, FieldInfo $field_info) {
parent::__construct($entity_type,$entity_info, $database, $field_info);
$this->bundleKey = !empty($this->entityInfo['entity_keys']['bundle']) ? $this->entityInfo['entity_keys']['bundle'] : FALSE;
$this->entityClass = $this->entityInfo['class'];
......@@ -367,6 +368,7 @@ public function save(EntityInterface $entity) {
$this->resetCache(array($entity->id()));
$entity->postSave($this, TRUE);
$this->invokeFieldMethod('update', $entity);
$this->saveFieldItems($entity, TRUE);
$this->invokeHook('update', $entity);
if ($this->dataTable) {
$this->invokeTranslationHooks($entity);
......@@ -389,6 +391,7 @@ public function save(EntityInterface $entity) {
$entity->enforceIsNew(FALSE);
$entity->postSave($this, FALSE);
$this->invokeFieldMethod('insert', $entity);
$this->saveFieldItems($entity, FALSE);
$this->invokeHook('insert', $entity);
}
......@@ -603,6 +606,7 @@ public function delete(array $entities) {
$entity_class::postDelete($this, $entities);
foreach ($entities as $entity) {
$this->invokeFieldMethod('delete', $entity);
$this->deleteFieldItems($entity);
$this->invokeHook('delete', $entity);
}
// Ignore slave server temporarily.
......
......@@ -333,8 +333,7 @@ public function getTranslationLanguages($include_default = TRUE) {
// Go through translatable properties and determine all languages for
// which translated values are available.
foreach (field_info_instances($this->entityType, $this->bundle()) as $field_name => $instance) {
$field = field_info_field($field_name);
if (field_is_translatable($this->entityType, $field) && isset($this->$field_name)) {
if (field_is_translatable($this->entityType, $instance->getField()) && isset($this->$field_name)) {
foreach (array_filter($this->$field_name) as $langcode => $value) {
$languages[$langcode] = TRUE;
}
......
......@@ -269,6 +269,7 @@ protected function actions(array $form, array &$form_state) {
*/
public function validate(array $form, array &$form_state) {
$entity = $this->buildEntity($form, $form_state);
$entity_type = $entity->entityType();
$entity_langcode = $entity->language()->id;
$violations = array();
......@@ -285,9 +286,9 @@ public function validate(array $form, array &$form_state) {
else {
// For BC entities, iterate through each field instance and
// instantiate NG items objects manually.
$definitions = \Drupal::entityManager()->getFieldDefinitions($entity->entityType(), $entity->bundle());
foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $field_name => $instance) {
$langcode = field_is_translatable($entity->entityType(), $instance->getField()) ? $entity_langcode : Language::LANGCODE_NOT_SPECIFIED;
$definitions = \Drupal::entityManager()->getFieldDefinitions($entity_type, $entity->bundle());
foreach (field_info_instances($entity_type, $entity->bundle()) as $field_name => $instance) {
$langcode = field_is_translatable($entity_type, $instance->getField()) ? $entity_langcode : Language::LANGCODE_NOT_SPECIFIED;
// Create the field object.
$items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
......@@ -304,7 +305,7 @@ public function validate(array $form, array &$form_state) {
// Map errors back to form elements.
if ($violations) {
foreach ($violations as $field_name => $field_violations) {
$langcode = field_is_translatable($entity->entityType(), field_info_field($field_name)) ? $entity_langcode : Language::LANGCODE_NOT_SPECIFIED;
$langcode = field_is_translatable($entity_type , field_info_field($entity_type, $field_name)) ? $entity_langcode : Language::LANGCODE_NOT_SPECIFIED;
$field_state = field_form_get_state($form['#parents'], $field_name, $langcode, $form_state);
$field_state['constraint_violations'] = $field_violations;
field_form_set_state($form['#parents'], $field_name, $langcode, $form_state, $field_state);
......@@ -433,9 +434,8 @@ protected function submitEntityLanguage(array $form, array &$form_state) {
$current_langcode = $this->isDefaultFormLangcode($form_state) ? $form_state['values']['langcode'] : $this->getFormLangcode($form_state);
foreach (field_info_instances($entity_type, $entity->bundle()) as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
$field = $instance->getField();
$field_name = $field->name;
if (isset($form[$field_name]['#language'])) {
$previous_langcode = $form[$field_name]['#language'];
......
......@@ -88,6 +88,15 @@ class EntityManager extends PluginManagerBase {
*/
protected $fieldDefinitions;
/**
* The root paths.
*
* @see \Drupal\Core\Entity\EntityManager::__construct().
*
* @var \Traversable
*/
protected $namespaces;
/**
* The string translationManager.
*
......@@ -114,22 +123,46 @@ class EntityManager extends PluginManagerBase {
*/
public function __construct(\Traversable $namespaces, ContainerInterface $container, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManager $language_manager, TranslationInterface $translation_manager) {
// Allow the plugin definition to be altered by hook_entity_info_alter().
$annotation_namespaces = array(
'Drupal\Core\Entity\Annotation' => DRUPAL_ROOT . '/core/lib',
);
$this->moduleHandler = $module_handler;
$this->cache = $cache;
$this->languageManager = $language_manager;
$this->namespaces = $namespaces;
$this->translationManager = $translation_manager;
$this->doDiscovery($namespaces);
$this->factory = new DefaultFactory($this->discovery);
$this->container = $container;
}
protected function doDiscovery($namespaces) {
$annotation_namespaces = array(
'Drupal\Core\Entity\Annotation' => DRUPAL_ROOT . '/core/lib',
);
$this->discovery = new AnnotatedClassDiscovery('Entity', $namespaces, $annotation_namespaces, 'Drupal\Core\Entity\Annotation\EntityType');
$this->discovery = new InfoHookDecorator($this->discovery, 'entity_info');
$this->discovery = new AlterDecorator($this->discovery, 'entity_info');
$this->discovery = new CacheDecorator($this->discovery, 'entity_info:' . $this->languageManager->getLanguage(Language::TYPE_INTERFACE)->id, 'cache', CacheBackendInterface::CACHE_PERMANENT, array('entity_info' => TRUE));
}
$this->factory = new DefaultFactory($this->discovery);
$this->container = $container;
/**
* Add more namespaces to the entity manager.
*
* This is usually only necessary for uninstall purposes.
*
* @todo Remove this method, along with doDiscovery(), when
* https://drupal.org/node/1199946 is fixed.
*
* @param \Traversable $namespaces
*
* @see comment_uninstall()
*/
public function addNamespaces(\Traversable $namespaces) {
reset($this->namespaces);
$iterator = new \AppendIterator;
$iterator->append(new \IteratorIterator($this->namespaces));
$iterator->append($namespaces);
$this->doDiscovery($iterator);
}
/**
......@@ -164,6 +197,9 @@ public function hasController($entity_type, $controller_type) {
*/
public function getControllerClass($entity_type, $controller_type, $nested = NULL) {
$definition = $this->getDefinition($entity_type);
if (!$definition) {
throw new \InvalidArgumentException(sprintf('The %s entity type does not exist.', $entity_type));
}
$definition = $definition['controllers'];
if (!$definition) {
throw new \InvalidArgumentException(sprintf('The entity type (%s) does not exist.', $entity_type));
......
......@@ -233,6 +233,22 @@ public function invokeFieldItemPrepareCache(EntityInterface $entity) {
}
}
/**
* Invokes a hook on behalf of the entity.
*
* @param string $hook
* One of 'presave', 'insert', 'update', 'predelete', 'delete', or
* 'revision_delete'.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
*/
protected function invokeHook($hook, EntityInterface $entity) {
// Invoke the hook.
module_invoke_all($this->entityType . '_' . $hook, $entity);
// Invoke the respective entity-level hook.
module_invoke_all('entity_' . $hook, $entity, $this->entityType);
}
/**
* Checks translation statuses and invoke the related hooks if needed.
*
......
<?php
/**
* @file
* Contains \Drupal\Core\Entity\FieldableEntityStorageControllerBase.
*/
namespace Drupal\Core\Entity;
use Drupal\field\FieldInterface;
use Drupal\field\FieldInstanceInterface;
use Symfony\Component\DependencyInjection\Container;
abstract class FieldableEntityStorageControllerBase extends EntityStorageControllerBase implements FieldableEntityStorageControllerInterface {
/**
* Loads values of configurable fields for a group of entities.
*
* Loads all fields for each entity object in a group of a single entity type.
* The loaded field values are added directly to the entity objects.
*
* This method is a wrapper that handles the field data cache. Subclasses
* need to implement the doLoadFieldItems() method with the actual storage
* logic.
*
* @param array $entities
* An array of entities keyed by entity ID.
* @param int $age
* FIELD_LOAD_CURRENT to load the most recent revision for all fields, or
* FIELD_LOAD_REVISION to load the version indicated by each entity.
*/
protected function loadFieldItems(array $entities, $age) {
if (empty($entities)) {
return;
}
// Only the most current revision of non-deleted fields for cacheable entity
// types can be cached.
$load_current = $age == FIELD_LOAD_CURRENT;
$info = entity_get_info($this->entityType);
$use_cache = $load_current && $info['field_cache'];
// Ensure we are working with a BC mode entity.
foreach ($entities as $id => $entity) {
$entities[$id] = $entity->getBCEntity();
}
// Assume all entities will need to be queried. Entities found in the cache
// will be removed from the list.
$queried_entities = $entities;
// Fetch available entities from cache, if applicable.
if ($use_cache) {
// Build the list of cache entries to retrieve.
$cids = array();
foreach ($entities as $id => $entity) {
$cids[] = "field:{$this->entityType}:$id";
}
$cache = cache('field')->getMultiple($cids);
// Put the cached field values back into the entities and remove them from
// the list of entities to query.
foreach ($entities as $id => $entity) {
$cid = "field:{$this->entityType}:$id";
if (isset($cache[$cid])) {
unset($queried_entities[$id]);
foreach ($cache[$cid]->data as $field_name => $values) {
$entity->$field_name = $values;
}
}
}
}
// Fetch other entities from their storage location.
if ($queried_entities) {
// Let the storage controller actually load the values.
$this->doLoadFieldItems($queried_entities, $age);
// Invoke the field type's prepareCache() method.
foreach ($queried_entities as $entity) {
$this->invokeFieldItemPrepareCache($entity);
}
// Build cache data.
if ($use_cache) {
foreach ($queried_entities as $id => $entity) {
$data = array();
$instances = field_info_instances($this->entityType, $entity->bundle());
foreach ($instances as $instance) {
$data[$instance['field_name']] = $queried_entities[$id]->{$instance['field_name']};
}
$cid = "field:{$this->entityType}:$id";
cache('field')->set($cid, $data);
}
}
}
}
/**
* Saves values of configurable fields for an entity.
*
* This method is a wrapper that handles the field data cache. Subclasses
* need to implement the doSaveFieldItems() method with the actual storage
* logic.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param bool $update
* TRUE if the entity is being updated, FALSE if it is being inserted.
*/
protected function saveFieldItems(EntityInterface $entity, $update = TRUE) {
// Ensure we are working with a BC mode entity.
$entity = $entity->getBCEntity();
$this->doSaveFieldItems($entity, $update);
if ($update) {
$entity_info = $entity->entityInfo();
if ($entity_info['field_cache']) {
cache('field')->delete('field:' . $entity->entityType() . ':' . $entity->id());
}
}
}
/**
* Deletes values of configurable fields for all revisions of an entity.
*
* This method is a wrapper that handles the field data cache. Subclasses
* need to implement the doDeleteFieldItems() method with the actual storage
* logic.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*/
protected function deleteFieldItems(EntityInterface $entity) {
// Ensure we are working with a BC mode entity.
$entity = $entity->getBCEntity();
$this->doDeleteFieldItems($entity);
$entity_info = $entity->entityInfo();
if ($entity_info['field_cache']) {
cache('field')->delete('field:' . $entity->entityType() . ':' . $entity->id());
}
}
/**
* Deletes values of configurable fields for a single revision of an entity.
*
* This method is a wrapper that handles the field data cache. Subclasses
* need to implement the doDeleteFieldItemsRevision() method with the actual
* storage logic.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity. It must have a revision ID attribute.
*/
protected function deleteFieldItemsRevision(EntityInterface $entity) {
$this->doDeleteFieldItemsRevision($entity->getBCEntity());
}
/**
* Loads values of configurable fields for a group of entities.
*
* This is the method that holds the actual storage logic.
*
* @param array $entities
* An array of entities keyed by entity ID.
* @param int $age
* FIELD_LOAD_CURRENT to load the most recent revision for all fields, or
* FIELD_LOAD_REVISION to load the version indicated by each entity.
*/
abstract protected function doLoadFieldItems($entities, $age);
/**
* Saves values of configurable fields for an entity.
*
* This is the method that holds the actual storage logic.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param bool $update
* TRUE if the entity is being updated, FALSE if it is being inserted.
*/
abstract protected function doSaveFieldItems(EntityInterface $entity, $update);
/**
* Deletes values of configurable fields for all revisions of an entity.
*
* This is the method that holds the actual storage logic.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*/
abstract protected function doDeleteFieldItems(EntityInterface $entity);
/**
* Deletes values of configurable fields for a single revision of an entity.
*
* This is the method that holds the actual storage logic.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*/
abstract protected function doDeleteFieldItemsRevision(EntityInterface $entity);
/**
* {@inheritdoc}
*/
public function onFieldCreate(FieldInterface $field) { }
/**
* {@inheritdoc}
*/
public function onFieldUpdate(FieldInterface $field) { }
/**
* {@inheritdoc}
*/
public function onFieldDelete(FieldInterface $field) { }
/**
* {@inheritdoc}
*/
public function onInstanceCreate(FieldInstanceInterface $instance) { }
/**
* {@inheritdoc}
*/
public function onInstanceUpdate(FieldInstanceInterface $instance) { }
/**
* {@inheritdoc}
*/
public function onInstanceDelete(FieldInstanceInterface $instance) { }
/**
* {@inheritdoc}
*/
public function onBundleCreate($bundle) { }
/**
* {@inheritdoc}
*/
public function onBundleRename($bundle, $bundle_new) { }
/**
* {@inheritdoc}
*/
public function onBundleDelete($bundle) { }
/**
* {@inheritdoc}
*/
public function onFieldItemsPurge(EntityInterface $entity, FieldInstanceInterface $instance) {
if ($values = $this->readFieldItemsToPurge($entity, $instance)) {
$field = $instance->getField();
$definition = _field_generate_entity_field_definition($field, $instance);
$items = \Drupal::typedData()->create($definition, $values, $field->getFieldName(), $entity);
$items->delete();
}
$this->purgeFieldItems($entity, $instance);
}
/**
* Reads values to be purged for a single field of a single entity.
*
* This method is called during field data purge, on fields for which
* onFieldDelete() or onFieldInstanceDelete() has previously run.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param \Drupal\field\FieldInstanceInterface $instance
* The field instance.
*
* @return array
* The field values, in their canonical array format (numerically indexed
* array of items, each item being a property/value array).
*/
abstract protected function readFieldItemsToPurge(EntityInterface $entity, FieldInstanceInterface $instance);
/**
* Removes field data from storage during purge.
*
* @param EntityInterface $entity
* The entity whose values are being purged.
* @param FieldInstanceInterface $instance
* The field whose values are bing purged.
*/
abstract protected function purgeFieldItems(EntityInterface $entity, FieldInstanceInterface $instance);
/**
* {@inheritdoc}
*/
public function onFieldPurge(FieldInterface $field) { }
}
<?php
/**
* @file
* Contains \Drupal\Core\Entity\ExtensibleEntityStorageControllerInterface.
*/
namespace Drupal\Core\Entity;
use Drupal\field\FieldInterface;
use Drupal\field\FieldInstanceInterface;
interface FieldableEntityStorageControllerInterface extends EntityStorageControllerInterface {
/**
* Allows reaction to the creation of a configurable field.
*
* @param \Drupal\field\FieldInterface $field
* The field being created.
*/
public function onFieldCreate(FieldInterface $field);
/**
* Allows reaction to the update of a configurable field.
*
* @param \Drupal\field\FieldInterface $field
* The field being updated.
*/
public function onFieldUpdate(FieldInterface $field);
/**
* Allows reaction to the deletion of a configurable 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\field\FieldInterface $field
* The field being deleted.
*
* @see fieldPurgeData()
*/
public function onFieldDelete(FieldInterface $field);
/**
* Allows reaction to the creation of a configurable field instance.
*
* @param \Drupal\field\FieldInstanceInterface $instance
* The instance being created.
*/
public function onInstanceCreate(FieldInstanceInterface $instance);
/**
* Allows reaction to the update of a configurable field instance.
*
* @param \Drupal\field\FieldInstanceInterface $instance
* The instance being updated.
*/
public function onInstanceUpdate(FieldInstanceInterface $instance);
/**
* Allows reaction to the deletion of a configurable field instance.
*
* 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\field\FieldInstanceInterface $instance
* The instance being deleted.
*
* @see fieldPurgeData()
*/
public function onInstanceDelete(FieldInstanceInterface $instance);
/**
* Allows reaction to a bundle being created.
*