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 @@
namespace Drupal\Core\Entity\Controller;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
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\Entity\EntityInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Routing\UrlGeneratorTrait;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
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:
* - An add title callback for entities without bundles.
* - An add title callback for entities with bundles.
* - The add-page callback.
* - An add title callback for entity types.
* - An add title callback for entity types with bundles.
* - A view title callback.
* - An edit title callback.
* - A delete title callback.
......@@ -30,6 +36,7 @@
class EntityController implements ContainerInjectionInterface {
use StringTranslationTrait;
use UrlGeneratorTrait;
/**
* The entity manager.
......@@ -52,6 +59,13 @@ class EntityController implements ContainerInjectionInterface {
*/
protected $entityRepository;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a new EntityController.
*
......@@ -61,14 +75,20 @@ class EntityController implements ContainerInjectionInterface {
* The entity type bundle info.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\Core\StringTranslation\TranslationInterface $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->entityTypeBundleInfo = $entity_type_bundle_info;
$this->entityRepository = $entity_repository;
$this->renderer = $renderer;
$this->stringTranslation = $string_translation;
$this->urlGenerator = $url_generator;
}
/**
......@@ -79,12 +99,83 @@ public static function create(ContainerInterface $container) {
$container->get('entity_type.manager'),
$container->get('entity_type.bundle.info'),
$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
* The entity type ID.
......@@ -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 @@
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.
*
* @see \Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider.
......@@ -21,6 +21,16 @@
*/
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}
*/
......
......@@ -24,6 +24,8 @@
* This class provides the following routes for entities, with title and access
* callbacks:
* - canonical
* - add-page
* - add-form
* - edit-form
* - delete-form
*
......@@ -78,8 +80,12 @@ public function getRoutes(EntityTypeInterface $entity_type) {
$entity_type_id = $entity_type->id();
if ($add_route = $this->getAddFormRoute($entity_type)) {
$collection->add("entity.{$entity_type_id}.add_form", $add_route);
if ($add_page_route = $this->getAddPageRoute($entity_type)) {
$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)) {
......@@ -97,6 +103,29 @@ public function getRoutes(EntityTypeInterface $entity_type) {
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.
*
......
<?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() {
'variables' => array('menu_items' => NULL),
'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) {
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
* @{
......
{#
/**
* @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:
foreign_key_name:
type: string
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() {
$bundles = array();
$entity_types = \Drupal::entityManager()->getDefinitions();
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'));
}
}
......
......@@ -7,3 +7,6 @@ view test entity translations:
title: 'View translations of test entities'
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;
/**
* Defines the Test entity bundle configuration entity.
*
* @ConfigEntityType(
* id = "entity_test_bundle",
* label = @Translation("Test entity bundle"),
* handlers = {
* "access" = "\Drupal\Core\Entity\EntityAccessControlHandler",
* "form" = {
* "default" = "\Drupal\Core\Entity\BundleEntityFormBase",
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
* },
* },
* admin_permission = "administer entity_test_with_bundle content",
* config_prefix = "entity_test_bundle",
* bundle_of = "entity_test_with_bundle",
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "label",
* "description",
* },
* links = {
* "add-form" = "/entity_test_bundle/add",
* }
* )
*/
class EntityTestBundle extends ConfigEntityBundleBase implements EntityDescriptionInterface {
/**
* The machine name.
*
* @var string
*/
protected $id;
/**
* The human-readable name.
*
* @var string
*/
protected $label;
/**
* The description.
*
* @var string
*/
protected $description;
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->description;
}
/**
* {@inheritdoc}
*/
public function setDescription($description) {
$this->description = $description;
return $this;
}
}
......@@ -38,7 +38,8 @@
* "langcode" = "langcode",
* },
* links = {
* "add-form" = "/entity_test_mul/add",
* "add-page" = "/entity_test_mul/add",
* "add-form" = "/entity_test_mul/add/{type}",
* "canonical" = "/entity_test_mul/manage/{entity_test_mul}",
* "edit-form" = "/entity_test_mul/manage/{entity_test_mul}/edit",
* "delete-form" = "/entity_test/delete/entity_test_mul/{entity_test_mul}",
......
<?php
/**
* @file
* Contains \Drupal\entity_test\Entity\EntityTestWithBundle.
*/
namespace Drupal\entity_test\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Defines the Test entity with bundle entity class.
*
* @ContentEntityType(
* id = "entity_test_with_bundle",
* label = @Translation("Test entity with bundle"),
* handlers = {
* "list_builder" = "Drupal\entity_test\EntityTestListBuilder",
* "view_builder" = "Drupal\entity_test\EntityTestViewBuilder",
* "access" = "\Drupal\Core\Entity\EntityAccessControlHandler",
* "form" = {
* "default" = "\Drupal\Core\Entity\ContentEntityForm",
* "delete" = "\Drupal\Core\Entity\EntityDeleteForm"
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
* },
* },
* base_table = "entity_test_with_bundle",
* admin_permission = "administer entity_test_with_bundle content",
* persistent_cache = FALSE,
* entity_keys = {
* "id" = "id",
* "uuid" = "uuid",
* "bundle" = "type",
* "label" = "name",
* "langcode" = "langcode",
* },
* bundle_entity_type = "entity_test_bundle",
* links = {
* "canonical" = "/entity_test_with_bundle/{entity_test_with_bundle}",
* "add-page" = "/entity_test_with_bundle/add",
* "add-form" = "/entity_test_with_bundle/add/{entity_test_bundle}",
* "edit-form" = "/entity_test_with_bundle/manage/{entity_test_with_bundle}/edit",
* "delete-form" = "/entity_test_with_bundle/delete/entity_test_with_bundle/{entity_test_with_bundle}",
* },
* )
*/
class EntityTestWithBundle extends ContentEntityBase {
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Name'))
->setDescription(t('The name of the test entity.'))
->setTranslatable(TRUE)
->setSetting('max_length', 32)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'string',
'weight' => -5,
])
->setDisplayOptions('form', [
'type' => 'string_textfield',
'weight' => -5,
]);
return $fields;
}
}
......@@ -25,11 +25,6 @@ public function routes() {
$routes = array();
foreach ($types as $entity_type_id) {
$routes["entity.$entity_type_id.add_form"] = new Route(
"$entity_type_id/add",
array('_entity_form' => "$entity_type_id.default"),
array('_permission' => 'administer entity_test content')
);
$routes["