CacheArray.php 7.43 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
<?php

/**
 * @file
 * Definition of CacheArray
 */

namespace Drupal\Core\Utility;

use ArrayAccess;
11
use Drupal\Core\Cache\CacheBackendInterface;
12
13
14
15
16
17
18
19
20
21
22
23
24

/**
 * Provides a caching wrapper to be used in place of large array structures.
 *
 * This class should be extended by systems that need to cache large amounts
 * of data and have it represented as an array to calling functions. These
 * arrays can become very large, so ArrayAccess is used to allow different
 * strategies to be used for caching internally (lazy loading, building caches
 * over time etc.). This can dramatically reduce the amount of data that needs
 * to be loaded from cache backends on each request, and memory usage from
 * static caches of that same data.
 *
 * Note that array_* functions do not work with ArrayAccess. Systems using
25
 * CacheArray should use this only internally. If providing API functions
26
 * that return the full array, this can be cached separately or returned
27
 * directly. However since CacheArray holds partial content by design, it
28
29
30
31
32
33
34
35
36
37
38
39
40
41
 * should be a normal PHP array or otherwise contain the full structure.
 *
 * Note also that due to limitations in PHP prior to 5.3.4, it is impossible to
 * write directly to the contents of nested arrays contained in this object.
 * Only writes to the top-level array elements are possible. So if you
 * previously had set $object['foo'] = array(1, 2, 'bar' => 'baz'), but later
 * want to change the value of 'bar' from 'baz' to 'foobar', you cannot do so
 * a targeted write like $object['foo']['bar'] = 'foobar'. Instead, you must
 * overwrite the entire top-level 'foo' array with the entire set of new
 * values: $object['foo'] = array(1, 2, 'bar' => 'foobar'). Due to this same
 * limitation, attempts to create references to any contained data, nested or
 * otherwise, will fail silently. So $var = &$object['foo'] will not throw an
 * error, and $var will be populated with the contents of $object['foo'], but
 * that data will be passed by value, not reference. For more information on
42
43
 * the PHP limitation, see the note in the official PHP documentation at
 * http://php.net/manual/arrayaccess.offsetget.php on ArrayAccess::offsetGet().
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
 *
 * By default, the class accounts for caches where calling functions might
 * request keys in the array that won't exist even after a cache rebuild. This
 * prevents situations where a cache rebuild would be triggered over and over
 * due to a 'missing' item. These cases are stored internally as a value of
 * NULL. This means that the offsetGet() and offsetExists() methods
 * must be overridden if caching an array where the top level values can
 * legitimately be NULL, and where $object->offsetExists() needs to correctly
 * return (equivalent to array_key_exists() vs. isset()). This should not
 * be necessary in the majority of cases.
 *
 * Classes extending this class must override at least the
 * resolveCacheMiss() method to have a working implementation.
 *
 * offsetSet() is not overridden by this class by default. In practice this
 * means that assigning an offset via arrayAccess will only apply while the
 * object is in scope and will not be written back to the persistent cache.
 * This follows a similar pattern to static vs. persistent caching in
62
 * procedural code. Extending classes may wish to alter this behavior, for
63
64
65
66
67
68
69
70
 * example by overriding offsetSet() and adding an automatic call to persist().
 *
 * @see SchemaCache
 */
abstract class CacheArray implements ArrayAccess {

  /**
   * A cid to pass to cache()->set() and cache()->get().
71
72
   *
   * @var string
73
74
75
   */
  protected $cid;

76
77
78
79
80
81
82
  /**
   * A tags array to pass to cache()->set().
   *
   * @var array
   */
  protected $tags;

83
84
  /**
   * A bin to pass to cache()->set() and cache()->get().
85
86
   *
   * @var string
87
88
89
90
91
   */
  protected $bin;

  /**
   * An array of keys to add to the cache at the end of the request.
92
93
   *
   * @var array
94
95
96
97
98
   */
  protected $keysToPersist = array();

