34ce858b13dc9d5c6d8c5e833d98a576d2285a73
[yaffs-website] / web / core / lib / Drupal / Core / Entity / EntityDisplayBase.php
1 <?php
2
3 namespace Drupal\Core\Entity;
4
5 use Drupal\Core\Config\Entity\ConfigEntityBase;
6 use Drupal\Core\Config\Entity\ConfigEntityInterface;
7 use Drupal\Core\Field\FieldDefinitionInterface;
8 use Drupal\Core\Entity\Display\EntityDisplayInterface;
9
10 /**
11  * Provides a common base class for entity view and form displays.
12  */
13 abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDisplayInterface {
14
15   /**
16    * The 'mode' for runtime EntityDisplay objects used to render entities with
17    * arbitrary display options rather than a configured view mode or form mode.
18    *
19    * @todo Prevent creation of a mode with this ID
20    *   https://www.drupal.org/node/2410727
21    */
22   const CUSTOM_MODE = '_custom';
23
24   /**
25    * Unique ID for the config entity.
26    *
27    * @var string
28    */
29   protected $id;
30
31   /**
32    * Entity type to be displayed.
33    *
34    * @var string
35    */
36   protected $targetEntityType;
37
38   /**
39    * Bundle to be displayed.
40    *
41    * @var string
42    */
43   protected $bundle;
44
45   /**
46    * A list of field definitions eligible for configuration in this display.
47    *
48    * @var \Drupal\Core\Field\FieldDefinitionInterface[]
49    */
50   protected $fieldDefinitions;
51
52   /**
53    * View or form mode to be displayed.
54    *
55    * @var string
56    */
57   protected $mode = self::CUSTOM_MODE;
58
59   /**
60    * Whether this display is enabled or not. If the entity (form) display
61    * is disabled, we'll fall back to the 'default' display.
62    *
63    * @var bool
64    */
65   protected $status;
66
67   /**
68    * List of component display options, keyed by component name.
69    *
70    * @var array
71    */
72   protected $content = [];
73
74   /**
75    * List of components that are set to be hidden.
76    *
77    * @var array
78    */
79   protected $hidden = [];
80
81   /**
82    * The original view or form mode that was requested (case of view/form modes
83    * being configured to fall back to the 'default' display).
84    *
85    * @var string
86    */
87   protected $originalMode;
88
89   /**
90    * The plugin objects used for this display, keyed by field name.
91    *
92    * @var array
93    */
94   protected $plugins = [];
95
96   /**
97    * Context in which this entity will be used (e.g. 'view', 'form').
98    *
99    * @var string
100    */
101   protected $displayContext;
102
103   /**
104    * The plugin manager used by this entity type.
105    *
106    * @var \Drupal\Component\Plugin\PluginManagerBase
107    */
108   protected $pluginManager;
109
110   /**
111    * The renderer.
112    *
113    * @var \Drupal\Core\Render\RendererInterface
114    */
115   protected $renderer;
116
117   /**
118    * {@inheritdoc}
119    */
120   public function __construct(array $values, $entity_type) {
121     if (!isset($values['targetEntityType']) || !isset($values['bundle'])) {
122       throw new \InvalidArgumentException('Missing required properties for an EntityDisplay entity.');
123     }
124
125     if (!$this->entityTypeManager()->getDefinition($values['targetEntityType'])->entityClassImplements(FieldableEntityInterface::class)) {
126       throw new \InvalidArgumentException('EntityDisplay entities can only handle fieldable entity types.');
127     }
128
129     $this->renderer = \Drupal::service('renderer');
130
131     // A plugin manager and a context type needs to be set by extending classes.
132     if (!isset($this->pluginManager)) {
133       throw new \RuntimeException('Missing plugin manager.');
134     }
135     if (!isset($this->displayContext)) {
136       throw new \RuntimeException('Missing display context type.');
137     }
138
139     parent::__construct($values, $entity_type);
140
141     $this->originalMode = $this->mode;
142
143     $this->init();
144   }
145
146   /**
147    * Initializes the display.
148    *
149    * This fills in default options for components:
150    * - that are not explicitly known as either "visible" or "hidden" in the
151    *   display,
152    * - or that are not supposed to be configurable.
153    */
154   protected function init() {
155     // Only populate defaults for "official" view modes and form modes.
156     if ($this->mode !== static::CUSTOM_MODE) {
157       $default_region = $this->getDefaultRegion();
158       // Fill in defaults for extra fields.
159       $context = $this->displayContext == 'view' ? 'display' : $this->displayContext;
160       $extra_fields = \Drupal::entityManager()->getExtraFields($this->targetEntityType, $this->bundle);
161       $extra_fields = isset($extra_fields[$context]) ? $extra_fields[$context] : [];
162       foreach ($extra_fields as $name => $definition) {
163         if (!isset($this->content[$name]) && !isset($this->hidden[$name])) {
164           // Extra fields are visible by default unless they explicitly say so.
165           if (!isset($definition['visible']) || $definition['visible'] == TRUE) {
166             $this->content[$name] = [
167               'weight' => $definition['weight']
168             ];
169           }
170           else {
171             $this->hidden[$name] = TRUE;
172           }
173         }
174         // Ensure extra fields have a 'region'.
175         if (isset($this->content[$name])) {
176           $this->content[$name] += ['region' => $default_region];
177         }
178       }
179
180       // Fill in defaults for fields.
181       $fields = $this->getFieldDefinitions();
182       foreach ($fields as $name => $definition) {
183         if (!$definition->isDisplayConfigurable($this->displayContext) || (!isset($this->content[$name]) && !isset($this->hidden[$name]))) {
184           $options = $definition->getDisplayOptions($this->displayContext);
185
186           // @todo Remove handling of 'type' in https://www.drupal.org/node/2799641.
187           if (!isset($options['region']) && !empty($options['type']) && $options['type'] === 'hidden') {
188             $options['region'] = 'hidden';
189             @trigger_error("Specifying 'type' => 'hidden' is deprecated, use 'region' => 'hidden' instead.", E_USER_DEPRECATED);
190           }
191
192           if (!empty($options['region']) && $options['region'] === 'hidden') {
193             $this->removeComponent($name);
194           }
195           elseif ($options) {
196             $options += ['region' => $default_region];
197             $this->setComponent($name, $options);
198           }
199           // Note: (base) fields that do not specify display options are not
200           // tracked in the display at all, in order to avoid cluttering the
201           // configuration that gets saved back.
202         }
203       }
204     }
205   }
206
207   /**
208    * {@inheritdoc}
209    */
210   public function getTargetEntityTypeId() {
211     return $this->targetEntityType;
212   }
213
214   /**
215    * {@inheritdoc}
216    */
217   public function getMode() {
218     return $this->get('mode');
219   }
220
221   /**
222    * {@inheritdoc}
223    */
224   public function getOriginalMode() {
225     return $this->get('originalMode');
226   }
227
228   /**
229    * {@inheritdoc}
230    */
231   public function getTargetBundle() {
232     return $this->bundle;
233   }
234
235   /**
236    * {@inheritdoc}
237    */
238   public function setTargetBundle($bundle) {
239     $this->set('bundle', $bundle);
240     return $this;
241   }
242
243   /**
244    * {@inheritdoc}
245    */
246   public function id() {
247     return $this->targetEntityType . '.' . $this->bundle . '.' . $this->mode;
248   }
249
250   /**
251    * {@inheritdoc}
252    */
253   public function preSave(EntityStorageInterface $storage) {
254     // Ensure that a region is set on each component.
255     foreach ($this->getComponents() as $name => $component) {
256       $this->handleHiddenType($name, $component);
257       // Ensure that a region is set.
258       if (isset($this->content[$name]) && !isset($component['region'])) {
259         // Directly set the component to bypass other changes in setComponent().
260         $this->content[$name]['region'] = $this->getDefaultRegion();
261       }
262     }
263
264     ksort($this->content);
265     ksort($this->hidden);
266     parent::preSave($storage);
267   }
268
269   /**
270    * Handles a component type of 'hidden'.
271    *
272    * @deprecated This method exists only for backwards compatibility.
273    *
274    * @todo Remove this in https://www.drupal.org/node/2799641.
275    *
276    * @param string $name
277    *   The name of the component.
278    * @param array $component
279    *   The component array.
280    */
281   protected function handleHiddenType($name, array $component) {
282     if (!isset($component['region']) && isset($component['type']) && $component['type'] === 'hidden') {
283       $this->removeComponent($name);
284     }
285   }
286
287   /**
288    * {@inheritdoc}
289    */
290   public function calculateDependencies() {
291     parent::calculateDependencies();
292     $target_entity_type = $this->entityManager()->getDefinition($this->targetEntityType);
293
294     // Create dependency on the bundle.
295     $bundle_config_dependency = $target_entity_type->getBundleConfigDependency($this->bundle);
296     $this->addDependency($bundle_config_dependency['type'], $bundle_config_dependency['name']);
297
298     // If field.module is enabled, add dependencies on 'field_config' entities
299     // for both displayed and hidden fields. We intentionally leave out base
300     // field overrides, since the field still exists without them.
301     if (\Drupal::moduleHandler()->moduleExists('field')) {
302       $components = $this->content + $this->hidden;
303       $field_definitions = $this->entityManager()->getFieldDefinitions($this->targetEntityType, $this->bundle);
304       foreach (array_intersect_key($field_definitions, $components) as $field_name => $field_definition) {
305         if ($field_definition instanceof ConfigEntityInterface && $field_definition->getEntityTypeId() == 'field_config') {
306           $this->addDependency('config', $field_definition->getConfigDependencyName());
307         }
308       }
309     }
310
311     // Depend on configured modes.
312     if ($this->mode != 'default') {
313       $mode_entity = $this->entityManager()->getStorage('entity_' . $this->displayContext . '_mode')->load($target_entity_type->id() . '.' . $this->mode);
314       $this->addDependency('config', $mode_entity->getConfigDependencyName());
315     }
316     return $this;
317   }
318
319   /**
320    * {@inheritdoc}
321    */
322   public function toArray() {
323     $properties = parent::toArray();
324     // Do not store options for fields whose display is not set to be
325     // configurable.
326     foreach ($this->getFieldDefinitions() as $field_name => $definition) {
327       if (!$definition->isDisplayConfigurable($this->displayContext)) {
328         unset($properties['content'][$field_name]);
329         unset($properties['hidden'][$field_name]);
330       }
331     }
332
333     return $properties;
334   }
335
336   /**
337    * {@inheritdoc}
338    */
339   public function createCopy($mode) {
340     $display = $this->createDuplicate();
341     $display->mode = $display->originalMode = $mode;
342     return $display;
343   }
344
345   /**
346    * {@inheritdoc}
347    */
348   public function getComponents() {
349     return $this->content;
350   }
351
352   /**
353    * {@inheritdoc}
354    */
355   public function getComponent($name) {
356     return isset($this->content[$name]) ? $this->content[$name] : NULL;
357   }
358
359   /**
360    * {@inheritdoc}
361    */
362   public function setComponent($name, array $options = []) {
363     // If no weight specified, make sure the field sinks at the bottom.
364     if (!isset($options['weight'])) {
365       $max = $this->getHighestWeight();
366       $options['weight'] = isset($max) ? $max + 1 : 0;
367     }
368
369     // For a field, fill in default options.
370     if ($field_definition = $this->getFieldDefinition($name)) {
371       $options = $this->pluginManager->prepareConfiguration($field_definition->getType(), $options);
372     }
373
374     // Ensure we always have an empty settings and array.
375     $options += ['settings' => [], 'third_party_settings' => []];
376
377     $this->content[$name] = $options;
378     unset($this->hidden[$name]);
379     unset($this->plugins[$name]);
380
381     return $this;
382   }
383
384   /**
385    * {@inheritdoc}
386    */
387   public function removeComponent($name) {
388     $this->hidden[$name] = TRUE;
389     unset($this->content[$name]);
390     unset($this->plugins[$name]);
391
392     return $this;
393   }
394
395   /**
396    * {@inheritdoc}
397    */
398   public function getHighestWeight() {
399     $weights = [];
400
401     // Collect weights for the components in the display.
402     foreach ($this->content as $options) {
403       if (isset($options['weight'])) {
404         $weights[] = $options['weight'];
405       }
406     }
407
408     // Let other modules feedback about their own additions.
409     $weights = array_merge($weights, \Drupal::moduleHandler()->invokeAll('field_info_max_weight', [$this->targetEntityType, $this->bundle, $this->displayContext, $this->mode]));
410
411     return $weights ? max($weights) : NULL;
412   }
413
414   /**
415    * Gets the field definition of a field.
416    */
417   protected function getFieldDefinition($field_name) {
418     $definitions = $this->getFieldDefinitions();
419     return isset($definitions[$field_name]) ? $definitions[$field_name] : NULL;
420   }
421
422   /**
423    * Gets the definitions of the fields that are candidate for display.
424    */
425   protected function getFieldDefinitions() {
426     if (!isset($this->fieldDefinitions)) {
427       $definitions = \Drupal::entityManager()->getFieldDefinitions($this->targetEntityType, $this->bundle);
428       // For "official" view modes and form modes, ignore fields whose
429       // definition states they should not be displayed.
430       if ($this->mode !== static::CUSTOM_MODE) {
431         $definitions = array_filter($definitions, [$this, 'fieldHasDisplayOptions']);
432       }
433       $this->fieldDefinitions = $definitions;
434     }
435
436     return $this->fieldDefinitions;
437   }
438
439   /**
440    * Determines if a field has options for a given display.
441    *
442    * @param \Drupal\Core\Field\FieldDefinitionInterface $definition
443    *   A field definition.
444    * @return array|null
445    */
446   private function fieldHasDisplayOptions(FieldDefinitionInterface $definition) {
447     // The display only cares about fields that specify display options.
448     // Discard base fields that are not rendered through formatters / widgets.
449     return $definition->getDisplayOptions($this->displayContext);
450   }
451
452   /**
453    * {@inheritdoc}
454    */
455   public function onDependencyRemoval(array $dependencies) {
456     $changed = parent::onDependencyRemoval($dependencies);
457     foreach ($dependencies['config'] as $entity) {
458       if ($entity->getEntityTypeId() == 'field_config') {
459         // Remove components for fields that are being deleted.
460         $this->removeComponent($entity->getName());
461         unset($this->hidden[$entity->getName()]);
462         $changed = TRUE;
463       }
464     }
465     foreach ($this->getComponents() as $name => $component) {
466       if ($renderer = $this->getRenderer($name)) {
467         if (in_array($renderer->getPluginDefinition()['provider'], $dependencies['module'])) {
468           // Revert to the defaults if the plugin that supplies the widget or
469           // formatter depends on a module that is being uninstalled.
470           $this->setComponent($name);
471           $changed = TRUE;
472         }
473
474         // Give this component the opportunity to react on dependency removal.
475         $component_removed_dependencies = $this->getPluginRemovedDependencies($renderer->calculateDependencies(), $dependencies);
476         if ($component_removed_dependencies) {
477           if ($renderer->onDependencyRemoval($component_removed_dependencies)) {
478             // Update component settings to reflect changes.
479             $component['settings'] = $renderer->getSettings();
480             $component['third_party_settings'] = [];
481             foreach ($renderer->getThirdPartyProviders() as $module) {
482               $component['third_party_settings'][$module] = $renderer->getThirdPartySettings($module);
483             }
484             $this->setComponent($name, $component);
485             $changed = TRUE;
486           }
487           // If there are unresolved deleted dependencies left, disable this
488           // component to avoid the removal of the entire display entity.
489           if ($this->getPluginRemovedDependencies($renderer->calculateDependencies(), $dependencies)) {
490             $this->removeComponent($name);
491             $arguments = [
492               '@display' => (string) $this->getEntityType()->getLabel(),
493               '@id' => $this->id(),
494               '@name' => $name,
495             ];
496             $this->getLogger()->warning("@display '@id': Component '@name' was disabled because its settings depend on removed dependencies.", $arguments);
497             $changed = TRUE;
498           }
499         }
500       }
501     }
502     return $changed;
503   }
504
505   /**
506    * Returns the plugin dependencies being removed.
507    *
508    * The function recursively computes the intersection between all plugin
509    * dependencies and all removed dependencies.
510    *
511    * Note: The two arguments do not have the same structure.
512    *
513    * @param array[] $plugin_dependencies
514    *   A list of dependencies having the same structure as the return value of
515    *   ConfigEntityInterface::calculateDependencies().
516    * @param array[] $removed_dependencies
517    *   A list of dependencies having the same structure as the input argument of
518    *   ConfigEntityInterface::onDependencyRemoval().
519    *
520    * @return array
521    *   A recursively computed intersection.
522    *
523    * @see \Drupal\Core\Config\Entity\ConfigEntityInterface::calculateDependencies()
524    * @see \Drupal\Core\Config\Entity\ConfigEntityInterface::onDependencyRemoval()
525    */
526   protected function getPluginRemovedDependencies(array $plugin_dependencies, array $removed_dependencies) {
527     $intersect = [];
528     foreach ($plugin_dependencies as $type => $dependencies) {
529       if ($removed_dependencies[$type]) {
530         // Config and content entities have the dependency names as keys while
531         // module and theme dependencies are indexed arrays of dependency names.
532         // @see \Drupal\Core\Config\ConfigManager::callOnDependencyRemoval()
533         if (in_array($type, ['config', 'content'])) {
534           $removed = array_intersect_key($removed_dependencies[$type], array_flip($dependencies));
535         }
536         else {
537           $removed = array_values(array_intersect($removed_dependencies[$type], $dependencies));
538         }
539         if ($removed) {
540           $intersect[$type] = $removed;
541         }
542       }
543     }
544     return $intersect;
545   }
546
547   /**
548    * Gets the default region.
549    *
550    * @return string
551    *   The default region for this display.
552    */
553   protected function getDefaultRegion() {
554     return 'content';
555   }
556
557   /**
558    * {@inheritdoc}
559    */
560   public function __sleep() {
561     // Only store the definition, not external objects or derived data.
562     $keys = array_keys($this->toArray());
563     // In addition, we need to keep the entity type and the "is new" status.
564     $keys[] = 'entityTypeId';
565     $keys[] = 'enforceIsNew';
566     // Keep track of the serialized keys, to avoid calling toArray() again in
567     // __wakeup(). Because of the way __sleep() works, the data has to be
568     // present in the object to be included in the serialized values.
569     $keys[] = '_serializedKeys';
570     $this->_serializedKeys = $keys;
571     return $keys;
572   }
573
574   /**
575    * {@inheritdoc}
576    */
577   public function __wakeup() {
578     // Determine what were the properties from toArray() that were saved in
579     // __sleep().
580     $keys = $this->_serializedKeys;
581     unset($this->_serializedKeys);
582     $values = array_intersect_key(get_object_vars($this), array_flip($keys));
583     // Run those values through the __construct(), as if they came from a
584     // regular entity load.
585     $this->__construct($values, $this->entityTypeId);
586   }
587
588   /**
589    * Provides the 'system' channel logger service.
590    *
591    * @return \Psr\Log\LoggerInterface
592    *   The 'system' channel logger.
593    */
594   protected function getLogger() {
595     return \Drupal::logger('system');
596   }
597
598 }