From d459c6227eff7d0be6ca3e1469d1b53d6f53272d Mon Sep 17 00:00:00 2001
From: Bojan Zivanovic <bojanz@gmail.com>
Date: Sun, 13 Dec 2015 14:31:05 +0100
Subject: [PATCH] Support bundles that aren't provided by entity types, add
 tests.

---
 .travis.yml                                   | 12 ++-
 entity.module                                 | 17 +---
 src/Controller/EntityCreateController.php     | 78 +++++++++++++---
 src/Routing/CreateHtmlRouteProvider.php       | 10 +--
 tests/Functional/CreateUITest.php             | 90 +++++++++++++++++++
 tests/Kernel/EntityRevisionLogTraitTest.php   |  8 ++
 tests/Kernel/RevisionBasicUITest.php          |  8 ++
 .../schema/entity_module_test.schema.yml      | 13 +++
 .../entity_module_test.permissions.yml        |  3 +
 .../src/Entity/EnhancedEntity.php             | 12 ++-
 .../src/Entity/EnhancedEntityBundle.php       | 79 ++++++++++++++++
 11 files changed, 293 insertions(+), 37 deletions(-)
 create mode 100644 tests/Functional/CreateUITest.php
 create mode 100644 tests/modules/entity_module_test/config/schema/entity_module_test.schema.yml
 create mode 100644 tests/modules/entity_module_test/entity_module_test.permissions.yml
 create mode 100644 tests/modules/entity_module_test/src/Entity/EnhancedEntityBundle.php

diff --git a/.travis.yml b/.travis.yml
index 7c84d51..0a4764e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -40,10 +40,20 @@ before_script:
   # Download Drupal 8 core.
   - travis_retry git clone --branch 8.0.x --depth 1 http://git.drupal.org/project/drupal.git
   - cd drupal
+  # Add a patch for BrowserTestBase
+  - curl -o 2636228-6.patch https://www.drupal.org/files/issues/2636228-6.patch
+  - patch -p1 < 2636228-6.patch
 
   # Reference entity in build site.
   - ln -s $TESTDIR modules/entity
 
+  # Start a web server on port 8888, run in the background; wait for
+  # initialization.
+  - nohup php -S localhost:8888 > /dev/null 2>&1 &
+
+  # Export web server URL for browser tests.
+  - export SIMPLETEST_BASE_URL=http://localhost:8888
+
 script:
   # Run the PHPUnit tests which also include the kernel tests.
-  - ./vendor/phpunit/phpunit/phpunit -c ./core/phpunit.xml.dist ./modules/entity
+  - ./vendor/phpunit/phpunit/phpunit -c ./core/phpunit.xml.dist --verbose ./modules/entity
diff --git a/entity.module b/entity.module
index 3ee6637..f184540 100644
--- a/entity.module
+++ b/entity.module
@@ -5,7 +5,6 @@
  * Provides expanded entity APIs.
  */
 
