Commit 0189add1 authored by webchick's avatar webchick

Issue #2304987 by Berdir, Wim Leers: Fixed Don't invalidate cache tags of...

Issue #2304987 by Berdir, Wim Leers: Fixed Don't invalidate cache tags of referenced entities, use entity list cache tags correctly, add test coverage for entity list cache tags.
parent b79a4bc4
......@@ -25,6 +25,7 @@
* "label" = "label"
* },
* admin_permission = "administer site configuration",
* list_cache_tags = { "rendered" }
* )
*/
class DateFormat extends ConfigEntityBase implements DateFormatInterface {
......@@ -98,11 +99,4 @@ public function getCacheTag() {
return ['rendered'];
}
/**
* {@inheritdoc}
*/
public function getListCacheTags() {
return ['rendered'];
}
}
......@@ -380,7 +380,6 @@ public function preSave(EntityStorageInterface $storage) {
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
$this->onSaveOrDelete();
$this->invalidateTagsOnSave($update);
}
......@@ -406,7 +405,7 @@ public static function preDelete(EntityStorageInterface $storage, array $entitie
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
self::invalidateTagsOnDelete($entities);
self::invalidateTagsOnDelete($storage->getEntityType(), $entities);
}
/**
......@@ -426,15 +425,8 @@ public function referencedEntities() {
* {@inheritdoc}
*/
public function getCacheTag() {
return [$this->entityTypeId . ':' . $this->id()];
}
/**
* {@inheritdoc}
*/
public function getListCacheTags() {
// @todo Add bundle-specific listing cache tag? https://drupal.org/node/2145751
return [$this->entityTypeId . 's'];
return [$this->entityTypeId . ':' . $this->id()];
}
/**
......@@ -461,26 +453,6 @@ public static function create(array $values = array()) {
return $entity_manager->getStorage($entity_manager->getEntityTypeFromClass(get_called_class()))->create($values);
}
/**
* Acts on an entity after it was saved or deleted.
*/
protected function onSaveOrDelete() {
$referenced_entities = array(
$this->getEntityTypeId() => array($this->id() => $this),
);
foreach ($this->referencedEntities() as $referenced_entity) {
$referenced_entities[$referenced_entity->getEntityTypeId()][$referenced_entity->id()] = $referenced_entity;
}
foreach ($referenced_entities as $entity_type => $entities) {
if ($this->entityManager()->hasHandler($entity_type, 'view_builder')) {
$this->entityManager()->getViewBuilder($entity_type)->resetCache($entities);
}
}
}
/**
* Invalidates an entity's cache tags upon save.
*
......@@ -492,7 +464,7 @@ protected function invalidateTagsOnSave($update) {
// updated entity may start to appear in a listing because it now meets that
// listing's filtering requirements. A newly created entity may start to
// appear in listings because it did not exist before.)
$tags = $this->getListCacheTags();
$tags = $this->getEntityType()->getListCacheTags();
if ($update) {
// An existing entity was updated, also invalidate its unique cache tag.
$tags = Cache::mergeTags($tags, $this->getCacheTag());
......@@ -504,19 +476,20 @@ protected function invalidateTagsOnSave($update) {
/**
* Invalidates an entity's cache tags upon delete.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityInterface[] $entities
* An array of entities.
*/
protected static function invalidateTagsOnDelete(array $entities) {
$tags = array();
protected static function invalidateTagsOnDelete(EntityTypeInterface $entity_type, array $entities) {
$tags = $entity_type->getListCacheTags();
foreach ($entities as $entity) {
// An entity was deleted: invalidate its own cache tag, but also its list
// cache tags. (A deleted entity may cause changes in a paged list on
// other pages than the one it's on. The one it's on is handled by its own
// cache tag, but subsequent list pages would not be invalidated, hence we
// must invalidate its list cache tags as well.)
$tags = Cache::mergeTags($tags, $entity->getCacheTag(), $entity->getListCacheTags());
$entity->onSaveOrDelete();
$tags = Cache::mergeTags($tags, $entity->getCacheTag());
}
Cache::invalidateTags($tags);
}
......
......@@ -399,15 +399,4 @@ public function getTypedData();
*/
public function getCacheTag();
/**
* The list cache tags associated with this entity.
*
* Enables code listing entities of this type to ensure that newly created
* entities show up immediately.
*
* @return array
* An array of cache tags.
*/
public function getListCacheTags();
}
......@@ -45,7 +45,7 @@ public function resetCache(array $ids = NULL);
* @param $ids
* An array of entity IDs, or NULL to load all entities.
*
* @return array
* @return \Drupal\Core\Entity\EntityInterface[]
* An array of entity objects indexed by their IDs. Returns an empty array
* if no matching entities found.
*/
......
......@@ -202,6 +202,13 @@ class EntityType implements EntityTypeInterface {
*/
protected $field_ui_base_route;
/**
* The list cache tags for this entity type.
*
* @var array
*/
protected $list_cache_tags = array();
/**
* Constructs a new EntityType.
*
......@@ -234,6 +241,12 @@ public function __construct($definition) {
$this->handlers += array(
'access' => 'Drupal\Core\Entity\EntityAccessControlHandler',
);
// Ensure a default list cache tag is set.
if (empty($this->list_cache_tags)) {
$this->list_cache_tags = [$definition['id'] . '_list'];
}
}
/**
......@@ -660,4 +673,11 @@ public function getGroupLabel() {
return !empty($this->group_label) ? (string) $this->group_label : $this->t('Other', array(), array('context' => 'Entity type group'));
}
/**
* {@inheritdoc}
*/
public function getListCacheTags() {
return $this->list_cache_tags;
}
}
......@@ -629,4 +629,13 @@ public function getUriCallback();
*/
public function setUriCallback($callback);
/**
* The list cache tags associated with this entity type.
*
* Enables code listing entities of this type to ensure that newly created
* entities show up immediately.
*
* @return string[]
*/
public function getListCacheTags();
}
......@@ -349,11 +349,20 @@ public function getCacheTag() {
* {@inheritdoc}
*/
public function resetCache(array $entities = NULL) {
// If no set of specific entities is provided, invalidate the entity view
// builder's cache tag. This will invalidate all entities rendered by this
// view builder.
// Otherwise, if a set of specific entities is provided, invalidate those
// specific entities only, plus their list cache tags, because any lists in
// which these entities are rendered, must be invalidated as well. However,
// even in this case, we might invalidate more cache items than necessary.
// When we have a way to invalidate only those cache items that have both
// the individual entity's cache tag and the view builder's cache tag, we'll
// be able to optimize this further.
if (isset($entities)) {
// Always invalidate the ENTITY_TYPE_list tag.
$tags = array($this->entityTypeId . '_list');
$tags = [];
foreach ($entities as $entity) {
$tags = Cache::mergeTags($tags, $entity->getCacheTag());
$tags = Cache::mergeTags($tags, $entity->getCacheTag(), $entity->getEntityType()->getListCacheTags());
}
Cache::invalidateTags($tags);
}
......
......@@ -31,6 +31,7 @@
* uri_callback = "Drupal\aggregator\Entity\Item::buildUri",
* base_table = "aggregator_item",
* render_cache = FALSE,
* list_cache_tags = { "aggregator_feed_list" },
* entity_keys = {
* "id" = "iid",
* "label" = "title",
......@@ -232,13 +233,6 @@ public function getCacheTag() {
return Feed::load($this->getFeedId())->getCacheTag();
}
/**
* {@inheritdoc}
*/
public function getListCacheTags() {
return Feed::load($this->getFeedId())->getListCacheTags();
}
/**
* Entity URI callback.
......
......@@ -73,7 +73,6 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
$build[$entity_id]['#cache']['tags'] = Cache::mergeTags(
$this->getCacheTag(), // Block view builder cache tag.
$entity->getCacheTag(), // Block entity cache tag.
$entity->getListCacheTags(), // Block entity list cache tags.
$plugin->getCacheTags() // Block plugin cache tags.
);
......
......@@ -154,16 +154,33 @@ public function calculateDependencies() {
return $this->dependencies;
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
// Entity::postSave() calls Entity::invalidateTagsOnSave(), which only
// handles the regular cases. The Block entity has one special case: a
// newly created block may *also* appear on any page in the current theme,
// so we must invalidate the associated block's cache tag (which includes
// the theme cache tag).
if (!$update) {
Cache::invalidateTags($this->getCacheTag());
}
}
/**
* {@inheritdoc}
*
* Block configuration entities are a special case: one block entity stores
* the placement of one block in one theme. Instead of using an entity type-
* specific list cache tag like most entities, use the cache tag of the theme
* this block is placed in instead.
* the placement of one block in one theme. Changing these entities may affect
* any page that is rendered in a certain theme, even if the block doesn't
* appear there currently. Hence a block configuration entity must also return
* the associated theme's cache tag.
*/
public function getListCacheTags() {
return array('theme:' . $this->theme);
public function getCacheTag() {
return Cache::mergeTags(parent::getCacheTag(), ['theme:' . $this->theme]);
}
/**
......
......@@ -14,6 +14,7 @@
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\node\NodeInterface;
use Drupal\node\NodeTypeInterface;
use Drupal\node\Entity\Node;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Template\Attribute;
......@@ -243,6 +244,11 @@ function book_node_view(array &$build, EntityInterface $node, EntityViewDisplayI
drupal_get_path('module', 'book') . '/css/book.theme.css',
),
),
// The book navigation is a listing of Node entities, so associate its
// list cache tag for correct invalidation.
'#cache' => [
'tags' => $node->getEntityType()->getListCacheTags(),
],
);
}
}
......
......@@ -8,6 +8,7 @@
namespace Drupal\book;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
/**
......@@ -84,6 +85,9 @@ public function bookExportHtml(NodeInterface $node) {
'#title' => $node->label(),
'#contents' => $contents,
'#depth' => $node->book['depth'],
'#cache' => [
'tags' => $node->getEntityType()->getListCacheTags(),
],
);
}
......
......@@ -10,6 +10,7 @@
use Drupal\book\BookExport;
use Drupal\book\BookManagerInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -115,6 +116,9 @@ public function bookRender() {
return array(
'#theme' => 'item_list',
'#items' => $book_list,
'#cache' => [
'tags' => \Drupal::entityManager()->getDefinition('node')->getListCacheTags(),
],
);
}
......
......@@ -9,7 +9,9 @@
use Drupal\comment\CommentManagerInterface;
use Drupal\comment\CommentStorageInterface;
use Drupal\comment\Entity\Comment;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityViewBuilderInterface;
use Drupal\Core\Entity\EntityFormBuilderInterface;
use Drupal\Core\Field\FieldItemListInterface;
......@@ -67,6 +69,13 @@ public static function defaultSettings() {
*/
protected $viewBuilder;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The entity form builder.
*
......@@ -87,8 +96,7 @@ public static function create(ContainerInterface $container, array $configuratio
$configuration['view_mode'],
$configuration['third_party_settings'],
$container->get('current_user'),
$container->get('entity.manager')->getStorage('comment'),
$container->get('entity.manager')->getViewBuilder('comment'),
$container->get('entity.manager'),
$container->get('entity.form_builder')
);
}
......@@ -112,18 +120,17 @@ public static function create(ContainerInterface $container, array $configuratio
* Third party settings.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\comment\CommentStorageInterface $comment_storage
* The comment storage.
* @param \Drupal\Core\Entity\EntityViewBuilderInterface $comment_view_builder
* The comment view builder.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager
* @param \Drupal\Core\Entity\EntityFormBuilderInterface $entity_form_builder
* The entity form builder.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, AccountInterface $current_user, CommentStorageInterface $comment_storage, EntityViewBuilderInterface $comment_view_builder, EntityFormBuilderInterface $entity_form_builder) {
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, AccountInterface $current_user, EntityManagerInterface $entity_manager, EntityFormBuilderInterface $entity_form_builder) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
$this->viewBuilder = $comment_view_builder;
$this->storage = $comment_storage;
$this->viewBuilder = $entity_manager->getViewBuilder('comment');
$this->storage = $entity_manager->getStorage('comment');
$this->currentUser = $current_user;
$this->entityManager = $entity_manager;
$this->entityFormBuilder = $entity_form_builder;
}
......@@ -150,19 +157,24 @@ public function viewElements(FieldItemListInterface $items) {
// Unpublished comments are not included in
// $entity->get($field_name)->comment_count, but unpublished comments
// should display if the user is an administrator.
if ((($entity->get($field_name)->comment_count && $this->currentUser->hasPermission('access comments')) ||
$this->currentUser->hasPermission('administer comments'))) {
$mode = $comment_settings['default_mode'];
$comments_per_page = $comment_settings['per_page'];
$comments = $this->storage->loadThread($entity, $field_name, $mode, $comments_per_page, $this->getSetting('pager_id'));
if ($comments) {
comment_prepare_thread($comments);
$build = $this->viewBuilder->viewMultiple($comments);
$build['pager']['#theme'] = 'pager';
if ($this->getSetting('pager_id')) {
$build['pager']['#element'] = $this->getSetting('pager_id');
if ($this->currentUser->hasPermission('access comments') || $this->currentUser->hasPermission('administer comments')) {
// This is a listing of Comment entities, so associate its list cache
// tag for correct invalidation.
$output['comments']['#cache']['tags'] = $this->entityManager->getDefinition('comment')->getListCacheTags();
if ($entity->get($field_name)->comment_count || $this->currentUser->hasPermission('administer comments')) {
$mode = $comment_settings['default_mode'];
$comments_per_page = $comment_settings['per_page'];
$comments = $this->storage->loadThread($entity, $field_name, $mode, $comments_per_page, $this->getSetting('pager_id'));
if ($comments) {
comment_prepare_thread($comments);
$build = $this->viewBuilder->viewMultiple($comments);
$build['pager']['#theme'] = 'pager';
if ($this->getSetting('pager_id')) {
$build['pager']['#element'] = $this->getSetting('pager_id');
}
$output['comments'] += $build;
}
$output['comments'] = $build;
}
}
......
......@@ -68,6 +68,7 @@ public function testCacheTags() {
$expected_cache_tags = array(
'entity_test_view',
'entity_test:' . $commented_entity->id(),
'comment_list',
);
sort($expected_cache_tags);
$this->assertEqual($build['#cache']['tags'], $expected_cache_tags, 'The test entity has the expected cache tags before it has comments.');
......@@ -96,7 +97,7 @@ public function testCacheTags() {
// https://drupal.org/node/597236 lands, it's a temporary work-around.
$commented_entity = entity_load('entity_test', $commented_entity->id(), TRUE);
// Verify cache tags on the rendered entity before it has comments.
// Verify cache tags on the rendered entity when it has comments.
$build = \Drupal::entityManager()
->getViewBuilder('entity_test')
->view($commented_entity);
......@@ -104,6 +105,7 @@ public function testCacheTags() {
$expected_cache_tags = array(
'entity_test_view',
'entity_test:' . $commented_entity->id(),
'comment_list',
'comment_view',
'comment:' . $comment->id(),
'filter_format:plain_text',
......
......@@ -49,8 +49,8 @@ public function testLocks() {
$methods = get_class_methods('Drupal\comment\Entity\Comment');
unset($methods[array_search('preSave', $methods)]);
unset($methods[array_search('postSave', $methods)]);
$methods[] = 'onSaveOrDelete';
$methods[] = 'onUpdateBundleEntity';
$methods[] = 'invalidateTagsOnSave';
$comment = $this->getMockBuilder('Drupal\comment\Entity\Comment')
->disableOriginalConstructor()
->setMethods($methods)
......@@ -79,12 +79,6 @@ public function testLocks() {
->method('get')
->with('status')
->will($this->returnValue((object) array('value' => NULL)));
$comment->expects($this->once())
->method('getCacheTag')
->will($this->returnValue(array('comment:' . $cid)));
$comment->expects($this->once())
->method('getListCacheTags')
->will($this->returnValue(array('comments')));
$storage = $this->getMock('Drupal\comment\CommentStorageInterface');
// preSave() should acquire the lock. (This is what's really being tested.)
......
......@@ -14,6 +14,7 @@
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Template\Attribute;
use Drupal\filter\Entity\FilterFormat;
use Drupal\filter\FilterFormatInterface;
/**
......@@ -139,7 +140,7 @@ function filter_formats(AccountInterface $account = NULL) {
else {
$formats['all'] = \Drupal::entityManager()->getStorage('filter_format')->loadByProperties(array('status' => TRUE));
uasort($formats['all'], 'Drupal\Core\Config\Entity\ConfigEntityBase::sort');
\Drupal::cache()->set("filter_formats:{$language_interface->id}", $formats['all'], Cache::PERMANENT, array('filter_formats'));
\Drupal::cache()->set("filter_formats:{$language_interface->id}", $formats['all'], Cache::PERMANENT, \Drupal::entityManager()->getDefinition('filter_format')->getListCacheTags());
}
}
......
......@@ -37,7 +37,7 @@ class FilterPluginManager extends DefaultPluginManager implements FallbackPlugin
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/Filter', $namespaces, $module_handler, 'Drupal\filter\Plugin\FilterInterface', 'Drupal\filter\Annotation\Filter');
$this->alterInfo('filter_info');
$this->setCacheBackend($cache_backend, 'filter_plugins', array('filter_formats'));
$this->setCacheBackend($cache_backend, 'filter_plugins');
}
/**
......
......@@ -64,7 +64,8 @@ public function testNode() {
'body' => array(
'value' => $this->randomMachineName(),
'format' => $this->randomMachineName(),
)
),
'revision_log' => $this->randomString(),
));
$node->save();
......@@ -160,13 +161,32 @@ public function testComment() {
));
$node->save();
$parent_comment = entity_create('comment', array(
'uid' => $user->id(),
'subject' => $this->randomMachineName(),
'comment_body' => [
'value' => $this->randomMachineName(),
'format' => NULL,
],
'entity_id' => $node->id(),
'entity_type' => 'node',
'field_name' => 'comment',
));
$parent_comment->save();
$comment = entity_create('comment', array(
'uid' => $user->id(),
'subject' => $this->randomMachineName(),
'comment_body' => $this->randomMachineName(),
'comment_body' => [
'value' => $this->randomMachineName(),
'format' => NULL,
],
'entity_id' => $node->id(),
'entity_type' => 'node',
'field_name' => 'comment'
'field_name' => 'comment',
'pid' => $parent_comment->id(),
'mail' => 'dries@drupal.org',
'homepage' => 'http://buytaert.net',
));
$comment->save();
......
......@@ -46,6 +46,7 @@
* "delete-form" = "entity.shortcut.delete_form",
* "edit-form" = "entity.shortcut.canonical",
* },
* list_cache_tags = { "shortcut_set_list" },
* bundle_entity_type = "shortcut_set"
* )
*/
......@@ -235,11 +236,4 @@ public function getCacheTag() {
return $this->shortcut_set->entity->getCacheTag();
}
/**
* {@inheritdoc}
*/
public function getListCacheTags() {
return $this->shortcut_set->entity->getListCacheTags();
}
}
......@@ -484,7 +484,7 @@
* exact same cache tag invalidation as any of the built-in entity types, with
* the ability to override any of the default behavior if needed.
* See \Drupal\Core\Entity\EntityInterface::getCacheTag(),
* \Drupal\Core\Entity\EntityInterface::getListCacheTags(),
* \Drupal\Core\Entity\EntityTypeInterface::getListCacheTags(),
* \Drupal\Core\Entity\Entity::invalidateTagsOnSave() and
* \Drupal\Core\Entity\Entity::invalidateTagsOnDelete().
*
......
......@@ -15,6 +15,13 @@
*/
abstract class PageCacheTagsTestBase extends WebTestBase {
/**
* {@inheritdoc}
*
* Always enable header dumping in page cache tags tests, this aids debugging.
*/
protected $dumpHeaders = TRUE;
/**
* {@inheritdoc}
*/
......@@ -50,6 +57,7 @@ protected function verifyPageCache($path, $hit_or_miss, $tags = FALSE) {
$cid = sha1(implode(':', $cid_parts));
$cache_entry = \Drupal::cache('render')->get($cid);
sort($cache_entry->tags);
$tags = array_unique($tags);
sort($tags);
$this->assertIdentical($cache_entry->tags, $tags);
}
......
......@@ -87,6 +87,7 @@ public function testEntityViewBuilderCacheWithReferences() {
// Create an entity reference field and an entity that will be referenced.
entity_reference_create_field('entity_test', 'entity_test', 'reference_field', 'Reference', 'entity_test');
entity_get_display('entity_test', 'entity_test', 'full')->setComponent('reference_field', [
'type' => 'entity_reference_entity_view',
'settings' => ['link' => FALSE],
])->save();
$entity_test_reference = $this->createTestEntity('entity_test');
......@@ -108,6 +109,7 @@ public function testEntityViewBuilderCacheWithReferences() {
// Create another entity that references the first one.
$entity_test = $this->createTestEntity('entity_test');
$entity_test->reference_field->entity = $entity_test_reference;
$entity_test->reference_field->access = TRUE;
$entity_test->save();
// Get a fully built entity view render array.
......@@ -124,7 +126,7 @@ public function testEntityViewBuilderCacheWithReferences() {
$this->assertTrue($this->container->get('cache.' . $bin)->get($cid), 'The entity render element has been cached.');
// Save the entity and verify that both cache entries have been deleted.
$entity_test->save();
$entity_test_reference->save();
$this->assertFalse($this->container->get('cache.' . $bin)->get($cid), 'The entity render cache has been cleared when the entity was deleted.');
$this->assertFalse($this->container->get('cache.' . $bin_reference)->get($cid_reference), 'The entity render cache for the referenced entity has been cleared when the entity was deleted.');
......
......@@ -60,5 +60,22 @@ entity.entity_test.list_referencing_entities:
requirements:
_access: 'TRUE'
entity.entity_test.list_labels_alphabetically:
path: '/entity_test/list_labels_alphabetically/{entity_type_id}'
defaults:
_content: '\Drupal\entity_test\Controller\EntityTestController::listEntitiesAlphabetically'
_title: 'List labels of entities of the given entity type alphabetically'
requirements:
_access: 'TRUE'
entity.entity_test.list_empty:
path: '/entity_test/list_empty/{entity_type_id}'
defaults:
_content: '\Drupal\entity_test\Controller\EntityTestController::listEntitiesEmpty'
_title: 'Empty list of entities of the given entity type, empty because no entities match the query'
requirements:
_access: 'TRUE'
route_callbacks:
- '\Drupal\entity_test\Routing\EntityTestRoutes::routes'
......@@ -7,6 +7,7 @@
namespace Drupal\entity_test\Controller;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\Query\QueryFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -121,4 +122,71 @@ public function listReferencingEntities($entity_reference_field_name, $reference
->viewMultiple($entities, 'full');
}
/**
* List entities of the given entity type labels, sorted alphabetically.
*
* @param string $entity_type_id
* The type of the entity being listed.
*
* @return array
* A renderable array.
*/
public function listEntitiesAlphabetically($entity_type_id) {
$entity_type_definition = $this->entityManager()->getDefinition($entity_type_id);
$query = $this->entityQueryFactory->get($entity_type_id);
// Sort by label field, if any.
if ($label_field = $entity_type_definition->getKey('label')) {
$query->sort($label_field);
}
$entities = $this->entityManager()
->getStorage($entity_type_id)
->loadMultiple($query->execute());
$cache_tags = [];
$labels = [];
foreach ($entities as $entity) {
$labels[] = $entity->label();
$cache_tags = Cache::mergeTags($cache_tags, $entity->getCacheTag());
}