Commit 076ebd34 authored by catch's avatar catch

Issue #2666792 by bojanz, dawehner, edysmp, heddn: Provide a route provider...

Issue #2666792 by bojanz, dawehner, edysmp, heddn: Provide a route provider for add-page of entities
parent 3d4a798a
...@@ -7,22 +7,28 @@ ...@@ -7,22 +7,28 @@
namespace Drupal\Core\Entity\Controller; namespace Drupal\Core\Entity\Controller;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Link;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Routing\UrlGeneratorTrait;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface; use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
/** /**
* Provides generic entity title callbacks for use in routing. * Provides the add-page and title callbacks for entities.
* *
* It provides: * It provides:
* - An add title callback for entities without bundles. * - The add-page callback.
* - An add title callback for entities with bundles. * - An add title callback for entity types.
* - An add title callback for entity types with bundles.
* - A view title callback. * - A view title callback.
* - An edit title callback. * - An edit title callback.
* - A delete title callback. * - A delete title callback.
...@@ -30,6 +36,7 @@ ...@@ -30,6 +36,7 @@
class EntityController implements ContainerInjectionInterface { class EntityController implements ContainerInjectionInterface {
use StringTranslationTrait; use StringTranslationTrait;
use UrlGeneratorTrait;
/** /**
* The entity manager. * The entity manager.
...@@ -52,6 +59,13 @@ class EntityController implements ContainerInjectionInterface { ...@@ -52,6 +59,13 @@ class EntityController implements ContainerInjectionInterface {
*/ */
protected $entityRepository; protected $entityRepository;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/** /**
* Constructs a new EntityController. * Constructs a new EntityController.
* *
...@@ -61,14 +75,20 @@ class EntityController implements ContainerInjectionInterface { ...@@ -61,14 +75,20 @@ class EntityController implements ContainerInjectionInterface {
* The entity type bundle info. * The entity type bundle info.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository. * The entity repository.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation. * The string translation.
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The url generator.
*/ */
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityRepositoryInterface $entity_repository, TranslationInterface $string_translation) { public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityRepositoryInterface $entity_repository, RendererInterface $renderer, TranslationInterface $string_translation, UrlGeneratorInterface $url_generator) {
$this->entityTypeManager = $entity_type_manager; $this->entityTypeManager = $entity_type_manager;
$this->entityTypeBundleInfo = $entity_type_bundle_info; $this->entityTypeBundleInfo = $entity_type_bundle_info;
$this->entityRepository = $entity_repository; $this->entityRepository = $entity_repository;
$this->renderer = $renderer;
$this->stringTranslation = $string_translation; $this->stringTranslation = $string_translation;
$this->urlGenerator = $url_generator;
} }
/** /**
...@@ -79,12 +99,83 @@ public static function create(ContainerInterface $container) { ...@@ -79,12 +99,83 @@ public static function create(ContainerInterface $container) {
$container->get('entity_type.manager'), $container->get('entity_type.manager'),
$container->get('entity_type.bundle.info'), $container->get('entity_type.bundle.info'),
$container->get('entity.repository'), $container->get('entity.repository'),
$container->get('string_translation') $container->get('renderer'),
$container->get('string_translation'),
$container->get('url_generator')
); );
} }
/** /**
* Provides a generic add title callback for entities without bundles. * Displays add links for the available bundles.
*
* Redirects to the add form if there's only one bundle available.
*
* @param string $entity_type_id
* The entity type ID.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse|array
* If there's only one available bundle, a redirect response.
* Otherwise, a render array with the add links for each bundle.
*/
public function addPage($entity_type_id) {
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id);
$bundle_key = $entity_type->getKey('bundle');
$bundle_entity_type_id = $entity_type->getBundleEntityType();
$build = [
'#theme' => 'entity_add_list',
'#bundles' => [],
];
if ($bundle_entity_type_id) {
$bundle_argument = $bundle_entity_type_id;
$bundle_entity_type = $this->entityTypeManager->getDefinition($bundle_entity_type_id);
$bundle_entity_type_label = $bundle_entity_type->getLowercaseLabel();
$build['#cache']['tags'] = $bundle_entity_type->getListCacheTags();
// Build the message shown when there are no bundles.
$link_text = $this->t('Add a new @entity_type.', ['@entity_type' => $bundle_entity_type_label]);
$link_route_name = 'entity.' . $bundle_entity_type->id() . '.add_form';
$build['#add_bundle_message'] = $this->t('There is no @entity_type yet. @add_link', [
'@entity_type' => $bundle_entity_type_label,
'@add_link' => Link::createFromRoute($link_text, $link_route_name)->toString(),
]);
// Filter out the bundles the user doesn't have access to.
$access_control_handler = $this->entityTypeManager->getAccessControlHandler($bundle_entity_type_id);
foreach ($bundles as $bundle_name => $bundle_info) {
$access = $access_control_handler->createAccess($bundle_name, NULL, [], TRUE);
if (!$access->isAllowed()) {
unset($bundles[$bundle_name]);
}
$this->renderer->addCacheableDependency($build, $access);
}
// Add descriptions from the bundle entities.
$bundles = $this->loadBundleDescriptions($bundles, $bundle_entity_type);
}
else {
$bundle_argument = $bundle_key;
}
$form_route_name = 'entity.' . $entity_type_id . '.add_form';
// Redirect if there's only one bundle available.
if (count($bundles) == 1) {
$bundle_names = array_keys($bundles);
$bundle_name = reset($bundle_names);
return $this->redirect($form_route_name, [$bundle_argument => $bundle_name]);
}
// Prepare the #bundles array for the template.
foreach ($bundles as $bundle_name => $bundle_info) {
$build['#bundles'][$bundle_name] = [
'label' => $bundle_info['label'],
'description' => isset($bundle_info['description']) ? $bundle_info['description'] : '',
'add_link' => Link::createFromRoute($bundle_info['label'], $form_route_name, [$bundle_argument => $bundle_name]),
];
}
return $build;
}
/**
* Provides a generic add title callback for an entity type.
* *
* @param string $entity_type_id * @param string $entity_type_id
* The entity type ID. * The entity type ID.
...@@ -209,4 +300,32 @@ protected function doGetEntity(RouteMatchInterface $route_match, EntityInterface ...@@ -209,4 +300,32 @@ protected function doGetEntity(RouteMatchInterface $route_match, EntityInterface
} }
} }
/**
* Expands the bundle information with descriptions, if known.
*
* @param array $bundles
* An array of bundle information.
* @param \Drupal\Core\Entity\EntityTypeInterface $bundle_entity_type
* The ID of the bundle entity type.
*
* @return array
* The expanded array of bundle information.
*/
protected function loadBundleDescriptions(array $bundles, EntityTypeInterface $bundle_entity_type) {
if (!$bundle_entity_type->isSubclassOf('\Drupal\Core\Entity\EntityDescriptionInterface')) {
return $bundles;
}
$bundle_names = array_keys($bundles);
$storage = $this->entityTypeManager->getStorage($bundle_entity_type->id());
/** @var \Drupal\Core\Entity\EntityDescriptionInterface[] $bundle_entities */
$bundle_entities = $storage->loadMultiple($bundle_names);
foreach ($bundles as $bundle_name => &$bundle_info) {
if (isset($bundle_entities[$bundle_name])) {
$bundle_info['description'] = $bundle_entities[$bundle_name]->getDescription();
}
}
return $bundles;
}
} }
<?php
/**
* @file
* Contains \Drupal\Core\Entity\EntityDescriptionInterface.
*/
namespace Drupal\Core\Entity;
/**
* Defines the interface for entities that have a description.
*/
interface EntityDescriptionInterface {
/**
* Gets the entity description.
*
* @return string
* The entity description.
*/
public function getDescription();
/**
* Sets the entity description.
*
* @param string $description
* The entity description.
*
* @return $this
*/
public function setDescription($description);
}
...@@ -10,9 +10,9 @@ ...@@ -10,9 +10,9 @@
use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeInterface;
/** /**
* Provides HTML routes for entities with administrative edit/delete pages. * Provides HTML routes for entities with administrative add/edit/delete pages.
* *
* Use this class if the edit and delete form routes should use the * Use this class if the add/edit/delete form routes should use the
* administrative theme. * administrative theme.
* *
* @see \Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider. * @see \Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider.
...@@ -21,6 +21,16 @@ ...@@ -21,6 +21,16 @@
*/ */
class AdminHtmlRouteProvider extends DefaultHtmlRouteProvider { class AdminHtmlRouteProvider extends DefaultHtmlRouteProvider {
/**
* {@inheritdoc}
*/
protected function getAddPageRoute(EntityTypeInterface $entity_type) {
if ($route = parent::getAddPageRoute($entity_type)) {
$route->setOption('_admin_route', TRUE);
return $route;
}
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
......
...@@ -24,6 +24,8 @@ ...@@ -24,6 +24,8 @@
* This class provides the following routes for entities, with title and access * This class provides the following routes for entities, with title and access
* callbacks: * callbacks:
* - canonical * - canonical
* - add-page
* - add-form
* - edit-form * - edit-form
* - delete-form * - delete-form
* *
...@@ -78,8 +80,12 @@ public function getRoutes(EntityTypeInterface $entity_type) { ...@@ -78,8 +80,12 @@ public function getRoutes(EntityTypeInterface $entity_type) {
$entity_type_id = $entity_type->id(); $entity_type_id = $entity_type->id();
if ($add_route = $this->getAddFormRoute($entity_type)) { if ($add_page_route = $this->getAddPageRoute($entity_type)) {
$collection->add("entity.{$entity_type_id}.add_form", $add_route); $collection->add("entity.{$entity_type_id}.add_page", $add_page_route);
}
if ($add_form_route = $this->getAddFormRoute($entity_type)) {
$collection->add("entity.{$entity_type_id}.add_form", $add_form_route);
} }
if ($canonical_route = $this->getCanonicalRoute($entity_type)) { if ($canonical_route = $this->getCanonicalRoute($entity_type)) {
...@@ -97,6 +103,29 @@ public function getRoutes(EntityTypeInterface $entity_type) { ...@@ -97,6 +103,29 @@ public function getRoutes(EntityTypeInterface $entity_type) {
return $collection; return $collection;
} }
/**
* Gets the add page route.
*
* Built only for entity types that have bundles.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getAddPageRoute(EntityTypeInterface $entity_type) {
if ($entity_type->hasLinkTemplate('add-page') && $entity_type->getKey('bundle')) {
$route = new Route($entity_type->getLinkTemplate('add-page'));
$route->setDefault('_controller', EntityController::class . '::addPage');
$route->setDefault('_title_callback', EntityController::class . '::addTitle');
$route->setDefault('entity_type_id', $entity_type->id());
$route->setRequirement('_entity_create_access', $entity_type->id());
return $route;
}
}
/** /**
* Gets the add-form route. * Gets the add-form route.
* *
......
<?php
/**
* @file
* Contains \Drupal\system\Tests\Entity\EntityAddUITest.
*/
namespace Drupal\system\Tests\Entity;
use Drupal\entity_test\Entity\EntityTestBundle;
use Drupal\entity_test\Entity\EntityTestMul;
use Drupal\entity_test\Entity\EntityTestWithBundle;
use Drupal\simpletest\WebTestBase;
use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
/**
* Tests the /add and /add/{type} controllers.
*
* @group entity
*/
class EntityAddUITest extends WebTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['entity_test'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$web_user = $this->drupalCreateUser([
"administer entity_test_with_bundle content",
"administer entity_test content",
]);
$this->drupalLogin($web_user);
}
/**
* Tests the add page for an entity type using bundle entities.
*/
public function testAddPageWithBundleEntities() {
$this->drupalGet('/entity_test_with_bundle/add');
// No bundles exist, the add bundle message should be present.
$this->assertText('There is no test entity bundle yet.');
$this->assertLink('Add a new test entity bundle.');
// One bundle exists, confirm redirection to the add-form.
EntityTestBundle::create([
'id' => 'test',
'label' => 'Test label',
'description' => 'My test description',
])->save();
$this->drupalGet('/entity_test_with_bundle/add');
$this->assertUrl('/entity_test_with_bundle/add/test');
// Two bundles exist, confirm both are shown.
EntityTestBundle::create([
'id' => 'test2',
'label' => 'Test2 label',
'description' => 'My test2 description',
])->save();
$this->drupalGet('/entity_test_with_bundle/add');
$this->assertLink('Test label');
$this->assertLink('Test2 label');
$this->assertText('My test description');
$this->assertText('My test2 description');
$this->clickLink('Test2 label');
$this->drupalGet('/entity_test_with_bundle/add/test2');
$this->drupalPostForm(NULL, ['name[0][value]' => 'test name'], t('Save'));
$entity = EntityTestWithBundle::load(1);
$this->assertEqual('test name', $entity->label());
}
/**
* Tests the add page for an entity type not using bundle entities.
*/
public function testAddPageWithoutBundleEntities() {
entity_test_create_bundle('test', 'Test label', 'entity_test_mul');
// Delete the default bundle, so that we can rely on our own.
entity_test_delete_bundle('entity_test_mul', 'entity_test_mul');
// One bundle exists, confirm redirection to the add-form.
$this->drupalGet('/entity_test_mul/add');
$this->assertUrl('/entity_test_mul/add/test');
// Two bundles exist, confirm both are shown.
entity_test_create_bundle('test2', 'Test2 label', 'entity_test_mul');
$this->drupalGet('/entity_test_mul/add');
$this->assertLink('Test label');
$this->assertLink('Test2 label');
$this->clickLink('Test2 label');
$this->drupalGet('/entity_test_mul/add/test2');
$this->drupalPostForm(NULL, ['name[0][value]' => 'test name'], t('Save'));
$entity = EntityTestMul::load(1);
$this->assertEqual('test name', $entity->label());
}
}
...@@ -217,6 +217,13 @@ function system_theme() { ...@@ -217,6 +217,13 @@ function system_theme() {
'variables' => array('menu_items' => NULL), 'variables' => array('menu_items' => NULL),
'file' => 'system.admin.inc', 'file' => 'system.admin.inc',
), ),
'entity_add_list' => array(
'variables' => array(
'bundles' => array(),
'add_bundle_message' => NULL,
),
'template' => 'entity-add-list',
),
)); ));
} }
...@@ -316,6 +323,25 @@ function system_theme_suggestions_field(array $variables) { ...@@ -316,6 +323,25 @@ function system_theme_suggestions_field(array $variables) {
return $suggestions; return $suggestions;
} }
/**
* Prepares variables for the list of available bundles.
*
* Default template: entity-add-list.html.twig.
*
* @param array $variables
* An associative array containing:
* - bundles: An array of bundles with the label, description, add_link keys.
* - add_bundle_message: The message shown when there are no bundles. Only
+ * available if the entity type uses bundle entities.
*/
function template_preprocess_entity_add_list(&$variables) {
foreach ($variables['bundles'] as $bundle_name => $bundle_info) {
$variables['bundles'][$bundle_name]['description'] = [
'#markup' => $bundle_info['description'],
];
}
}
/** /**
* @defgroup authorize Authorized operations * @defgroup authorize Authorized operations
* @{ * @{
......
{#
/**
* @file
* Default theme implementation to present a list of available bundles.
*
* Available variables:
* - bundles: A list of bundles, each with the following properties:
* - label: Bundle label.
* - description: Bundle description.
* - add_link: Link to create an entity of this bundle.
* - add_bundle_message: The message shown when there are no bundles. Only
* available if the entity type uses bundle entities.
*
* @see template_preprocess_entity_add_list()
*
* @ingroup themeable
*/
#}
{% if bundles is not empty %}
<dl>
{% for bundle in bundles %}
<dt>{{ bundle.add_link }}</dt>
<dd>{{ bundle.description }}</dd>
{% endfor %}
</dl>
{% elseif add_bundle_message is not empty %}
<p>
{{ add_bundle_message }}
</p>
{% endif %}
...@@ -13,3 +13,17 @@ field.storage_settings.shape: ...@@ -13,3 +13,17 @@ field.storage_settings.shape:
foreign_key_name: foreign_key_name:
type: string type: string
label: 'Foreign key name' label: 'Foreign key name'
entity_test.entity_test_bundle.*:
type: config_entity
label: 'Entity test bundle'
mapping:
label:
type: label
label: 'Label'
id:
type: string
label: 'Machine-readable name'
description:
type: text
label: 'Description'
...@@ -196,7 +196,7 @@ function entity_test_entity_bundle_info() { ...@@ -196,7 +196,7 @@ function entity_test_entity_bundle_info() {
$bundles = array(); $bundles = array();
$entity_types = \Drupal::entityManager()->getDefinitions(); $entity_types = \Drupal::entityManager()->getDefinitions();
foreach ($entity_types as $entity_type_id => $entity_type) { foreach ($entity_types as $entity_type_id => $entity_type) {
if ($entity_type->getProvider() == 'entity_test') { if ($entity_type->getProvider() == 'entity_test' && $entity_type_id != 'entity_test_with_bundle') {
$bundles[$entity_type_id] = \Drupal::state()->get($entity_type_id . '.bundles') ?: array($entity_type_id => array('label' => 'Entity Test Bundle')); $bundles[$entity_type_id] = \Drupal::state()->get($entity_type_id . '.bundles') ?: array($entity_type_id => array('label' => 'Entity Test Bundle'));
} }
} }
......
...@@ -7,3 +7,6 @@ view test entity translations: ...@@ -7,3 +7,6 @@ view test entity translations:
title: 'View translations of test entities' title: 'View translations of test entities'
view test entity field: view test entity field:
title: 'View test entity field' title: 'View test entity field'
administer entity_test_with_bundle content:
title: 'administer entity_test_with_bundle content'
description: 'administer entity_test_with_bundle content'
<?php
/**
* @file
* Contains \Drupal\entity_test\Entity\EntityTestBundle.
*/
namespace Drupal\entity_test\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
use Drupal\Core\Entity\EntityDescriptionInterface;
/**