  /**
   * Storage for the data itself.
99
100
   *
   * @var array
101
102
103
104
   */
  protected $storage = array();

  /**
105
   * Constructs a CacheArray object.
106
   *
107
   * @param string $cid
108
   *   The cid for the array being cached.
109
   * @param string $bin
110
   *   The bin to cache the array.
111
112
   * @param array $tags
   *   (optional) The tags to specify for the cache item.
113
   */
114
  public function __construct($cid, $bin, $tags = array()) {
115
116
    $this->cid = $cid;
    $this->bin = $bin;
117
    $this->tags = $tags;
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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202

    if ($cached = cache($bin)->get($this->cid)) {
     $this->storage = $cached->data;
    }
  }

  /**
   * Implements ArrayAccess::offsetExists().
   */
  public function offsetExists($offset) {
    return $this->offsetGet($offset) !== NULL;
  }

  /**
   * Implements ArrayAccess::offsetGet().
   */
  public function offsetGet($offset) {
    if (isset($this->storage[$offset]) || array_key_exists($offset, $this->storage)) {
      return $this->storage[$offset];
    }
    else {
      return $this->resolveCacheMiss($offset);
    }
  }

  /**
   * Implements ArrayAccess::offsetSet().
   */
  public function offsetSet($offset, $value) {
    $this->storage[$offset] = $value;
  }

  /**
   * Implements ArrayAccess::offsetUnset().
   */
  public function offsetUnset($offset) {
    unset($this->storage[$offset]);
  }

  /**
   * Flags an offset value to be written to the persistent cache.
   *
   * If a value is assigned to a cache object with offsetSet(), by default it
   * will not be written to the persistent cache unless it is flagged with this
   * method. This allows items to be cached for the duration of a request,
   * without necessarily writing back to the persistent cache at the end.
   *
   * @param $offset
   *   The array offset that was request.
   * @param $persist
   *   Optional boolean to specify whether the offset should be persisted or
   *   not, defaults to TRUE. When called with $persist = FALSE the offset will
   *   be unflagged so that it will not written at the end of the request.
   */
  protected function persist($offset, $persist = TRUE) {
    $this->keysToPersist[$offset] = $persist;
  }

  /**
   * Resolves a cache miss.
   *
   * When an offset is not found in the object, this is treated as a cache
   * miss. This method allows classes implementing the interface to look up
   * the actual value and allow it to be cached.
   *
   * @param $offset
   *   The offset that was requested.
   *
   * @return
   *   The value of the offset, or NULL if no value was found.
   */
  abstract protected function resolveCacheMiss($offset);

  /**
   * Writes a value to the persistent cache immediately.
   *
   * @param $data
   *   The data to write to the persistent cache.
   * @param $lock
   *   Whether to acquire a lock before writing to cache.
   */
  protected function set($data, $lock = TRUE) {
    // Lock cache writes to help avoid stampedes.
    // To implement locking for cache misses, override __construct().
    $lock_name = $this->cid . ':' . $this->bin;
203
    if (!$lock || lock()->acquire($lock_name)) {
204
205
206
      if ($cached = cache($this->bin)->get($this->cid)) {
        $data = $cached->data + $data;
      }
207
      cache($this->bin)->set($this->cid, $data, CacheBackendInterface::CACHE_PERMANENT, $this->tags);
208
      if ($lock) {
209
        lock()->release($lock_name);
210
211
212
213
214
      }
    }
  }

  /**
215
   * Destructs the CacheArray object.
216
217
218
219
220
221
222
223
224
225
226
227
228
   */
  public function __destruct() {
    $data = array();
    foreach ($this->keysToPersist as $offset => $persist) {
      if ($persist) {
        $data[$offset] = $this->storage[$offset];
      }
    }
    if (!empty($data)) {
      $this->set($data);
    }
  }
}