EntityViewBuilderTest.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. <?php
  2. namespace Drupal\KernelTests\Core\Entity;
  3. use Drupal\Core\Entity\EntityViewBuilder;
  4. use Drupal\Core\Language\LanguageInterface;
  5. use Drupal\Core\Cache\Cache;
  6. use Drupal\Core\Entity\Entity\EntityViewDisplay;
  7. use Drupal\Core\Entity\Entity\EntityViewMode;
  8. use Drupal\field\Entity\FieldConfig;
  9. use Drupal\field\Entity\FieldStorageConfig;
  10. use Drupal\language\Entity\ConfigurableLanguage;
  11. use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
  12. use Drupal\user\Entity\Role;
  13. use Drupal\user\RoleInterface;
  14. /**
  15. * Tests the entity view builder.
  16. *
  17. * @group Entity
  18. */
  19. class EntityViewBuilderTest extends EntityKernelTestBase {
  20. use EntityReferenceTestTrait;
  21. /**
  22. * {@inheritdoc}
  23. */
  24. protected function setUp() {
  25. parent::setUp();
  26. $this->installConfig(['user', 'entity_test']);
  27. // Give anonymous users permission to view test entities.
  28. Role::load(RoleInterface::ANONYMOUS_ID)
  29. ->grantPermission('view test entity')
  30. ->save();
  31. }
  32. /**
  33. * Tests entity render cache handling.
  34. */
  35. public function testEntityViewBuilderCache() {
  36. /** @var \Drupal\Core\Render\RendererInterface $renderer */
  37. $renderer = $this->container->get('renderer');
  38. $cache_contexts_manager = \Drupal::service("cache_contexts_manager");
  39. $cache = \Drupal::cache();
  40. // Force a request via GET so we can get drupal_render() cache working.
  41. $request = \Drupal::request();
  42. $request_method = $request->server->get('REQUEST_METHOD');
  43. $request->setMethod('GET');
  44. $entity_test = $this->createTestEntity('entity_test');
  45. // Test that new entities (before they are saved for the first time) do not
  46. // generate a cache entry.
  47. $build = $this->container->get('entity_type.manager')->getViewBuilder('entity_test')->view($entity_test, 'full');
  48. $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts', 'max-age'], 'The render array element of new (unsaved) entities is not cached, but does have cache tags set.');
  49. // Get a fully built entity view render array.
  50. $entity_test->save();
  51. $build = $this->container->get('entity_type.manager')->getViewBuilder('entity_test')->view($entity_test, 'full');
  52. $cid_parts = array_merge($build['#cache']['keys'], $cache_contexts_manager->convertTokensToKeys(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'])->getKeys());
  53. $cid = implode(':', $cid_parts);
  54. $bin = $build['#cache']['bin'];
  55. // Mock the build array to not require the theme registry.
  56. unset($build['#theme']);
  57. $build['#markup'] = 'entity_render_test';
  58. // Test that a cache entry is created.
  59. $renderer->renderRoot($build);
  60. $this->assertNotEmpty($this->container->get('cache.' . $bin)->get($cid), 'The entity render element has been cached.');
  61. // Re-save the entity and check that the cache entry has been deleted.
  62. $cache->set('kittens', 'Kitten data', Cache::PERMANENT, $build['#cache']['tags']);
  63. $entity_test->save();
  64. $this->assertFalse($this->container->get('cache.' . $bin)->get($cid), 'The entity render cache has been cleared when the entity was saved.');
  65. $this->assertFalse($cache->get('kittens'), 'The entity saving has invalidated cache tags.');
  66. // Rebuild the render array (creating a new cache entry in the process) and
  67. // delete the entity to check the cache entry is deleted.
  68. unset($build['#printed']);
  69. $renderer->renderRoot($build);
  70. $this->assertNotEmpty($this->container->get('cache.' . $bin)->get($cid), 'The entity render element has been cached.');
  71. $entity_test->delete();
  72. $this->assertFalse($this->container->get('cache.' . $bin)->get($cid), 'The entity render cache has been cleared when the entity was deleted.');
  73. // Restore the previous request method.
  74. $request->setMethod($request_method);
  75. }
  76. /**
  77. * Tests entity render cache with references.
  78. */
  79. public function testEntityViewBuilderCacheWithReferences() {
  80. /** @var \Drupal\Core\Render\RendererInterface $renderer */
  81. $renderer = $this->container->get('renderer');
  82. $cache_contexts_manager = \Drupal::service("cache_contexts_manager");
  83. // Force a request via GET so we can get drupal_render() cache working.
  84. $request = \Drupal::request();
  85. $request_method = $request->server->get('REQUEST_METHOD');
  86. $request->setMethod('GET');
  87. // Create an entity reference field and an entity that will be referenced.
  88. $this->createEntityReferenceField('entity_test', 'entity_test', 'reference_field', 'Reference', 'entity_test');
  89. \Drupal::service('entity_display.repository')
  90. ->getViewDisplay('entity_test', 'entity_test', 'full')
  91. ->setComponent('reference_field', [
  92. 'type' => 'entity_reference_entity_view',
  93. 'settings' => ['link' => FALSE],
  94. ])->save();
  95. $entity_test_reference = $this->createTestEntity('entity_test');
  96. $entity_test_reference->save();
  97. // Get a fully built entity view render array for the referenced entity.
  98. $build = $this->container->get('entity_type.manager')->getViewBuilder('entity_test')->view($entity_test_reference, 'full');
  99. $cid_parts = array_merge($build['#cache']['keys'], $cache_contexts_manager->convertTokensToKeys(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'])->getKeys());
  100. $cid_reference = implode(':', $cid_parts);
  101. $bin_reference = $build['#cache']['bin'];
  102. // Mock the build array to not require the theme registry.
  103. unset($build['#theme']);
  104. $build['#markup'] = 'entity_render_test';
  105. $renderer->renderRoot($build);
  106. // Test that a cache entry was created for the referenced entity.
  107. $this->assertNotEmpty($this->container->get('cache.' . $bin_reference)->get($cid_reference), 'The entity render element for the referenced entity has been cached.');
  108. // Create another entity that references the first one.
  109. $entity_test = $this->createTestEntity('entity_test');
  110. $entity_test->reference_field->entity = $entity_test_reference;
  111. $entity_test->save();
  112. // Get a fully built entity view render array.
  113. $build = $this->container->get('entity_type.manager')->getViewBuilder('entity_test')->view($entity_test, 'full');
  114. $cid_parts = array_merge($build['#cache']['keys'], $cache_contexts_manager->convertTokensToKeys(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'])->getKeys());
  115. $cid = implode(':', $cid_parts);
  116. $bin = $build['#cache']['bin'];
  117. // Mock the build array to not require the theme registry.
  118. unset($build['#theme']);
  119. $build['#markup'] = 'entity_render_test';
  120. $renderer->renderRoot($build);
  121. // Test that a cache entry is created.
  122. $this->assertNotEmpty($this->container->get('cache.' . $bin)->get($cid), 'The entity render element has been cached.');
  123. // Save the entity and verify that both cache entries have been deleted.
  124. $entity_test_reference->save();
  125. $this->assertFalse($this->container->get('cache.' . $bin)->get($cid), 'The entity render cache has been cleared when the entity was deleted.');
  126. $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.');
  127. // Restore the previous request method.
  128. $request->setMethod($request_method);
  129. }
  130. /**
  131. * Tests entity render cache toggling.
  132. */
  133. public function testEntityViewBuilderCacheToggling() {
  134. $entity_test = $this->createTestEntity('entity_test');
  135. $entity_test->save();
  136. // Test a view mode in default conditions: render caching is enabled for
  137. // the entity type and the view mode.
  138. $build = $this->container->get('entity_type.manager')->getViewBuilder('entity_test')->view($entity_test, 'full');
  139. $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts', 'max-age', 'keys', 'bin'], 'A view mode with render cache enabled has the correct output (cache tags, keys, contexts, max-age and bin).');
  140. // Test that a view mode can opt out of render caching.
  141. $build = $this->container->get('entity_type.manager')->getViewBuilder('entity_test')->view($entity_test, 'test');
  142. $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts', 'max-age'], 'A view mode with render cache disabled has the correct output (only cache tags, contexts and max-age).');
  143. // Test that an entity type can opt out of render caching completely.
  144. $this->installEntitySchema('entity_test_label');
  145. $entity_test_no_cache = $this->createTestEntity('entity_test_label');
  146. $entity_test_no_cache->save();
  147. $build = $this->container->get('entity_type.manager')->getViewBuilder('entity_test_label')->view($entity_test_no_cache, 'full');
  148. $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts', 'max-age'], 'An entity type can opt out of render caching regardless of view mode configuration, but always has cache tags, contexts and max-age set.');
  149. }
  150. /**
  151. * Tests weighting of display components.
  152. */
  153. public function testEntityViewBuilderWeight() {
  154. /** @var \Drupal\Core\Render\RendererInterface $renderer */
  155. $renderer = $this->container->get('renderer');
  156. // Set a weight for the label component.
  157. \Drupal::service('entity_display.repository')
  158. ->getViewDisplay('entity_test', 'entity_test', 'full')
  159. ->setComponent('label', ['weight' => 20])
  160. ->save();
  161. // Create and build a test entity.
  162. $entity_test = $this->createTestEntity('entity_test');
  163. $view = $this->container->get('entity_type.manager')->getViewBuilder('entity_test')->view($entity_test, 'full');
  164. $renderer->renderRoot($view);
  165. // Check that the weight is respected.
  166. $this->assertEqual($view['label']['#weight'], 20, 'The weight of a display component is respected.');
  167. }
  168. /**
  169. * Tests EntityViewBuilder::viewField() language awareness.
  170. */
  171. public function testViewField() {
  172. // Allow access to view translations as well.
  173. Role::load(RoleInterface::ANONYMOUS_ID)
  174. ->grantPermission('view test entity translations')
  175. ->save();
  176. $this->enableModules([
  177. 'language',
  178. 'content_translation',
  179. ]);
  180. $this->installEntitySchema('entity_test_mul');
  181. $en = ConfigurableLanguage::create(['id' => 'en']);
  182. $en->save();
  183. $es = ConfigurableLanguage::create(['id' => 'es']);
  184. $es->save();
  185. $this->container->get('content_translation.manager')->setEnabled('entity_test_mul', 'entity_test_mul', TRUE);
  186. $this->createEntityReferenceField('entity_test_mul', 'entity_test_mul', 'reference_field', 'Reference', 'entity_test_mul');
  187. // Make the entity reference field non-translatable to confirm it still
  188. // renders the correct language when displayed as an entity reference.
  189. $field = FieldConfig::loadByName('entity_test_mul', 'entity_test_mul', 'reference_field');
  190. $field->set('translatable', FALSE)->save();
  191. // Create fields and displays for the test entity.
  192. FieldStorageConfig::create([
  193. 'field_name' => 'text',
  194. 'entity_type' => 'entity_test_mul',
  195. 'type' => 'string',
  196. ])->save();
  197. FieldConfig::create([
  198. 'field_name' => 'text',
  199. 'entity_type' => 'entity_test_mul',
  200. 'bundle' => 'entity_test_mul',
  201. 'label' => 'Translated text',
  202. 'translatable' => TRUE,
  203. ])->save();
  204. EntityViewMode::create([
  205. 'id' => 'entity_test_mul.full',
  206. 'targetEntityType' => 'entity_test_mul',
  207. 'status' => FALSE,
  208. 'enabled' => TRUE,
  209. 'label' => 'Full',
  210. ])->save();
  211. $display = EntityViewDisplay::create([
  212. 'targetEntityType' => 'entity_test_mul',
  213. 'bundle' => 'entity_test_mul',
  214. 'mode' => 'full',
  215. 'label' => 'My view mode',
  216. 'status' => TRUE,
  217. ])
  218. ->setComponent('reference_field', [
  219. 'type' => 'entity_reference_entity_view',
  220. 'settings' => [
  221. 'view_mode' => 'full',
  222. ],
  223. ])
  224. ->setComponent('text', [
  225. 'type' => 'string',
  226. 'region' => 'content',
  227. ]);
  228. $display->save();
  229. // Create the entity that will be displayed in the entity reference field
  230. // of the main entity.
  231. $referenced_entity = $this->createTestEntity('entity_test_mul');
  232. $referenced_entity->addTranslation('es', $referenced_entity->getTranslation('en')->toArray());
  233. $referenced_entity->set('text', 'Text in English');
  234. $referenced_entity->getTranslation('es')->text = 'Text in Spanish';
  235. // The entity that will reference $referenced_entity.
  236. $main_entity = $this->createTestEntity('entity_test_mul');
  237. $main_entity->addTranslation('es', $main_entity->getTranslation('en')->toArray());
  238. $main_entity->set('reference_field', $referenced_entity);
  239. $view_builder = $this->container->get('entity_type.manager')->getViewBuilder('entity_test_mul');
  240. $renderer = $this->container->get('renderer');
  241. // Build the view for the reference field and render in English - the site
  242. // default. Confirm the reference field shows the content of the English
  243. // translation.
  244. $reference_field = $main_entity->get('reference_field');
  245. $reference_field_array_english = $view_builder->viewField($reference_field, 'full');
  246. $rendered_reference_field_english = $renderer->renderRoot($reference_field_array_english);
  247. $this->assertStringContainsString('Text in English', (string) $rendered_reference_field_english);
  248. // Change the default language to Spanish and render the reference
  249. // field again. It should display the contents of the Spanish translation.
  250. \Drupal::service('language.default')->set($es);
  251. \Drupal::languageManager()->reset();
  252. \Drupal::languageManager()->getCurrentLanguage();
  253. $reference_field_array_spanish = $view_builder->viewField($reference_field, 'full');
  254. $rendered_reference_field_spanish = $renderer->renderRoot($reference_field_array_spanish);
  255. $this->assertStringContainsString('Text in Spanish', (string) $rendered_reference_field_spanish);
  256. }
  257. /**
  258. * Creates an entity for testing.
  259. *
  260. * @param string $entity_type
  261. * The entity type.
  262. *
  263. * @return \Drupal\Core\Entity\EntityInterface
  264. * The created entity.
  265. */
  266. protected function createTestEntity($entity_type) {
  267. $data = [
  268. 'bundle' => $entity_type,
  269. 'name' => $this->randomMachineName(),
  270. ];
  271. return $this->container->get('entity_type.manager')->getStorage($entity_type)->create($data);
  272. }
  273. /**
  274. * Tests that viewing an entity without template does not specify #theme.
  275. */
  276. public function testNoTemplate() {
  277. // Ensure that an entity type without explicit view builder uses the
  278. // default.
  279. $entity_type_manager = \Drupal::entityTypeManager();
  280. $entity_type = $entity_type_manager->getDefinition('entity_test_base_field_display');
  281. $this->assertTrue($entity_type->hasViewBuilderClass());
  282. $this->assertEquals(EntityViewBuilder::class, $entity_type->getViewBuilderClass());
  283. // Ensure that an entity without matching template does not have a #theme
  284. // key.
  285. $entity = $this->createTestEntity('entity_test');
  286. $build = $entity_type_manager->getViewBuilder('entity_test')->view($entity);
  287. $this->assertEquals($entity, $build['#entity_test']);
  288. $this->assertArrayNotHasKey('#theme', $build);
  289. }
  290. }