Commit 65be82a1 authored by alexpott's avatar alexpott

Issue #2095283 by Berdir, chx, amateescu, vladan.me: Remove non-storage logic...

Issue #2095283 by Berdir, chx, amateescu, vladan.me: Remove non-storage logic from the storage controllers.
parent f392d38e
......@@ -141,11 +141,11 @@ public function loadMultiple(array $ids = NULL) {
$queried_entities = $this->buildQuery($ids);
}
// Pass all entities loaded from the database through $this->attachLoad(),
// Pass all entities loaded from the database through $this->postLoad(),
// which calls the
// entity type specific load callback, for example hook_node_type_load().
if (!empty($queried_entities)) {
$this->attachLoad($queried_entities);
$this->postLoad($queried_entities);
$entities += $queried_entities;
}
......@@ -285,38 +285,6 @@ protected function buildQuery($ids, $revision_id = FALSE) {
return $result;
}
/**
* Attaches data to entities upon loading.
*
* This will attach fields, if the entity is fieldable. It calls
* hook_entity_load() for modules which need to add data to all entities.
* It also calls hook_TYPE_load() on the loaded entities. For example
* hook_node_load() or hook_user_load(). If your hook_TYPE_load()
* expects special parameters apart from the queried entities, you can set
* $this->hookLoadArguments prior to calling the method.
* See Drupal\node\NodeStorageController::attachLoad() for an example.
*
* @param $queried_entities
* Associative array of query results, keyed on the entity ID.
* @param $revision_id
* ID of the revision that was loaded, or FALSE if the most current revision
* was loaded.
*/
protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
// Call hook_entity_load().
foreach (\Drupal::moduleHandler()->getImplementations('entity_load') as $module) {
$function = $module . '_entity_load';
$function($queried_entities, $this->entityType);
}
// Call hook_TYPE_load(). The first argument for hook_TYPE_load() are
// always the queried entities, followed by additional arguments set in
// $this->hookLoadArguments.
$args = array_merge(array($queried_entities), $this->hookLoadArguments);
foreach (\Drupal::moduleHandler()->getImplementations($this->entityType . '_load') as $module) {
call_user_func_array($module . '_' . $this->entityType . '_load', $args);
}
}
/**
* Implements Drupal\Core\Entity\EntityStorageControllerInterface::create().
*/
......
......@@ -142,11 +142,11 @@ public function loadMultiple(array $ids = NULL) {
$queried_entities = $query_result->fetchAllAssoc($this->idKey);
}
// Pass all entities loaded from the database through $this->attachLoad(),
// Pass all entities loaded from the database through $this->postLoad(),
// which attaches fields (if supported by the entity type) and calls the
// entity type specific load callback, for example hook_node_load().
if (!empty($queried_entities)) {
$this->attachLoad($queried_entities);
$this->postLoad($queried_entities);
$entities += $queried_entities;
}
......@@ -244,22 +244,6 @@ protected function buildQuery($ids, $revision_id = FALSE) {
return $query;
}
/**
* Attaches data to entities upon loading.
*
* @param $queried_entities
* Associative array of query results, keyed on the entity ID.
* @param $load_revision
* (optional) TRUE if the revision should be loaded, defaults to FALSE.
*/
protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
// Call hook_entity_load().
foreach (\Drupal::moduleHandler()->getImplementations('entity_load') as $module) {
$function = $module . '_entity_load';
$function($queried_entities, $this->entityType);
}
}
/**
* {@inheritdoc}
*/
......
......@@ -356,7 +356,7 @@ public static function postDelete(EntityStorageControllerInterface $storage_cont
/**
* {@inheritdoc}
*/
public static function postLoad(EntityStorageControllerInterface $storage_controller, array $entities) {
public static function postLoad(EntityStorageControllerInterface $storage_controller, array &$entities) {
}
/**
......
......@@ -207,14 +207,14 @@ public static function preDelete(EntityStorageControllerInterface $storage_contr
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities);
/**
* Acts on loaded entities before the load hook is invoked.
* Acts on loaded entities.
*
* @param EntityStorageControllerInterface $storage_controller
* The entity storage controller object.
* @param array $entities
* An array of entities.
*/
public static function postLoad(EntityStorageControllerInterface $storage_controller, array $entities);
public static function postLoad(EntityStorageControllerInterface $storage_controller, array &$entities);
/**
* Creates a duplicate of the entity.
......
......@@ -44,15 +44,6 @@ abstract class EntityStorageControllerBase implements EntityStorageControllerInt
*/
protected $entityInfo;
/**
* Additional arguments to pass to hook_TYPE_load().
*
* Set before calling Drupal\Core\Entity\DatabaseStorageController::attachLoad().
*
* @var array
*/
protected $hookLoadArguments = array();
/**
* Name of the entity's ID field in the entity database table.
*
......@@ -84,6 +75,20 @@ public function __construct($entity_type, $entity_info) {
$this->cache = !empty($this->entityInfo['static_cache']);
}
/**
* {@inheritdoc}
*/
public function entityType() {
return $this->entityType;
}
/**
* {@inheritdoc}
*/
public function entityInfo() {
return $this->entityInfo;
}
/**
* {@inheritdoc}
*/
......@@ -152,4 +157,25 @@ protected function invokeHook($hook, EntityInterface $entity) {
module_invoke_all('entity_' . $hook, $entity, $this->entityType);
}
/**
* Attaches data to entities upon loading.
*
* @param array $queried_entities
* Associative array of query results, keyed on the entity ID.
*/
protected function postLoad(array &$queried_entities) {
$entity_class = $this->entityInfo['class'];
$entity_class::postLoad($this, $queried_entities);
// Call hook_entity_load().
foreach (\Drupal::moduleHandler()->getImplementations('entity_load') as $module) {
$function = $module . '_entity_load';
$function($queried_entities, $this->entityType);
}
// Call hook_TYPE_load().
foreach (\Drupal::moduleHandler()->getImplementations($this->entityType . '_load') as $module) {
$function = $module . '_' . $this->entityType . '_load';
$function($queried_entities);
}
}
}
......@@ -150,7 +150,24 @@ public function save(EntityInterface $entity);
* Gets the name of the service for the query for this entity storage.
*
* @return string
* The name of the service for the query for this entity storage.
*/
public function getQueryServicename();
/**
* Returns the entity type.
*
* @return string
* The entity type.
*/
public function entityType();
/**
* Returns the entity info.
*
* @return string
* The entity info.
*/
public function entityInfo();
}
......@@ -229,11 +229,11 @@ public function loadMultiple(array $ids = NULL) {
$queried_entities = $query_result->fetchAllAssoc($this->idKey);
}
// Pass all entities loaded from the database through $this->attachLoad(),
// Pass all entities loaded from the database through $this->postLoad(),
// which attaches fields (if supported by the entity type) and calls the
// entity type specific load callback, for example hook_node_load().
if (!empty($queried_entities)) {
$this->attachLoad($queried_entities);
$this->postLoad($queried_entities);
$entities += $queried_entities;
}
......@@ -271,13 +271,11 @@ public function load($id) {
*
* @param array $records
* Associative array of query results, keyed on the entity ID.
* @param bool $load_revision
* (optional) TRUE if the revision should be loaded, defaults to FALSE.
*
* @return array
* An array of entity objects implementing the EntityInterface.
*/
protected function mapFromStorageRecords(array $records, $load_revision = FALSE) {
protected function mapFromStorageRecords(array $records) {
$entities = array();
foreach ($records as $id => $record) {
$entities[$id] = array();
......@@ -305,7 +303,7 @@ protected function mapFromStorageRecords(array $records, $load_revision = FALSE)
$entities[$id] = new $this->entityClass($entities[$id], $this->entityType, $bundle);
}
}
$this->attachPropertyData($entities, $load_revision);
$this->attachPropertyData($entities);
return $entities;
}
......@@ -314,10 +312,8 @@ protected function mapFromStorageRecords(array $records, $load_revision = FALSE)
*
* @param array &$entities
* Associative array of entities, keyed on the entity ID.
* @param int $revision_id
* (optional) The revision to be loaded. Defaults to FALSE.
*/
protected function attachPropertyData(array &$entities, $revision_id = FALSE) {
protected function attachPropertyData(array &$entities) {
if ($this->dataTable) {
// If a revision table is available, we need all the properties of the
// latest revision. Otherwise we fall back to the data table.
......@@ -328,17 +324,12 @@ protected function attachPropertyData(array &$entities, $revision_id = FALSE) {
->orderBy('data.' . $this->idKey);
if ($this->revisionDataTable) {
if ($revision_id) {
$query->condition($this->revisionKey, $revision_id);
}
else {
// Get the revision IDs.
$revision_ids = array();
foreach ($entities as $values) {
$revision_ids[] = $values[$this->revisionKey][Language::LANGCODE_DEFAULT];
}
$query->condition($this->revisionKey, $revision_ids);
// Get the revision IDs.
$revision_ids = array();
foreach ($entities as $values) {
$revision_ids[] = is_object($values) ? $values->getRevisionId() : $values[$this->revisionKey][Language::LANGCODE_DEFAULT];
}
$query->condition($this->revisionKey, $revision_ids);
}
$data = $query->execute();
......@@ -397,11 +388,11 @@ public function loadRevision($revision_id) {
$query_result = $this->buildQuery(array(), $revision_id)->execute();
$queried_entities = $query_result->fetchAllAssoc($this->idKey);
// Pass the loaded entities from the database through $this->attachLoad(),
// Pass the loaded entities from the database through $this->postLoad(),
// which attaches fields (if supported by the entity type) and calls the
// entity type specific load callback, for example hook_node_load().
if (!empty($queried_entities)) {
$this->attachLoad($queried_entities, $revision_id);
$this->postLoad($queried_entities);
}
return reset($queried_entities);
}
......@@ -544,30 +535,17 @@ protected function buildQuery($ids, $revision_id = FALSE) {
*
* @param $queried_entities
* Associative array of query results, keyed on the entity ID.
* @param $load_revision
* (optional) TRUE if the revision should be loaded, defaults to FALSE.
*/
protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
protected function postLoad(array &$queried_entities) {
// Map the loaded records into entity objects and according fields.
$queried_entities = $this->mapFromStorageRecords($queried_entities, $load_revision);
$queried_entities = $this->mapFromStorageRecords($queried_entities);
// Attach field values.
if ($this->entityInfo['fieldable']) {
$this->loadFieldItems($queried_entities, $load_revision ? static::FIELD_LOAD_REVISION : static::FIELD_LOAD_CURRENT);
$this->loadFieldItems($queried_entities);
}
// Call hook_entity_load().
foreach (\Drupal::moduleHandler()->getImplementations('entity_load') as $module) {
$function = $module . '_entity_load';
$function($queried_entities, $this->entityType);
}
// Call hook_TYPE_load(). The first argument for hook_TYPE_load() are
// always the queried entities, followed by additional arguments set in
// $this->hookLoadArguments.
$args = array_merge(array($queried_entities), $this->hookLoadArguments);
foreach (\Drupal::moduleHandler()->getImplementations($this->entityType . '_load') as $module) {
call_user_func_array($module . '_' . $this->entityType . '_load', $args);
}
parent::postLoad($queried_entities);
}
/**
......
......@@ -27,17 +27,20 @@ abstract class FieldableEntityStorageControllerBase extends EntityStorageControl
*
* @param array $entities
* An array of entities keyed by entity ID.
* @param int $age
* EntityStorageControllerInterface::FIELD_LOAD_CURRENT to load the most
* recent revision for all fields, or
* EntityStorageControllerInterface::FIELD_LOAD_REVISION to load the version
* indicated by each entity.
*/
protected function loadFieldItems(array $entities, $age) {
protected function loadFieldItems(array $entities) {
if (empty($entities)) {
return;
}
$age = static::FIELD_LOAD_CURRENT;
foreach ($entities as $entity) {
if (!$entity->isDefaultRevision()) {
$age = static::FIELD_LOAD_REVISION;
break;
}
}
// Only the most current revision of non-deleted fields for cacheable entity
// types can be cached.
$load_current = $age == static::FIELD_LOAD_CURRENT;
......
<?php
/**
* @file
* Contains \Drupal\custom_block\CustomBlockStorageController.
*/
namespace Drupal\custom_block;
use Drupal\Core\Entity\FieldableDatabaseStorageController;
use Drupal\Core\Entity\EntityStorageControllerInterface;
/**
* Controller class for custom blocks.
*
* This extends the Drupal\Core\Entity\DatabaseStorageController class,
* adding required special handling for custom block entities.
*/
class CustomBlockStorageController extends FieldableDatabaseStorageController {
/**
* Overrides \Drupal\Core\Entity\DatabaseStorageController::attachLoad().
*/
protected function attachLoad(&$blocks, $load_revision = FALSE) {
// Create an array of block types for passing as a load argument.
// Note that blocks at this point are still \StdClass objects returned from
// the database.
foreach ($blocks as $entity) {
$types[$entity->type] = $entity->type;
}
// Besides the list of blocks, pass one additional argument to
// hook_custom_block_load(), containing a list of block types that were
// loaded.
$this->hookLoadArguments = array($types);
parent::attachLoad($blocks, $load_revision);
}
}
......@@ -22,7 +22,7 @@
* label = @Translation("Custom Block"),
* bundle_label = @Translation("Custom Block type"),
* controllers = {
* "storage" = "Drupal\custom_block\CustomBlockStorageController",
* "storage" = "Drupal\Core\Entity\FieldableDatabaseStorageController",
* "access" = "Drupal\custom_block\CustomBlockAccessController",
* "list" = "Drupal\custom_block\CustomBlockListController",
* "view_builder" = "Drupal\custom_block\CustomBlockViewBuilder",
......
<?php
/**
* @file
* Contains \Drupal\custom_block\Tests\CustomBlockLoadHooksTest.
*/
namespace Drupal\custom_block\Tests;
/**
* Tests for the hooks invoked during custom_block_load().
*/
class CustomBlockLoadHooksTest extends CustomBlockTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('custom_block_test');
/**
* Declares test information.
*/
public static function getInfo() {
return array(
'name' => 'Custom Block load hooks',
'description' => 'Test the hooks invoked when a custom block is being loaded.',
'group' => 'Custom Block',
);
}
/**
* Tests that hook_custom_block_load() is invoked correctly.
*/
public function testHookCustomBlockLoad() {
$other_bundle = $this->createCustomBlockType('other');
// Create some sample articles and pages.
$custom_block1 = $this->createCustomBlock();
$custom_block2 = $this->createCustomBlock();
$custom_block3 = $this->createCustomBlock();
$custom_block4 = $this->createCustomBlock(FALSE, $other_bundle->id());
// Check that when a set of custom blocks that only contains basic blocks is
// loaded, the properties added to the custom block by
// custom_block_test_load_custom_block() correctly reflect the expected
// values.
$custom_blocks = entity_load_multiple_by_properties('custom_block', array('type' => 'basic'));
$loaded_custom_block = end($custom_blocks);
$this->assertEqual($loaded_custom_block->custom_block_test_loaded_ids, array(
$custom_block1->id(),
$custom_block2->id(),
$custom_block3->id(),
), 'hook_custom_block_load() received the correct list of custom_block IDs the first time it was called.');
$this->assertEqual($loaded_custom_block->custom_block_test_loaded_types, array('basic'), 'hook_custom_block_load() received the correct list of custom block types the first time it was called.');
// Now, as part of the same page request, load a set of custom_blocks that contain
// both basic and other bundle, and make sure the parameters passed to
// custom_block_test_custom_block_load() are correctly updated.
$custom_blocks = entity_load_multiple('custom_block', \Drupal::entityQuery('custom_block')->execute(), TRUE);
$loaded_custom_block = end($custom_blocks);
$this->assertEqual($loaded_custom_block->custom_block_test_loaded_ids, array(
$custom_block1->id(),
$custom_block2->id(),
$custom_block3->id(),
$custom_block4->id(),
), 'hook_custom_block_load() received the correct list of custom_block IDs the second time it was called.');
$this->assertEqual($loaded_custom_block->custom_block_test_loaded_types, array('basic', 'other'), 'hook_custom_block_load() received the correct list of custom_block types the second time it was called.');
}
}
......@@ -10,22 +10,6 @@
use Drupal\custom_block\Entity\CustomBlock;
/**
* Implements hook_custom_block_load().
*/
function custom_block_test_custom_block_load($custom_blocks, $types) {
// Add properties to each loaded custom_block which record the parameters that
// were passed in to this function, so the tests can check that (a) this hook
// was called, and (b) the parameters were what we expected them to be.
$ids = array_keys($custom_blocks);
ksort($ids);
sort($types);
foreach ($custom_blocks as $custom_block) {
$custom_block->custom_block_test_loaded_ids = $ids;
$custom_block->custom_block_test_loaded_types = $types;
}
}
/**
* Implements hook_custom_block_view().
*/
......
......@@ -435,7 +435,7 @@ function book_children($book_link) {
/**
* Implements hook_node_load().
*/
function book_node_load($nodes, $types) {
function book_node_load($nodes) {
$result = db_query("SELECT * FROM {book} b INNER JOIN {menu_links} ml ON b.mlid = ml.mlid WHERE b.nid IN (:nids)", array(':nids' => array_keys($nodes)), array('fetch' => PDO::FETCH_ASSOC));
foreach ($result as $record) {
$nodes[$record['nid']]->book = $record;
......
......@@ -39,12 +39,12 @@ protected function buildQuery($ids, $revision_id = FALSE) {
/**
* {@inheritdoc}
*/
protected function attachLoad(&$records, $load_revision = FALSE) {
protected function postLoad(array &$queried_entities) {
// Prepare standard comment fields.
foreach ($records as &$record) {
foreach ($queried_entities as &$record) {
$record->name = $record->uid ? $record->registered_name : $record->name;
}
parent::attachLoad($records, $load_revision);
parent::postLoad($queried_entities);
}
/**
......
......@@ -575,6 +575,42 @@ public function postSave(EntityStorageControllerInterface $storage_controller, $
_menu_clear_page_cache();
}
/**
* @inheritdoc}
*/
public static function postLoad(EntityStorageControllerInterface $storage_controller, array &$entities) {
parent::postLoad($storage_controller, $entities);
$routes = array();
foreach ($entities as $menu_link) {
$menu_link->options = unserialize($menu_link->options);
$menu_link->route_parameters = unserialize($menu_link->route_parameters);
// Use the weight property from the menu link.
$menu_link->router_item['weight'] = $menu_link->weight;
// By default use the menu_name as type.
$menu_link->bundle = $menu_link->menu_name;
// For all links that have an associated route, load the route object now
// and save it on the object. That way we avoid a select N+1 problem later.
if ($menu_link->route_name) {
$routes[$menu_link->id()] = $menu_link->route_name;
}
}
// Now mass-load any routes needed and associate them.
if ($routes) {
$route_objects = \Drupal::service('router.route_provider')->getRoutesByNames($routes);
foreach ($routes as $entity_id => $route) {
// Not all stored routes will be valid on load.
if (isset($route_objects[$route])) {
$entities[$entity_id]->setRouteObject($route_objects[$route]);
}
}
}
}
/**
* {@inheritdoc}
*/
......
......@@ -105,46 +105,6 @@ protected function buildQuery($ids, $revision_id = FALSE) {
return $query;
}
/**
* Overrides DatabaseStorageController::attachLoad().
*
* @todo Don't call parent::attachLoad() at all because we want to be able to
* control the entity load hooks.
*/
protected function attachLoad(&$menu_links, $load_revision = FALSE) {
$routes = array();
foreach ($menu_links as &$menu_link) {
$menu_link->options = unserialize($menu_link->options);
$menu_link->route_parameters = unserialize($menu_link->route_parameters);
// Use the weight property from the menu link.
$menu_link->router_item['weight'] = $menu_link->weight;
// By default use the menu_name as type.
$menu_link->bundle = $menu_link->menu_name;
// For all links that have an associated route, load the route object now
// and save it on the object. That way we avoid a select N+1 problem later.
if ($menu_link->route_name) {
$routes[$menu_link->id()] = $menu_link->route_name;
}
}
// Now mass-load any routes needed and associate them.
if ($routes) {
$route_objects = $this->routeProvider<