BubbleableMetadata.php 5.51 KB
Newer Older
1 2 3 4 5
<?php

namespace Drupal\Core\Render;

use Drupal\Component\Utility\NestedArray;
6
use Drupal\Core\Cache\CacheableMetadata;
7 8 9 10 11 12

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

15
  use AttachmentsTrait;
16 17 18 19

  /**
   * Merges the values of another bubbleable metadata object with this one.
   *
20
   * @param \Drupal\Core\Cache\CacheableMetadata $other
21
   *   The other bubbleable metadata object.
22
   *
23 24 25
   * @return static
   *   A new bubbleable metadata object, with the merged data.
   */
26 27
  public function merge(CacheableMetadata $other) {
    $result = parent::merge($other);
28 29 30

    // This is called many times per request, so avoid merging unless absolutely
    // necessary.
31
    if ($other instanceof BubbleableMetadata) {
32 33
      if (empty($this->attachments)) {
        $result->attachments = $other->attachments;
34
      }
35 36
      elseif (empty($other->attachments)) {
        $result->attachments = $this->attachments;
37 38
      }
      else {
39
        $result->attachments = static::mergeAttachments($this->attachments, $other->attachments);
40
      }
41
    }
42

43 44 45 46 47 48 49 50 51 52
    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) {
53
    parent::applyTo($build);
54
    $build['#attached'] = $this->attachments;
55 56 57 58 59 60 61 62 63 64 65
  }

  /**
   * 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) {
66
    $meta = parent::createFromRenderArray($build);
67
    $meta->attachments = (isset($build['#attached'])) ? $build['#attached'] : [];
68 69 70
    return $meta;
  }

71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
  /**
   * 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;
  }

92 93 94 95 96 97 98 99 100 101 102 103 104
  /**
   * {@inheritdoc}
   */
  public function addCacheableDependency($other_object) {
    parent::addCacheableDependency($other_object);

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

    return $this;
  }

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
  /**
   * 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 behave
    // idempotently.
    if (!empty($a['drupalSettings']) && !empty($b['drupalSettings'])) {
152
      $drupalSettings = NestedArray::mergeDeepArray([$a['drupalSettings'], $b['drupalSettings']], TRUE);
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
      // 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;
  }

177
}