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 Memcache as MemcacheResource;
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 Memcache extends AbstractAdapter implements
22    AvailableSpaceCapableInterface,
23    FlushableInterface,
24    TotalSpaceCapableInterface
25{
26    /**
27     * Has this instance been initialized
28     *
29     * @var bool
30     */
31    protected $initialized = false;
32
33    /**
34     * The memcache resource manager
35     *
36     * @var null|MemcacheResourceManager
37     */
38    protected $resourceManager;
39
40    /**
41     * The memcache resource id
42     *
43     * @var null|string
44     */
45    protected $resourceId;
46
47    /**
48     * The namespace prefix
49     *
50     * @var string
51     */
52    protected $namespacePrefix = '';
53
54    /**
55     * Constructor
56     *
57     * @param  null|array|Traversable|MemcacheOptions $options
58     * @throws Exception\ExceptionInterface
59     */
60    public function __construct($options = null)
61    {
62        if (version_compare('2.0.0', phpversion('memcache')) > 0) {
63            throw new Exception\ExtensionNotLoadedException("Missing ext/memcache version >= 2.0.0");
64        }
65
66        parent::__construct($options);
67
68        // reset initialized flag on update option(s)
69        $initialized = & $this->initialized;
70        $this->getEventManager()->attach('option', function () use (& $initialized) {
71            $initialized = false;
72        });
73    }
74
75    /**
76     * Initialize the internal memcache resource
77     *
78     * @return MemcacheResource
79     */
80    protected function getMemcacheResource()
81    {
82        if ($this->initialized) {
83            return $this->resourceManager->getResource($this->resourceId);
84        }
85
86        $options = $this->getOptions();
87
88        // get resource manager and resource id
89        $this->resourceManager = $options->getResourceManager();
90        $this->resourceId      = $options->getResourceId();
91
92        // init namespace prefix
93        $this->namespacePrefix = '';
94        $namespace = $options->getNamespace();
95        if ($namespace !== '') {
96            $this->namespacePrefix = $namespace . $options->getNamespaceSeparator();
97        }
98
99        // update initialized flag
100        $this->initialized = true;
101
102        return $this->resourceManager->getResource($this->resourceId);
103    }
104
105    /* options */
106
107    /**
108     * Set options.
109     *
110     * @param  array|Traversable|MemcacheOptions $options
111     * @return Memcache
112     * @see    getOptions()
113     */
114    public function setOptions($options)
115    {
116        if (!$options instanceof MemcacheOptions) {
117            $options = new MemcacheOptions($options);
118        }
119
120        return parent::setOptions($options);
121    }
122
123    /**
124     * Get options.
125     *
126     * @return MemcacheOptions
127     * @see setOptions()
128     */
129    public function getOptions()
130    {
131        if (!$this->options) {
132            $this->setOptions(new MemcacheOptions());
133        }
134        return $this->options;
135    }
136
137    /**
138     * @param  mixed $value
139     * @return int
140     */
141    protected function getWriteFlag(& $value)
142    {
143        if (!$this->getOptions()->getCompression()) {
144            return 0;
145        }
146        // Don't compress numeric or boolean types
147        return (is_bool($value) || is_int($value) || is_float($value)) ? 0 : MEMCACHE_COMPRESSED;
148    }
149
150    /* FlushableInterface */
151
152    /**
153     * Flush the whole storage
154     *
155     * @return bool
156     */
157    public function flush()
158    {
159        $memc = $this->getMemcacheResource();
160        if (!$memc->flush()) {
161            return new Exception\RuntimeException("Memcache flush failed");
162        }
163        return true;
164    }
165
166    /* TotalSpaceCapableInterface */
167
168    /**
169     * Get total space in bytes
170     *
171     * @return int|float
172     */
173    public function getTotalSpace()
174    {
175        $memc  = $this->getMemcacheResource();
176        $stats = $memc->getExtendedStats();
177        if ($stats === false) {
178            return new Exception\RuntimeException("Memcache getStats failed");
179        }
180
181        $mem = array_pop($stats);
182        return $mem['limit_maxbytes'];
183    }
184
185    /* AvailableSpaceCapableInterface */
186
187    /**
188     * Get available space in bytes
189     *
190     * @return int|float
191     */
192    public function getAvailableSpace()
193    {
194        $memc  = $this->getMemcacheResource();
195        $stats = $memc->getExtendedStats();
196        if ($stats === false) {
197            throw new Exception\RuntimeException('Memcache getStats failed');
198        }
199
200        $mem = array_pop($stats);
201        return $mem['limit_maxbytes'] - $mem['bytes'];
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        $memc        = $this->getMemcacheResource();
218        $internalKey = $this->namespacePrefix . $normalizedKey;
219
220        $result = $memc->get($internalKey);
221        $success = ($result !== false);
222        if ($result === false) {
223            return;
224        }
225
226        $casToken = $result;
227        return $result;
228    }
229
230    /**
231     * Internal method to get multiple items.
232     *
233     * @param  array $normalizedKeys
234     * @return array Associative array of keys and values
235     * @throws Exception\ExceptionInterface
236     */
237    protected function internalGetItems(array & $normalizedKeys)
238    {
239        $memc = $this->getMemcacheResource();
240
241        foreach ($normalizedKeys as & $normalizedKey) {
242            $normalizedKey = $this->namespacePrefix . $normalizedKey;
243        }
244
245        $result = $memc->get($normalizedKeys);
246        if ($result === false) {
247            return array();
248        }
249
250        // remove namespace prefix from result
251        if ($this->namespacePrefix !== '') {
252            $tmp            = array();
253            $nsPrefixLength = strlen($this->namespacePrefix);
254            foreach ($result as $internalKey => & $value) {
255                $tmp[substr($internalKey, $nsPrefixLength)] = & $value;
256            }
257            $result = $tmp;
258        }
259
260        return $result;
261    }
262
263    /**
264     * Internal method to test if an item exists.
265     *
266     * @param  string $normalizedKey
267     * @return bool
268     * @throws Exception\ExceptionInterface
269     */
270    protected function internalHasItem(& $normalizedKey)
271    {
272        $memc  = $this->getMemcacheResource();
273        $value = $memc->get($this->namespacePrefix . $normalizedKey);
274        return ($value !== false);
275    }
276
277    /**
278     * Internal method to test multiple items.
279     *
280     * @param  array $normalizedKeys
281     * @return array Array of found keys
282     * @throws Exception\ExceptionInterface
283     */
284    protected function internalHasItems(array & $normalizedKeys)
285    {
286        $memc = $this->getMemcacheResource();
287
288        foreach ($normalizedKeys as & $normalizedKey) {
289            $normalizedKey = $this->namespacePrefix . $normalizedKey;
290        }
291
292        $result = $memc->get($normalizedKeys);
293        if ($result === false) {
294            return array();
295        }
296
297        // Convert to a single list
298        $result = array_keys($result);
299
300        // remove namespace prefix
301        if ($result && $this->namespacePrefix !== '') {
302            $nsPrefixLength = strlen($this->namespacePrefix);
303            foreach ($result as & $internalKey) {
304                $internalKey = substr($internalKey, $nsPrefixLength);
305            }
306        }
307
308        return $result;
309    }
310
311    /**
312     * Get metadata of multiple items
313     *
314     * @param  array $normalizedKeys
315     * @return array Associative array of keys and metadata
316     * @throws Exception\ExceptionInterface
317     */
318    protected function internalGetMetadatas(array & $normalizedKeys)
319    {
320        $memc = $this->getMemcacheResource();
321
322        foreach ($normalizedKeys as & $normalizedKey) {
323            $normalizedKey = $this->namespacePrefix . $normalizedKey;
324        }
325
326        $result = $memc->get($normalizedKeys);
327        if ($result === false) {
328            return array();
329        }
330
331        // remove namespace prefix and use an empty array as metadata
332        if ($this->namespacePrefix === '') {
333            foreach ($result as & $value) {
334                $value = array();
335            }
336            return $result;
337        }
338
339        $final          = array();
340        $nsPrefixLength = strlen($this->namespacePrefix);
341        foreach (array_keys($result) as $internalKey) {
342            $final[substr($internalKey, $nsPrefixLength)] = array();
343        }
344        return $final;
345    }
346
347    /* writing */
348
349    /**
350     * Internal method to store an item.
351     *
352     * @param  string $normalizedKey
353     * @param  mixed  $value
354     * @return bool
355     * @throws Exception\ExceptionInterface
356     */
357    protected function internalSetItem(& $normalizedKey, & $value)
358    {
359        $memc       = $this->getMemcacheResource();
360        $expiration = $this->expirationTime();
361        $flag       = $this->getWriteFlag($value);
362
363        if (!$memc->set($this->namespacePrefix . $normalizedKey, $value, $flag, $expiration)) {
364            throw new Exception\RuntimeException('Memcache set value failed');
365        }
366
367        return true;
368    }
369
370    /**
371     * Add an item.
372     *
373     * @param  string $normalizedKey
374     * @param  mixed  $value
375     * @return bool
376     * @throws Exception\ExceptionInterface
377     */
378    protected function internalAddItem(& $normalizedKey, & $value)
379    {
380        $memc       = $this->getMemcacheResource();
381        $expiration = $this->expirationTime();
382        $flag       = $this->getWriteFlag($value);
383
384        return $memc->add($this->namespacePrefix . $normalizedKey, $value, $flag, $expiration);
385    }
386
387    /**
388     * Internal method to replace an existing item.
389     *
390     * @param  string $normalizedKey
391     * @param  mixed  $value
392     * @return bool
393     * @throws Exception\ExceptionInterface
394     */
395    protected function internalReplaceItem(& $normalizedKey, & $value)
396    {
397        $memc       = $this->getMemcacheResource();
398        $expiration = $this->expirationTime();
399        $flag       = $this->getWriteFlag($value);
400
401        return $memc->replace($this->namespacePrefix . $normalizedKey, $value, $flag, $expiration);
402    }
403
404    /**
405     * Internal method to remove an item.
406     *
407     * @param  string $normalizedKey
408     * @return bool
409     * @throws Exception\ExceptionInterface
410     */
411    protected function internalRemoveItem(& $normalizedKey)
412    {
413        $memc   = $this->getMemcacheResource();
414        // Delete's second parameter (timeout) is deprecated and not supported.
415        // Values other than 0 may cause delete to fail.
416        // http://www.php.net/manual/memcache.delete.php
417        return $memc->delete($this->namespacePrefix . $normalizedKey, 0);
418    }
419
420    /**
421     * Internal method to increment an item.
422     *
423     * @param  string $normalizedKey
424     * @param  int    $value
425     * @return int|bool The new value on success, false on failure
426     * @throws Exception\ExceptionInterface
427     */
428    protected function internalIncrementItem(& $normalizedKey, & $value)
429    {
430        $memc        = $this->getMemcacheResource();
431        $internalKey = $this->namespacePrefix . $normalizedKey;
432        $value       = (int) $value;
433        $newValue    = $memc->increment($internalKey, $value);
434
435        if ($newValue !== false) {
436            return $newValue;
437        }
438
439        // Set initial value. Don't use compression!
440        // http://www.php.net/manual/memcache.increment.php
441        $newValue = $value;
442        if (!$memc->add($internalKey, $newValue, 0, $this->expirationTime())) {
443            throw new Exception\RuntimeException('Memcache unable to add increment value');
444        }
445
446        return $newValue;
447    }
448
449    /**
450     * Internal method to decrement an item.
451     *
452     * @param  string $normalizedKey
453     * @param  int    $value
454     * @return int|bool The new value on success, false on failure
455     * @throws Exception\ExceptionInterface
456     */
457    protected function internalDecrementItem(& $normalizedKey, & $value)
458    {
459        $memc        = $this->getMemcacheResource();
460        $internalKey = $this->namespacePrefix . $normalizedKey;
461        $value       = (int) $value;
462        $newValue    = $memc->decrement($internalKey, $value);
463
464        if ($newValue !== false) {
465            return $newValue;
466        }
467
468        // Set initial value. Don't use compression!
469        // http://www.php.net/manual/memcache.decrement.php
470        $newValue = -$value;
471        if (!$memc->add($internalKey, $newValue, 0, $this->expirationTime())) {
472            throw new Exception\RuntimeException('Memcache unable to add decrement value');
473        }
474
475        return $newValue;
476    }
477
478    /* status */
479
480    /**
481     * Internal method to get capabilities of this adapter
482     *
483     * @return Capabilities
484     */
485    protected function internalGetCapabilities()
486    {
487        if ($this->capabilities !== null) {
488            return $this->capabilities;
489        }
490
491        if (version_compare('3.0.3', phpversion('memcache')) <= 0) {
492            // In ext/memcache v3.0.3:
493            // Scalar data types (int, bool, double) are preserved by get/set.
494            // http://pecl.php.net/package/memcache/3.0.3
495            //
496            // This effectively removes support for `boolean` types since
497            // "not found" return values are === false.
498            $supportedDatatypes = array(
499                'NULL'     => true,
500                'boolean'  => false,
501                'integer'  => true,
502                'double'   => true,
503                'string'   => true,
504                'array'    => true,
505                'object'   => 'object',
506                'resource' => false,
507            );
508        } else {
509            // In stable 2.x ext/memcache versions, scalar data types are
510            // converted to strings and must be manually cast back to original
511            // types by the user.
512            //
513            // ie. It is impossible to know if the saved value: (string)"1"
514            // was previously: (bool)true, (int)1, or (string)"1".
515            // Similarly, the saved value: (string)""
516            // might have previously been: (bool)false or (string)""
517            $supportedDatatypes = array(
518                'NULL'     => true,
519                'boolean'  => 'boolean',
520                'integer'  => 'integer',
521                'double'   => 'double',
522                'string'   => true,
523                'array'    => true,
524                'object'   => 'object',
525                'resource' => false,
526            );
527        }
528
529        $this->capabilityMarker = new stdClass();
530        $this->capabilities     = new Capabilities(
531            $this,
532            $this->capabilityMarker,
533            array(
534                'supportedDatatypes' => $supportedDatatypes,
535                'supportedMetadata'  => array(),
536                'minTtl'             => 1,
537                'maxTtl'             => 0,
538                'staticTtl'          => true,
539                'ttlPrecision'       => 1,
540                'useRequestTime'     => false,
541                'expiredRead'        => false,
542                'maxKeyLength'       => 255,
543                'namespaceIsPrefix'  => true,
544            )
545        );
546
547        return $this->capabilities;
548    }
549
550    /* internal */
551
552    /**
553     * Get expiration time by ttl
554     *
555     * Some storage commands involve sending an expiration value (relative to
556     * an item or to an operation requested by the client) to the server. In
557     * all such cases, the actual value sent may either be Unix time (number of
558     * seconds since January 1, 1970, as an integer), or a number of seconds
559     * starting from current time. In the latter case, this number of seconds
560     * may not exceed 60*60*24*30 (number of seconds in 30 days); if the
561     * expiration value is larger than that, the server will consider it to be
562     * real Unix time value rather than an offset from current time.
563     *
564     * @return int
565     */
566    protected function expirationTime()
567    {
568        $ttl = $this->getOptions()->getTtl();
569        if ($ttl > 2592000) {
570            return time() + $ttl;
571        }
572        return $ttl;
573    }
574}
575