1<?php
2/**
3 * Zend Framework (http://framework.zend.com/)
4 *
5 * @link      http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license   http://framework.zend.com/license/new-bsd New BSD License
8 */
9
10namespace Zend\Cache\Storage\Adapter;
11
12use APCIterator as BaseApcIterator;
13use stdClass;
14use Traversable;
15use Zend\Cache\Exception;
16use Zend\Cache\Storage\AvailableSpaceCapableInterface;
17use Zend\Cache\Storage\Capabilities;
18use Zend\Cache\Storage\ClearByNamespaceInterface;
19use Zend\Cache\Storage\ClearByPrefixInterface;
20use Zend\Cache\Storage\FlushableInterface;
21use Zend\Cache\Storage\IterableInterface;
22use Zend\Cache\Storage\TotalSpaceCapableInterface;
23
24class Apc extends AbstractAdapter implements
25    AvailableSpaceCapableInterface,
26    ClearByNamespaceInterface,
27    ClearByPrefixInterface,
28    FlushableInterface,
29    IterableInterface,
30    TotalSpaceCapableInterface
31{
32    /**
33     * Buffered total space in bytes
34     *
35     * @var null|int|float
36     */
37    protected $totalSpace;
38
39    /**
40     * Constructor
41     *
42     * @param  null|array|Traversable|ApcOptions $options
43     * @throws Exception\ExceptionInterface
44     */
45    public function __construct($options = null)
46    {
47        if (version_compare('3.1.6', phpversion('apc')) > 0) {
48            throw new Exception\ExtensionNotLoadedException("Missing ext/apc >= 3.1.6");
49        }
50
51        $enabled = ini_get('apc.enabled');
52        if (PHP_SAPI == 'cli') {
53            $enabled = $enabled && (bool) ini_get('apc.enable_cli');
54        }
55
56        if (!$enabled) {
57            throw new Exception\ExtensionNotLoadedException(
58                "ext/apc is disabled - see 'apc.enabled' and 'apc.enable_cli'"
59            );
60        }
61
62        parent::__construct($options);
63    }
64
65    /* options */
66
67    /**
68     * Set options.
69     *
70     * @param  array|Traversable|ApcOptions $options
71     * @return Apc
72     * @see    getOptions()
73     */
74    public function setOptions($options)
75    {
76        if (!$options instanceof ApcOptions) {
77            $options = new ApcOptions($options);
78        }
79
80        return parent::setOptions($options);
81    }
82
83    /**
84     * Get options.
85     *
86     * @return ApcOptions
87     * @see    setOptions()
88     */
89    public function getOptions()
90    {
91        if (!$this->options) {
92            $this->setOptions(new ApcOptions());
93        }
94        return $this->options;
95    }
96
97    /* TotalSpaceCapableInterface */
98
99    /**
100     * Get total space in bytes
101     *
102     * @return int|float
103     */
104    public function getTotalSpace()
105    {
106        if ($this->totalSpace === null) {
107            $smaInfo = apc_sma_info(true);
108            $this->totalSpace = $smaInfo['num_seg'] * $smaInfo['seg_size'];
109        }
110
111        return $this->totalSpace;
112    }
113
114    /* AvailableSpaceCapableInterface */
115
116    /**
117     * Get available space in bytes
118     *
119     * @return int|float
120     */
121    public function getAvailableSpace()
122    {
123        $smaInfo = apc_sma_info(true);
124        return $smaInfo['avail_mem'];
125    }
126
127    /* IterableInterface */
128
129    /**
130     * Get the storage iterator
131     *
132     * @return ApcIterator
133     */
134    public function getIterator()
135    {
136        $options   = $this->getOptions();
137        $namespace = $options->getNamespace();
138        $prefix    = '';
139        $pattern   = null;
140        if ($namespace !== '') {
141            $prefix  = $namespace . $options->getNamespaceSeparator();
142            $pattern = '/^' . preg_quote($prefix, '/') . '/';
143        }
144
145        $baseIt = new BaseApcIterator('user', $pattern, 0, 1, APC_LIST_ACTIVE);
146        return new ApcIterator($this, $baseIt, $prefix);
147    }
148
149    /* FlushableInterface */
150
151    /**
152     * Flush the whole storage
153     *
154     * @return bool
155     */
156    public function flush()
157    {
158        return apc_clear_cache('user');
159    }
160
161    /* ClearByNamespaceInterface */
162
163    /**
164     * Remove items by given namespace
165     *
166     * @param string $namespace
167     * @return bool
168     */
169    public function clearByNamespace($namespace)
170    {
171        $namespace = (string) $namespace;
172        if ($namespace === '') {
173            throw new Exception\InvalidArgumentException('No namespace given');
174        }
175
176        $options = $this->getOptions();
177        $prefix  = $namespace . $options->getNamespaceSeparator();
178        $pattern = '/^' . preg_quote($prefix, '/') . '/';
179        return apc_delete(new BaseApcIterator('user', $pattern, 0, 1, APC_LIST_ACTIVE));
180    }
181
182    /* ClearByPrefixInterface */
183
184    /**
185     * Remove items matching given prefix
186     *
187     * @param string $prefix
188     * @return bool
189     */
190    public function clearByPrefix($prefix)
191    {
192        $prefix = (string) $prefix;
193        if ($prefix === '') {
194            throw new Exception\InvalidArgumentException('No prefix given');
195        }
196
197        $options   = $this->getOptions();
198        $namespace = $options->getNamespace();
199        $nsPrefix  = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
200        $pattern = '/^' . preg_quote($nsPrefix . $prefix, '/') . '/';
201        return apc_delete(new BaseApcIterator('user', $pattern, 0, 1, APC_LIST_ACTIVE));
202    }
203
204    /* reading */
205
206    /**
207     * Internal method to get an item.
208     *
209     * @param  string  $normalizedKey
210     * @param  bool $success
211     * @param  mixed   $casToken
212     * @return mixed Data on success, null on failure
213     * @throws Exception\ExceptionInterface
214     */
215    protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null)
216    {
217        $options     = $this->getOptions();
218        $namespace   = $options->getNamespace();
219        $prefix      = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
220        $internalKey = $prefix . $normalizedKey;
221        $result      = apc_fetch($internalKey, $success);
222
223        if (!$success) {
224            return;
225        }
226
227        $casToken = $result;
228        return $result;
229    }
230
231    /**
232     * Internal method to get multiple items.
233     *
234     * @param  array $normalizedKeys
235     * @return array Associative array of keys and values
236     * @throws Exception\ExceptionInterface
237     */
238    protected function internalGetItems(array & $normalizedKeys)
239    {
240        $options   = $this->getOptions();
241        $namespace = $options->getNamespace();
242        if ($namespace === '') {
243            return apc_fetch($normalizedKeys);
244        }
245
246        $prefix       = $namespace . $options->getNamespaceSeparator();
247        $internalKeys = array();
248        foreach ($normalizedKeys as $normalizedKey) {
249            $internalKeys[] = $prefix . $normalizedKey;
250        }
251
252        $fetch = apc_fetch($internalKeys);
253
254        // remove namespace prefix
255        $prefixL = strlen($prefix);
256        $result  = array();
257        foreach ($fetch as $internalKey => & $value) {
258            $result[substr($internalKey, $prefixL)] = $value;
259        }
260
261        return $result;
262    }
263
264    /**
265     * Internal method to test if an item exists.
266     *
267     * @param  string $normalizedKey
268     * @return bool
269     * @throws Exception\ExceptionInterface
270     */
271    protected function internalHasItem(& $normalizedKey)
272    {
273        $options   = $this->getOptions();
274        $namespace = $options->getNamespace();
275        $prefix    = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
276        return apc_exists($prefix . $normalizedKey);
277    }
278
279    /**
280     * Internal method to test multiple items.
281     *
282     * @param  array $normalizedKeys
283     * @return array Array of found keys
284     * @throws Exception\ExceptionInterface
285     */
286    protected function internalHasItems(array & $normalizedKeys)
287    {
288        $options   = $this->getOptions();
289        $namespace = $options->getNamespace();
290        if ($namespace === '') {
291            // array_filter with no callback will remove entries equal to FALSE
292            return array_keys(array_filter(apc_exists($normalizedKeys)));
293        }
294
295        $prefix       = $namespace . $options->getNamespaceSeparator();
296        $internalKeys = array();
297        foreach ($normalizedKeys as $normalizedKey) {
298            $internalKeys[] = $prefix . $normalizedKey;
299        }
300
301        $exists  = apc_exists($internalKeys);
302        $result  = array();
303        $prefixL = strlen($prefix);
304        foreach ($exists as $internalKey => $bool) {
305            if ($bool === true) {
306                $result[] = substr($internalKey, $prefixL);
307            }
308        }
309
310        return $result;
311    }
312
313    /**
314     * Get metadata of an item.
315     *
316     * @param  string $normalizedKey
317     * @return array|bool Metadata on success, false on failure
318     * @throws Exception\ExceptionInterface
319     */
320    protected function internalGetMetadata(& $normalizedKey)
321    {
322        $options     = $this->getOptions();
323        $namespace   = $options->getNamespace();
324        $prefix      = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
325        $internalKey = $prefix . $normalizedKey;
326
327        // @see http://pecl.php.net/bugs/bug.php?id=22564
328        if (!apc_exists($internalKey)) {
329            $metadata = false;
330        } else {
331            $format   = APC_ITER_ALL ^ APC_ITER_VALUE ^ APC_ITER_TYPE ^ APC_ITER_REFCOUNT;
332            $regexp   = '/^' . preg_quote($internalKey, '/') . '$/';
333            $it       = new BaseApcIterator('user', $regexp, $format, 100, APC_LIST_ACTIVE);
334            $metadata = $it->current();
335        }
336
337        if (!$metadata) {
338            return false;
339        }
340
341        $this->normalizeMetadata($metadata);
342        return $metadata;
343    }
344
345    /**
346     * Get metadata of multiple items
347     *
348     * @param  array $normalizedKeys
349     * @return array Associative array of keys and metadata
350     *
351     * @triggers getMetadatas.pre(PreEvent)
352     * @triggers getMetadatas.post(PostEvent)
353     * @triggers getMetadatas.exception(ExceptionEvent)
354     */
355    protected function internalGetMetadatas(array & $normalizedKeys)
356    {
357        $keysRegExp = array();
358        foreach ($normalizedKeys as $normalizedKey) {
359            $keysRegExp[] = preg_quote($normalizedKey, '/');
360        }
361
362        $options   = $this->getOptions();
363        $namespace = $options->getNamespace();
364        $prefixL   = 0;
365
366        if ($namespace === '') {
367            $pattern = '/^(' . implode('|', $keysRegExp) . ')' . '$/';
368        } else {
369            $prefix  = $namespace . $options->getNamespaceSeparator();
370            $prefixL = strlen($prefix);
371            $pattern = '/^' . preg_quote($prefix, '/') . '(' . implode('|', $keysRegExp) . ')' . '$/';
372        }
373
374        $format  = APC_ITER_ALL ^ APC_ITER_VALUE ^ APC_ITER_TYPE ^ APC_ITER_REFCOUNT;
375        $it      = new BaseApcIterator('user', $pattern, $format, 100, APC_LIST_ACTIVE);
376        $result  = array();
377        foreach ($it as $internalKey => $metadata) {
378            // @see http://pecl.php.net/bugs/bug.php?id=22564
379            if (!apc_exists($internalKey)) {
380                continue;
381            }
382
383            $this->normalizeMetadata($metadata);
384            $result[substr($internalKey, $prefixL)] = $metadata;
385        }
386
387        return $result;
388    }
389
390    /* writing */
391
392    /**
393     * Internal method to store an item.
394     *
395     * @param  string $normalizedKey
396     * @param  mixed  $value
397     * @return bool
398     * @throws Exception\ExceptionInterface
399     */
400    protected function internalSetItem(& $normalizedKey, & $value)
401    {
402        $options     = $this->getOptions();
403        $namespace   = $options->getNamespace();
404        $prefix      = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
405        $internalKey = $prefix . $normalizedKey;
406        $ttl         = $options->getTtl();
407
408        if (!apc_store($internalKey, $value, $ttl)) {
409            $type = is_object($value) ? get_class($value) : gettype($value);
410            throw new Exception\RuntimeException(
411                "apc_store('{$internalKey}', <{$type}>, {$ttl}) failed"
412            );
413        }
414
415        return true;
416    }
417
418    /**
419     * Internal method to store multiple items.
420     *
421     * @param  array $normalizedKeyValuePairs
422     * @return array Array of not stored keys
423     * @throws Exception\ExceptionInterface
424     */
425    protected function internalSetItems(array & $normalizedKeyValuePairs)
426    {
427        $options   = $this->getOptions();
428        $namespace = $options->getNamespace();
429        if ($namespace === '') {
430            return array_keys(apc_store($normalizedKeyValuePairs, null, $options->getTtl()));
431        }
432
433        $prefix                = $namespace . $options->getNamespaceSeparator();
434        $internalKeyValuePairs = array();
435        foreach ($normalizedKeyValuePairs as $normalizedKey => &$value) {
436            $internalKey = $prefix . $normalizedKey;
437            $internalKeyValuePairs[$internalKey] = &$value;
438        }
439
440        $failedKeys = apc_store($internalKeyValuePairs, null, $options->getTtl());
441        $failedKeys = array_keys($failedKeys);
442
443        // remove prefix
444        $prefixL = strlen($prefix);
445        foreach ($failedKeys as & $key) {
446            $key = substr($key, $prefixL);
447        }
448
449        return $failedKeys;
450    }
451
452    /**
453     * Add an item.
454     *
455     * @param  string $normalizedKey
456     * @param  mixed  $value
457     * @return bool
458     * @throws Exception\ExceptionInterface
459     */
460    protected function internalAddItem(& $normalizedKey, & $value)
461    {
462        $options     = $this->getOptions();
463        $namespace   = $options->getNamespace();
464        $prefix      = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
465        $internalKey = $prefix . $normalizedKey;
466        $ttl         = $options->getTtl();
467
468        if (!apc_add($internalKey, $value, $ttl)) {
469            if (apc_exists($internalKey)) {
470                return false;
471            }
472
473            $type = is_object($value) ? get_class($value) : gettype($value);
474            throw new Exception\RuntimeException(
475                "apc_add('{$internalKey}', <{$type}>, {$ttl}) failed"
476            );
477        }
478
479        return true;
480    }
481
482    /**
483     * Internal method to add multiple items.
484     *
485     * @param  array $normalizedKeyValuePairs
486     * @return array Array of not stored keys
487     * @throws Exception\ExceptionInterface
488     */
489    protected function internalAddItems(array & $normalizedKeyValuePairs)
490    {
491        $options   = $this->getOptions();
492        $namespace = $options->getNamespace();
493        if ($namespace === '') {
494            return array_keys(apc_add($normalizedKeyValuePairs, null, $options->getTtl()));
495        }
496
497        $prefix                = $namespace . $options->getNamespaceSeparator();
498        $internalKeyValuePairs = array();
499        foreach ($normalizedKeyValuePairs as $normalizedKey => $value) {
500            $internalKey = $prefix . $normalizedKey;
501            $internalKeyValuePairs[$internalKey] = $value;
502        }
503
504        $failedKeys = apc_add($internalKeyValuePairs, null, $options->getTtl());
505        $failedKeys = array_keys($failedKeys);
506
507        // remove prefix
508        $prefixL = strlen($prefix);
509        foreach ($failedKeys as & $key) {
510            $key = substr($key, $prefixL);
511        }
512
513        return $failedKeys;
514    }
515
516    /**
517     * Internal method to replace an existing item.
518     *
519     * @param  string $normalizedKey
520     * @param  mixed  $value
521     * @return bool
522     * @throws Exception\ExceptionInterface
523     */
524    protected function internalReplaceItem(& $normalizedKey, & $value)
525    {
526        $options     = $this->getOptions();
527        $namespace   = $options->getNamespace();
528        $prefix      = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
529        $internalKey = $prefix . $normalizedKey;
530
531        if (!apc_exists($internalKey)) {
532            return false;
533        }
534
535        $ttl = $options->getTtl();
536        if (!apc_store($internalKey, $value, $ttl)) {
537            $type = is_object($value) ? get_class($value) : gettype($value);
538            throw new Exception\RuntimeException(
539                "apc_store('{$internalKey}', <{$type}>, {$ttl}) failed"
540            );
541        }
542
543        return true;
544    }
545
546    /**
547     * Internal method to remove an item.
548     *
549     * @param  string $normalizedKey
550     * @return bool
551     * @throws Exception\ExceptionInterface
552     */
553    protected function internalRemoveItem(& $normalizedKey)
554    {
555        $options   = $this->getOptions();
556        $namespace = $options->getNamespace();
557        $prefix    = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
558        return apc_delete($prefix . $normalizedKey);
559    }
560
561    /**
562     * Internal method to remove multiple items.
563     *
564     * @param  array $normalizedKeys
565     * @return array Array of not removed keys
566     * @throws Exception\ExceptionInterface
567     */
568    protected function internalRemoveItems(array & $normalizedKeys)
569    {
570        $options   = $this->getOptions();
571        $namespace = $options->getNamespace();
572        if ($namespace === '') {
573            return apc_delete($normalizedKeys);
574        }
575
576        $prefix       = $namespace . $options->getNamespaceSeparator();
577        $internalKeys = array();
578        foreach ($normalizedKeys as $normalizedKey) {
579            $internalKeys[] = $prefix . $normalizedKey;
580        }
581
582        $failedKeys = apc_delete($internalKeys);
583
584        // remove prefix
585        $prefixL = strlen($prefix);
586        foreach ($failedKeys as & $key) {
587            $key = substr($key, $prefixL);
588        }
589
590        return $failedKeys;
591    }
592
593    /**
594     * Internal method to increment an item.
595     *
596     * @param  string $normalizedKey
597     * @param  int    $value
598     * @return int|bool The new value on success, false on failure
599     * @throws Exception\ExceptionInterface
600     */
601    protected function internalIncrementItem(& $normalizedKey, & $value)
602    {
603        $options     = $this->getOptions();
604        $namespace   = $options->getNamespace();
605        $prefix      = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
606        $internalKey = $prefix . $normalizedKey;
607        $value       = (int) $value;
608        $newValue    = apc_inc($internalKey, $value);
609
610        // initial value
611        if ($newValue === false) {
612            $ttl      = $options->getTtl();
613            $newValue = $value;
614            if (!apc_add($internalKey, $newValue, $ttl)) {
615                throw new Exception\RuntimeException(
616                    "apc_add('{$internalKey}', {$newValue}, {$ttl}) failed"
617                );
618            }
619        }
620
621        return $newValue;
622    }
623
624    /**
625     * Internal method to decrement an item.
626     *
627     * @param  string $normalizedKey
628     * @param  int    $value
629     * @return int|bool The new value on success, false on failure
630     * @throws Exception\ExceptionInterface
631     */
632    protected function internalDecrementItem(& $normalizedKey, & $value)
633    {
634        $options     = $this->getOptions();
635        $namespace   = $options->getNamespace();
636        $prefix      = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
637        $internalKey = $prefix . $normalizedKey;
638        $value       = (int) $value;
639        $newValue    = apc_dec($internalKey, $value);
640
641        // initial value
642        if ($newValue === false) {
643            $ttl      = $options->getTtl();
644            $newValue = -$value;
645            if (!apc_add($internalKey, $newValue, $ttl)) {
646                throw new Exception\RuntimeException(
647                    "apc_add('{$internalKey}', {$newValue}, {$ttl}) failed"
648                );
649            }
650        }
651
652        return $newValue;
653    }
654
655    /* status */
656
657    /**
658     * Internal method to get capabilities of this adapter
659     *
660     * @return Capabilities
661     */
662    protected function internalGetCapabilities()
663    {
664        if ($this->capabilities === null) {
665            $marker       = new stdClass();
666            $capabilities = new Capabilities(
667                $this,
668                $marker,
669                array(
670                    'supportedDatatypes' => array(
671                        'NULL'     => true,
672                        'boolean'  => true,
673                        'integer'  => true,
674                        'double'   => true,
675                        'string'   => true,
676                        'array'    => true,
677                        'object'   => 'object',
678                        'resource' => false,
679                    ),
680                    'supportedMetadata' => array(
681                        'internal_key',
682                        'atime', 'ctime', 'mtime', 'rtime',
683                        'size', 'hits', 'ttl',
684                    ),
685                    'minTtl'             => 1,
686                    'maxTtl'             => 0,
687                    'staticTtl'          => true,
688                    'ttlPrecision'       => 1,
689                    'useRequestTime'     => (bool) ini_get('apc.use_request_time'),
690                    'expiredRead'        => false,
691                    'maxKeyLength'       => 5182,
692                    'namespaceIsPrefix'  => true,
693                    'namespaceSeparator' => $this->getOptions()->getNamespaceSeparator(),
694                )
695            );
696
697            // update namespace separator on change option
698            $this->getEventManager()->attach('option', function ($event) use ($capabilities, $marker) {
699                $params = $event->getParams();
700
701                if (isset($params['namespace_separator'])) {
702                    $capabilities->setNamespaceSeparator($marker, $params['namespace_separator']);
703                }
704            });
705
706            $this->capabilities     = $capabilities;
707            $this->capabilityMarker = $marker;
708        }
709
710        return $this->capabilities;
711    }
712
713    /* internal */
714
715    /**
716     * Normalize metadata to work with APC
717     *
718     * @param  array $metadata
719     * @return void
720     */
721    protected function normalizeMetadata(array & $metadata)
722    {
723        $apcMetadata = $metadata;
724        $metadata = array(
725            'internal_key' => isset($metadata['key']) ? $metadata['key'] : $metadata['info'],
726            'atime'        => isset($metadata['access_time']) ? $metadata['access_time'] : $metadata['atime'],
727            'ctime'        => isset($metadata['creation_time']) ? $metadata['creation_time'] : $metadata['ctime'],
728            'mtime'        => isset($metadata['modified_time']) ? $metadata['modified_time'] : $metadata['mtime'],
729            'rtime'        => isset($metadata['deletion_time']) ? $metadata['deletion_time'] : $metadata['dtime'],
730            'size'         => $metadata['mem_size'],
731            'hits'         => isset($metadata['nhits']) ? $metadata['nhits'] : $metadata['num_hits'],
732            'ttl'          => $metadata['ttl'],
733        );
734    }
735
736    /**
737     * Internal method to set an item only if token matches
738     *
739     * @param  mixed  $token
740     * @param  string $normalizedKey
741     * @param  mixed  $value
742     * @return bool
743     * @see    getItem()
744     * @see    setItem()
745     */
746    protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value)
747    {
748        if (is_int($token) && is_int($value)) {
749            return apc_cas($normalizedKey, $token, $value);
750        }
751
752        return parent::internalCheckAndSetItem($token, $normalizedKey, $value);
753    }
754}
755