Completed
Push — 2538542-path ( aff5d3...8ec75c )
by Frédéric G.
02:32
created

Cache::getMultiple()   B

Complexity

Conditions 4
Paths 13

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 27
rs 8.5806
cc 4
eloc 16
nc 13
nop 1
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 22 and the first side effect is on line 14.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
/**
4
 * @file
5
 * Contains \Drupal\mongodb_cache\Cache.
6
 *
7
 * This is the actual MongoDB cache backend. It replaces the core cache backend
8
 * file. See README.md for details.
9
 */
10
11
namespace Drupal\mongodb_cache;
12
13
14
include_once __DIR__ . '/../mongodb.module';
15
16
/**
17
 * MongoDB cache implementation.
18
 *
19
 * This is Drupal's default cache implementation. It uses the MongoDB to store
20
 * cached data. Each cache bin corresponds to a collection by the same name.
21
 */
22
class Cache implements \DrupalCacheInterface {
23
  /**
24
   * The name of the collection holding the cache data.
25
   *
26
   * @var string
27
   */
28
  protected $bin;
29
30
  /**
31
   * A closure wrapping MongoBinData::__construct() with its default $type.
32
   *
33
   * @var \Closure
34
   */
35
  protected $binDataCreator;
36
37
  /**
38
   * The collection holding the cache data.
39
   *
40
   * @var \MongoCollection|\MongoDebugCollection|\MongoDummy
41
   */
42
  protected $collection;
43
44
  /**
45
   * Has a connection exception already been notified ?
46
   *
47
   * @var bool
48
   *
49
   * @see \Drupal\mongodb_cache\Cache::notifyException()
50
   *
51
   * This is a static, because the plugin assumes that connection errors will be
52
   * share between all bins, under the hypothesis that all bins will be using
53
   * the same connection.
54
   */
55
  protected static $isExceptionNotified = FALSE;
56
57
  /**
58
   * The default write options for this collection: unsafe mode.
59
   *
60
   * @var array
61
   *
62
   * @see self::__construct()
63
   */
64
  protected $unsafe;
65
66
  /**
67
   * The name of the state variable holding the latest bin expire timestamp.
68
   *
69
   * @var string
70
   */
71
  protected $flushVarName;
72
73
  /**
74
   * The number of seconds during which a new flush will be ignored.
75
   *
76
   * @var int
77
   *
78
   * @see self::__construct()
79
   */
80
  protected $stampedeDelay;
81
82
  /**
83
   * Constructor.
84
   *
85
   * @param string $bin
86
   *   The name of the cache bin for which to build a backend.
87
   */
88
  public function __construct($bin) {
89
    $this->bin = $bin;
90
    $this->collection = mongodb_collection($bin);
91
92
    // Default is FALSE: this is a cache, so a missed write is not an issue.
93
    $this->unsafe = mongodb_default_write_options(FALSE);
94
95
    $this->stampedeDelay = variable_get('mongodb_cache_stampede_delay', 5);
96
    $this->flushVarName = "flush_cache_{$bin}";
97
98
    $this->binDataCreator = $this->getBinDataCreator();
99
  }
100
101
  /**
102
   * Display an exception error message only once.
103
   *
104
   * @param \MongoConnectionException $e
0 ignored issues
show
introduced by
Missing parameter comment
Loading history...
105
   */
106
  protected static function notifyException(\MongoConnectionException $e) {
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $e. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
107
    if (!self::$isExceptionNotified) {
108
      drupal_set_message(t('MongoDB cache problem %exception.', [
109
        '%exception' => $e->getMessage(),
110
      ]), 'error');
111
      self::$isExceptionNotified = TRUE;
112
    }
113
  }
114
115
  /**
116
   * An alternate \MongoBinData constructor using default $type.
117
   *
118
   * @param mixed $data
119
   *   The data to convert to \MongoBinData.
120
   *
121
   * @return \Closure
122
   *   The alternate constructor with $type following the extension version.
123
   */
124
  protected function createBinData($data) {
125
    $creator = $this->binDataCreator;
126
    $result = $creator($data);
127
    return $result;
128
  }
129
130
  /**
131
   * Return the proper MongoBinData constructor with its type argument.
132
   *
133
   * The signature of \MongoBinData::__construct() changed in 1.2.11 to require
134
   * $type and default to BYTE_ARRAY, then again in 1.5.0 to default to GENERIC.
135
   *
136
   * @return \Closure
137
   *   A closure wrapping the constructor with its expected $type.
138
   */
139
  protected function getBinDataCreator() {
140
    if (version_compare('mongo', '1.2.11') < 0) {
141
      $result = function ($data) {
142
        return new \MongoBinData($data);
143
      };
144
    }
145
    else {
146
      $type = version_compare('mongo', '1.5.0') < 0
147
        ? \MongoBinData::BYTE_ARRAY
148
        : \MongoBinData::GENERIC;
149
      $result = function ($data) use($type) {
150
        return new \MongoBinData($data, $type);
151
      };
152
    }
153
154
    return $result;
155
  }
156
157
  /**
158
   * Return the timestamp of the latest flush.
159
   *
160
   * @return int
161
   *   A UNIX timestamp.
162
   */
163
  protected function getFlushTimestamp() {
164
    $result = intval(variable_get($this->flushVarName, 0));
165
    return $result;
166
  }
167
168
  /**
169
   * Record a timestamp as marking the latest flush for the current bin.
170
   *
171
   * As this performs a variable_set(), it is a costly operation.
172
   *
173
   * @param int $timestamp
174
   *   A UNIX timestamp. May be 0.
175
   */
176
  protected function setFlushTimestamp($timestamp) {
177
    variable_set($this->flushVarName, $timestamp);
178
  }
179
180
  /**
181
   * {@inheritdoc}
182
   */
183
  public function get($cid) {
184
    try {
185
      // Garbage collection necessary when enforcing a minimum cache lifetime.
186
      $this->garbageCollection();
187
188
      $cache = $this->collection->findOne(['_id' => (string) $cid]);
189
      $result = $this->prepareItem($cache);
190
    }
191
    catch (\MongoConnectionException $e) {
192
      self::notifyException($e);
193
      $result = FALSE;
194
    }
195
196
    return $result;
197
  }
198
199
  /**
200
   * {@inheritdoc}
201
   */
202
  public function getMultiple(&$cids) {
203
    $cache = [];
204
    try {
205
      // Garbage collection necessary when enforcing a minimum cache lifetime.
206
      $this->garbageCollection();
207
208
      $criteria = [
209
        '_id' => [
210
          '$in' => array_map('strval', $cids),
211
        ]
212
      ];
213
      $result = $this->collection->find($criteria);
214
215
      foreach ($result as $item) {
216
        $item = $this->prepareItem($item);
217
        if ($item) {
218
          $cache[$item->cid] = $item;
219
        }
220
      }
221
      $cids = array_diff($cids, array_keys($cache));
222
    }
223
    catch (\MongoConnectionException $e) {
224
      self::notifyException($e);
225
    }
226
227
    return $cache;
228
  }
229
230
  /**
231
   * Garbage collection for get() and getMultiple().
232
   */
233
  protected function garbageCollection() {
234
    // Garbage collection only required when enforcing a minimum cache lifetime.
235
    $flush_timestamp = $this->getFlushTimestamp();
236
    if ($flush_timestamp && ($flush_timestamp + variable_get('cache_lifetime', 0) <= REQUEST_TIME)) {
237
      // Reset the variable immediately to prevent a meltdown under heavy load.
238
      $this->setFlushTimestamp(0);
239
240
      // Remove non-permanently cached items from the collection.
241
      $criteria = [
242
        'expire' => [
243
          '$lte' => $flush_timestamp,
244
          '$ne' => CACHE_PERMANENT,
245
        ],
246
      ];
247
      try {
248
        $this->collection->remove($criteria, $this->unsafe);
249
      }
250
      catch (\MongoConnectionException $e) {
251
        self::notifyException($e);
252
      }
253
254
      // Re-enable the expiration mechanism.
255
      $this->setFlushTimestamp(REQUEST_TIME + $this->stampedeDelay);
256
    }
257
  }
258
259
  /**
260
   * Prepare a cached item.
261
   *
262
   * Checks that items are either permanent not yet expired, and unserializes
263
   * data as appropriate.
264
   *
265
   * @param array|null $cache
266
   *   An item loaded from cache_get() or cache_get_multiple().
267
   *
268
   * @return false|object
269
   *   The item with data unserialized as appropriate or FALSE if there is no
270
   *   valid item to load.
271
   */
272
  protected function prepareItem($cache) {
273
    if (!$cache || !isset($cache['data'])) {
274
      return FALSE;
275
    }
276
277
    unset($cache['_id']);
278
    $cache = (object) $cache;
279
280
    // If enforcing a minimum cache lifetime, validate that the data is
281
    // currently valid for this user before we return it by making sure the
282
    // cache entry was created before the timestamp in the current session's
283
    // cache timer. The cache variable is loaded into the $user object by
284
    // _drupal_session_read() in session.inc. If the data is permanent or we're
285
    // not enforcing a minimum cache lifetime always return the cached data.
286
    if ($cache->expire != CACHE_PERMANENT && variable_get('cache_lifetime', 0)
287
      && isset($_SESSION['cache_expiration'][$this->bin])
288
      && $_SESSION['cache_expiration'][$this->bin] > $cache->created) {
289
      // These cached data are too old and thus not valid for us, ignore it.
290
      return FALSE;
291
    }
292
    if ($cache->data instanceof \MongoBinData) {
293
      $cache->data = $cache->data->bin;
294
    }
295
    if ($cache->serialized) {
296
      $cache->data = unserialize($cache->data);
297
    }
298
299
    return $cache;
300
  }
301
302
  /**
303
   * {@inheritdoc}
304
   */
305
  public function set($cid, $data, $expire = CACHE_PERMANENT) {
306
    $scalar = is_scalar($data);
307
    $entry = array(
308
      '_id' => (string) $cid,
309
      'cid' => (string) $cid,
310
      'created' => REQUEST_TIME,
311
      'expire' => $expire,
312
      'serialized' => !$scalar,
313
      'data' => $scalar ? $data : serialize($data),
314
    );
315
316
    // Use MongoBinData for non-UTF8 strings.
317
    if (is_string($entry['data']) && !drupal_validate_utf8($entry['data'])) {
318
      $entry['data'] = $this->createBinData($entry['data']);
319
    }
320
321
    try {
322
      $this->collection->save($entry, $this->unsafe);
323
    }
324
    // Multiple possible exceptions on save(), not just connection-related.
325
    catch (\MongoException $e) {
326
      self::notifyException($e);
327
      // The database may not be available, so we'll ignore cache_set requests.
328
    }
329
  }
330
331
  /**
332
   * Attempt removing data from the collection, notifying on exceptions.
333
   *
334
   * @param array|null $criteria
335
   *   NULL means to remove all documents from the collection.
336
   */
337
  protected function attemptRemove($criteria = NULL) {
338
    try {
339
      $this->collection->remove($criteria, $this->unsafe);
340
    }
341
    catch (\MongoConnectionException $e) {
342
      self::notifyException($e);
343
    }
344
  }
345
346
  /**
347
   * {@inheritdoc}
348
   */
349
  public function clear($cid = NULL, $wildcard = FALSE) {
350
    $minimum_lifetime = variable_get('cache_lifetime', 0);
351
352
    if (empty($cid)) {
353
      if ($minimum_lifetime) {
354
        // We store the time in the current user's $user->cache variable which
355
        // will be saved into the sessions bin by _drupal_session_write(). We
356
        // then simulate that the cache was flushed for this user by not
357
        // returning cached data that was cached before the timestamp.
358
        $_SESSION['cache_expiration'][$this->bin] = REQUEST_TIME;
359
360
        $flush_timestamp = $this->getFlushTimestamp();
361
        if (empty($flush_timestamp)) {
362
          // This is the first request to clear the cache, start a timer.
363
          $this->setFlushTimestamp(REQUEST_TIME);
364
        }
365
        elseif (REQUEST_TIME > ($flush_timestamp + $minimum_lifetime)) {
366
          // Clear the cache for everyone, cache_lifetime seconds have passed
367
          // since the first request to clear the cache.
368
          $criteria = [
369
            'expire' => [
370
              '$ne' => CACHE_PERMANENT,
371
              '$lte' => REQUEST_TIME,
372
            ],
373
          ];
374
          $this->attemptRemove($criteria);
375
          $this->setFlushTimestamp(REQUEST_TIME + $this->stampedeDelay);
376
        }
377
      }
378
      else {
379
        // No minimum cache lifetime, flush all temporary cache entries now.
380
        $criteria = [
381
          'expire' => [
382
            '$ne' => CACHE_PERMANENT,
383
            '$lte' => REQUEST_TIME,
384
          ],
385
        ];
386
        $this->attemptRemove($criteria);
387
      }
388
    }
389
    else {
390
      if ($wildcard) {
391
        if ($cid == '*') {
392
          $criteria = [];
393
          $this->attemptRemove($criteria);
394
        }
395
        else {
396
          $criteria = [
397
            'cid' => new \MongoRegex('/' . preg_quote($cid) . '.*/'),
398
          ];
399
          $this->attemptRemove($criteria);
400
        }
401
      }
402
      elseif (is_array($cid)) {
403
        // Delete in chunks in case a large array is passed.
404
        do {
405
          $criteria = [
406
            'cid' => [
407
              '$in' => array_map('strval', array_splice($cid, 0, 1000)),
408
            ]
409
          ];
410
          $this->attemptRemove($criteria);
411
        } while (count($cid));
412
      }
413
      else {
414
        $criteria = [
415
          '_id' => (string) $cid,
416
        ];
417
        $this->attemptRemove($criteria);
418
      }
419
    }
420
  }
421
422
  /**
423
   * {@inheritdoc}
424
   */
425
  public function isEmpty() {
426
    try {
427
      // Faster than findOne().
428
      $result = !$this->collection->find([], ['_id' => 1])->limit(1)->next();
0 ignored issues
show
Bug introduced by
The method find does only exist in MongoCollection and MongoDebugCollection, but not in MongoDummy.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
429
    }
430
    catch (\MongoConnectionException $e) {
431
      // An unreachable cache is as good as empty.
432
      $result = TRUE;
433
      self::notifyException($e);
434
    }
435
436
    return $result;
437
  }
438
439
}
440