Apc   F
last analyzed

Complexity

Total Complexity 96

Size/Duplication

Total Lines 717
Duplicated Lines 10.88 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
wmc 96
lcom 1
cbo 2
dl 78
loc 717
rs 1.883
c 0
b 0
f 0

27 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 3
A get() 3 19 4
B getMulti() 3 27 7
A set() 22 22 3
A setMulti() 5 35 5
A delete() 0 13 2
A deleteMulti() 0 37 4
A add() 22 22 3
A replace() 0 21 3
B cas() 3 45 7
A increment() 10 10 3
A decrement() 10 10 3
A touch() 0 28 4
A flush() 0 6 1
A getCollection() 0 4 1
B doIncrement() 0 46 9
A ttl() 0 9 3
A lock() 0 19 3
A acquire() 0 23 3
A unlock() 0 11 2
A expire() 0 16 3
B apcu_fetch() 0 32 6
B apcu_store() 0 31 6
A apcu_delete() 0 8 2
A apcu_add() 0 8 2
A apcu_clear_cache() 0 8 2
A APCuIterator() 0 18 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Apc often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Apc, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace MatthiasMullie\Scrapbook\Adapters;
4
5
use APCIterator;
6
use APCuIterator;
7
use MatthiasMullie\Scrapbook\Adapters\Collections\Apc as Collection;
8
use MatthiasMullie\Scrapbook\Exception\Exception;
9
use MatthiasMullie\Scrapbook\KeyValueStore;
10
11
/**
12
 * APC adapter. Basically just a wrapper over apc_* functions, but in an
13
 * exchangeable (KeyValueStore) interface.
14
 *
15
 * @author Matthias Mullie <[email protected]>
16
 * @copyright Copyright (c) 2014, Matthias Mullie. All rights reserved
17
 * @license LICENSE MIT
18
 */
