Mercurial > hg > isophonics-drupal-site
comparison core/lib/Drupal/Core/Entity/EntityDisplayBase.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 7a779792577d |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
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->hidden[$name] = TRUE; | |
194 } | |
195 elseif ($options) { | |
196 $options += ['region' => $default_region]; | |
197 $this->content[$name] = $this->pluginManager->prepareConfiguration($definition->getType(), $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, $update = TRUE) { | |
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, $update); | |
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 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 } |