summaryrefslogtreecommitdiffstatshomepage
path: root/core/lib/Drupal/Core/Render/BubbleableMetadata.php
blob: 0d6eae4001d24f65958806fc26d527f8dc628ea8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
<?php

namespace Drupal\Core\Render;

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheableMetadata;

/**
 * Value object used for bubbleable rendering metadata.
 *
 * @see \Drupal\Core\Render\RendererInterface::render()
 */
class BubbleableMetadata extends CacheableMetadata implements AttachmentsInterface {

  use AttachmentsTrait;

  /**
   * Creates a new bubbleable metadata object by merging this one with another.
   *
   * @param \Drupal\Core\Cache\CacheableMetadata $other
   *   The other bubbleable metadata object.
   *
   * @return static
   *   A new bubbleable metadata object, with the merged data.
   */
  public function merge(CacheableMetadata $other) {
    $result = parent::merge($other);

    // This is called many times per request, so avoid merging unless absolutely
    // necessary.
    if ($other instanceof BubbleableMetadata) {
      if (empty($this->attachments)) {
        $result->attachments = $other->attachments;
      }
      elseif (empty($other->attachments)) {
        $result->attachments = $this->attachments;
      }
      else {
        $result->attachments = static::mergeAttachments($this->attachments, $other->attachments);
      }
    }

    return $result;
  }

  /**
   * Applies the values of this bubbleable metadata object to a render array.
   *
   * @param array &$build
   *   A render array.
   */
  public function applyTo(array &$build) {
    parent::applyTo($build);
    $build['#attached'] = $this->attachments;
  }

  /**
   * Creates a bubbleable metadata object with values taken from a render array.
   *
   * @param array $build
   *   A render array.
   *
   * @return static
   */
  public static function createFromRenderArray(array $build) {
    $meta = parent::createFromRenderArray($build);
    $meta->attachments = (isset($build['#attached'])) ? $build['#attached'] : [];
    return $meta;
  }

  /**
   * Creates a bubbleable metadata object from a depended object.
   *
   * @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $object
   *   The object whose cacheability metadata to retrieve. If it implements
   *   CacheableDependencyInterface, its cacheability metadata will be used,
   *   otherwise, the passed in object must be assumed to be uncacheable, so
   *   max-age 0 is set.
   *
   * @return static
   */
  public static function createFromObject($object) {
    $meta = parent::createFromObject($object);

    if ($object instanceof AttachmentsInterface) {
      $meta->attachments = $object->getAttachments();
    }

    return $meta;
  }

  /**
   * {@inheritdoc}
   */
  public function addCacheableDependency($other_object) {
    parent::addCacheableDependency($other_object);

    if ($other_object instanceof AttachmentsInterface) {
      $this->addAttachments($other_object->getAttachments());
    }

    return $this;
  }

  /**
   * Merges two attachments arrays (which live under the '#attached' key).
   *
   * The values under the 'drupalSettings' key are merged in a special way, to
   * match the behavior of:
   *
   * @code
   *   jQuery.extend(true, {}, $settings_items[0], $settings_items[1], ...)
   * @endcode
   *
   * This means integer indices are preserved just like string indices are,
   * rather than re-indexed as is common in PHP array merging.
   *
   * Example:
   * @code
   * function module1_page_attachments(&$page) {
   *   $page['a']['#attached']['drupalSettings']['foo'] = ['a', 'b', 'c'];
   * }
   * function module2_page_attachments(&$page) {
   *   $page['#attached']['drupalSettings']['foo'] = ['d'];
   * }
   * // When the page is rendered after the above code, and the browser runs the
   * // resulting <SCRIPT> tags, the value of drupalSettings.foo is
   * // ['d', 'b', 'c'], not ['a', 'b', 'c', 'd'].
   * @endcode
   *
   * By following jQuery.extend() merge logic rather than common PHP array merge
   * logic, the following are ensured:
   * - Attaching JavaScript settings is idempotent: attaching the same settings
   *   twice does not change the output sent to the browser.
   * - If pieces of the page are rendered in separate PHP requests and the
   *   returned settings are merged by JavaScript, the resulting settings are
   *   the same as if rendered in one PHP request and merged by PHP.
   *
   * @param array $a
   *   An attachments array.
   * @param array $b
   *   Another attachments array.
   *
   * @return array
   *   The merged attachments array.
   */
  public static function mergeAttachments(array $a, array $b) {
    // If both #attached arrays contain drupalSettings, then merge them
    // correctly; adding the same settings multiple times needs to be
    // idempotent.
    if (!empty($a['drupalSettings']) && !empty($b['drupalSettings'])) {
      $drupalSettings = NestedArray::mergeDeepArray([$a['drupalSettings'], $b['drupalSettings']], TRUE);
      // No need for re-merging them.
      unset($a['drupalSettings']);
      unset($b['drupalSettings']);
    }
    // Optimize merging of placeholders: no need for deep merging.
    if (!empty($a['placeholders']) && !empty($b['placeholders'])) {
      $placeholders = $a['placeholders'] + $b['placeholders'];
      // No need for re-merging them.
      unset($a['placeholders']);
      unset($b['placeholders']);
    }
    // Apply the normal merge.
    $a = array_merge_recursive($a, $b);
    if (isset($drupalSettings)) {
      // Save the custom merge for the drupalSettings.
      $a['drupalSettings'] = $drupalSettings;
    }
    if (isset($placeholders)) {
      // Save the custom merge for the placeholders.
      $a['placeholders'] = $placeholders;
    }
    return $a;
  }

}