EntityDisplayBase.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  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::service('entity_field.manager')->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("Support for using 'type' => 'hidden' in a component is deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use 'region' => 'hidden' instead. See https://www.drupal.org/node/2801513", 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. // @todo Remove this BC layer in Drupal 9.
  222. // @see https://www.drupal.org/project/drupal/issues/2799641
  223. if (!isset($component['region']) && isset($component['type']) && $component['type'] === 'hidden') {
  224. @trigger_error("Support for using 'type' => 'hidden' in a component is deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use 'region' => 'hidden' instead. See https://www.drupal.org/node/2801513", E_USER_DEPRECATED);
  225. $this->removeComponent($name);
  226. }
  227. // Ensure that a region is set.
  228. if (isset($this->content[$name]) && !isset($component['region'])) {
  229. // Directly set the component to bypass other changes in setComponent().
  230. $this->content[$name]['region'] = $this->getDefaultRegion();
  231. }
  232. }
  233. ksort($this->content);
  234. ksort($this->hidden);
  235. parent::preSave($storage);
  236. }
  237. /**
  238. * Handles a component type of 'hidden'.
  239. *
  240. * The logic of this method has been duplicated inline in the preSave()
  241. * method so that this method may remain deprecated and trigger an error.
  242. *
  243. * @param string $name
  244. * The name of the component.
  245. * @param array $component
  246. * The component array.
  247. *
  248. * @deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. No
  249. * replacement is provided.
  250. *
  251. * @see https://www.drupal.org/node/2801513
  252. */
  253. protected function handleHiddenType($name, array $component) {
  254. @trigger_error(__METHOD__ . ' is deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. No replacement is provided. See https://www.drupal.org/node/2801513', E_USER_DEPRECATED);
  255. if (!isset($component['region']) && isset($component['type']) && $component['type'] === 'hidden') {
  256. $this->removeComponent($name);
  257. }
  258. }
  259. /**
  260. * {@inheritdoc}
  261. */
  262. public function calculateDependencies() {
  263. parent::calculateDependencies();
  264. $target_entity_type = $this->entityTypeManager()->getDefinition($this->targetEntityType);
  265. // Create dependency on the bundle.
  266. $bundle_config_dependency = $target_entity_type->getBundleConfigDependency($this->bundle);
  267. $this->addDependency($bundle_config_dependency['type'], $bundle_config_dependency['name']);
  268. // If field.module is enabled, add dependencies on 'field_config' entities
  269. // for both displayed and hidden fields. We intentionally leave out base
  270. // field overrides, since the field still exists without them.
  271. if (\Drupal::moduleHandler()->moduleExists('field')) {
  272. $components = $this->content + $this->hidden;
  273. $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($this->targetEntityType, $this->bundle);
  274. foreach (array_intersect_key($field_definitions, $components) as $field_name => $field_definition) {
  275. if ($field_definition instanceof ConfigEntityInterface && $field_definition->getEntityTypeId() == 'field_config') {
  276. $this->addDependency('config', $field_definition->getConfigDependencyName());
  277. }
  278. }
  279. }
  280. // Depend on configured modes.
  281. if ($this->mode != 'default') {
  282. $mode_entity = $this->entityTypeManager()->getStorage('entity_' . $this->displayContext . '_mode')->load($target_entity_type->id() . '.' . $this->mode);
  283. $this->addDependency('config', $mode_entity->getConfigDependencyName());
  284. }
  285. return $this;
  286. }
  287. /**
  288. * {@inheritdoc}
  289. */
  290. public function toArray() {
  291. $properties = parent::toArray();
  292. // Do not store options for fields whose display is not set to be
  293. // configurable.
  294. foreach ($this->getFieldDefinitions() as $field_name => $definition) {
  295. if (!$definition->isDisplayConfigurable($this->displayContext)) {
  296. unset($properties['content'][$field_name]);
  297. unset($properties['hidden'][$field_name]);
  298. }
  299. }
  300. return $properties;
  301. }
  302. /**
  303. * {@inheritdoc}
  304. */
  305. public function createCopy($mode) {
  306. $display = $this->createDuplicate();
  307. $display->mode = $display->originalMode = $mode;
  308. return $display;
  309. }
  310. /**
  311. * {@inheritdoc}
  312. */
  313. public function getComponents() {
  314. return $this->content;
  315. }
  316. /**
  317. * {@inheritdoc}
  318. */
  319. public function getComponent($name) {
  320. return isset($this->content[$name]) ? $this->content[$name] : NULL;
  321. }
  322. /**
  323. * {@inheritdoc}
  324. */
  325. public function setComponent($name, array $options = []) {
  326. // If no weight specified, make sure the field sinks at the bottom.
  327. if (!isset($options['weight'])) {
  328. $max = $this->getHighestWeight();
  329. $options['weight'] = isset($max) ? $max + 1 : 0;
  330. }
  331. // For a field, fill in default options.
  332. if ($field_definition = $this->getFieldDefinition($name)) {
  333. $options = $this->pluginManager->prepareConfiguration($field_definition->getType(), $options);
  334. }
  335. // Ensure we always have an empty settings and array.
  336. $options += ['settings' => [], 'third_party_settings' => []];
  337. $this->content[$name] = $options;
  338. unset($this->hidden[$name]);
  339. unset($this->plugins[$name]);
  340. return $this;
  341. }
  342. /**
  343. * {@inheritdoc}
  344. */
  345. public function removeComponent($name) {
  346. $this->hidden[$name] = TRUE;
  347. unset($this->content[$name]);
  348. unset($this->plugins[$name]);
  349. return $this;
  350. }
  351. /**
  352. * {@inheritdoc}
  353. */
  354. public function getHighestWeight() {
  355. $weights = [];
  356. // Collect weights for the components in the display.
  357. foreach ($this->content as $options) {
  358. if (isset($options['weight'])) {
  359. $weights[] = $options['weight'];
  360. }
  361. }
  362. // Let other modules feedback about their own additions.
  363. $weights = array_merge($weights, \Drupal::moduleHandler()->invokeAll('field_info_max_weight', [$this->targetEntityType, $this->bundle, $this->displayContext, $this->mode]));
  364. return $weights ? max($weights) : NULL;
  365. }
  366. /**
  367. * Gets the field definition of a field.
  368. */
  369. protected function getFieldDefinition($field_name) {
  370. $definitions = $this->getFieldDefinitions();
  371. return isset($definitions[$field_name]) ? $definitions[$field_name] : NULL;
  372. }
  373. /**
  374. * Gets the definitions of the fields that are candidate for display.
  375. */
  376. protected function getFieldDefinitions() {
  377. if (!isset($this->fieldDefinitions)) {
  378. $definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($this->targetEntityType, $this->bundle);
  379. // For "official" view modes and form modes, ignore fields whose
  380. // definition states they should not be displayed.
  381. if ($this->mode !== static::CUSTOM_MODE) {
  382. $definitions = array_filter($definitions, [$this, 'fieldHasDisplayOptions']);
  383. }
  384. $this->fieldDefinitions = $definitions;
  385. }
  386. return $this->fieldDefinitions;
  387. }
  388. /**
  389. * Determines if a field has options for a given display.
  390. *
  391. * @param \Drupal\Core\Field\FieldDefinitionInterface $definition
  392. * A field definition.
  393. *
  394. * @return array|null
  395. */
  396. private function fieldHasDisplayOptions(FieldDefinitionInterface $definition) {
  397. // The display only cares about fields that specify display options.
  398. // Discard base fields that are not rendered through formatters / widgets.
  399. return $definition->getDisplayOptions($this->displayContext);
  400. }
  401. /**
  402. * {@inheritdoc}
  403. */
  404. public function onDependencyRemoval(array $dependencies) {
  405. $changed = parent::onDependencyRemoval($dependencies);
  406. foreach ($dependencies['config'] as $entity) {
  407. if ($entity->getEntityTypeId() == 'field_config') {
  408. // Remove components for fields that are being deleted.
  409. $this->removeComponent($entity->getName());
  410. unset($this->hidden[$entity->getName()]);
  411. $changed = TRUE;
  412. }
  413. }
  414. foreach ($this->getComponents() as $name => $component) {
  415. if ($renderer = $this->getRenderer($name)) {
  416. if (in_array($renderer->getPluginDefinition()['provider'], $dependencies['module'])) {
  417. // Revert to the defaults if the plugin that supplies the widget or
  418. // formatter depends on a module that is being uninstalled.
  419. $this->setComponent($name);
  420. $changed = TRUE;
  421. }
  422. // Give this component the opportunity to react on dependency removal.
  423. $component_removed_dependencies = $this->getPluginRemovedDependencies($renderer->calculateDependencies(), $dependencies);
  424. if ($component_removed_dependencies) {
  425. if ($renderer->onDependencyRemoval($component_removed_dependencies)) {
  426. // Update component settings to reflect changes.
  427. $component['settings'] = $renderer->getSettings();
  428. $component['third_party_settings'] = [];
  429. foreach ($renderer->getThirdPartyProviders() as $module) {
  430. $component['third_party_settings'][$module] = $renderer->getThirdPartySettings($module);
  431. }
  432. $this->setComponent($name, $component);
  433. $changed = TRUE;
  434. }
  435. // If there are unresolved deleted dependencies left, disable this
  436. // component to avoid the removal of the entire display entity.
  437. if ($this->getPluginRemovedDependencies($renderer->calculateDependencies(), $dependencies)) {
  438. $this->removeComponent($name);
  439. $arguments = [
  440. '@display' => (string) $this->getEntityType()->getLabel(),
  441. '@id' => $this->id(),
  442. '@name' => $name,
  443. ];
  444. $this->getLogger()->warning("@display '@id': Component '@name' was disabled because its settings depend on removed dependencies.", $arguments);
  445. $changed = TRUE;
  446. }
  447. }
  448. }
  449. }
  450. return $changed;
  451. }
  452. /**
  453. * Returns the plugin dependencies being removed.
  454. *
  455. * The function recursively computes the intersection between all plugin
  456. * dependencies and all removed dependencies.
  457. *
  458. * Note: The two arguments do not have the same structure.
  459. *
  460. * @param array[] $plugin_dependencies
  461. * A list of dependencies having the same structure as the return value of
  462. * ConfigEntityInterface::calculateDependencies().
  463. * @param array[] $removed_dependencies
  464. * A list of dependencies having the same structure as the input argument of
  465. * ConfigEntityInterface::onDependencyRemoval().
  466. *
  467. * @return array
  468. * A recursively computed intersection.
  469. *
  470. * @see \Drupal\Core\Config\Entity\ConfigEntityInterface::calculateDependencies()
  471. * @see \Drupal\Core\Config\Entity\ConfigEntityInterface::onDependencyRemoval()
  472. */
  473. protected function getPluginRemovedDependencies(array $plugin_dependencies, array $removed_dependencies) {
  474. $intersect = [];
  475. foreach ($plugin_dependencies as $type => $dependencies) {
  476. if ($removed_dependencies[$type]) {
  477. // Config and content entities have the dependency names as keys while
  478. // module and theme dependencies are indexed arrays of dependency names.
  479. // @see \Drupal\Core\Config\ConfigManager::callOnDependencyRemoval()
  480. if (in_array($type, ['config', 'content'])) {
  481. $removed = array_intersect_key($removed_dependencies[$type], array_flip($dependencies));
  482. }
  483. else {
  484. $removed = array_values(array_intersect($removed_dependencies[$type], $dependencies));
  485. }
  486. if ($removed) {
  487. $intersect[$type] = $removed;
  488. }
  489. }
  490. }
  491. return $intersect;
  492. }
  493. /**
  494. * Gets the default region.
  495. *
  496. * @return string
  497. * The default region for this display.
  498. */
  499. protected function getDefaultRegion() {
  500. return 'content';
  501. }
  502. /**
  503. * {@inheritdoc}
  504. */
  505. public function __sleep() {
  506. // Only store the definition, not external objects or derived data.
  507. $keys = array_keys($this->toArray());
  508. // In addition, we need to keep the entity type and the "is new" status.
  509. $keys[] = 'entityTypeId';
  510. $keys[] = 'enforceIsNew';
  511. // Keep track of the serialized keys, to avoid calling toArray() again in
  512. // __wakeup(). Because of the way __sleep() works, the data has to be
  513. // present in the object to be included in the serialized values.
  514. $keys[] = '_serializedKeys';
  515. $this->_serializedKeys = $keys;
  516. return $keys;
  517. }
  518. /**
  519. * {@inheritdoc}
  520. */
  521. public function __wakeup() {
  522. // Determine what were the properties from toArray() that were saved in
  523. // __sleep().
  524. $keys = $this->_serializedKeys;
  525. unset($this->_serializedKeys);
  526. $values = array_intersect_key(get_object_vars($this), array_flip($keys));
  527. // Run those values through the __construct(), as if they came from a
  528. // regular entity load.
  529. $this->__construct($values, $this->entityTypeId);
  530. }
  531. /**
  532. * Provides the 'system' channel logger service.
  533. *
  534. * @return \Psr\Log\LoggerInterface
  535. * The 'system' channel logger.
  536. */
  537. protected function getLogger() {
  538. return \Drupal::logger('system');
  539. }
  540. }