Commit 8293016c authored by catch's avatar catch

Issue #2928888 by amateescu, hchonov, timmillwood, Berdir, larowlan: Add a...

Issue #2928888 by amateescu, hchonov, timmillwood, Berdir, larowlan: Add a hook_entity_preload() for modules that need to load a different revision than the default one
parent 4284388b
......@@ -472,6 +472,39 @@ public function purgeFieldData(FieldDefinitionInterface $field_definition, $batc
*/
public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {}
/**
* {@inheritdoc}
*/
protected function preLoad(array &$ids = NULL) {
$entities = [];
// Call hook_entity_preload().
$preload_ids = $ids ?: [];
$preload_entities = $this->moduleHandler()->invokeAll('entity_preload', [$preload_ids, $this->entityTypeId]);
foreach ((array) $preload_entities as $entity) {
$entities[$entity->id()] = $entity;
}
if ($entities) {
// If any entities were pre-loaded, remove them from the IDs still to
// load.
if ($ids !== NULL) {
$ids = array_keys(array_diff_key(array_flip($ids), $entities));
}
// If we had to load all the entities ($ids was set to NULL), get an array
// of IDs that still need to be loaded.
else {
$result = $this->getQuery()
->accessCheck(FALSE)
->condition($this->entityType->getKey('id'), array_keys($entities), 'NOT IN')
->execute();
$ids = array_values($result);
}
}
return $entities;
}
/**
* {@inheritdoc}
*/
......@@ -975,6 +1008,7 @@ protected function setPersistentCache($entities) {
* {@inheritdoc}
*/
public function loadUnchanged($id) {
$entities = [];
$ids = [$id];
// The cache invalidation in the parent has the side effect that loading the
......@@ -983,13 +1017,21 @@ public function loadUnchanged($id) {
// by explicitly removing the entity from the static cache.
parent::resetCache($ids);
// Gather entities from a 'preload' hook. This hook can be used by modules
// that need, for example, to return a different revision than the default
// one for revisionable entity types.
$preloaded_entities = $this->preLoad($ids);
if (!empty($preloaded_entities)) {
$entities += $preloaded_entities;
}
// The default implementation in the parent class unsets the current cache
// and then reloads the entity. That is slow, especially if this is done
// repeatedly in the same request, e.g. when validating and then saving
// an entity. Optimize this for content entities by trying to load them
// directly from the persistent cache again, as in contrast to the static
// cache the persistent one will never be changed until the entity is saved.
$entities = $this->getFromPersistentCache($ids);
$entities += $this->getFromPersistentCache($ids);
if (!$entities) {
$entities[$id] = $this->load($id);
......
......@@ -272,6 +272,17 @@ public function loadMultiple(array $ids = NULL) {
}
}
// Gather entities from a 'preload' method. This method can invoke a hook to
// be used by modules that need, for example, to swap the default revision
// of an entity with a different one. Even though the base entity storage
// class does not actually invoke any preload hooks, we need to call the
// method here so we can add the pre-loaded entity objects to the static
// cache below.
$preloaded_entities = $this->preLoad($ids);
if (!empty($preloaded_entities)) {
$entities += $preloaded_entities;
}
// Load any remaining entities from the database. This is the case if $ids
// is set to NULL (so we load all entities) or if there are any ids left to
// load.
......@@ -288,7 +299,11 @@ public function loadMultiple(array $ids = NULL) {
}
if ($this->entityType->isStaticallyCacheable()) {
// Add entities to the cache.
// Add pre-loaded entities to the cache.
if (!empty($preloaded_entities)) {
$this->setStaticCache($preloaded_entities);
}
// Add queried entities to the cache.
if (!empty($queried_entities)) {
$this->setStaticCache($queried_entities);
}
......@@ -323,6 +338,20 @@ public function loadMultiple(array $ids = NULL) {
*/
abstract protected function doLoadMultiple(array $ids = NULL);
/**
* Gathers entities from a 'preload' step.
*
* @param array|null &$ids
* If not empty, return entities that match these IDs. IDs that were found
* will be removed from the list.
*
* @return \Drupal\Core\Entity\EntityInterface[]
* Associative array of entities, keyed by the entity ID.
*/
protected function preLoad(array &$ids = NULL) {
return [];
}
/**
* Attaches data to entities upon loading.
*
......
......@@ -958,6 +958,32 @@ function hook_ENTITY_TYPE_revision_create(Drupal\Core\Entity\EntityInterface $ne
$new_revision->set('untranslatable_field', $entity->get('untranslatable_field'));
}
/**
* Act on an array of entity IDs before they are loaded.
*
* This hook can be used by modules that need, for example, to return a
* different revision than the default one.
*
* @param array $ids
* An array of entity IDs that have to be loaded.
* @param string $entity_type_id
* The type of entities being loaded (i.e. node, user, comment).
*
* @return \Drupal\Core\Entity\EntityInterface[]
* An array of pre-loaded entity objects.
*
* @ingroup entity_crud
*/
function hook_entity_preload(array $ids, $entity_type_id) {
$entities = [];
foreach ($ids as $id) {
$entities[] = mymodule_swap_revision($id);
}
return $entities;
}
/**
* Act on entities when loaded.
*
......
......@@ -175,6 +175,13 @@ function entity_crud_hook_test_user_insert() {
$GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
}
/**
* Implements hook_entity_preload().
*/
function entity_crud_hook_test_entity_preload(array $entities, $type) {
$GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called for type ' . $type);
}
/**
* Implements hook_entity_load().
*/
......
......@@ -57,23 +57,24 @@ public static function create(ContainerInterface $container) {
}
/**
* Acts on entities when loaded.
* Acts on entity IDs before they are loaded.
*
* @see hook_entity_load()
* @see hook_entity_preload()
*/
public function entityLoad(array &$entities, $entity_type_id) {
public function entityPreload(array $ids, $entity_type_id) {
$entities = [];
// Only run if the entity type can belong to a workspace and we are in a
// non-default workspace.
if (!$this->workspaceManager->shouldAlterOperations($this->entityTypeManager->getDefinition($entity_type_id))) {
return;
return $entities;
}
// Get a list of revision IDs for entities that have a revision set for the
// current active workspace. If an entity has multiple revisions set for a
// workspace, only the one with the highest ID is returned.
$entity_ids = array_keys($entities);
$max_revision_id = 'max_target_entity_revision_id';
$results = $this->entityTypeManager
$query = $this->entityTypeManager
->getStorage('workspace_association')
->getAggregateQuery()
->accessCheck(FALSE)
......@@ -81,22 +82,14 @@ public function entityLoad(array &$entities, $entity_type_id) {
->aggregate('target_entity_revision_id', 'MAX', NULL, $max_revision_id)
->groupBy('target_entity_id')
->condition('target_entity_type_id', $entity_type_id)
->condition('target_entity_id', $entity_ids, 'IN')
->condition('workspace', $this->workspaceManager->getActiveWorkspace()->id())
->execute();
// Since hook_entity_load() is called on both regular entity load as well as
// entity revision load, we need to prevent infinite recursion by checking
// whether the default revisions were already swapped with the workspace
// revision.
// @todo This recursion protection should be removed when
// https://www.drupal.org/project/drupal/issues/2928888 is resolved.
if ($results) {
$results = array_filter($results, function ($result) use ($entities, $max_revision_id) {
return $entities[$result['target_entity_id']]->getRevisionId() != $result[$max_revision_id];
});
->condition('workspace', $this->workspaceManager->getActiveWorkspace()->id());
if ($ids) {
$query->condition('target_entity_id', $ids, 'IN');
}
$results = $query->execute();
if ($results) {
/** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage($entity_type_id);
......@@ -108,6 +101,8 @@ public function entityLoad(array &$entities, $entity_type_id) {
$entities[$revision->id()] = $revision;
}
}
return $entities;
}
/**
......
......@@ -66,12 +66,12 @@ function workspaces_field_info_alter(&$definitions) {
}
/**
* Implements hook_entity_load().
* Implements hook_entity_preload().
*/
function workspaces_entity_load(array &$entities, $entity_type_id) {
function workspaces_entity_preload(array $ids, $entity_type_id) {
return \Drupal::service('class_resolver')
->getInstanceFromDefinition(EntityOperations::class)
->entityLoad($entities, $entity_type_id);
->entityPreload($ids, $entity_type_id);
}
/**
......
......@@ -22,6 +22,7 @@
*
* Tested hooks are:
* - hook_entity_insert() and hook_ENTITY_TYPE_insert()
* - hook_entity_preload()
* - hook_entity_load() and hook_ENTITY_TYPE_load()
* - hook_entity_update() and hook_ENTITY_TYPE_update()
* - hook_entity_predelete() and hook_ENTITY_TYPE_predelete()
......@@ -321,6 +322,7 @@ public function testNodeHooks() {
$node = Node::load($node->id());
$this->assertHookMessageOrder([
'entity_crud_hook_test_entity_preload called for type node',
'entity_crud_hook_test_entity_load called for type node',
'entity_crud_hook_test_node_load called',
]);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment