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 Memcached as MemcachedResource;
13use stdClass;
14use Traversable;
15use Zend\Cache\Exception;
16use Zend\Cache\Storage\AvailableSpaceCapableInterface;
17use Zend\Cache\Storage\Capabilities;
18use Zend\Cache\Storage\FlushableInterface;
19use Zend\Cache\Storage\TotalSpaceCapableInterface;
20
21class Memcached extends AbstractAdapter implements
22    AvailableSpaceCapableInterface,
23    FlushableInterface,
24    TotalSpaceCapableInterface
25{
26    /**
27     * Major version of ext/memcached
28     *
29     * @var null|int
30     */
31    protected static $extMemcachedMajorVersion;
32
33    /**
34     * Has this instance be initialized
35     *
36     * @var bool
37     */
38    protected $initialized = false;
39
40    /**
41     * The memcached resource manager
42     *
43     * @var null|MemcachedResourceManager
44     */
45    protected $resourceManager;
46
47    /**
48     * The memcached resource id
49     *
50     * @var null|string
51     */
52    protected $resourceId;
53
54    /**
55     * The namespace prefix
56     *
57     * @var string
58     */
59    protected $namespacePrefix = '';
60
61    /**
62     * Constructor
63     *
64     * @param  null|array|Traversable|MemcachedOptions $options
65     * @throws Exception\ExceptionInterface
66     */
67    public function __construct($options = null)
68    {
69        if (static::$extMemcachedMajorVersion === null) {
70            $v = (string) phpversion('memcached');
71            static::$extMemcachedMajorVersion = ($v !== '') ? (int) $v[0] : 0;
72        }
73
74        if (static::$extMemcachedMajorVersion < 1) {
75            throw new Exception\ExtensionNotLoadedException('Need ext/memcached version >= 1.0.0');
76        }
77
78        parent::__construct($options);
79
80        // reset initialized flag on update option(s)
81        $initialized = & $this->initialized;
82        $this->getEventManager()->attach('option', function () use (& $initialized) {
83            $initialized = false;
84        });
85    }
86
87    /**
88     * Initialize the internal memcached resource
89     *
90     * @return MemcachedResource
91     */
92    protected function getMemcachedResource()
93    {
94        if (!$this->initialized) {
95            $options = $this->getOptions();
96
97            // get resource manager and resource id
98            $this->resourceManager = $options->getResourceManager();
99            $this->resourceId      = $options->getResourceId();
100
101            // init namespace prefix
102            $namespace = $options->getNamespace();
103            if ($namespace !== '') {
104                $this->namespacePrefix = $namespace . $options->getNamespaceSeparator();
105            } else {
106                $this->namespacePrefix = '';
107            }
108
109            // update initialized flag
110            $this->initialized = true;
111        }
112
113        return $this->resourceManager->getResource($this->resourceId);
114    }
115
116    /* options */
117
118    /**
119     * Set options.
120     *
121     * @param  array|Traversable|MemcachedOptions $options
122     * @return Memcached
123     * @see    getOptions()
124     */
125    public function setOptions($options)
126    {
127        if (!$options instanceof MemcachedOptions) {
128            $options = new MemcachedOptions($options);
129        }
130
131        return parent::setOptions($options);
132    }
133
134    /**
135     * Get options.
136     *
137     * @return MemcachedOptions
138     * @see setOptions()
139     */
140    public function getOptions()
141    {
142        if (!$this->options) {
143            $this->setOptions(new MemcachedOptions());
144        }
145        return $this->options;
146    }
147
148    /* FlushableInterface */
149
150    /**
151     * Flush the whole storage
152     *
153     * @return bool
154     */
155    public function flush()
156    {
157        $memc = $this->getMemcachedResource();
158        if (!$memc->flush()) {
159            throw $this->getExceptionByResultCode($memc->getResultCode());
160        }
161        return true;
162    }
163
164    /* TotalSpaceCapableInterface */
165
166    /**
167     * Get total space in bytes
168     *
169     * @return int|float
170     */
171    public function getTotalSpace()
172    {
173        $memc  = $this->getMemcachedResource();
174        $stats = $memc->getStats();
175        if ($stats === false) {
176            throw new Exception\RuntimeException($memc->getResultMessage());
177        }
178
179        $mem = array_pop($stats);
180        return $mem['limit_maxbytes'];
181    }
182
183    /* AvailableSpaceCapableInterface */
184
185    /**
186     * Get available space in bytes
187     *
188     * @return int|float
189     */
190    public function getAvailableSpace()
191    {
192        $memc  = $this->getMemcachedResource();
193        $stats = $memc->getStats();
194        if ($stats === false) {
195            throw new Exception\RuntimeException($memc->getResultMessage());
196        }
197
198        $mem = array_pop($stats);
199        return $mem['limit_maxbytes'] - $mem['bytes'];
200    }
201
202    /* reading */
203
204    /**
205     * Internal method to get an item.
206     *
207     * @param  string  $normalizedKey
208     * @param  bool $success
209     * @param  mixed   $casToken
210     * @return mixed Data on success, null on failure
211     * @throws Exception\ExceptionInterface
212     */
213    protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null)
214    {
215        $memc        = $this->getMemcachedResource();
216        $internalKey = $this->namespacePrefix . $normalizedKey;
217
218        if (func_num_args() > 2) {
219            $result = $memc->get($internalKey, null, $casToken);
220        } else {
221            $result = $memc->get($internalKey);
222        }
223
224        $success = true;
225        if ($result === false) {
226            $rsCode = $memc->getResultCode();
227            if ($rsCode == MemcachedResource::RES_NOTFOUND) {
228                $result = null;
229                $success = false;
230            } elseif ($rsCode) {
231                $success = false;
232                throw $this->getExceptionByResultCode($rsCode);
233            }
234        }
235
236        return $result;
237    }
238
239    /**
240     * Internal method to get multiple items.
241     *
242     * @param  array $normalizedKeys
243     * @return array Associative array of keys and values
244     * @throws Exception\ExceptionInterface
245     */
246    protected function internalGetItems(array & $normalizedKeys)
247    {
248        $memc = $this->getMemcachedResource();
249
250        foreach ($normalizedKeys as & $normalizedKey) {
251            $normalizedKey = $this->namespacePrefix . $normalizedKey;
252        }
253
254        $result = $memc->getMulti($normalizedKeys);
255        if ($result === false) {
256            throw $this->getExceptionByResultCode($memc->getResultCode());
257        }
258
259        // remove namespace prefix from result
260        if ($result && $this->namespacePrefix !== '') {
261            $tmp            = array();
262            $nsPrefixLength = strlen($this->namespacePrefix);
263            foreach ($result as $internalKey => & $value) {
264                $tmp[substr($internalKey, $nsPrefixLength)] = & $value;
265            }
266            $result = $tmp;
267        }
268
269        return $result;
270    }
271
272    /**
273     * Internal method to test if an item exists.
274     *
275     * @param  string $normalizedKey
276     * @return bool
277     * @throws Exception\ExceptionInterface
278     */
279    protected function internalHasItem(& $normalizedKey)
280    {
281        $memc  = $this->getMemcachedResource();
282        $value = $memc->get($this->namespacePrefix . $normalizedKey);
283        if ($value === false) {
284            $rsCode = $memc->getResultCode();
285            if ($rsCode == MemcachedResource::RES_SUCCESS) {
286                return true;
287            } elseif ($rsCode == MemcachedResource::RES_NOTFOUND) {
288                return false;
289            } else {
290                throw $this->getExceptionByResultCode($rsCode);
291            }
292        }
293
294        return true;
295    }
296
297    /**
298     * Internal method to test multiple items.
299     *
300     * @param  array $normalizedKeys
301     * @return array Array of found keys
302     * @throws Exception\ExceptionInterface
303     */
304    protected function internalHasItems(array & $normalizedKeys)
305    {
306        $memc = $this->getMemcachedResource();
307
308        foreach ($normalizedKeys as & $normalizedKey) {
309            $normalizedKey = $this->namespacePrefix . $normalizedKey;
310        }
311
312        $result = $memc->getMulti($normalizedKeys);
313        if ($result === false) {
314            throw $this->getExceptionByResultCode($memc->getResultCode());
315        }
316
317        // Convert to a simgle list
318        $result = array_keys($result);
319
320        // remove namespace prefix
321        if ($result && $this->namespacePrefix !== '') {
322            $nsPrefixLength = strlen($this->namespacePrefix);
323            foreach ($result as & $internalKey) {
324                $internalKey = substr($internalKey, $nsPrefixLength);
325            }
326        }
327
328        return $result;
329    }
330
331    /**
332     * Get metadata of multiple items
333     *
334     * @param  array $normalizedKeys
335     * @return array Associative array of keys and metadata
336     * @throws Exception\ExceptionInterface
337     */
338    protected function internalGetMetadatas(array & $normalizedKeys)
339    {
340        $memc = $this->getMemcachedResource();
341
342        foreach ($normalizedKeys as & $normalizedKey) {
343            $normalizedKey = $this->namespacePrefix . $normalizedKey;
344        }
345
346        $result = $memc->getMulti($normalizedKeys);
347        if ($result === false) {
348            throw $this->getExceptionByResultCode($memc->getResultCode());
349        }
350
351        // remove namespace prefix and use an empty array as metadata
352        if ($this->namespacePrefix !== '') {
353            $tmp            = array();
354            $nsPrefixLength = strlen($this->namespacePrefix);
355            foreach (array_keys($result) as $internalKey) {
356                $tmp[substr($internalKey, $nsPrefixLength)] = array();
357            }
358            $result = $tmp;
359        } else {
360            foreach ($result as & $value) {
361                $value = array();
362            }
363        }
364
365        return $result;
366    }
367
368    /* writing */
369
370    /**
371     * Internal method to store an item.
372     *
373     * @param  string $normalizedKey
374     * @param  mixed  $value
375     * @return bool
376     * @throws Exception\ExceptionInterface
377     */
378    protected function internalSetItem(& $normalizedKey, & $value)
379    {
380        $memc       = $this->getMemcachedResource();
381        $expiration = $this->expirationTime();
382        if (!$memc->set($this->namespacePrefix . $normalizedKey, $value, $expiration)) {
383            throw $this->getExceptionByResultCode($memc->getResultCode());
384        }
385
386        return true;
387    }
388
389    /**
390     * Internal method to store multiple items.
391     *
392     * @param  array $normalizedKeyValuePairs
393     * @return array Array of not stored keys
394     * @throws Exception\ExceptionInterface
395     */
396    protected function internalSetItems(array & $normalizedKeyValuePairs)
397    {
398        $memc       = $this->getMemcachedResource();
399        $expiration = $this->expirationTime();
400
401        $namespacedKeyValuePairs = array();
402        foreach ($normalizedKeyValuePairs as $normalizedKey => & $value) {
403            $namespacedKeyValuePairs[$this->namespacePrefix . $normalizedKey] = & $value;
404        }
405
406        if (!$memc->setMulti($namespacedKeyValuePairs, $expiration)) {
407            throw $this->getExceptionByResultCode($memc->getResultCode());
408        }
409
410        return array();
411    }
412
413    /**
414     * Add an item.
415     *
416     * @param  string $normalizedKey
417     * @param  mixed  $value
418     * @return bool
419     * @throws Exception\ExceptionInterface
420     */
421    protected function internalAddItem(& $normalizedKey, & $value)
422    {
423        $memc       = $this->getMemcachedResource();
424        $expiration = $this->expirationTime();
425        if (!$memc->add($this->namespacePrefix . $normalizedKey, $value, $expiration)) {
426            if ($memc->getResultCode() == MemcachedResource::RES_NOTSTORED) {
427                return false;
428            }
429            throw $this->getExceptionByResultCode($memc->getResultCode());
430        }
431
432        return true;
433    }
434
435    /**
436     * Internal method to replace an existing item.
437     *
438     * @param  string $normalizedKey
439     * @param  mixed  $value
440     * @return bool
441     * @throws Exception\ExceptionInterface
442     */
443    protected function internalReplaceItem(& $normalizedKey, & $value)
444    {
445        $memc       = $this->getMemcachedResource();
446        $expiration = $this->expirationTime();
447        if (!$memc->replace($this->namespacePrefix . $normalizedKey, $value, $expiration)) {
448            $rsCode = $memc->getResultCode();
449            if ($rsCode == MemcachedResource::RES_NOTSTORED) {
450                return false;
451            }
452            throw $this->getExceptionByResultCode($rsCode);
453        }
454
455        return true;
456    }
457
458    /**
459     * Internal method to set an item only if token matches
460     *
461     * @param  mixed  $token
462     * @param  string $normalizedKey
463     * @param  mixed  $value
464     * @return bool
465     * @throws Exception\ExceptionInterface
466     * @see    getItem()
467     * @see    setItem()
468     */
469    protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value)
470    {
471        $memc       = $this->getMemcachedResource();
472        $expiration = $this->expirationTime();
473        $result     = $memc->cas($token, $this->namespacePrefix . $normalizedKey, $value, $expiration);
474
475        if ($result === false) {
476            $rsCode = $memc->getResultCode();
477            if ($rsCode !== 0 && $rsCode != MemcachedResource::RES_DATA_EXISTS) {
478                throw $this->getExceptionByResultCode($rsCode);
479            }
480        }
481
482        return $result;
483    }
484
485    /**
486     * Internal method to remove an item.
487     *
488     * @param  string $normalizedKey
489     * @return bool
490     * @throws Exception\ExceptionInterface
491     */
492    protected function internalRemoveItem(& $normalizedKey)
493    {
494        $memc   = $this->getMemcachedResource();
495        $result = $memc->delete($this->namespacePrefix . $normalizedKey);
496
497        if ($result === false) {
498            $rsCode = $memc->getResultCode();
499            if ($rsCode == MemcachedResource::RES_NOTFOUND) {
500                return false;
501            } elseif ($rsCode != MemcachedResource::RES_SUCCESS) {
502                throw $this->getExceptionByResultCode($rsCode);
503            }
504        }
505
506        return true;
507    }
508
509    /**
510     * Internal method to remove multiple items.
511     *
512     * @param  array $normalizedKeys
513     * @return array Array of not removed keys
514     * @throws Exception\ExceptionInterface
515     */
516    protected function internalRemoveItems(array & $normalizedKeys)
517    {
518        // support for removing multiple items at once has been added in ext/memcached-2.0.0
519        if (static::$extMemcachedMajorVersion < 2) {
520            return parent::internalRemoveItems($normalizedKeys);
521        }
522
523        $memc = $this->getMemcachedResource();
524
525        foreach ($normalizedKeys as & $normalizedKey) {
526            $normalizedKey = $this->namespacePrefix . $normalizedKey;
527        }
528
529        $rsCodes = $memc->deleteMulti($normalizedKeys);
530
531        $missingKeys = array();
532        foreach ($rsCodes as $key => $rsCode) {
533            if ($rsCode !== true && $rsCode != MemcachedResource::RES_SUCCESS) {
534                if ($rsCode != MemcachedResource::RES_NOTFOUND) {
535                    throw $this->getExceptionByResultCode($rsCode);
536                }
537                $missingKeys[] = $key;
538            }
539        }
540
541        // remove namespace prefix
542        if ($missingKeys && $this->namespacePrefix !== '') {
543            $nsPrefixLength = strlen($this->namespacePrefix);
544            foreach ($missingKeys as & $missingKey) {
545                $missingKey = substr($missingKey, $nsPrefixLength);
546            }
547        }
548
549        return $missingKeys;
550    }
551
552    /**
553     * Internal method to increment an item.
554     *
555     * @param  string $normalizedKey
556     * @param  int    $value
557     * @return int|bool The new value on success, false on failure
558     * @throws Exception\ExceptionInterface
559     */
560    protected function internalIncrementItem(& $normalizedKey, & $value)
561    {
562        $memc        = $this->getMemcachedResource();
563        $internalKey = $this->namespacePrefix . $normalizedKey;
564        $value       = (int) $value;
565        $newValue    = $memc->increment($internalKey, $value);
566
567        if ($newValue === false) {
568            $rsCode = $memc->getResultCode();
569
570            // initial value
571            if ($rsCode == MemcachedResource::RES_NOTFOUND) {
572                $newValue = $value;
573                $memc->add($internalKey, $newValue, $this->expirationTime());
574                $rsCode = $memc->getResultCode();
575            }
576
577            if ($rsCode) {
578                throw $this->getExceptionByResultCode($rsCode);
579            }
580        }
581
582        return $newValue;
583    }
584
585    /**
586     * Internal method to decrement an item.
587     *
588     * @param  string $normalizedKey
589     * @param  int    $value
590     * @return int|bool The new value on success, false on failure
591     * @throws Exception\ExceptionInterface
592     */
593    protected function internalDecrementItem(& $normalizedKey, & $value)
594    {
595        $memc        = $this->getMemcachedResource();
596        $internalKey = $this->namespacePrefix . $normalizedKey;
597        $value       = (int) $value;
598        $newValue    = $memc->decrement($internalKey, $value);
599
600        if ($newValue === false) {
601            $rsCode = $memc->getResultCode();
602
603            // initial value
604            if ($rsCode == MemcachedResource::RES_NOTFOUND) {
605                $newValue = -$value;
606                $memc->add($internalKey, $newValue, $this->expirationTime());
607                $rsCode = $memc->getResultCode();
608            }
609
610            if ($rsCode) {
611                throw $this->getExceptionByResultCode($rsCode);
612            }
613        }
614
615        return $newValue;
616    }
617
618    /* status */
619
620    /**
621     * Internal method to get capabilities of this adapter
622     *
623     * @return Capabilities
624     */
625    protected function internalGetCapabilities()
626    {
627        if ($this->capabilities === null) {
628            $this->capabilityMarker = new stdClass();
629            $this->capabilities     = new Capabilities(
630                $this,
631                $this->capabilityMarker,
632                array(
633                    'supportedDatatypes' => array(
634                        'NULL'     => true,
635                        'boolean'  => true,
636                        'integer'  => true,
637                        'double'   => true,
638                        'string'   => true,
639                        'array'    => true,
640                        'object'   => 'object',
641                        'resource' => false,
642                    ),
643                    'supportedMetadata'  => array(),
644                    'minTtl'             => 1,
645                    'maxTtl'             => 0,
646                    'staticTtl'          => true,
647                    'ttlPrecision'       => 1,
648                    'useRequestTime'     => false,
649                    'expiredRead'        => false,
650                    'maxKeyLength'       => 255,
651                    'namespaceIsPrefix'  => true,
652                )
653            );
654        }
655
656        return $this->capabilities;
657    }
658
659    /* internal */
660
661    /**
662     * Get expiration time by ttl
663     *
664     * Some storage commands involve sending an expiration value (relative to
665     * an item or to an operation requested by the client) to the server. In
666     * all such cases, the actual value sent may either be Unix time (number of
667     * seconds since January 1, 1970, as an integer), or a number of seconds
668     * starting from current time. In the latter case, this number of seconds
669     * may not exceed 60*60*24*30 (number of seconds in 30 days); if the
670     * expiration value is larger than that, the server will consider it to be
671     * real Unix time value rather than an offset from current time.
672     *
673     * @return int
674     */
675    protected function expirationTime()
676    {
677        $ttl = $this->getOptions()->getTtl();
678        if ($ttl > 2592000) {
679            return time() + $ttl;
680        }
681        return $ttl;
682    }
683
684    /**
685     * Generate exception based of memcached result code
686     *
687     * @param int $code
688     * @return Exception\RuntimeException
689     * @throws Exception\InvalidArgumentException On success code
690     */
691    protected function getExceptionByResultCode($code)
692    {
693        switch ($code) {
694            case MemcachedResource::RES_SUCCESS:
695                throw new Exception\InvalidArgumentException(
696                    "The result code '{$code}' (SUCCESS) isn't an error"
697                );
698
699            default:
700                return new Exception\RuntimeException($this->getMemcachedResource()->getResultMessage());
701        }
702    }
703}
704