diff --git a/core/includes/entity.inc b/core/includes/entity.inc
index b651e32..2b504aa 100644
--- a/core/includes/entity.inc
+++ b/core/includes/entity.inc
@@ -47,6 +47,7 @@ function entity_get_info($entity_type = NULL) {
'fieldable' => FALSE,
'entity class' => 'Drupal\Core\Entity\Entity',
'controller class' => 'Drupal\Core\Entity\DatabaseStorageController',
+ 'list controller class' => 'Drupal\Core\Entity\EntityListController',
'form controller class' => array(
'default' => 'Drupal\Core\Entity\EntityFormController',
),
@@ -530,3 +531,21 @@ function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_st
field_attach_submit($entity_type, $entity, $form, $form_state);
}
}
+
+/**
+ * Returns an entity list controller for a given entity type.
+ *
+ * @param string $entity_type
+ * The type of the entity.
+ *
+ * @return Drupal\Core\Entity\EntityListControllerInterface
+ * An entity list controller.
+ *
+ * @see hook_entity_info()
+ */
+function entity_list_controller($entity_type) {
+ $storage = entity_get_controller($entity_type);
+ $entity_info = entity_get_info($entity_type);
+ $class = $entity_info['list controller class'];
+ return new $class($entity_type, $storage);
+}
diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityListController.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityListController.php
new file mode 100644
index 0000000..3bd4b82
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityListController.php
@@ -0,0 +1,27 @@
+entityType = $entity_type;
+ $this->storage = $storage;
+ $this->entityInfo = entity_get_info($this->entityType);
+ }
+
+ /**
+ * Implements Drupal\Core\Entity\EntityListControllerInterface::getStorageController().
+ */
+ public function getStorageController() {
+ return $this->storage;
+ }
+
+ /**
+ * Implements Drupal\Core\Entity\EntityListControllerInterface::load().
+ */
+ public function load() {
+ return $this->storage->load();
+ }
+
+ /**
+ * Implements Drupal\Core\Entity\EntityListControllerInterface::getOperations().
+ */
+ public function getOperations(EntityInterface $entity) {
+ $uri = $entity->uri();
+ $operations['edit'] = array(
+ 'title' => t('Edit'),
+ 'href' => $uri['path'] . '/edit',
+ 'options' => $uri['options'],
+ 'weight' => 10,
+ );
+ $operations['delete'] = array(
+ 'title' => t('Delete'),
+ 'href' => $uri['path'] . '/delete',
+ 'options' => $uri['options'],
+ 'weight' => 100,
+ );
+ return $operations;
+ }
+
+ /**
+ * Retrieves the entity list path from the entity information.
+ *
+ * @return string
+ * The internal system path where the entity list will be rendered.
+ *
+ * @todo What is this method for, other than fetching the list path? Is this
+ * for http://drupal.org/node/1783964 ? Should it be on the interface?
+ */
+ public function getPath() {
+ return $this->entityInfo['list path'];
+ }
+
+ /**
+ * Builds the header row.
+ *
+ * @return array
+ * An array of header strings.
+ */
+ public function buildHeader() {
+ $row['label'] = t('Label');
+ $row['id'] = t('Machine name');
+ $row['operations'] = t('Operations');
+ return $row;
+ }
+
+ /**
+ * Builds an array of data for each row.
+ *
+ * @param Drupal\Core\Entity\EntityInterface $entity
+ * The entity for this row of the list.
+ *
+ * @return array
+ * An array of fields to use for this entity.
+ */
+ public function buildRow(EntityInterface $entity) {
+ $row['label'] = $entity->label();
+ $row['id'] = $entity->id();
+ $operations = $this->buildOperations($entity);
+ $row['operations'] = drupal_render($operations);
+ return $row;
+ }
+
+ /**
+ * Renders a list of operation links.
+ *
+ * @param Drupal\Core\Entity\EntityInterface $entity
+ * The entity on which the linked operations will be performed.
+ *
+ * @return array
+ * A renderable array of operation links.
+ */
+ public function buildOperations(EntityInterface $entity) {
+ // Retrieve and sort operations.
+ $operations = $this->getOperations($entity);
+ uasort($operations, 'drupal_sort_weight');
+ $build = array(
+ '#theme' => 'links',
+ '#links' => $operations,
+ );
+ return $build;
+ }
+
+ /**
+ * Implements Drupal\Core\Entity\EntityListControllerInterface::render().
+ */
+ public function render() {
+ $build = array(
+ '#theme' => 'table',
+ '#header' => $this->buildHeader(),
+ '#rows' => array(),
+ '#empty' => t('There is no @label yet. Add one.', array(
+ '@label' => $this->entityInfo['label'],
+ '@add-url' => url($this->getPath() . '/add'),
+ )),
+ );
+ foreach ($this->load() as $entity) {
+ $build['#rows'][$entity->id()] = $this->buildRow($entity);
+ }
+ return $build;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/EntityListControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityListControllerInterface.php
new file mode 100644
index 0000000..85790c3
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/EntityListControllerInterface.php
@@ -0,0 +1,58 @@
+ 'Configuration entity list',
+ 'description' => 'Tests the listing of configuration entities.',
+ 'group' => 'Configuration',
+ );
+ }
+
+ /**
+ * Tests entity list controller methods.
+ */
+ function testList() {
+ $controller = entity_list_controller('config_test');
+
+ // Test getStorageController() method.
+ $this->assertTrue($controller->getStorageController() instanceof EntityStorageControllerInterface, 'EntityStorageController instance in storage.');
+
+ // Get a list of ConfigTest entities and confirm that it contains the
+ // ConfigTest entity provided by the config_test module.
+ // @see config_test.dynamic.default.yml
+ $list = $controller->load();
+ $this->assertEqual(count($list), 1, '1 ConfigTest entity found.');
+ $entity = $list['default'];
+ $this->assertTrue(!empty($entity), '"Default" ConfigTest entity ID found.');
+ $this->assertTrue($entity instanceof ConfigTest, '"Default" ConfigTest entity is an instance of ConfigTest.');
+
+ // Test getOperations() method.
+ $uri = $entity->uri();
+ $expected_operations = array(
+ 'edit' => array (
+ 'title' => 'Edit',
+ 'href' => 'admin/structure/config_test/manage/default/edit',
+ 'options' => $uri['options'],
+ 'weight' => 10,
+ ),
+ 'delete' => array (
+ 'title' => 'Delete',
+ 'href' => 'admin/structure/config_test/manage/default/delete',
+ 'options' => $uri['options'],
+ 'weight' => 100,
+ ),
+ );
+ $actual_operations = $controller->getOperations($entity);
+ $this->assertIdentical($expected_operations, $actual_operations, 'Return value from getOperations matches expected.');
+
+ // Test getPath() method.
+ $this->assertIdentical($controller->getPath(), 'admin/structure/config_test', 'Return value from getPath is correct.');
+
+ // Test buildHeader() method.
+ $expected_items = array(
+ 'label' => 'Label',
+ 'id' => 'Machine name',
+ 'operations' => 'Operations',
+ );
+ $actual_items = $controller->buildHeader();
+ $this->assertIdentical($expected_items, $actual_items, 'Return value from buildHeader matches expected.');
+
+ // Test buildRow() method.
+ $build_operations = $controller->buildOperations($entity);
+ $expected_items = array(
+ 'label' => 'Default',
+ 'id' => 'default',
+ 'operations' => render($build_operations),
+ );
+ $actual_items = $controller->buildRow($entity);
+ $this->assertIdentical($expected_items, $actual_items, 'Return value from buildRow matches expected.');
+ }
+
+ /**
+ * Tests the listing UI.
+ */
+ function testListUI() {
+ // Get the list callback page.
+ $page = $this->drupalGet('admin/structure/config_test');
+
+ // Test for the page title.
+ $element = $this->xpath('//h1[@id="page-title"]');
+ $this->assertEqual((string) $element[0], 'Test configuration', 'Page title found.');
+
+ // Test for the table.
+ $element = $this->xpath('//div[@id="content"]/table');
+ $this->assertTrue($element, 'Configuration entity list table found.');
+
+ // Test the table header.
+ $elements = $this->xpath('//div[@id="content"]/table/thead/tr/th');
+ $this->assertEqual(count($elements), 3, 'Correct number of table header cells found.');
+
+ // Test the contents of each th cell.
+ $expected_items = array('Label', 'Machine name', 'Operations');
+ foreach ($elements as $key => $element) {
+ $this->assertIdentical((string) $element[0], $expected_items[$key]);
+ }
+
+ // Check the number of table row cells.
+ $elements = $this->xpath('//div[@id="content"]/table/tbody/tr[@class="odd"]/td');
+ $this->assertEqual(count($elements), 3, 'Correct number of table row cells found.');
+
+ // Check the contents of each row cell.
+ $this->assertIdentical((string) $elements[0], 'Default');
+ $this->assertIdentical((string) $elements[1], 'default');
+ $this->assertTrue($elements[2]->children()->xpath('//ul'), 'Operations list found.');
+
+ // Verify the 'Add' action link is present.
+ $this->assertLink('Add test configuration');
+ $this->clickLink('Add test configuration');
+ $this->assertResponse(200);
+
+ $this->drupalSetContent($page);
+
+ // Verify that the expected operation links work.
+ foreach (array('Edit', 'Delete') as $link) {
+ $this->drupalSetContent($page);
+ $this->assertLink($link);
+ $this->clickLink($link);
+ $this->assertResponse(200);
+ }
+ }
+
+}
diff --git a/core/modules/config/tests/config_test/config_test.module b/core/modules/config/tests/config_test/config_test.module
index 61d92df..439d6a7 100644
--- a/core/modules/config/tests/config_test/config_test.module
+++ b/core/modules/config/tests/config_test/config_test.module
@@ -82,6 +82,8 @@ function config_test_entity_info() {
'label' => 'Test configuration',
'controller class' => 'Drupal\Core\Config\Entity\ConfigStorageController',
'entity class' => 'Drupal\config_test\ConfigTest',
+ 'list controller class' => 'Drupal\Core\Config\Entity\ConfigEntityListController',
+ 'list path' => 'admin/structure/config_test',
'uri callback' => 'config_test_uri',
'config prefix' => 'config_test.dynamic',
'entity keys' => array(
@@ -176,36 +178,8 @@ function config_test_delete($id) {
* Page callback; Lists available ConfigTest objects.
*/
function config_test_list_page() {
- $entities = entity_load_multiple('config_test');
- uasort($entities, 'Drupal\Core\Config\Entity\ConfigEntityBase::sort');
-
- $rows = array();
- foreach ($entities as $config_test) {
- $uri = $config_test->uri();
- $row = array();
- $row['name']['data'] = array(
- '#type' => 'link',
- '#title' => $config_test->label(),
- '#href' => $uri['path'],
- '#options' => $uri['options'],
- );
- $row['delete']['data'] = array(
- '#type' => 'link',
- '#title' => t('Delete'),
- '#href' => $uri['path'] . '/delete',
- '#options' => $uri['options'],
- );
- $rows[] = $row;
- }
- $build = array(
- '#theme' => 'table',
- '#header' => array('Name', 'Operations'),
- '#rows' => $rows,
- '#empty' => format_string('No test configuration defined. Add some', array(
- '@add-url' => url('admin/structure/config_test/add'),
- )),
- );
- return $build;
+ $controller = entity_list_controller('config_test');
+ return $controller->render();
}
/**