BubbleableMetadata.php 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. <?php
  2. namespace Drupal\Core\Render;
  3. use Drupal\Component\Utility\NestedArray;
  4. use Drupal\Core\Cache\CacheableMetadata;
  5. /**
  6. * Value object used for bubbleable rendering metadata.
  7. *
  8. * @see \Drupal\Core\Render\RendererInterface::render()
  9. */
  10. class BubbleableMetadata extends CacheableMetadata implements AttachmentsInterface {
  11. use AttachmentsTrait;
  12. /**
  13. * Merges the values of another bubbleable metadata object with this one.
  14. *
  15. * @param \Drupal\Core\Cache\CacheableMetadata $other
  16. * The other bubbleable metadata object.
  17. *
  18. * @return static
  19. * A new bubbleable metadata object, with the merged data.
  20. */
  21. public function merge(CacheableMetadata $other) {
  22. $result = parent::merge($other);
  23. // This is called many times per request, so avoid merging unless absolutely
  24. // necessary.
  25. if ($other instanceof BubbleableMetadata) {
  26. if (empty($this->attachments)) {
  27. $result->attachments = $other->attachments;
  28. }
  29. elseif (empty($other->attachments)) {
  30. $result->attachments = $this->attachments;
  31. }
  32. else {
  33. $result->attachments = static::mergeAttachments($this->attachments, $other->attachments);
  34. }
  35. }
  36. return $result;
  37. }
  38. /**
  39. * Applies the values of this bubbleable metadata object to a render array.
  40. *
  41. * @param array &$build
  42. * A render array.
  43. */
  44. public function applyTo(array &$build) {
  45. parent::applyTo($build);
  46. $build['#attached'] = $this->attachments;
  47. }
  48. /**
  49. * Creates a bubbleable metadata object with values taken from a render array.
  50. *
  51. * @param array $build
  52. * A render array.
  53. *
  54. * @return static
  55. */
  56. public static function createFromRenderArray(array $build) {
  57. $meta = parent::createFromRenderArray($build);
  58. $meta->attachments = (isset($build['#attached'])) ? $build['#attached'] : [];
  59. return $meta;
  60. }
  61. /**
  62. * Creates a bubbleable metadata object from a depended object.
  63. *
  64. * @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $object
  65. * The object whose cacheability metadata to retrieve. If it implements
  66. * CacheableDependencyInterface, its cacheability metadata will be used,
  67. * otherwise, the passed in object must be assumed to be uncacheable, so
  68. * max-age 0 is set.
  69. *
  70. * @return static
  71. */
  72. public static function createFromObject($object) {
  73. $meta = parent::createFromObject($object);
  74. if ($object instanceof AttachmentsInterface) {
  75. $meta->attachments = $object->getAttachments();
  76. }
  77. return $meta;
  78. }
  79. /**
  80. * {@inheritdoc}
  81. */
  82. public function addCacheableDependency($other_object) {
  83. parent::addCacheableDependency($other_object);
  84. if ($other_object instanceof AttachmentsInterface) {
  85. $this->addAttachments($other_object->getAttachments());
  86. }
  87. return $this;
  88. }
  89. /**
  90. * Merges two attachments arrays (which live under the '#attached' key).
  91. *
  92. * The values under the 'drupalSettings' key are merged in a special way, to
  93. * match the behavior of:
  94. *
  95. * @code
  96. * jQuery.extend(true, {}, $settings_items[0], $settings_items[1], ...)
  97. * @endcode
  98. *
  99. * This means integer indices are preserved just like string indices are,
  100. * rather than re-indexed as is common in PHP array merging.
  101. *
  102. * Example:
  103. * @code
  104. * function module1_page_attachments(&$page) {
  105. * $page['a']['#attached']['drupalSettings']['foo'] = ['a', 'b', 'c'];
  106. * }
  107. * function module2_page_attachments(&$page) {
  108. * $page['#attached']['drupalSettings']['foo'] = ['d'];
  109. * }
  110. * // When the page is rendered after the above code, and the browser runs the
  111. * // resulting <SCRIPT> tags, the value of drupalSettings.foo is
  112. * // ['d', 'b', 'c'], not ['a', 'b', 'c', 'd'].
  113. * @endcode
  114. *
  115. * By following jQuery.extend() merge logic rather than common PHP array merge
  116. * logic, the following are ensured:
  117. * - Attaching JavaScript settings is idempotent: attaching the same settings
  118. * twice does not change the output sent to the browser.
  119. * - If pieces of the page are rendered in separate PHP requests and the
  120. * returned settings are merged by JavaScript, the resulting settings are
  121. * the same as if rendered in one PHP request and merged by PHP.
  122. *
  123. * @param array $a
  124. * An attachments array.
  125. * @param array $b
  126. * Another attachments array.
  127. *
  128. * @return array
  129. * The merged attachments array.
  130. */
  131. public static function mergeAttachments(array $a, array $b) {
  132. // If both #attached arrays contain drupalSettings, then merge them
  133. // correctly; adding the same settings multiple times needs to behave
  134. // idempotently.
  135. if (!empty($a['drupalSettings']) && !empty($b['drupalSettings'])) {
  136. $drupalSettings = NestedArray::mergeDeepArray([$a['drupalSettings'], $b['drupalSettings']], TRUE);
  137. // No need for re-merging them.
  138. unset($a['drupalSettings']);
  139. unset($b['drupalSettings']);
  140. }
  141. // Optimize merging of placeholders: no need for deep merging.
  142. if (!empty($a['placeholders']) && !empty($b['placeholders'])) {
  143. $placeholders = $a['placeholders'] + $b['placeholders'];
  144. // No need for re-merging them.
  145. unset($a['placeholders']);
  146. unset($b['placeholders']);
  147. }
  148. // Apply the normal merge.
  149. $a = array_merge_recursive($a, $b);
  150. if (isset($drupalSettings)) {
  151. // Save the custom merge for the drupalSettings.
  152. $a['drupalSettings'] = $drupalSettings;
  153. }
  154. if (isset($placeholders)) {
  155. // Save the custom merge for the placeholders.
  156. $a['placeholders'] = $placeholders;
  157. }
  158. return $a;
  159. }
  160. }