EntityDisplayBase.php 19 KB

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