-use Drupal\Core\Link;
 use Drupal\Core\Url;
 
 /**
@@ -17,7 +16,6 @@ function entity_theme() {
       'variables' => [
         'bundles' => [],
         'bundle_type' => NULL,
-        'form_route_name' => NULL,
       ],
       'template' => 'entity-add-list',
     ],
@@ -32,8 +30,7 @@ function entity_theme() {
  * @param array $variables
  *   An associative array containing:
  *   - bundle_type: The entity type of the bundles.
- *   - bundles: An array of bundles.
- *   - form_route_name: The add form route.
+ *   - bundles: An array of bundles with the label, description, add_link keys.
  */
 function template_preprocess_entity_add_list(&$variables) {
   $bundle_type = \Drupal::entityTypeManager()->getDefinition($variables['bundle_type']);
@@ -42,15 +39,9 @@ function template_preprocess_entity_add_list(&$variables) {
     'bundle_type_label' => $bundle_type->getLowercaseLabel(),
   ];
 
-  foreach ($variables['bundles'] as $bundle) {
-    $bundle_id = $bundle->id();
-    $variables['bundles'][$bundle_id] = [
-      'add_link' => Link::createFromRoute($bundle->label(), $variables['form_route_name'], [$bundle_type->id() => $bundle_id]),
+  foreach ($variables['bundles'] as $bundle_name => $bundle_info) {
+    $variables['bundles'][$bundle_name]['description'] = [
+      '#markup' => $bundle_info['description'],
     ];
-    if ($bundle instanceof \Drupal\entity\Entity\EntityDescriptionInterface) {
-      $variables['bundles'][$bundle_id]['description'] = [
-        '#markup' => $bundle->getDescription(),
-      ];
-    }
   }
 }
diff --git a/src/Controller/EntityCreateController.php b/src/Controller/EntityCreateController.php
index d9014fd..bece86b 100644
--- a/src/Controller/EntityCreateController.php
+++ b/src/Controller/EntityCreateController.php
@@ -11,9 +11,10 @@ use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Url;
+use Drupal\Core\Link;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
  * A generic controller for creating entities.
@@ -74,6 +75,7 @@ class EntityCreateController extends ControllerBase {
   public function addPage($entity_type_id, Request $request) {
     $entity_type = $this->entityTypeManager()->getDefinition($entity_type_id);
     $bundle_type = $entity_type->getBundleEntityType();
+    $bundle_key = $entity_type->getKey('bundle');
     $form_route_name = 'entity.' . $entity_type_id . '.add_form';
     $build = [
       '#theme' => 'entity_add_list',
@@ -81,25 +83,32 @@ class EntityCreateController extends ControllerBase {
         'tags' => $entity_type->getListCacheTags(),
       ],
       '#bundle_type' => $bundle_type,
-      '#form_route_name' => $form_route_name,
     ];
-    $bundles = array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type_id));
+    $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id);
     // Filter out the bundles the user doesn't have access to.
     $access_control_handler = $this->entityTypeManager()->getAccessControlHandler($bundle_type);
-    foreach ($bundles as $index => $bundle_name) {
+    foreach ($bundles as $bundle_name => $bundle_info) {
       $access = $access_control_handler->createAccess($bundle_name, NULL, [], TRUE);
       if (!$access->isAllowed()) {
-        unset($bundles[$index]);
+        unset($bundles[$bundle_name]);
       }
       $this->renderer->addCacheableDependency($build, $access);
     }
     // Redirect if there's only one bundle available.
     if (count($bundles) == 1) {
-      $bundle_name = reset($bundles);
-      return $this->redirect($form_route_name, [$bundle_type => $bundle_name]);
+      $bundle_names = array_keys($bundles);
+      $bundle_name = reset($bundle_names);
+      return $this->redirect($form_route_name, [$bundle_key => $bundle_name]);
+    }
+    // Prepare the #bundles array for the template.
+    $bundles = $this->loadBundleDescriptions($bundles, $bundle_type);
+    foreach ($bundles as $bundle_name => $bundle_info) {
+      $build['#bundles'][$bundle_name] = [
+        'label' => $bundle_info['label'],
+        'description' => $bundle_info['description'],
+        'add_link' => Link::createFromRoute($bundle_info['label'], $form_route_name, [$bundle_key => $bundle_name]),
+      ];
     }
-    // The theme function needs the full bundle entities.
-    $build['#bundles'] = $this->entityTypeManager->getStorage($bundle_type)->loadMultiple($bundles);
 
     return $build;
   }
@@ -133,9 +142,14 @@ class EntityCreateController extends ControllerBase {
     $entity_type = $this->entityTypeManager()->getDefinition($entity_type_id);
     $values = [];
     // Entities of this type have bundles, one was provided in the url.
-    if ($bundle_type = $entity_type->getBundleEntityType()) {
-      $bundle_key = $entity_type->getKey('bundle');
-      $values[$bundle_key] = $route_match->getRawParameter($bundle_type);
+    if ($bundle_key = $entity_type->getKey('bundle')) {
+      $bundle_name = $route_match->getRawParameter($bundle_key);
+      $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id);
+      if (empty($bundle_name) || !isset($bundles[$bundle_name])) {
+        // The bundle parameter is invalid.
+        throw new NotFoundHttpException();
+      }
+      $values[$bundle_key] = $bundle_name;
     }
     $entity = $this->entityTypeManager()->getStorage($entity_type_id)->create($values);
 
@@ -155,9 +169,10 @@ class EntityCreateController extends ControllerBase {
    */
   public function addFormTitle($entity_type_id, RouteMatchInterface $route_match) {
     $entity_type = $this->entityTypeManager()->getDefinition($entity_type_id);
+    $bundle_key = $entity_type->getKey('bundle');
     $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id);
-    if (count($bundles) > 1) {
-      $bundle_name = $route_match->getRawParameter($bundle_type);
+    if ($bundle_key && count($bundles) > 1) {
+      $bundle_name = $route_match->getRawParameter($bundle_key);
       $title = $this->t('Add @bundle', ['@bundle' => $bundles[$bundle_name]['label']]);
     }
     else {
@@ -167,4 +182,39 @@ class EntityCreateController extends ControllerBase {
     return $title;
   }
 
+  /**
+   * Expands the bundle information with descriptions, if known.
+   *
+   * @param array $bundles
+   *   An array of bundle information.
+   * @param string $bundle_type
+   *   The id of the bundle entity type.
+   *
+   * @return array
+   *   The expanded array of bundle information.
+   */
+  protected function loadBundleDescriptions(array $bundles, $bundle_type) {
+    // Ensure the presence of the description key.
+    foreach ($bundles as $bundle_name => &$bundle_info) {
+      $bundle_info['description'] = '';
+    }
+    // Only bundles provided by entity types have descriptions.
+    if (empty($bundle_type)) {
+      return $bundles;
+    }
+    $bundle_entity_type = $this->entityTypeManager()->getDefinition($bundle_type);
+    if (!$bundle_entity_type->isSubclassOf('\Drupal\entity\Entity\EntityDescriptionInterface')) {
+      return $bundles;
+    }
+    $bundle_names = array_keys($bundles);
+    $bundle_entities = $this->entityTypeManager->getStorage($bundle_type)->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;
+  }
+
 }
diff --git a/src/Routing/CreateHtmlRouteProvider.php b/src/Routing/CreateHtmlRouteProvider.php
index e3be78a..221b598 100644
--- a/src/Routing/CreateHtmlRouteProvider.php
+++ b/src/Routing/CreateHtmlRouteProvider.php
@@ -41,7 +41,7 @@ class CreateHtmlRouteProvider implements EntityRouteProviderInterface {
   /**
    * Returns the add page route.
    *
-   * Built only for entity types that have bundle entity types.
+   * Built only for entity types that have bundles.
    *
    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
    *   The entity type.
@@ -50,7 +50,7 @@ class CreateHtmlRouteProvider implements EntityRouteProviderInterface {
    *   The generated route, if available.
    */
   protected function addPageRoute(EntityTypeInterface $entity_type) {
-    if ($entity_type->hasLinkTemplate('add-page') && $entity_type->getBundleEntityType()) {
+    if ($entity_type->hasLinkTemplate('add-page') && $entity_type->getKey('bundle')) {
       $route = new Route($entity_type->getLinkTemplate('add-page'));
       $route->setDefault('_controller', '\Drupal\entity\Controller\EntityCreateController::addPage');
       $route->setDefault('_title_callback', '\Drupal\entity\Controller\EntityCreateController::addPageTitle');
@@ -77,12 +77,6 @@ class CreateHtmlRouteProvider implements EntityRouteProviderInterface {
       $route->setDefault('_title_callback', '\Drupal\entity\Controller\EntityCreateController::addFormTitle');
       $route->setDefault('entity_type_id', $entity_type->id());
       $route->setRequirement('_entity_create_access', $entity_type->id());
-      // The route needs a bundle parameter.
-      if ($bundle_type = $entity_type->getBundleEntityType()) {
-        $route->setOption('parameters', [
-          $bundle_type => ['type' => 'entity:' . $bundle_type],
-        ]);
-      }
 
       return $route;
     }
diff --git a/tests/Functional/CreateUITest.php b/tests/Functional/CreateUITest.php
new file mode 100644
index 0000000..5c2022e
--- /dev/null
+++ b/tests/Functional/CreateUITest.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\entity\Functional\CreateUITest.
+ */
+
+namespace Drupal\Tests\entity\Functional;
+
+use Drupal\entity_module_test\Entity\EnhancedEntity;
+use Drupal\entity_module_test\Entity\EnhancedEntityBundle;
+use Drupal\simpletest\BrowserTestBase;
+
+/**
+ * Tests the entity creation UI provided by EntityCreateController.
+ *
+ * @group entity
+ * @runTestsInSeparateProcesses
+ * @preserveGlobalState disabled
+ */
+class CreateUITest extends BrowserTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['entity_module_test', 'user', 'entity'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    EnhancedEntityBundle::create([
+      'id' => 'first',
+      'label' => 'First',
+      'description' => 'The first bundle',
+    ])->save();
+    $account = $this->drupalCreateUser(['administer entity_test_enhanced']);
+    $this->drupalLogin($account);
+  }
+
+  /**
+   * Tests the add page.
+   */
+  public function testAddPage() {
+    // This test revealed that if the first bundle gets created in testAddPage
+    // before drupalGet(), the built cache won't get reset when the second
+    // bundle is created. This is a BrowserTestBase specific bug.
+    // @todo Remove comment when the bug is fixed.
+
+    // When only one bundle exists, the add page should redirect to the form.
+    $this->drupalGet('/entity_test_enhanced/add');
+    $this->assertSession()->addressEquals('/entity_test_enhanced/add/first');
+
+    EnhancedEntityBundle::create([
+      'id' => 'second',
+      'label' => 'Second',
+      'description' => 'The <b>second</b> bundle',
+    ])->save();
+    $this->drupalGet('/entity_test_enhanced/add');
+    $assert = $this->assertSession();
+    $assert->addressEquals('/entity_test_enhanced/add');
+    $assert->statusCodeEquals(200);
+    $assert->elementTextContains('css', '.page-title', 'Add entity test with enhancements');
+    // @todo Bundle links.
+  }
+
+  /**
+   * Tests the add form.
+   */
+  public function testAddForm() {
+    $this->drupalGet('/entity_test_enhanced/add/first');
+    $assert = $this->assertSession();
+    $assert->elementTextContains('css', '.page-title', 'Add entity test with enhancements');
+    $assert->elementExists('css', 'form.entity-test-enhanced-first-add-form');
+
+    // In case of multiple bundles, the current one is a part of the page title.
+    EnhancedEntityBundle::create([
+      'id' => 'second',
+      'label' => 'Second',
+      'description' => 'The <b>second</b> bundle',
+    ])->save();
+    $this->drupalGet('/entity_test_enhanced/add/first');
+    $this->assertSession()->elementTextContains('css', '.page-title', 'Add First');
+  }
+
+}
diff --git a/tests/Kernel/EntityRevisionLogTraitTest.php b/tests/Kernel/EntityRevisionLogTraitTest.php
index d2c7a87..a0b3b38 100644
--- a/tests/Kernel/EntityRevisionLogTraitTest.php
+++ b/tests/Kernel/EntityRevisionLogTraitTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\Tests\entity\Kernel;
 
 use Drupal\entity_module_test\Entity\EnhancedEntity;
+use Drupal\entity_module_test\Entity\EnhancedEntityBundle;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\user\Entity\User;
 
@@ -29,6 +30,12 @@ class EntityRevisionLogTraitTest extends KernelTestBase {
 
     $this->installEntitySchema('user');
     $this->installSchema('system', 'sequences');
+
+    $bundle = EnhancedEntityBundle::create([
+      'id' => 'default',
+      'label' => 'Default',
+    ]);
+    $bundle->save();
   }
 
   public function testEntityRevisionLog() {
@@ -43,6 +50,7 @@ class EntityRevisionLogTraitTest extends KernelTestBase {
 
     /** @var \Drupal\entity\Revision\EntityRevisionLogInterface $entity */
     $entity = EnhancedEntity::create([
+      'type' => 'default',
       'revision_user' => $user->id(),
       'revision_created' => 1447941735,
       'revision_log_message' => 'Test message',
diff --git a/tests/Kernel/RevisionBasicUITest.php b/tests/Kernel/RevisionBasicUITest.php
index e19fdef..b716c66 100644
--- a/tests/Kernel/RevisionBasicUITest.php
+++ b/tests/Kernel/RevisionBasicUITest.php
@@ -8,6 +8,7 @@
 namespace Drupal\Tests\entity\Kernel;
 
 use Drupal\entity_module_test\Entity\EnhancedEntity;
+use Drupal\entity_module_test\Entity\EnhancedEntityBundle;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\user\Entity\Role;
 use Drupal\user\Entity\User;
@@ -33,12 +34,19 @@ class RevisionBasicUITest extends KernelTestBase {
     $this->installEntitySchema('entity_test_enhanced');
     $this->installSchema('system', 'router');
 
+    $bundle = EnhancedEntityBundle::create([
+      'id' => 'default',
+      'label' => 'Default',
+    ]);
+    $bundle->save();
+
     \Drupal::service('router.builder')->rebuild();
   }
 
   public function testRevisionView() {
     $entity = EnhancedEntity::create([
       'name' => 'rev 1',
+      'type' => 'default',
     ]);
     $entity->save();
 
diff --git a/tests/modules/entity_module_test/config/schema/entity_module_test.schema.yml b/tests/modules/entity_module_test/config/schema/entity_module_test.schema.yml
new file mode 100644
index 0000000..c20ef33
--- /dev/null
+++ b/tests/modules/entity_module_test/config/schema/entity_module_test.schema.yml
@@ -0,0 +1,13 @@
+entity_module_test.entity_test_enhanced_bundle.*:
+  type: config_entity
+  label: 'Entity test with enhancments - Bundle'
+  mapping:
+    id:
+      type: string
+      label: 'Type'
+    label:
+      type: label
+      label: 'Label'
+    description:
+      type: text
+      label: 'Description'
diff --git a/tests/modules/entity_module_test/entity_module_test.permissions.yml b/tests/modules/entity_module_test/entity_module_test.permissions.yml
new file mode 100644
index 0000000..b41f84b
--- /dev/null
+++ b/tests/modules/entity_module_test/entity_module_test.permissions.yml
@@ -0,0 +1,3 @@
+'administer entity_test_enhanced':
+  title: 'Administer entity_test_enhanced'
+  'restrict access': TRUE
diff --git a/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php b/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php
index 9cefdac..765c43e 100644
--- a/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php
+++ b/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php
@@ -21,8 +21,14 @@ use Drupal\entity\Revision\EntityRevisionLogTrait;
  *   label = @Translation("Entity test with enhancements"),
  *   handlers = {
  *     "storage" = "\Drupal\Core\Entity\Sql\SqlContentEntityStorage",
+ *     "form" = {
+ *       "add" = "\Drupal\Core\Entity\ContentEntityForm",
+ *       "edit" = "\Drupal\Core\Entity\ContentEntityForm",
+ *       "delete" = "\Drupal\Core\Entity\EntityDeleteForm",
+ *     },
  *     "route_provider" = {
  *       "revision" = "\Drupal\entity\Routing\RevisionRouteProvider",
+ *       "create" = "\Drupal\entity\Routing\CreateHtmlRouteProvider",
  *     },
  *   },
  *   base_table = "entity_test_enhanced",
@@ -34,12 +40,16 @@ use Drupal\entity\Revision\EntityRevisionLogTrait;
  *   admin_permission = "administer entity_test_enhanced",
  *   entity_keys = {
  *     "id" = "id",
+ *     "bundle" = "type",
  *     "revision" = "vid",
  *     "langcode" = "langcode",
  *   },
  *   links = {
+ *     "add-page" = "/entity_test_enhanced/add",
+ *     "add-form" = "/entity_test_enhanced/add/{type}",
  *     "revision" = "/entity_test_enhanced/{entity_test_enhanced}/revisions/{entity_test_enhanced_revision}/view",
- *   }
+ *   },
+ *   bundle_entity_type = "entity_test_enhanced_bundle"
  * )
  */
 class EnhancedEntity extends ContentEntityBase {
diff --git a/tests/modules/entity_module_test/src/Entity/EnhancedEntityBundle.php b/tests/modules/entity_module_test/src/Entity/EnhancedEntityBundle.php
new file mode 100644
index 0000000..afe49a7
--- /dev/null
+++ b/tests/modules/entity_module_test/src/Entity/EnhancedEntityBundle.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\entity_module_test\Entity\EnhancedEntityBundle.
+ */
+
+namespace Drupal\entity_module_test\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
+use Drupal\entity\Entity\EntityDescriptionInterface;
+
+/**
+ * Provides bundles for the test entity.
+ *
+ * @ConfigEntityType(
+ *   id = "entity_test_enhanced_bundle",
+ *   label = @Translation("Entity test with enhancments - Bundle"),
+ *   handlers = {
+ *     "route_provider" = {
+ *       "create" = "\Drupal\entity\Routing\CreateHtmlRouteProvider",
+ *     },
+ *   },
+ *   admin_permission = "administer entity_test_enhanced",
+ *   config_prefix = "entity_test_enhanced_bundle",
+ *   bundle_of = "entity_test_enhanced",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label"
+ *   },
+ *   config_export = {
+ *     "id",
+ *     "label",
+ *     "description"
+ *   },
+ *   links = {
+ *     "add-form" = "/entity_test_enhanced_bundle/add",
+ *   },
+ * )
+ */
+class EnhancedEntityBundle extends ConfigEntityBundleBase implements EntityDescriptionInterface {
+
+  /**
+   * The bundle ID.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The bundle label.
+   *
+   * @var string
+   */
+  protected $label;
+
+  /**
+   * The bundle description.
+   *
+   * @var string
+   */
+  protected $description;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->description;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setDescription($description) {
+    $this->description = $description;
+    return $this;
+  }
+
+}
-- 
GitLab