19
class Apc implements KeyValueStore
20
{
21
    /**
22
     * APC does this crazy thing of only deleting expired data on every new
23
     * (page) request, not checking it when you actually retrieve the value
24
     * (which you may just have set in the same request)
25
     * Since it's totally possible to store values that expire in the same
26
     * request, we'll keep track of those expiration times here & filter them
27
     * out in case they did expire.
28
     *
29
     * @see http://stackoverflow.com/questions/11750223/apc-user-cache-entries-not-expiring
30
     *
31
     * @var array
32
     */
33
    protected $expires = array();
34
35
    public function __construct()
36
    {
37
        if (!function_exists('apcu_fetch') && !function_exists('apc_fetch')) {
38
            throw new Exception('ext-apc(u) is not installed.');
39
        }
40
    }
41
42
    /**
43
     * {@inheritdoc}
44
     */
45
    public function get($key, &$token = null)
46
    {
47
        // check for values that were just stored in this request but have
48
        // actually expired by now
49 View Code Duplication
        if (isset($this->expires[$key]) && $this->expires[$key] < time()) {
0 ignored issues
show
Duplication introduced by Matthias Mullie
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
50
            return false;
51
        }
52
53
        $value = $this->apcu_fetch($key, $success);
54
        if ($success === false) {
55
            $token = null;
56
57
            return false;
58
        }
59
60
        $token = serialize($value);
61
62
        return $value;
63
    }
64
65
    /**
66
     * {@inheritdoc}
67
     */
68
    public function getMulti(array $keys, array &$tokens = null)
69
    {
70
        $tokens = array();
71
        if (empty($keys)) {
72
            return array();
73
        }
74
75
        // check for values that were just stored in this request but have
76
        // actually expired by now
77
        foreach ($keys as $i => $key) {
78 View Code Duplication
            if (isset($this->expires[$key]) && $this->expires[$key] < time()) {
0 ignored issues
show
Duplication introduced by Matthias Mullie
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
79
                unset($keys[$i]);
80
            }
81
        }
82
83
        $values = $this->apcu_fetch($keys);
84
        if ($values === false) {
85
            return array();
86
        }
87
88
        $tokens = array();
89
        foreach ($values as $key => $value) {
90
            $tokens[$key] = serialize($value);
91
        }
92
93
        return $values;
94
    }
95
96
    /**
97
     * {@inheritdoc}
98
     */
99 View Code Duplication
    public function set($key, $value, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
100
    {
101
        $ttl = $this->ttl($expire);
102
103
        // negative TTLs don't always seem to properly treat the key as deleted
104
        if ($ttl < 0) {
105
            $this->delete($key);
106
107
            return true;
108
        }
109
110
        // lock required for CAS
111
        if (!$this->lock($key)) {
112
            return false;
113
        }
114
115
        $success = $this->apcu_store($key, $value, $ttl);
0 ignored issues
show
Bug Compatibility introduced by Matthias Mullie
The expression $this->apcu_store($key, $value, $ttl); of type boolean|boolean[] adds the type boolean[] to the return on line 119 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::set of type boolean.
Loading history...
116
        $this->expire($key, $ttl);
117
        $this->unlock($key);
118
119
        return $success;
120
    }
121
122
    /**
123
     * {@inheritdoc}
124
     */
125
    public function setMulti(array $items, $expire = 0)
126
    {
127
        if (empty($items)) {
128
            return array();
129
        }
130
131
        $ttl = $this->ttl($expire);
132
133
        // negative TTLs don't always seem to properly treat the key as deleted
134 View Code Duplication
        if ($ttl < 0) {
0 ignored issues
show
Duplication introduced by Matthias Mullie
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
135
            $this->deleteMulti(array_keys($items));
136
137
            return array_fill_keys(array_keys($items), true);
138
        }
139
140
        // attempt to get locks for all items
141
        $locked = $this->lock(array_keys($items));
142
        $locked = array_fill_keys($locked, null);
143
        $failed = array_diff_key($items, $locked);
144
        $items = array_intersect_key($items, $locked);
145
146
        if ($items) {
0 ignored issues
show
Bug Best Practice introduced by Matthias Mullie
The expression $items of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
147
            // only write to those where lock was acquired
148
            $this->apcu_store($items, null, $ttl);
149
            $this->expire(array_keys($items), $ttl);
150
            $this->unlock(array_keys($items));
151
        }
152
153
        $return = array();
154
        foreach ($items as $key => $value) {
155
            $return[$key] = !array_key_exists($key, $failed);
156
        }
157
158
        return $return;
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164
    public function delete($key)
165
    {
166
        // lock required for CAS
167
        if (!$this->lock($key)) {
168
            return false;
169
        }
170
171
        $success = $this->apcu_delete($key);
0 ignored issues
show
Bug Compatibility introduced by Matthias Mullie
The expression $this->apcu_delete($key); of type boolean|string[] adds the type string[] to the return on line 175 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::delete of type boolean.
Loading history...
172
        unset($this->expires[$key]);
173
        $this->unlock($key);
174
175
        return $success;
176
    }
177
178
    /**
179
     * {@inheritdoc}
180
     */
181
    public function deleteMulti(array $keys)
182
    {
183
        if (empty($keys)) {
184
            return array();
185
        }
186
187
        // attempt to get locks for all items
188
        $locked = $this->lock($keys);
189
        $failed = array_diff($keys, $locked);
190
        $keys = array_intersect($keys, $locked);
191
192
        // only delete those where lock was acquired
193
        if ($keys) {
0 ignored issues
show
Bug Best Practice introduced by Matthias Mullie
The expression $keys of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
194
            /**
195
             * Contrary to the docs, apc_delete also accepts an array of
196
             * multiple keys to be deleted. Docs for apcu_delete are ok in this
197
             * regard.
198
             * But both are flawed in terms of return value in this case: an
199
             * array with failed keys is returned.
200
             *
201
             * @see http://php.net/manual/en/function.apc-delete.php
202
             *
203
             * @var string[]
204
             */
205
            $result = $this->apcu_delete($keys);
206
            $failed = array_merge($failed, $result);
207
            $this->unlock($keys);
208
        }
209
210
        $return = array();
211
        foreach ($keys as $key) {
212
            $return[$key] = !in_array($key, $failed);
213
            unset($this->expires[$key]);
214
        }
215
216
        return $return;
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     */
222 View Code Duplication
    public function add($key, $value, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
223
    {
224
        $ttl = $this->ttl($expire);
225
226
        // negative TTLs don't always seem to properly treat the key as deleted
227
        if ($ttl < 0) {
228
            // don't add - it's expired already; just check if it already
229
            // existed to return true/false as expected from add()
230
            return $this->get($key) === false;
231
        }
232
233
        // lock required for CAS
234
        if (!$this->lock($key)) {
235
            return false;
236
        }
237
238
        $success = $this->apcu_add($key, $value, $ttl);
0 ignored issues
show
Bug Compatibility introduced by Matthias Mullie
The expression $this->apcu_add($key, $value, $ttl); of type boolean|boolean[] adds the type boolean[] to the return on line 242 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::add of type boolean.
Loading history...
239
        $this->expire($key, $ttl);
240
        $this->unlock($key);
241
242
        return $success;
243
    }
244
245
    /**
246
     * {@inheritdoc}
247
     */
248
    public function replace($key, $value, $expire = 0)
249
    {
250
        $ttl = $this->ttl($expire);
251
252
        // APC doesn't support replace; I'll use get to check key existence,
253
        // then safely replace with cas
254
        $current = $this->get($key, $token);
255
        if ($current === false) {
256
            return false;
257
        }
258
259
        // negative TTLs don't always seem to properly treat the key as deleted
260
        if ($ttl < 0) {
261
            $this->delete($key);
262
263
            return true;
264
        }
265
266
        // no need for locking - cas will do that
267
        return $this->cas($token, $key, $value, $ttl);
0 ignored issues
show
Bug Compatibility introduced by Matthias Mullie
The expression $this->cas($token, $key, $value, $ttl); of type boolean|boolean[] adds the type boolean[] to the return on line 267 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::replace of type boolean.
Loading history...
268
    }
269
270
    /**
271
     * {@inheritdoc}
272
     */
273
    public function cas($token, $key, $value, $expire = 0)
274
    {
275
        $ttl = $this->ttl($expire);
276
277
        // lock required because we can't perform an atomic CAS
278
        if (!$this->lock($key)) {
279
            return false;
280
        }
281
282
        // check for values that were just stored in this request but have
283
        // actually expired by now
284 View Code Duplication
        if (isset($this->expires[$key]) && $this->expires[$key] < time()) {
0 ignored issues
show
Duplication introduced by Matthias Mullie
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
285
            return false;
286
        }
287
288
        // get current value, to compare with token
289
        $compare = $this->apcu_fetch($key);
290
291
        if ($compare === false) {
292
            $this->unlock($key);
293
294
            return false;
295
        }
296
297
        if ($token !== serialize($compare)) {
298
            $this->unlock($key);
299
300
            return false;
301
        }
302
303
        // negative TTLs don't always seem to properly treat the key as deleted
304
        if ($ttl < 0) {
305
            $this->apcu_delete($key);
306
            unset($this->expires[$key]);
307
            $this->unlock($key);
308
309
            return true;
310
        }
311
312
        $success = $this->apcu_store($key, $value, $ttl);
0 ignored issues
show
Bug Compatibility introduced by Matthias Mullie
The expression $this->apcu_store($key, $value, $ttl); of type boolean|boolean[] adds the type boolean[] to the return on line 316 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::cas of type boolean.
Loading history...
313
        $this->expire($key, $ttl);
314
        $this->unlock($key);
315
316
        return $success;
317
    }
318
319
    /**
320
     * {@inheritdoc}
321
     */
322 View Code Duplication
    public function increment($key, $offset = 1, $initial = 0, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
323
    {
324
        if ($offset <= 0 || $initial < 0) {
325
            return false;
326
        }
327
328
        // not doing apc_inc because that one it doesn't let us set an initial
329
        // value or TTL
330
        return $this->doIncrement($key, $offset, $initial, $expire);
0 ignored issues
show
Bug Compatibility introduced by Matthias Mullie
The expression $this->doIncrement($key,...et, $initial, $expire); of type integer|false|double adds the type double to the return on line 330 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::increment of type integer|boolean.
Loading history...
331
    }
332
333
    /**
334
     * {@inheritdoc}
335
     */
336 View Code Duplication
    public function decrement($key, $offset = 1, $initial = 0, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
337
    {
338
        if ($offset <= 0 || $initial < 0) {
339
            return false;
340
        }
341
342
        // not doing apc_dec because that one it doesn't let us set an initial
343
        // value or TTL
344
        return $this->doIncrement($key, -$offset, $initial, $expire);
0 ignored issues
show
Bug Compatibility introduced by Matthias Mullie
The expression $this->doIncrement($key,...et, $initial, $expire); of type integer|false|double adds the type double to the return on line 344 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::decrement of type integer|boolean.
Loading history...
345
    }
346
347
    /**
348
     * {@inheritdoc}
349
     */
350
    public function touch($key, $expire)
351
    {
352
        $ttl = $this->ttl($expire);
353
354
        // shortcut - expiring is similar to deleting, but the former has no
355
        // 1-operation equivalent
356
        if ($ttl < 0) {
357
            return $this->delete($key);
0 ignored issues
show
Bug Compatibility introduced by Matthias Mullie
The expression $this->delete($key); of type boolean|string[] adds the type string[] to the return on line 357 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::touch of type boolean.
Loading history...
358
        }
359
360
        // get existing TTL & quit early if it's that one already
361
        $iterator = $this->APCuIterator('/^'.preg_quote($key, '/').'$/', \APC_ITER_VALUE | \APC_ITER_TTL, 1, \APC_LIST_ACTIVE);
362
        $current = $iterator->current();
363
        if (!$current) {
364
            // doesn't exist
365
            return false;
366
        }
367
        if ($current['ttl'] === $ttl) {
368
            // that's the TTL already, no need to reset it
369
            return true;
370
        }
371
372
        // generate CAS token to safely CAS existing value with new TTL
373
        $value = $current['value'];
374
        $token = serialize($value);
375
376
        return $this->cas($token, $key, $value, $ttl);
0 ignored issues
show
Bug Compatibility introduced by Matthias Mullie
The expression $this->cas($token, $key, $value, $ttl); of type boolean|boolean[] adds the type boolean[] to the return on line 376 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::touch of type boolean.
Loading history...
377
    }
378
379
    /**
380
     * {@inheritdoc}
381
     */
382
    public function flush()
383
    {
384
        $this->expires = array();
385
386
        return $this->apcu_clear_cache();
387
    }
388
389
    /**
390
     * {@inheritdoc}
391
     */
392
    public function getCollection($name)
393
    {
394
        return new Collection($this, $name);
395
    }
396
397
    /**
398
     * Shared between increment/decrement: both have mostly the same logic
399
     * (decrement just increments a negative value), but need their validation
400
     * split up (increment won't accept negative values).
401
     *
402
     * @param string $key
403
     * @param int    $offset
404
     * @param int    $initial
405
     * @param int    $expire
406
     *
407
     * @return int|bool
408
     */
409
    protected function doIncrement($key, $offset, $initial, $expire)
410
    {
411
        $ttl = $this->ttl($expire);
412
413
        /*
414
         * APC has apc_inc & apc_dec, which work great. However, they don't
415
         * allow for a TTL to be set.
416
         * I could use apc_inc & apc_dec & then do a touch, but touch also
417
         * doesn't have an APC implementation & needs a get & cas. That would
418
         * be 2 operations + CAS.
419
         * Instead, I'll just do a get, implement the increase or decrease in
420
         * PHP, then CAS the new value = 1 operation + CAS.
421
         */
422
        $value = $this->get($key, $token);
423
        if ($value === false) {
424
            // don't even set initial value, it's already expired...
425
            if ($ttl < 0) {
426
                return $initial;
427
            }
428
429
            // no need for locking - set will do that
430
            $success = $this->add($key, $initial, $ttl);
431
432
            return $success ? $initial : false;
433
        }
434
435
        if (!is_numeric($value) || $value < 0) {
436
            return false;
437
        }
438
439
        $value += $offset;
440
        // value can never be lower than 0
441
        $value = max(0, $value);
442
443
        // negative TTLs don't always seem to properly treat the key as deleted
444
        if ($ttl < 0) {
445
            $success = $this->delete($key);
446
447
            return $success ? $value : false;
448
        }
449
450
        // no need for locking - cas will do that
451
        $success = $this->cas($token, $key, $value, $ttl);
452
453
        return $success ? $value : false;
454
    }
455
456
    /**
457
     * APC expects true TTL, not expiration timestamp.
458
     *
459
     * @param int $expire
460
     *
461
     * @return int TTL in seconds
462
     */
463
    protected function ttl($expire)
464
    {
465
        // relative time in seconds, <30 days
466
        if ($expire < 30 * 24 * 60 * 60) {
467
            $expire += time();
468
        }
469
470
        return $expire ? $expire - time() : 0;
471
    }
472
473
    /**
474
     * Acquire a lock. If we failed to acquire a lock, it'll automatically try
475
     * again in 1ms, for a maximum of 10 times.
476
     *
477
     * APC provides nothing that would allow us to do CAS. To "emulate" CAS,
478
     * we'll work with locks: all cache writes also briefly create a lock
479
     * cache entry (yup: #writes * 3, for lock & unlock - luckily, they're
480
     * not over the network)
481
     * Writes are disallows when a lock can't be obtained (= locked by
482
     * another write), which makes it possible for us to first retrieve,
483
     * compare & then set in a nob-atomic way.
484
     * However, there's a possibility for interference with direct APC
485
     * access touching the same keys - e.g. other scripts, not using this
486
     * class. If CAS is of importance, make sure the only things touching
487
     * APC on your server is using these classes!
488
     *
489
     * @param string|string[] $keys
490
     *
491
     * @return array Array of successfully locked keys
492
     */
493
    protected function lock($keys)
494
    {
495
        // both string (single key) and array (multiple) are accepted
496
        $keys = (array) $keys;
497
498
        $locked = array();
499
        for ($i = 0; $i < 10; ++$i) {
500
            $locked += $this->acquire($keys);
501
            $keys = array_diff($keys, $locked);
502
503
            if (empty($keys)) {
504
                break;
505
            }
506
507
            usleep(1);
508
        }
509
510
        return $locked;
511
    }
512
513
    /**
514
     * Acquire a lock - required to provide CAS functionality.
515
     *
516
     * @param string|string[] $keys
517
     *
518
     * @return string[] Array of successfully locked keys
519
     */
520
    protected function acquire($keys)
521
    {
522
        $keys = (array) $keys;
523
524
        $values = array();
525
        foreach ($keys as $key) {
526
            $values["scrapbook.lock.$key"] = null;
527
        }
528
529
        // there's no point in locking longer than max allowed execution time
530
        // for this script
531
        $ttl = ini_get('max_execution_time');
532
533
        // lock these keys, then compile a list of successfully locked keys
534
        // (using the returned failure array)
535
        $result = (array) $this->apcu_add($values, null, $ttl);
536
        $failed = array();
537
        foreach ($result as $key => $err) {
538
            $failed[] = substr($key, strlen('scrapbook.lock.'));
539
        }
540
541
        return array_diff($keys, $failed);
542
    }
543
544
    /**
545
     * Release a lock.
546
     *
547
     * @param string|string[] $keys
548
     *
549
     * @return bool
550
     */
551
    protected function unlock($keys)
552
    {
553
        $keys = (array) $keys;
554
        foreach ($keys as $i => $key) {
555
            $keys[$i] = "scrapbook.lock.$key";
556
        }
557
558
        $this->apcu_delete($keys);
559
560
        return true;
561
    }
562
563
    /**
564
     * Store the expiration time for items we're setting in this request, to
565
     * work around APC's behavior of only clearing expires per page request.
566
     *
567
     * @see static::$expires
568
     *
569
     * @param array|string $key
570
     * @param int          $ttl
571
     */
572
    protected function expire($key = array(), $ttl = 0)
573
    {
574
        if ($ttl === 0) {
575
            // when storing indefinitely, there's no point in keeping it around,
576
            // it won't just expire
577
            return;
578
        }
579
580
        // $key can be both string (1 key) or array (multiple)
581
        $keys = (array) $key;
582
583
        $time = time() + $ttl;
584
        foreach ($keys as $key) {
585
            $this->expires[$key] = $time;
586
        }
587
    }
588
589
    /**
590
     * @param string|string[] $key
591
     * @param bool            $success
592
     *
593
     * @return mixed|false
594
     */
595
    protected function apcu_fetch($key, &$success = null)
596
    {
597
        /*
598
         * $key can also be numeric, in which case APC is able to retrieve it,
599
         * but will have an invalid $key in the results array, and trying to
600
         * locate it by its $key in that array will fail with `undefined index`.
601
         * I'll work around this by requesting those values 1 by 1.
602
         */
603
        if (is_array($key)) {
604
            $nums = array_filter($key, 'is_numeric');
605
            if ($nums) {
0 ignored issues
show
Bug Best Practice introduced by Matthias Mullie
The expression $nums of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
606
                $values = array();
607
                foreach ($nums as $k) {
608
                    $values[$k] = $this->apcu_fetch((string) $k, $success);
609
                }
610
611
                $remaining = array_diff($key, $nums);
612
                if ($remaining) {
0 ignored issues
show
Bug Best Practice introduced by Matthias Mullie
The expression $remaining of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
613
                    $values += $this->apcu_fetch($remaining, $success2);
614
                    $success &= $success2;
615
                }
616
617
                return $values;
618
            }
619
        }
620
621
        if (function_exists('apcu_fetch')) {
622
            return apcu_fetch($key, $success);
623
        } else {
624
            return apc_fetch($key, $success);
625
        }
626
    }
627
628
    /**
629
     * @param string|string[] $key
630
     * @param mixed           $var
631
     * @param int             $ttl
632
     *
633
     * @return bool|bool[]
634
     */
635
    protected function apcu_store($key, $var, $ttl = 0)
636
    {
637
        /*
638
         * $key can also be a [$key => $value] array, where key is numeric,
639
         * but got cast to int by PHP. APC doesn't seem to store such numerical
640
         * key, so we'll have to take care of those one by one.
641
         */
642
        if (is_array($key)) {
643
            $nums = array_filter(array_keys($key), 'is_numeric');
644
            if ($nums) {
0 ignored issues
show
Bug Best Practice introduced by Matthias Mullie
The expression $nums of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
645
                $success = array();
646
                $nums = array_intersect_key($key, array_fill_keys($nums, null));
647
                foreach ($nums as $k => $v) {
648
                    $success[$k] = $this->apcu_store((string) $k, $v, $ttl);
649
                }
650
651
                $remaining = array_diff_key($key, $nums);
652
                if ($remaining) {
0 ignored issues
show
Bug Best Practice introduced by Matthias Mullie
The expression $remaining of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
653
                    $success += $this->apcu_store($remaining, $var, $ttl);
654
                }
655
656
                return $success;
0 ignored issues
show
Bug Best Practice introduced by Matthias Mullie
The return type of return $success; (array) is incompatible with the return type documented by MatthiasMullie\Scrapbook\Adapters\Apc::apcu_store of type boolean|boolean[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
657
            }
658
        }
659
660
        if (function_exists('apcu_store')) {
661
            return apcu_store($key, $var, $ttl);
662
        } else {
663
            return apc_store($key, $var, $ttl);
664
        }
665
    }
666
667
    /**
668
     * @param string|string[]|APCIterator|APCuIterator $key
669
     *
670
     * @return bool|string[]
671
     */
672
    protected function apcu_delete($key)
673
    {
674
        if (function_exists('apcu_delete')) {
675
            return apcu_delete($key);
676
        } else {
677
            return apc_delete($key);
678
        }
679
    }
680
681
    /**
682
     * @param string|string[] $key
683
     * @param mixed           $var
684
     * @param int             $ttl
685
     *
686
     * @return bool|bool[]
687
     */
688
    protected function apcu_add($key, $var, $ttl = 0)
689
    {
690
        if (function_exists('apcu_add')) {
691
            return apcu_add($key, $var, $ttl);
692
        } else {
693
            return apc_add($key, $var, $ttl);
694
        }
695
    }
696
697
    /**
698
     * @return bool
699
     */
700
    protected function apcu_clear_cache()
701
    {
702
        if (function_exists('apcu_clear_cache')) {
703
            return apcu_clear_cache();
704
        } else {
705
            return apc_clear_cache('user');
706
        }
707
    }
708
709
    /**
710
     * @param string|string[]|null $search
711
     * @param int                  $format
712
     * @param int                  $chunk_size
713
     * @param int                  $list
714
     *
715
     * @return APCIterator|APCuIterator
716
     */
717
    protected function APCuIterator($search = null, $format = null, $chunk_size = null, $list = null)
0 ignored issues
show
Unused Code introduced by Matthias Mullie
The parameter $search is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by Matthias Mullie
The parameter $format is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by Matthias Mullie
The parameter $chunk_size is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by Matthias Mullie
The parameter $list is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
718
    {
719
        $arguments = func_get_args();
720
721
        if (class_exists('APCuIterator', false)) {
722
            // I can't set the defaults parameter values because the APC_ or
723
            // APCU_ constants may not exist, so I'll just initialize from
724
            // func_get_args, not passing those params that haven't been set
725
            $reflect = new \ReflectionClass('APCuIterator');
726
727
            return $reflect->newInstanceArgs($arguments);
728
        } else {
729
            array_unshift($arguments, 'user');
730
            $reflect = new \ReflectionClass('APCIterator');
731
732
            return $reflect->newInstanceArgs($arguments);
733
        }
734
    }
735
}
736