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