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 ArrayObject;
13use SplObjectStorage;
14use stdClass;
15use Traversable;
16use Zend\Cache\Exception;
17use Zend\Cache\Storage\Capabilities;
18use Zend\Cache\Storage\Event;
19use Zend\Cache\Storage\ExceptionEvent;
20use Zend\Cache\Storage\Plugin;
21use Zend\Cache\Storage\PostEvent;
22use Zend\Cache\Storage\StorageInterface;
23use Zend\EventManager\EventManager;
24use Zend\EventManager\EventManagerInterface;
25use Zend\EventManager\EventsCapableInterface;
26
27abstract class AbstractAdapter implements StorageInterface, EventsCapableInterface
28{
29    /**
30     * The used EventManager if any
31     *
32     * @var null|EventManagerInterface
33     */
34    protected $events = null;
35
36    /**
37     * Event handles of this adapter
38     * @var array
39     */
40    protected $eventHandles = array();
41
42    /**
43     * The plugin registry
44     *
45     * @var SplObjectStorage Registered plugins
46     */
47    protected $pluginRegistry;
48
49    /**
50     * Capabilities of this adapter
51     *
52     * @var null|Capabilities
53     */
54    protected $capabilities = null;
55
56    /**
57     * Marker to change capabilities
58     *
59     * @var null|object
60     */
61    protected $capabilityMarker;
62
63    /**
64     * options
65     *
66     * @var mixed
67     */
68    protected $options;
69
70    /**
71     * Constructor
72     *
73     * @param  null|array|Traversable|AdapterOptions $options
74     * @throws Exception\ExceptionInterface
75     */
76    public function __construct($options = null)
77    {
78        if ($options) {
79            $this->setOptions($options);
80        }
81    }
82
83    /**
84     * Destructor
85     *
86     * detach all registered plugins to free
87     * event handles of event manager
88     *
89     * @return void
90     */
91    public function __destruct()
92    {
93        foreach ($this->getPluginRegistry() as $plugin) {
94            $this->removePlugin($plugin);
95        }
96
97        if ($this->eventHandles) {
98            $events = $this->getEventManager();
99            foreach ($this->eventHandles as $handle) {
100                $events->detach($handle);
101            }
102        }
103    }
104
105    /* configuration */
106
107    /**
108     * Set options.
109     *
110     * @param  array|Traversable|AdapterOptions $options
111     * @return AbstractAdapter
112     * @see    getOptions()
113     */
114    public function setOptions($options)
115    {
116        if ($this->options !== $options) {
117            if (!$options instanceof AdapterOptions) {
118                $options = new AdapterOptions($options);
119            }
120
121            if ($this->options) {
122                $this->options->setAdapter(null);
123            }
124            $options->setAdapter($this);
125            $this->options = $options;
126
127            $event = new Event('option', $this, new ArrayObject($options->toArray()));
128            $this->getEventManager()->trigger($event);
129        }
130        return $this;
131    }
132
133    /**
134     * Get options.
135     *
136     * @return AdapterOptions
137     * @see setOptions()
138     */
139    public function getOptions()
140    {
141        if (!$this->options) {
142            $this->setOptions(new AdapterOptions());
143        }
144        return $this->options;
145    }
146
147    /**
148     * Enable/Disable caching.
149     *
150     * Alias of setWritable and setReadable.
151     *
152     * @see    setWritable()
153     * @see    setReadable()
154     * @param  bool $flag
155     * @return AbstractAdapter
156     */
157    public function setCaching($flag)
158    {
159        $flag    = (bool) $flag;
160        $options = $this->getOptions();
161        $options->setWritable($flag);
162        $options->setReadable($flag);
163        return $this;
164    }
165
166    /**
167     * Get caching enabled.
168     *
169     * Alias of getWritable and getReadable.
170     *
171     * @see    getWritable()
172     * @see    getReadable()
173     * @return bool
174     */
175    public function getCaching()
176    {
177        $options = $this->getOptions();
178        return ($options->getWritable() && $options->getReadable());
179    }
180
181    /* Event/Plugin handling */
182
183    /**
184     * Get the event manager
185     *
186     * @return EventManagerInterface
187     */
188    public function getEventManager()
189    {
190        if ($this->events === null) {
191            $this->events = new EventManager(array(__CLASS__, get_class($this)));
192        }
193        return $this->events;
194    }
195
196    /**
197     * Trigger a pre event and return the event response collection
198     *
199     * @param  string $eventName
200     * @param  ArrayObject $args
201     * @return \Zend\EventManager\ResponseCollection All handler return values
202     */
203    protected function triggerPre($eventName, ArrayObject $args)
204    {
205        return $this->getEventManager()->trigger(new Event($eventName . '.pre', $this, $args));
206    }
207
208    /**
209     * Triggers the PostEvent and return the result value.
210     *
211     * @param  string      $eventName
212     * @param  ArrayObject $args
213     * @param  mixed       $result
214     * @return mixed
215     */
216    protected function triggerPost($eventName, ArrayObject $args, & $result)
217    {
218        $postEvent = new PostEvent($eventName . '.post', $this, $args, $result);
219        $eventRs   = $this->getEventManager()->trigger($postEvent);
220
221        return $eventRs->stopped()
222            ? $eventRs->last()
223            : $postEvent->getResult();
224    }
225
226    /**
227     * Trigger an exception event
228     *
229     * If the ExceptionEvent has the flag "throwException" enabled throw the
230     * exception after trigger else return the result.
231     *
232     * @param  string      $eventName
233     * @param  ArrayObject $args
234     * @param  mixed       $result
235     * @param  \Exception  $exception
236     * @throws Exception\ExceptionInterface
237     * @return mixed
238     */
239    protected function triggerException($eventName, ArrayObject $args, & $result, \Exception $exception)
240    {
241        $exceptionEvent = new ExceptionEvent($eventName . '.exception', $this, $args, $result, $exception);
242        $eventRs        = $this->getEventManager()->trigger($exceptionEvent);
243
244        if ($exceptionEvent->getThrowException()) {
245            throw $exceptionEvent->getException();
246        }
247
248        return $eventRs->stopped()
249            ? $eventRs->last()
250            : $exceptionEvent->getResult();
251    }
252
253    /**
254     * Check if a plugin is registered
255     *
256     * @param  Plugin\PluginInterface $plugin
257     * @return bool
258     */
259    public function hasPlugin(Plugin\PluginInterface $plugin)
260    {
261        $registry = $this->getPluginRegistry();
262        return $registry->contains($plugin);
263    }
264
265    /**
266     * Register a plugin
267     *
268     * @param  Plugin\PluginInterface $plugin
269     * @param  int                    $priority
270     * @return AbstractAdapter Fluent interface
271     * @throws Exception\LogicException
272     */
273    public function addPlugin(Plugin\PluginInterface $plugin, $priority = 1)
274    {
275        $registry = $this->getPluginRegistry();
276        if ($registry->contains($plugin)) {
277            throw new Exception\LogicException(sprintf(
278                'Plugin of type "%s" already registered',
279                get_class($plugin)
280            ));
281        }
282
283        $plugin->attach($this->getEventManager(), $priority);
284        $registry->attach($plugin);
285
286        return $this;
287    }
288
289    /**
290     * Unregister an already registered plugin
291     *
292     * @param  Plugin\PluginInterface $plugin
293     * @return AbstractAdapter Fluent interface
294     * @throws Exception\LogicException
295     */
296    public function removePlugin(Plugin\PluginInterface $plugin)
297    {
298        $registry = $this->getPluginRegistry();
299        if ($registry->contains($plugin)) {
300            $plugin->detach($this->getEventManager());
301            $registry->detach($plugin);
302        }
303        return $this;
304    }
305
306    /**
307     * Return registry of plugins
308     *
309     * @return SplObjectStorage
310     */
311    public function getPluginRegistry()
312    {
313        if (!$this->pluginRegistry instanceof SplObjectStorage) {
314            $this->pluginRegistry = new SplObjectStorage();
315        }
316        return $this->pluginRegistry;
317    }
318
319    /* reading */
320
321    /**
322     * Get an item.
323     *
324     * @param  string  $key
325     * @param  bool $success
326     * @param  mixed   $casToken
327     * @return mixed Data on success, null on failure
328     * @throws Exception\ExceptionInterface
329     *
330     * @triggers getItem.pre(PreEvent)
331     * @triggers getItem.post(PostEvent)
332     * @triggers getItem.exception(ExceptionEvent)
333     */
334    public function getItem($key, & $success = null, & $casToken = null)
335    {
336        if (!$this->getOptions()->getReadable()) {
337            $success = false;
338            return;
339        }
340
341        $this->normalizeKey($key);
342
343        $argn = func_num_args();
344        $args = array(
345            'key' => & $key,
346        );
347        if ($argn > 1) {
348            $args['success'] = & $success;
349        }
350        if ($argn > 2) {
351            $args['casToken'] = & $casToken;
352        }
353        $args = new ArrayObject($args);
354
355        try {
356            $eventRs = $this->triggerPre(__FUNCTION__, $args);
357
358            if ($eventRs->stopped()) {
359                $result = $eventRs->last();
360            } elseif ($args->offsetExists('success') && $args->offsetExists('casToken')) {
361                $result = $this->internalGetItem($args['key'], $args['success'], $args['casToken']);
362            } elseif ($args->offsetExists('success')) {
363                $result = $this->internalGetItem($args['key'], $args['success']);
364            } else {
365                $result = $this->internalGetItem($args['key']);
366            }
367
368            return $this->triggerPost(__FUNCTION__, $args, $result);
369        } catch (\Exception $e) {
370            $result = null;
371            $success = false;
372            return $this->triggerException(__FUNCTION__, $args, $result, $e);
373        }
374    }
375
376    /**
377     * Internal method to get an item.
378     *
379     * @param  string  $normalizedKey
380     * @param  bool $success
381     * @param  mixed   $casToken
382     * @return mixed Data on success, null on failure
383     * @throws Exception\ExceptionInterface
384     */
385    abstract protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null);
386
387    /**
388     * Get multiple items.
389     *
390     * @param  array $keys
391     * @return array Associative array of keys and values
392     * @throws Exception\ExceptionInterface
393     *
394     * @triggers getItems.pre(PreEvent)
395     * @triggers getItems.post(PostEvent)
396     * @triggers getItems.exception(ExceptionEvent)
397     */
398    public function getItems(array $keys)
399    {
400        if (!$this->getOptions()->getReadable()) {
401            return array();
402        }
403
404        $this->normalizeKeys($keys);
405        $args = new ArrayObject(array(
406            'keys' => & $keys,
407        ));
408
409        try {
410            $eventRs = $this->triggerPre(__FUNCTION__, $args);
411
412            $result = $eventRs->stopped()
413                ? $eventRs->last()
414                : $this->internalGetItems($args['keys']);
415
416            return $this->triggerPost(__FUNCTION__, $args, $result);
417        } catch (\Exception $e) {
418            $result = array();
419            return $this->triggerException(__FUNCTION__, $args, $result, $e);
420        }
421    }
422
423    /**
424     * Internal method to get multiple items.
425     *
426     * @param  array $normalizedKeys
427     * @return array Associative array of keys and values
428     * @throws Exception\ExceptionInterface
429     */
430    protected function internalGetItems(array & $normalizedKeys)
431    {
432        $success = null;
433        $result  = array();
434        foreach ($normalizedKeys as $normalizedKey) {
435            $value = $this->internalGetItem($normalizedKey, $success);
436            if ($success) {
437                $result[$normalizedKey] = $value;
438            }
439        }
440
441        return $result;
442    }
443
444    /**
445     * Test if an item exists.
446     *
447     * @param  string $key
448     * @return bool
449     * @throws Exception\ExceptionInterface
450     *
451     * @triggers hasItem.pre(PreEvent)
452     * @triggers hasItem.post(PostEvent)
453     * @triggers hasItem.exception(ExceptionEvent)
454     */
455    public function hasItem($key)
456    {
457        if (!$this->getOptions()->getReadable()) {
458            return false;
459        }
460
461        $this->normalizeKey($key);
462        $args = new ArrayObject(array(
463            'key' => & $key,
464        ));
465
466        try {
467            $eventRs = $this->triggerPre(__FUNCTION__, $args);
468
469            $result = $eventRs->stopped()
470                ? $eventRs->last()
471                : $this->internalHasItem($args['key']);
472
473            return $this->triggerPost(__FUNCTION__, $args, $result);
474        } catch (\Exception $e) {
475            $result = false;
476            return $this->triggerException(__FUNCTION__, $args, $result, $e);
477        }
478    }
479
480    /**
481     * Internal method to test if an item exists.
482     *
483     * @param  string $normalizedKey
484     * @return bool
485     * @throws Exception\ExceptionInterface
486     */
487    protected function internalHasItem(& $normalizedKey)
488    {
489        $success = null;
490        $this->internalGetItem($normalizedKey, $success);
491        return $success;
492    }
493
494    /**
495     * Test multiple items.
496     *
497     * @param  array $keys
498     * @return array Array of found keys
499     * @throws Exception\ExceptionInterface
500     *
501     * @triggers hasItems.pre(PreEvent)
502     * @triggers hasItems.post(PostEvent)
503     * @triggers hasItems.exception(ExceptionEvent)
504     */
505    public function hasItems(array $keys)
506    {
507        if (!$this->getOptions()->getReadable()) {
508            return array();
509        }
510
511        $this->normalizeKeys($keys);
512        $args = new ArrayObject(array(
513            'keys' => & $keys,
514        ));
515
516        try {
517            $eventRs = $this->triggerPre(__FUNCTION__, $args);
518
519            $result = $eventRs->stopped()
520                ? $eventRs->last()
521                : $this->internalHasItems($args['keys']);
522
523            return $this->triggerPost(__FUNCTION__, $args, $result);
524        } catch (\Exception $e) {
525            $result = array();
526            return $this->triggerException(__FUNCTION__, $args, $result, $e);
527        }
528    }
529
530    /**
531     * Internal method to test multiple items.
532     *
533     * @param  array $normalizedKeys
534     * @return array Array of found keys
535     * @throws Exception\ExceptionInterface
536     */
537    protected function internalHasItems(array & $normalizedKeys)
538    {
539        $result = array();
540        foreach ($normalizedKeys as $normalizedKey) {
541            if ($this->internalHasItem($normalizedKey)) {
542                $result[] = $normalizedKey;
543            }
544        }
545        return $result;
546    }
547
548    /**
549     * Get metadata of an item.
550     *
551     * @param  string $key
552     * @return array|bool Metadata on success, false on failure
553     * @throws Exception\ExceptionInterface
554     *
555     * @triggers getMetadata.pre(PreEvent)
556     * @triggers getMetadata.post(PostEvent)
557     * @triggers getMetadata.exception(ExceptionEvent)
558     */
559    public function getMetadata($key)
560    {
561        if (!$this->getOptions()->getReadable()) {
562            return false;
563        }
564
565        $this->normalizeKey($key);
566        $args = new ArrayObject(array(
567            'key' => & $key,
568        ));
569
570        try {
571            $eventRs = $this->triggerPre(__FUNCTION__, $args);
572
573            $result = $eventRs->stopped()
574                ? $eventRs->last()
575                : $this->internalGetMetadata($args['key']);
576
577            return $this->triggerPost(__FUNCTION__, $args, $result);
578        } catch (\Exception $e) {
579            $result = false;
580            return $this->triggerException(__FUNCTION__, $args, $result, $e);
581        }
582    }
583
584    /**
585     * Internal method to get metadata of an item.
586     *
587     * @param  string $normalizedKey
588     * @return array|bool Metadata on success, false on failure
589     * @throws Exception\ExceptionInterface
590     */
591    protected function internalGetMetadata(& $normalizedKey)
592    {
593        if (!$this->internalHasItem($normalizedKey)) {
594            return false;
595        }
596
597        return array();
598    }
599
600    /**
601     * Get multiple metadata
602     *
603     * @param  array $keys
604     * @return array Associative array of keys and metadata
605     * @throws Exception\ExceptionInterface
606     *
607     * @triggers getMetadatas.pre(PreEvent)
608     * @triggers getMetadatas.post(PostEvent)
609     * @triggers getMetadatas.exception(ExceptionEvent)
610     */
611    public function getMetadatas(array $keys)
612    {
613        if (!$this->getOptions()->getReadable()) {
614            return array();
615        }
616
617        $this->normalizeKeys($keys);
618        $args = new ArrayObject(array(
619            'keys' => & $keys,
620        ));
621
622        try {
623            $eventRs = $this->triggerPre(__FUNCTION__, $args);
624
625            $result = $eventRs->stopped()
626                ? $eventRs->last()
627                : $this->internalGetMetadatas($args['keys']);
628
629            return $this->triggerPost(__FUNCTION__, $args, $result);
630        } catch (\Exception $e) {
631            $result = array();
632            return $this->triggerException(__FUNCTION__, $args, $result, $e);
633        }
634    }
635
636    /**
637     * Internal method to get multiple metadata
638     *
639     * @param  array $normalizedKeys
640     * @return array Associative array of keys and metadata
641     * @throws Exception\ExceptionInterface
642     */
643    protected function internalGetMetadatas(array & $normalizedKeys)
644    {
645        $result = array();
646        foreach ($normalizedKeys as $normalizedKey) {
647            $metadata = $this->internalGetMetadata($normalizedKey);
648            if ($metadata !== false) {
649                $result[$normalizedKey] = $metadata;
650            }
651        }
652        return $result;
653    }
654
655    /* writing */
656
657    /**
658     * Store an item.
659     *
660     * @param  string $key
661     * @param  mixed  $value
662     * @return bool
663     * @throws Exception\ExceptionInterface
664     *
665     * @triggers setItem.pre(PreEvent)
666     * @triggers setItem.post(PostEvent)
667     * @triggers setItem.exception(ExceptionEvent)
668     */
669    public function setItem($key, $value)
670    {
671        if (!$this->getOptions()->getWritable()) {
672            return false;
673        }
674
675        $this->normalizeKey($key);
676        $args = new ArrayObject(array(
677            'key'   => & $key,
678            'value' => & $value,
679        ));
680
681        try {
682            $eventRs = $this->triggerPre(__FUNCTION__, $args);
683
684            $result = $eventRs->stopped()
685                ? $eventRs->last()
686                : $this->internalSetItem($args['key'], $args['value']);
687
688            return $this->triggerPost(__FUNCTION__, $args, $result);
689        } catch (\Exception $e) {
690            $result = false;
691            return $this->triggerException(__FUNCTION__, $args, $result, $e);
692        }
693    }
694
695    /**
696     * Internal method to store an item.
697     *
698     * @param  string $normalizedKey
699     * @param  mixed  $value
700     * @return bool
701     * @throws Exception\ExceptionInterface
702     */
703    abstract protected function internalSetItem(& $normalizedKey, & $value);
704
705    /**
706     * Store multiple items.
707     *
708     * @param  array $keyValuePairs
709     * @return array Array of not stored keys
710     * @throws Exception\ExceptionInterface
711     *
712     * @triggers setItems.pre(PreEvent)
713     * @triggers setItems.post(PostEvent)
714     * @triggers setItems.exception(ExceptionEvent)
715     */
716    public function setItems(array $keyValuePairs)
717    {
718        if (!$this->getOptions()->getWritable()) {
719            return array_keys($keyValuePairs);
720        }
721
722        $this->normalizeKeyValuePairs($keyValuePairs);
723        $args = new ArrayObject(array(
724            'keyValuePairs' => & $keyValuePairs,
725        ));
726
727        try {
728            $eventRs = $this->triggerPre(__FUNCTION__, $args);
729
730            $result = $eventRs->stopped()
731                ? $eventRs->last()
732                : $this->internalSetItems($args['keyValuePairs']);
733
734            return $this->triggerPost(__FUNCTION__, $args, $result);
735        } catch (\Exception $e) {
736            $result = array_keys($keyValuePairs);
737            return $this->triggerException(__FUNCTION__, $args, $result, $e);
738        }
739    }
740
741    /**
742     * Internal method to store multiple items.
743     *
744     * @param  array $normalizedKeyValuePairs
745     * @return array Array of not stored keys
746     * @throws Exception\ExceptionInterface
747     */
748    protected function internalSetItems(array & $normalizedKeyValuePairs)
749    {
750        $failedKeys = array();
751        foreach ($normalizedKeyValuePairs as $normalizedKey => $value) {
752            if (!$this->internalSetItem($normalizedKey, $value)) {
753                $failedKeys[] = $normalizedKey;
754            }
755        }
756        return $failedKeys;
757    }
758
759    /**
760     * Add an item.
761     *
762     * @param  string $key
763     * @param  mixed  $value
764     * @return bool
765     * @throws Exception\ExceptionInterface
766     *
767     * @triggers addItem.pre(PreEvent)
768     * @triggers addItem.post(PostEvent)
769     * @triggers addItem.exception(ExceptionEvent)
770     */
771    public function addItem($key, $value)
772    {
773        if (!$this->getOptions()->getWritable()) {
774            return false;
775        }
776
777        $this->normalizeKey($key);
778        $args = new ArrayObject(array(
779            'key'   => & $key,
780            'value' => & $value,
781        ));
782
783        try {
784            $eventRs = $this->triggerPre(__FUNCTION__, $args);
785
786            $result = $eventRs->stopped()
787                ? $eventRs->last()
788                : $this->internalAddItem($args['key'], $args['value']);
789
790            return $this->triggerPost(__FUNCTION__, $args, $result);
791        } catch (\Exception $e) {
792            $result = false;
793            return $this->triggerException(__FUNCTION__, $args, $result, $e);
794        }
795    }
796
797    /**
798     * Internal method to add an item.
799     *
800     * @param  string $normalizedKey
801     * @param  mixed  $value
802     * @return bool
803     * @throws Exception\ExceptionInterface
804     */
805    protected function internalAddItem(& $normalizedKey, & $value)
806    {
807        if ($this->internalHasItem($normalizedKey)) {
808            return false;
809        }
810        return $this->internalSetItem($normalizedKey, $value);
811    }
812
813    /**
814     * Add multiple items.
815     *
816     * @param  array $keyValuePairs
817     * @return array Array of not stored keys
818     * @throws Exception\ExceptionInterface
819     *
820     * @triggers addItems.pre(PreEvent)
821     * @triggers addItems.post(PostEvent)
822     * @triggers addItems.exception(ExceptionEvent)
823     */
824    public function addItems(array $keyValuePairs)
825    {
826        if (!$this->getOptions()->getWritable()) {
827            return array_keys($keyValuePairs);
828        }
829
830        $this->normalizeKeyValuePairs($keyValuePairs);
831        $args = new ArrayObject(array(
832            'keyValuePairs' => & $keyValuePairs,
833        ));
834
835        try {
836            $eventRs = $this->triggerPre(__FUNCTION__, $args);
837
838            $result = $eventRs->stopped()
839                ? $eventRs->last()
840                : $this->internalAddItems($args['keyValuePairs']);
841
842            return $this->triggerPost(__FUNCTION__, $args, $result);
843        } catch (\Exception $e) {
844            $result = array_keys($keyValuePairs);
845            return $this->triggerException(__FUNCTION__, $args, $result, $e);
846        }
847    }
848
849    /**
850     * Internal method to add multiple items.
851     *
852     * @param  array $normalizedKeyValuePairs
853     * @return array Array of not stored keys
854     * @throws Exception\ExceptionInterface
855     */
856    protected function internalAddItems(array & $normalizedKeyValuePairs)
857    {
858        $result = array();
859        foreach ($normalizedKeyValuePairs as $normalizedKey => $value) {
860            if (!$this->internalAddItem($normalizedKey, $value)) {
861                $result[] = $normalizedKey;
862            }
863        }
864        return $result;
865    }
866
867    /**
868     * Replace an existing item.
869     *
870     * @param  string $key
871     * @param  mixed  $value
872     * @return bool
873     * @throws Exception\ExceptionInterface
874     *
875     * @triggers replaceItem.pre(PreEvent)
876     * @triggers replaceItem.post(PostEvent)
877     * @triggers replaceItem.exception(ExceptionEvent)
878     */
879    public function replaceItem($key, $value)
880    {
881        if (!$this->getOptions()->getWritable()) {
882            return false;
883        }
884
885        $this->normalizeKey($key);
886        $args = new ArrayObject(array(
887            'key'   => & $key,
888            'value' => & $value,
889        ));
890
891        try {
892            $eventRs = $this->triggerPre(__FUNCTION__, $args);
893
894            $result = $eventRs->stopped()
895                ? $eventRs->last()
896                : $this->internalReplaceItem($args['key'], $args['value']);
897
898            return $this->triggerPost(__FUNCTION__, $args, $result);
899        } catch (\Exception $e) {
900            $result = false;
901            return $this->triggerException(__FUNCTION__, $args, $result, $e);
902        }
903    }
904
905    /**
906     * Internal method to replace an existing item.
907     *
908     * @param  string $normalizedKey
909     * @param  mixed  $value
910     * @return bool
911     * @throws Exception\ExceptionInterface
912     */
913    protected function internalReplaceItem(& $normalizedKey, & $value)
914    {
915        if (!$this->internalhasItem($normalizedKey)) {
916            return false;
917        }
918
919        return $this->internalSetItem($normalizedKey, $value);
920    }
921
922    /**
923     * Replace multiple existing items.
924     *
925     * @param  array $keyValuePairs
926     * @return array Array of not stored keys
927     * @throws Exception\ExceptionInterface
928     *
929     * @triggers replaceItems.pre(PreEvent)
930     * @triggers replaceItems.post(PostEvent)
931     * @triggers replaceItems.exception(ExceptionEvent)
932     */
933    public function replaceItems(array $keyValuePairs)
934    {
935        if (!$this->getOptions()->getWritable()) {
936            return array_keys($keyValuePairs);
937        }
938
939        $this->normalizeKeyValuePairs($keyValuePairs);
940        $args = new ArrayObject(array(
941            'keyValuePairs' => & $keyValuePairs,
942        ));
943
944        try {
945            $eventRs = $this->triggerPre(__FUNCTION__, $args);
946
947            $result = $eventRs->stopped()
948                ? $eventRs->last()
949                : $this->internalReplaceItems($args['keyValuePairs']);
950
951            return $this->triggerPost(__FUNCTION__, $args, $result);
952        } catch (\Exception $e) {
953            $result = array_keys($keyValuePairs);
954            return $this->triggerException(__FUNCTION__, $args, $result, $e);
955        }
956    }
957
958    /**
959     * Internal method to replace multiple existing items.
960     *
961     * @param  array $normalizedKeyValuePairs
962     * @return array Array of not stored keys
963     * @throws Exception\ExceptionInterface
964     */
965    protected function internalReplaceItems(array & $normalizedKeyValuePairs)
966    {
967        $result = array();
968        foreach ($normalizedKeyValuePairs as $normalizedKey => $value) {
969            if (!$this->internalReplaceItem($normalizedKey, $value)) {
970                $result[] = $normalizedKey;
971            }
972        }
973        return $result;
974    }
975
976    /**
977     * Set an item only if token matches
978     *
979     * It uses the token received from getItem() to check if the item has
980     * changed before overwriting it.
981     *
982     * @param  mixed  $token
983     * @param  string $key
984     * @param  mixed  $value
985     * @return bool
986     * @throws Exception\ExceptionInterface
987     * @see    getItem()
988     * @see    setItem()
989     */
990    public function checkAndSetItem($token, $key, $value)
991    {
992        if (!$this->getOptions()->getWritable()) {
993            return false;
994        }
995
996        $this->normalizeKey($key);
997        $args = new ArrayObject(array(
998            'token' => & $token,
999            'key'   => & $key,
1000            'value' => & $value,
1001        ));
1002
1003        try {
1004            $eventRs = $this->triggerPre(__FUNCTION__, $args);
1005
1006            $result = $eventRs->stopped()
1007                ? $eventRs->last()
1008                : $this->internalCheckAndSetItem($args['token'], $args['key'], $args['value']);
1009
1010            return $this->triggerPost(__FUNCTION__, $args, $result);
1011        } catch (\Exception $e) {
1012            $result = false;
1013            return $this->triggerException(__FUNCTION__, $args, $result, $e);
1014        }
1015    }
1016
1017    /**
1018     * Internal method to set an item only if token matches
1019     *
1020     * @param  mixed  $token
1021     * @param  string $normalizedKey
1022     * @param  mixed  $value
1023     * @return bool
1024     * @throws Exception\ExceptionInterface
1025     * @see    getItem()
1026     * @see    setItem()
1027     */
1028    protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value)
1029    {
1030        $oldValue = $this->internalGetItem($normalizedKey);
1031        if ($oldValue !== $token) {
1032            return false;
1033        }
1034
1035        return $this->internalSetItem($normalizedKey, $value);
1036    }
1037
1038    /**
1039     * Reset lifetime of an item
1040     *
1041     * @param  string $key
1042     * @return bool
1043     * @throws Exception\ExceptionInterface
1044     *
1045     * @triggers touchItem.pre(PreEvent)
1046     * @triggers touchItem.post(PostEvent)
1047     * @triggers touchItem.exception(ExceptionEvent)
1048     */
1049    public function touchItem($key)
1050    {
1051        if (!$this->getOptions()->getWritable()) {
1052            return false;
1053        }
1054
1055        $this->normalizeKey($key);
1056        $args = new ArrayObject(array(
1057            'key' => & $key,
1058        ));
1059
1060        try {
1061            $eventRs = $this->triggerPre(__FUNCTION__, $args);
1062
1063            $result = $eventRs->stopped()
1064                ? $eventRs->last()
1065                : $this->internalTouchItem($args['key']);
1066
1067            return $this->triggerPost(__FUNCTION__, $args, $result);
1068        } catch (\Exception $e) {
1069            $result = false;
1070            return $this->triggerException(__FUNCTION__, $args, $result, $e);
1071        }
1072    }
1073
1074    /**
1075     * Internal method to reset lifetime of an item
1076     *
1077     * @param  string $normalizedKey
1078     * @return bool
1079     * @throws Exception\ExceptionInterface
1080     */
1081    protected function internalTouchItem(& $normalizedKey)
1082    {
1083        $success = null;
1084        $value   = $this->internalGetItem($normalizedKey, $success);
1085        if (!$success) {
1086            return false;
1087        }
1088
1089        return $this->internalReplaceItem($normalizedKey, $value);
1090    }
1091
1092    /**
1093     * Reset lifetime of multiple items.
1094     *
1095     * @param  array $keys
1096     * @return array Array of not updated keys
1097     * @throws Exception\ExceptionInterface
1098     *
1099     * @triggers touchItems.pre(PreEvent)
1100     * @triggers touchItems.post(PostEvent)
1101     * @triggers touchItems.exception(ExceptionEvent)
1102     */
1103    public function touchItems(array $keys)
1104    {
1105        if (!$this->getOptions()->getWritable()) {
1106            return $keys;
1107        }
1108
1109        $this->normalizeKeys($keys);
1110        $args = new ArrayObject(array(
1111            'keys' => & $keys,
1112        ));
1113
1114        try {
1115            $eventRs = $this->triggerPre(__FUNCTION__, $args);
1116
1117            $result = $eventRs->stopped()
1118                ? $eventRs->last()
1119                : $this->internalTouchItems($args['keys']);
1120
1121            return $this->triggerPost(__FUNCTION__, $args, $result);
1122        } catch (\Exception $e) {
1123            return $this->triggerException(__FUNCTION__, $args, $keys, $e);
1124        }
1125    }
1126
1127    /**
1128     * Internal method to reset lifetime of multiple items.
1129     *
1130     * @param  array $normalizedKeys
1131     * @return array Array of not updated keys
1132     * @throws Exception\ExceptionInterface
1133     */
1134    protected function internalTouchItems(array & $normalizedKeys)
1135    {
1136        $result = array();
1137        foreach ($normalizedKeys as $normalizedKey) {
1138            if (!$this->internalTouchItem($normalizedKey)) {
1139                $result[] = $normalizedKey;
1140            }
1141        }
1142        return $result;
1143    }
1144
1145    /**
1146     * Remove an item.
1147     *
1148     * @param  string $key
1149     * @return bool
1150     * @throws Exception\ExceptionInterface
1151     *
1152     * @triggers removeItem.pre(PreEvent)
1153     * @triggers removeItem.post(PostEvent)
1154     * @triggers removeItem.exception(ExceptionEvent)
1155     */
1156    public function removeItem($key)
1157    {
1158        if (!$this->getOptions()->getWritable()) {
1159            return false;
1160        }
1161
1162        $this->normalizeKey($key);
1163        $args = new ArrayObject(array(
1164            'key' => & $key,
1165        ));
1166
1167        try {
1168            $eventRs = $this->triggerPre(__FUNCTION__, $args);
1169
1170            $result = $eventRs->stopped()
1171                ? $eventRs->last()
1172                : $this->internalRemoveItem($args['key']);
1173
1174            return $this->triggerPost(__FUNCTION__, $args, $result);
1175        } catch (\Exception $e) {
1176            $result = false;
1177            return $this->triggerException(__FUNCTION__, $args, $result, $e);
1178        }
1179    }
1180
1181    /**
1182     * Internal method to remove an item.
1183     *
1184     * @param  string $normalizedKey
1185     * @return bool
1186     * @throws Exception\ExceptionInterface
1187     */
1188    abstract protected function internalRemoveItem(& $normalizedKey);
1189
1190    /**
1191     * Remove multiple items.
1192     *
1193     * @param  array $keys
1194     * @return array Array of not removed keys
1195     * @throws Exception\ExceptionInterface
1196     *
1197     * @triggers removeItems.pre(PreEvent)
1198     * @triggers removeItems.post(PostEvent)
1199     * @triggers removeItems.exception(ExceptionEvent)
1200     */
1201    public function removeItems(array $keys)
1202    {
1203        if (!$this->getOptions()->getWritable()) {
1204            return $keys;
1205        }
1206
1207        $this->normalizeKeys($keys);
1208        $args = new ArrayObject(array(
1209            'keys' => & $keys,
1210        ));
1211
1212        try {
1213            $eventRs = $this->triggerPre(__FUNCTION__, $args);
1214
1215            $result = $eventRs->stopped()
1216                ? $eventRs->last()
1217                : $this->internalRemoveItems($args['keys']);
1218
1219            return $this->triggerPost(__FUNCTION__, $args, $result);
1220        } catch (\Exception $e) {
1221            return $this->triggerException(__FUNCTION__, $args, $keys, $e);
1222        }
1223    }
1224
1225    /**
1226     * Internal method to remove multiple items.
1227     *
1228     * @param  array $normalizedKeys
1229     * @return array Array of not removed keys
1230     * @throws Exception\ExceptionInterface
1231     */
1232    protected function internalRemoveItems(array & $normalizedKeys)
1233    {
1234        $result = array();
1235        foreach ($normalizedKeys as $normalizedKey) {
1236            if (!$this->internalRemoveItem($normalizedKey)) {
1237                $result[] = $normalizedKey;
1238            }
1239        }
1240        return $result;
1241    }
1242
1243    /**
1244     * Increment an item.
1245     *
1246     * @param  string $key
1247     * @param  int    $value
1248     * @return int|bool The new value on success, false on failure
1249     * @throws Exception\ExceptionInterface
1250     *
1251     * @triggers incrementItem.pre(PreEvent)
1252     * @triggers incrementItem.post(PostEvent)
1253     * @triggers incrementItem.exception(ExceptionEvent)
1254     */
1255    public function incrementItem($key, $value)
1256    {
1257        if (!$this->getOptions()->getWritable()) {
1258            return false;
1259        }
1260
1261        $this->normalizeKey($key);
1262        $args = new ArrayObject(array(
1263            'key'   => & $key,
1264            'value' => & $value,
1265        ));
1266
1267        try {
1268            $eventRs = $this->triggerPre(__FUNCTION__, $args);
1269
1270            $result = $eventRs->stopped()
1271                ? $eventRs->last()
1272                : $this->internalIncrementItem($args['key'], $args['value']);
1273
1274            return $this->triggerPost(__FUNCTION__, $args, $result);
1275        } catch (\Exception $e) {
1276            $result = false;
1277            return $this->triggerException(__FUNCTION__, $args, $result, $e);
1278        }
1279    }
1280
1281    /**
1282     * Internal method to increment an item.
1283     *
1284     * @param  string $normalizedKey
1285     * @param  int    $value
1286     * @return int|bool The new value on success, false on failure
1287     * @throws Exception\ExceptionInterface
1288     */
1289    protected function internalIncrementItem(& $normalizedKey, & $value)
1290    {
1291        $success  = null;
1292        $value    = (int) $value;
1293        $get      = (int) $this->internalGetItem($normalizedKey, $success);
1294        $newValue = $get + $value;
1295
1296        if ($success) {
1297            $this->internalReplaceItem($normalizedKey, $newValue);
1298        } else {
1299            $this->internalAddItem($normalizedKey, $newValue);
1300        }
1301
1302        return $newValue;
1303    }
1304
1305    /**
1306     * Increment multiple items.
1307     *
1308     * @param  array $keyValuePairs
1309     * @return array Associative array of keys and new values
1310     * @throws Exception\ExceptionInterface
1311     *
1312     * @triggers incrementItems.pre(PreEvent)
1313     * @triggers incrementItems.post(PostEvent)
1314     * @triggers incrementItems.exception(ExceptionEvent)
1315     */
1316    public function incrementItems(array $keyValuePairs)
1317    {
1318        if (!$this->getOptions()->getWritable()) {
1319            return array();
1320        }
1321
1322        $this->normalizeKeyValuePairs($keyValuePairs);
1323        $args = new ArrayObject(array(
1324            'keyValuePairs' => & $keyValuePairs,
1325        ));
1326
1327        try {
1328            $eventRs = $this->triggerPre(__FUNCTION__, $args);
1329
1330            $result = $eventRs->stopped()
1331                ? $eventRs->last()
1332                : $this->internalIncrementItems($args['keyValuePairs']);
1333
1334            return $this->triggerPost(__FUNCTION__, $args, $result);
1335        } catch (\Exception $e) {
1336            $result = array();
1337            return $this->triggerException(__FUNCTION__, $args, $result, $e);
1338        }
1339    }
1340
1341    /**
1342     * Internal method to increment multiple items.
1343     *
1344     * @param  array $normalizedKeyValuePairs
1345     * @return array Associative array of keys and new values
1346     * @throws Exception\ExceptionInterface
1347     */
1348    protected function internalIncrementItems(array & $normalizedKeyValuePairs)
1349    {
1350        $result = array();
1351        foreach ($normalizedKeyValuePairs as $normalizedKey => $value) {
1352            $newValue = $this->internalIncrementItem($normalizedKey, $value);
1353            if ($newValue !== false) {
1354                $result[$normalizedKey] = $newValue;
1355            }
1356        }
1357        return $result;
1358    }
1359
1360    /**
1361     * Decrement an item.
1362     *
1363     * @param  string $key
1364     * @param  int    $value
1365     * @return int|bool The new value on success, false on failure
1366     * @throws Exception\ExceptionInterface
1367     *
1368     * @triggers decrementItem.pre(PreEvent)
1369     * @triggers decrementItem.post(PostEvent)
1370     * @triggers decrementItem.exception(ExceptionEvent)
1371     */
1372    public function decrementItem($key, $value)
1373    {
1374        if (!$this->getOptions()->getWritable()) {
1375            return false;
1376        }
1377
1378        $this->normalizeKey($key);
1379        $args = new ArrayObject(array(
1380            'key'   => & $key,
1381            'value' => & $value,
1382        ));
1383
1384        try {
1385            $eventRs = $this->triggerPre(__FUNCTION__, $args);
1386
1387            $result = $eventRs->stopped()
1388                ? $eventRs->last()
1389                : $this->internalDecrementItem($args['key'], $args['value']);
1390
1391            return $this->triggerPost(__FUNCTION__, $args, $result);
1392        } catch (\Exception $e) {
1393            $result = false;
1394            return $this->triggerException(__FUNCTION__, $args, $result, $e);
1395        }
1396    }
1397
1398    /**
1399     * Internal method to decrement an item.
1400     *
1401     * @param  string $normalizedKey
1402     * @param  int    $value
1403     * @return int|bool The new value on success, false on failure
1404     * @throws Exception\ExceptionInterface
1405     */
1406    protected function internalDecrementItem(& $normalizedKey, & $value)
1407    {
1408        $success  = null;
1409        $value    = (int) $value;
1410        $get      = (int) $this->internalGetItem($normalizedKey, $success);
1411        $newValue = $get - $value;
1412
1413        if ($success) {
1414            $this->internalReplaceItem($normalizedKey, $newValue);
1415        } else {
1416            $this->internalAddItem($normalizedKey, $newValue);
1417        }
1418
1419        return $newValue;
1420    }
1421
1422    /**
1423     * Decrement multiple items.
1424     *
1425     * @param  array $keyValuePairs
1426     * @return array Associative array of keys and new values
1427     * @throws Exception\ExceptionInterface
1428     *
1429     * @triggers incrementItems.pre(PreEvent)
1430     * @triggers incrementItems.post(PostEvent)
1431     * @triggers incrementItems.exception(ExceptionEvent)
1432     */
1433    public function decrementItems(array $keyValuePairs)
1434    {
1435        if (!$this->getOptions()->getWritable()) {
1436            return array();
1437        }
1438
1439        $this->normalizeKeyValuePairs($keyValuePairs);
1440        $args = new ArrayObject(array(
1441            'keyValuePairs' => & $keyValuePairs,
1442        ));
1443
1444        try {
1445            $eventRs = $this->triggerPre(__FUNCTION__, $args);
1446
1447            $result = $eventRs->stopped()
1448                ? $eventRs->last()
1449                : $this->internalDecrementItems($args['keyValuePairs']);
1450
1451            return $this->triggerPost(__FUNCTION__, $args, $result);
1452        } catch (\Exception $e) {
1453            $result = array();
1454            return $this->triggerException(__FUNCTION__, $args, $result, $e);
1455        }
1456    }
1457
1458    /**
1459     * Internal method to decrement multiple items.
1460     *
1461     * @param  array $normalizedKeyValuePairs
1462     * @return array Associative array of keys and new values
1463     * @throws Exception\ExceptionInterface
1464     */
1465    protected function internalDecrementItems(array & $normalizedKeyValuePairs)
1466    {
1467        $result = array();
1468        foreach ($normalizedKeyValuePairs as $normalizedKey => $value) {
1469            $newValue = $this->decrementItem($normalizedKey, $value);
1470            if ($newValue !== false) {
1471                $result[$normalizedKey] = $newValue;
1472            }
1473        }
1474        return $result;
1475    }
1476
1477    /* status */
1478
1479    /**
1480     * Get capabilities of this adapter
1481     *
1482     * @return Capabilities
1483     * @triggers getCapabilities.pre(PreEvent)
1484     * @triggers getCapabilities.post(PostEvent)
1485     * @triggers getCapabilities.exception(ExceptionEvent)
1486     */
1487    public function getCapabilities()
1488    {
1489        $args = new ArrayObject();
1490
1491        try {
1492            $eventRs = $this->triggerPre(__FUNCTION__, $args);
1493
1494            $result = $eventRs->stopped()
1495                ? $eventRs->last()
1496                : $this->internalGetCapabilities();
1497
1498            return $this->triggerPost(__FUNCTION__, $args, $result);
1499        } catch (\Exception $e) {
1500            $result = false;
1501            return $this->triggerException(__FUNCTION__, $args, $result, $e);
1502        }
1503    }
1504
1505    /**
1506     * Internal method to get capabilities of this adapter
1507     *
1508     * @return Capabilities
1509     */
1510    protected function internalGetCapabilities()
1511    {
1512        if ($this->capabilities === null) {
1513            $this->capabilityMarker = new stdClass();
1514            $this->capabilities     = new Capabilities($this, $this->capabilityMarker);
1515        }
1516        return $this->capabilities;
1517    }
1518
1519    /* internal */
1520
1521    /**
1522     * Validates and normalizes a key
1523     *
1524     * @param  string $key
1525     * @return void
1526     * @throws Exception\InvalidArgumentException On an invalid key
1527     */
1528    protected function normalizeKey(& $key)
1529    {
1530        $key = (string) $key;
1531
1532        if ($key === '') {
1533            throw new Exception\InvalidArgumentException(
1534                "An empty key isn't allowed"
1535            );
1536        } elseif (($p = $this->getOptions()->getKeyPattern()) && !preg_match($p, $key)) {
1537            throw new Exception\InvalidArgumentException(
1538                "The key '{$key}' doesn't match against pattern '{$p}'"
1539            );
1540        }
1541    }
1542
1543    /**
1544     * Validates and normalizes multiple keys
1545     *
1546     * @param  array $keys
1547     * @return void
1548     * @throws Exception\InvalidArgumentException On an invalid key
1549     */
1550    protected function normalizeKeys(array & $keys)
1551    {
1552        if (!$keys) {
1553            throw new Exception\InvalidArgumentException(
1554                "An empty list of keys isn't allowed"
1555            );
1556        }
1557
1558        array_walk($keys, array($this, 'normalizeKey'));
1559        $keys = array_values(array_unique($keys));
1560    }
1561
1562    /**
1563     * Validates and normalizes an array of key-value pairs
1564     *
1565     * @param  array $keyValuePairs
1566     * @return void
1567     * @throws Exception\InvalidArgumentException On an invalid key
1568     */
1569    protected function normalizeKeyValuePairs(array & $keyValuePairs)
1570    {
1571        $normalizedKeyValuePairs = array();
1572        foreach ($keyValuePairs as $key => $value) {
1573            $this->normalizeKey($key);
1574            $normalizedKeyValuePairs[$key] = $value;
1575        }
1576        $keyValuePairs = $normalizedKeyValuePairs;
1577    }
1578}
1579