EntityViewBuilder.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. <?php
  2. namespace Drupal\Core\Entity;
  3. use Drupal\Component\Utility\Crypt;
  4. use Drupal\Core\Cache\Cache;
  5. use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
  6. use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
  7. use Drupal\Core\Entity\Entity\EntityViewDisplay;
  8. use Drupal\Core\Field\FieldItemInterface;
  9. use Drupal\Core\Field\FieldItemListInterface;
  10. use Drupal\Core\Language\LanguageManagerInterface;
  11. use Drupal\Core\Render\Element;
  12. use Drupal\Core\Security\TrustedCallbackInterface;
  13. use Drupal\Core\Theme\Registry;
  14. use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface;
  15. use Symfony\Component\DependencyInjection\ContainerInterface;
  16. /**
  17. * Base class for entity view builders.
  18. *
  19. * @ingroup entity_api
  20. */
  21. class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterface, EntityViewBuilderInterface, TrustedCallbackInterface {
  22. use DeprecatedServicePropertyTrait;
  23. /**
  24. * {@inheritdoc}
  25. */
  26. protected $deprecatedProperties = ['entityManager' => 'entity.manager'];
  27. /**
  28. * The type of entities for which this view builder is instantiated.
  29. *
  30. * @var string
  31. */
  32. protected $entityTypeId;
  33. /**
  34. * Information about the entity type.
  35. *
  36. * @var \Drupal\Core\Entity\EntityTypeInterface
  37. */
  38. protected $entityType;
  39. /**
  40. * The entity repository service.
  41. *
  42. * @var \Drupal\Core\Entity\EntityRepositoryInterface
  43. */
  44. protected $entityRepository;
  45. /**
  46. * The entity display repository.
  47. *
  48. * @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
  49. */
  50. protected $entityDisplayRepository;
  51. /**
  52. * The cache bin used to store the render cache.
  53. *
  54. * @var string
  55. */
  56. protected $cacheBin = 'render';
  57. /**
  58. * The language manager.
  59. *
  60. * @var \Drupal\Core\Language\LanguageManagerInterface
  61. */
  62. protected $languageManager;
  63. /**
  64. * The theme registry.
  65. *
  66. * @var \Drupal\Core\Theme\Registry
  67. */
  68. protected $themeRegistry;
  69. /**
  70. * The EntityViewDisplay objects created for individual field rendering.
  71. *
  72. * @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface[]
  73. *
  74. * @see \Drupal\Core\Entity\EntityViewBuilder::getSingleFieldDisplay()
  75. */
  76. protected $singleFieldDisplays;
  77. /**
  78. * Constructs a new EntityViewBuilder.
  79. *
  80. * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
  81. * The entity type definition.
  82. * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
  83. * The entity repository service.
  84. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
  85. * The language manager.
  86. * @param \Drupal\Core\Theme\Registry $theme_registry
  87. * The theme registry.
  88. * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
  89. * The entity display repository.
  90. */
  91. public function __construct(EntityTypeInterface $entity_type, EntityRepositoryInterface $entity_repository, LanguageManagerInterface $language_manager, Registry $theme_registry = NULL, EntityDisplayRepositoryInterface $entity_display_repository = NULL) {
  92. $this->entityTypeId = $entity_type->id();
  93. $this->entityType = $entity_type;
  94. $this->entityRepository = $entity_repository;
  95. $this->languageManager = $language_manager;
  96. $this->themeRegistry = $theme_registry ?: \Drupal::service('theme.registry');
  97. if (!$entity_display_repository) {
  98. @trigger_error('Calling EntityViewBuilder::__construct() with the $entity_repository argument is supported in drupal:8.7.0 and will be required before drupal:9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
  99. $entity_display_repository = \Drupal::service('entity_display.repository');
  100. }
  101. $this->entityDisplayRepository = $entity_display_repository;
  102. }
  103. /**
  104. * {@inheritdoc}
  105. */
  106. public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
  107. return new static(
  108. $entity_type,
  109. $container->get('entity.repository'),
  110. $container->get('language_manager'),
  111. $container->get('theme.registry'),
  112. $container->get('entity_display.repository')
  113. );
  114. }
  115. /**
  116. * {@inheritdoc}
  117. */
  118. public function view(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
  119. $build_list = $this->viewMultiple([$entity], $view_mode, $langcode);
  120. // The default ::buildMultiple() #pre_render callback won't run, because we
  121. // extract a child element of the default renderable array. Thus we must
  122. // assign an alternative #pre_render callback that applies the necessary
  123. // transformations and then still calls ::buildMultiple().
  124. $build = $build_list[0];
  125. $build['#pre_render'][] = [$this, 'build'];
  126. return $build;
  127. }
  128. /**
  129. * {@inheritdoc}
  130. */
  131. public static function trustedCallbacks() {
  132. return ['build', 'buildMultiple'];
  133. }
  134. /**
  135. * {@inheritdoc}
  136. */
  137. public function viewMultiple(array $entities = [], $view_mode = 'full', $langcode = NULL) {
  138. $build_list = [
  139. '#sorted' => TRUE,
  140. '#pre_render' => [[$this, 'buildMultiple']],
  141. ];
  142. $weight = 0;
  143. foreach ($entities as $key => $entity) {
  144. // Ensure that from now on we are dealing with the proper translation
  145. // object.
  146. $entity = $this->entityRepository->getTranslationFromContext($entity, $langcode);
  147. // Set build defaults.
  148. $build_list[$key] = $this->getBuildDefaults($entity, $view_mode);
  149. $entityType = $this->entityTypeId;
  150. $this->moduleHandler()->alter([$entityType . '_build_defaults', 'entity_build_defaults'], $build_list[$key], $entity, $view_mode);
  151. $build_list[$key]['#weight'] = $weight++;
  152. }
  153. return $build_list;
  154. }
  155. /**
  156. * Provides entity-specific defaults to the build process.
  157. *
  158. * @param \Drupal\Core\Entity\EntityInterface $entity
  159. * The entity for which the defaults should be provided.
  160. * @param string $view_mode
  161. * The view mode that should be used.
  162. *
  163. * @return array
  164. */
  165. protected function getBuildDefaults(EntityInterface $entity, $view_mode) {
  166. // Allow modules to change the view mode.
  167. $context = [];
  168. $this->moduleHandler()->alter('entity_view_mode', $view_mode, $entity, $context);
  169. $build = [
  170. "#{$this->entityTypeId}" => $entity,
  171. '#view_mode' => $view_mode,
  172. // Collect cache defaults for this entity.
  173. '#cache' => [
  174. 'tags' => Cache::mergeTags($this->getCacheTags(), $entity->getCacheTags()),
  175. 'contexts' => $entity->getCacheContexts(),
  176. 'max-age' => $entity->getCacheMaxAge(),
  177. ],
  178. ];
  179. // Add the default #theme key if a template exists for it.
  180. if ($this->themeRegistry->getRuntime()->has($this->entityTypeId)) {
  181. $build['#theme'] = $this->entityTypeId;
  182. }
  183. // Cache the rendered output if permitted by the view mode and global entity
  184. // type configuration.
  185. if ($this->isViewModeCacheable($view_mode) && !$entity->isNew() && $entity->isDefaultRevision() && $this->entityType->isRenderCacheable()) {
  186. $build['#cache'] += [
  187. 'keys' => [
  188. 'entity_view',
  189. $this->entityTypeId,
  190. $entity->id(),
  191. $view_mode,
  192. ],
  193. 'bin' => $this->cacheBin,
  194. ];
  195. if ($entity instanceof TranslatableDataInterface && count($entity->getTranslationLanguages()) > 1) {
  196. $build['#cache']['keys'][] = $entity->language()->getId();
  197. }
  198. }
  199. return $build;
  200. }
  201. /**
  202. * Builds an entity's view; augments entity defaults.
  203. *
  204. * This function is assigned as a #pre_render callback in ::view().
  205. *
  206. * It transforms the renderable array for a single entity to the same
  207. * structure as if we were rendering multiple entities, and then calls the
  208. * default ::buildMultiple() #pre_render callback.
  209. *
  210. * @param array $build
  211. * A renderable array containing build information and context for an entity
  212. * view.
  213. *
  214. * @return array
  215. * The updated renderable array.
  216. *
  217. * @see \Drupal\Core\Render\RendererInterface::render()
  218. */
  219. public function build(array $build) {
  220. $build_list = [$build];
  221. $build_list = $this->buildMultiple($build_list);
  222. return $build_list[0];
  223. }
  224. /**
  225. * Builds multiple entities' views; augments entity defaults.
  226. *
  227. * This function is assigned as a #pre_render callback in ::viewMultiple().
  228. *
  229. * By delaying the building of an entity until the #pre_render processing in
  230. * drupal_render(), the processing cost of assembling an entity's renderable
  231. * array is saved on cache-hit requests.
  232. *
  233. * @param array $build_list
  234. * A renderable array containing build information and context for an
  235. * entity view.
  236. *
  237. * @return array
  238. * The updated renderable array.
  239. *
  240. * @see \Drupal\Core\Render\RendererInterface::render()
  241. */
  242. public function buildMultiple(array $build_list) {
  243. // Build the view modes and display objects.
  244. $view_modes = [];
  245. $entity_type_key = "#{$this->entityTypeId}";
  246. $view_hook = "{$this->entityTypeId}_view";
  247. // Find the keys for the ContentEntities in the build; Store entities for
  248. // rendering by view_mode.
  249. $children = Element::children($build_list);
  250. foreach ($children as $key) {
  251. if (isset($build_list[$key][$entity_type_key])) {
  252. $entity = $build_list[$key][$entity_type_key];
  253. if ($entity instanceof FieldableEntityInterface) {
  254. $view_modes[$build_list[$key]['#view_mode']][$key] = $entity;
  255. }
  256. }
  257. }
  258. // Build content for the displays represented by the entities.
  259. foreach ($view_modes as $view_mode => $view_mode_entities) {
  260. $displays = EntityViewDisplay::collectRenderDisplays($view_mode_entities, $view_mode);
  261. $this->buildComponents($build_list, $view_mode_entities, $displays, $view_mode);
  262. foreach (array_keys($view_mode_entities) as $key) {
  263. // Allow for alterations while building, before rendering.
  264. $entity = $build_list[$key][$entity_type_key];
  265. $display = $displays[$entity->bundle()];
  266. $this->moduleHandler()->invokeAll($view_hook, [&$build_list[$key], $entity, $display, $view_mode]);
  267. $this->moduleHandler()->invokeAll('entity_view', [&$build_list[$key], $entity, $display, $view_mode]);
  268. $this->addContextualLinks($build_list[$key], $entity);
  269. $this->alterBuild($build_list[$key], $entity, $display, $view_mode);
  270. // Assign the weights configured in the display.
  271. // @todo: Once https://www.drupal.org/node/1875974 provides the missing
  272. // API, only do it for 'extra fields', since other components have
  273. // been taken care of in EntityViewDisplay::buildMultiple().
  274. foreach ($display->getComponents() as $name => $options) {
  275. if (isset($build_list[$key][$name])) {
  276. $build_list[$key][$name]['#weight'] = $options['weight'];
  277. }
  278. }
  279. // Allow modules to modify the render array.
  280. $this->moduleHandler()->alter([$view_hook, 'entity_view'], $build_list[$key], $entity, $display);
  281. }
  282. }
  283. return $build_list;
  284. }
  285. /**
  286. * {@inheritdoc}
  287. */
  288. public function buildComponents(array &$build, array $entities, array $displays, $view_mode) {
  289. $entities_by_bundle = [];
  290. foreach ($entities as $id => $entity) {
  291. // Initialize the field item attributes for the fields being displayed.
  292. // The entity can include fields that are not displayed, and the display
  293. // can include components that are not fields, so we want to act on the
  294. // intersection. However, the entity can have many more fields than are
  295. // displayed, so we avoid the cost of calling $entity->getProperties()
  296. // by iterating the intersection as follows.
  297. foreach ($displays[$entity->bundle()]->getComponents() as $name => $options) {
  298. if ($entity->hasField($name)) {
  299. foreach ($entity->get($name) as $item) {
  300. $item->_attributes = [];
  301. }
  302. }
  303. }
  304. // Group the entities by bundle.
  305. $entities_by_bundle[$entity->bundle()][$id] = $entity;
  306. }
  307. // Invoke hook_entity_prepare_view().
  308. $this->moduleHandler()->invokeAll('entity_prepare_view', [$this->entityTypeId, $entities, $displays, $view_mode]);
  309. // Let the displays build their render arrays.
  310. foreach ($entities_by_bundle as $bundle => $bundle_entities) {
  311. $display_build = $displays[$bundle]->buildMultiple($bundle_entities);
  312. foreach ($bundle_entities as $id => $entity) {
  313. $build[$id] += $display_build[$id];
  314. }
  315. }
  316. }
  317. /**
  318. * Add contextual links.
  319. *
  320. * @param array $build
  321. * The render array that is being created.
  322. * @param \Drupal\Core\Entity\EntityInterface $entity
  323. * The entity to be prepared.
  324. */
  325. protected function addContextualLinks(array &$build, EntityInterface $entity) {
  326. if ($entity->isNew()) {
  327. return;
  328. }
  329. $key = $entity->getEntityTypeId();
  330. $rel = 'canonical';
  331. if ($entity instanceof ContentEntityInterface && !$entity->isDefaultRevision()) {
  332. $rel = 'revision';
  333. $key .= '_revision';
  334. }
  335. if ($entity->hasLinkTemplate($rel)) {
  336. $build['#contextual_links'][$key] = [
  337. 'route_parameters' => $entity->toUrl($rel)->getRouteParameters(),
  338. ];
  339. if ($entity instanceof EntityChangedInterface) {
  340. $build['#contextual_links'][$key]['metadata'] = [
  341. 'changed' => $entity->getChangedTime(),
  342. ];
  343. }
  344. }
  345. }
  346. /**
  347. * Specific per-entity building.
  348. *
  349. * @param array $build
  350. * The render array that is being created.
  351. * @param \Drupal\Core\Entity\EntityInterface $entity
  352. * The entity to be prepared.
  353. * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display
  354. * The entity view display holding the display options configured for the
  355. * entity components.
  356. * @param string $view_mode
  357. * The view mode that should be used to prepare the entity.
  358. */
  359. protected function alterBuild(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {}
  360. /**
  361. * {@inheritdoc}
  362. */
  363. public function getCacheTags() {
  364. return [$this->entityTypeId . '_view'];
  365. }
  366. /**
  367. * {@inheritdoc}
  368. */
  369. public function resetCache(array $entities = NULL) {
  370. // If no set of specific entities is provided, invalidate the entity view
  371. // builder's cache tag. This will invalidate all entities rendered by this
  372. // view builder.
  373. // Otherwise, if a set of specific entities is provided, invalidate those
  374. // specific entities only, plus their list cache tags, because any lists in
  375. // which these entities are rendered, must be invalidated as well. However,
  376. // even in this case, we might invalidate more cache items than necessary.
  377. // When we have a way to invalidate only those cache items that have both
  378. // the individual entity's cache tag and the view builder's cache tag, we'll
  379. // be able to optimize this further.
  380. if (isset($entities)) {
  381. $tags = [];
  382. foreach ($entities as $entity) {
  383. $tags = Cache::mergeTags($tags, $entity->getCacheTags());
  384. $tags = Cache::mergeTags($tags, $entity->getEntityType()->getListCacheTags());
  385. }
  386. Cache::invalidateTags($tags);
  387. }
  388. else {
  389. Cache::invalidateTags($this->getCacheTags());
  390. }
  391. }
  392. /**
  393. * Determines whether the view mode is cacheable.
  394. *
  395. * @param string $view_mode
  396. * Name of the view mode that should be rendered.
  397. *
  398. * @return bool
  399. * TRUE if the view mode can be cached, FALSE otherwise.
  400. */
  401. protected function isViewModeCacheable($view_mode) {
  402. if ($view_mode == 'default') {
  403. // The 'default' is not an actual view mode.
  404. return TRUE;
  405. }
  406. $view_modes_info = $this->entityDisplayRepository->getViewModes($this->entityTypeId);
  407. return !empty($view_modes_info[$view_mode]['cache']);
  408. }
  409. /**
  410. * {@inheritdoc}
  411. */
  412. public function viewField(FieldItemListInterface $items, $display_options = []) {
  413. /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
  414. $entity = $items->getEntity();
  415. // If the field is not translatable and the entity is, then the field item
  416. // list always points to the default translation of the entity. Attempt to
  417. // fetch it in the current content language.
  418. if (!$items->getFieldDefinition()->isTranslatable() && $entity->isTranslatable()) {
  419. $entity = $this->entityRepository->getTranslationFromContext($entity);
  420. }
  421. $field_name = $items->getFieldDefinition()->getName();
  422. $display = $this->getSingleFieldDisplay($entity, $field_name, $display_options);
  423. $output = [];
  424. $build = $display->build($entity);
  425. if (isset($build[$field_name])) {
  426. $output = $build[$field_name];
  427. }
  428. return $output;
  429. }
  430. /**
  431. * {@inheritdoc}
  432. */
  433. public function viewFieldItem(FieldItemInterface $item, $display = []) {
  434. $entity = $item->getEntity();
  435. $field_name = $item->getFieldDefinition()->getName();
  436. // Clone the entity since we are going to modify field values.
  437. $clone = clone $entity;
  438. // Push the item as the single value for the field, and defer to viewField()
  439. // to build the render array for the whole list.
  440. $clone->{$field_name}->setValue([$item->getValue()]);
  441. $elements = $this->viewField($clone->{$field_name}, $display);
  442. // Extract the part of the render array we need.
  443. $output = isset($elements[0]) ? $elements[0] : [];
  444. if (isset($elements['#access'])) {
  445. $output['#access'] = $elements['#access'];
  446. }
  447. return $output;
  448. }
  449. /**
  450. * Gets an EntityViewDisplay for rendering an individual field.
  451. *
  452. * @param \Drupal\Core\Entity\EntityInterface $entity
  453. * The entity.
  454. * @param string $field_name
  455. * The field name.
  456. * @param string|array $display_options
  457. * The display options passed to the viewField() method.
  458. *
  459. * @return \Drupal\Core\Entity\Display\EntityViewDisplayInterface
  460. */
  461. protected function getSingleFieldDisplay($entity, $field_name, $display_options) {
  462. if (is_string($display_options)) {
  463. // View mode: use the Display configured for the view mode.
  464. $view_mode = $display_options;
  465. $display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode);
  466. // Hide all fields except the current one.
  467. foreach (array_keys($entity->getFieldDefinitions()) as $name) {
  468. if ($name != $field_name) {
  469. $display->removeComponent($name);
  470. }
  471. }
  472. }
  473. else {
  474. // Array of custom display options: use a runtime Display for the
  475. // '_custom' view mode. Persist the displays created, to reduce the number
  476. // of objects (displays and formatter plugins) created when rendering a
  477. // series of fields individually for cases such as views tables.
  478. $entity_type_id = $entity->getEntityTypeId();
  479. $bundle = $entity->bundle();
  480. $key = $entity_type_id . ':' . $bundle . ':' . $field_name . ':' . Crypt::hashBase64(serialize($display_options));
  481. if (!isset($this->singleFieldDisplays[$key])) {
  482. $this->singleFieldDisplays[$key] = EntityViewDisplay::create([
  483. 'targetEntityType' => $entity_type_id,
  484. 'bundle' => $bundle,
  485. 'status' => TRUE,
  486. ])->setComponent($field_name, $display_options);
  487. }
  488. $display = $this->singleFieldDisplays[$key];
  489. }
  490. return $display;
  491. }
  492. }