<?php
/**
 * Zend Framework (http://framework.zend.com/)
 *
 * @link      http://github.com/zendframework/zf2 for the canonical source repository
 * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd New BSD License
 */

namespace Zend\Cache\Storage\Adapter;

use 
ArrayObject;
use 
SplObjectStorage;
use 
stdClass;
use 
Traversable;
use 
Zend\Cache\Exception;
use 
Zend\Cache\Storage\Capabilities;
use 
Zend\Cache\Storage\Event;
use 
Zend\Cache\Storage\ExceptionEvent;
use 
Zend\Cache\Storage\Plugin;
use 
Zend\Cache\Storage\PostEvent;
use 
Zend\Cache\Storage\StorageInterface;
use 
Zend\EventManager\EventManager;
use 
Zend\EventManager\EventManagerInterface;
use 
Zend\EventManager\EventsCapableInterface;

abstract class 
AbstractAdapter implements StorageInterfaceEventsCapableInterface
{
    
/**
     * The used EventManager if any
     *
     * @var null|EventManagerInterface
     */
    
protected $events null;

    
/**
     * Event handles of this adapter
     * @var array
     */
    
protected $eventHandles = array();

    
/**
     * The plugin registry
     *
     * @var SplObjectStorage Registered plugins
     */
    
protected $pluginRegistry;

    
/**
     * Capabilities of this adapter
     *
     * @var null|Capabilities
     */
    
protected $capabilities null;

    
/**
     * Marker to change capabilities
     *
     * @var null|object
     */
    
protected $capabilityMarker;

    
/**
     * options
     *
     * @var mixed
     */
    
protected $options;

    
/**
     * Constructor
     *
     * @param  null|array|Traversable|AdapterOptions $options
     * @throws Exception\ExceptionInterface
     */
    
public function __construct($options null)
    {
        if (
$options) {
            
$this->setOptions($options);
        }
    }

    
/**
     * Destructor
     *
     * detach all registered plugins to free
     * event handles of event manager
     *
     * @return void
     */
    
public function __destruct()
    {
        foreach (
$this->getPluginRegistry() as $plugin) {
            
$this->removePlugin($plugin);
        }

        if (
$this->eventHandles) {
            
$events $this->getEventManager();
            foreach (
$this->eventHandles as $handle) {
                
$events->detach($handle);
            }
        }
    }

    
/* configuration */

    /**
     * Set options.
     *
     * @param  array|Traversable|AdapterOptions $options
     * @return AbstractAdapter
     * @see    getOptions()
     */
    
public function setOptions($options)
    {
        if (
$this->options !== $options) {
            if (!
$options instanceof AdapterOptions) {
                
$options = new AdapterOptions($options);
            }

            if (
$this->options) {
                
$this->options->setAdapter(null);
            }
            
$options->setAdapter($this);
            
$this->options $options;

            
$event = new Event('option'$this, new ArrayObject($options->toArray()));
            
$this->getEventManager()->trigger($event);
        }
        return 
$this;
    }

    
/**
     * Get options.
     *
     * @return AdapterOptions
     * @see setOptions()
     */
    
public function getOptions()
    {
        if (!
$this->options) {
            
$this->setOptions(new AdapterOptions());
        }
        return 
$this->options;
    }

    
/**
     * Enable/Disable caching.
     *
     * Alias of setWritable and setReadable.
     *
     * @see    setWritable()
     * @see    setReadable()
     * @param  bool $flag
     * @return AbstractAdapter
     */
    
public function setCaching($flag)
    {
        
$flag    = (bool) $flag;
        
$options $this->getOptions();
        
$options->setWritable($flag);
        
$options->setReadable($flag);
        return 
$this;
    }

    
/**
     * Get caching enabled.
     *
     * Alias of getWritable and getReadable.
     *
     * @see    getWritable()
     * @see    getReadable()
     * @return bool
     */
    
public function getCaching()
    {
        
$options $this->getOptions();
        return (
$options->getWritable() && $options->getReadable());
    }

    
/* Event/Plugin handling */

    /**
     * Get the event manager
     *
     * @return EventManagerInterface
     */
    
public function getEventManager()
    {
        if (
$this->events === null) {
            
$this->events = new EventManager(array(__CLASS__get_called_class()));
        }
        return 
$this->events;
    }

    
/**
     * Trigger an pre event and return the event response collection
     *
     * @param  string $eventName
     * @param  ArrayObject $args
     * @return \Zend\EventManager\ResponseCollection All handler return values
     */
    
protected function triggerPre($eventNameArrayObject $args)
    {
        return 
$this->getEventManager()->trigger(new Event($eventName '.pre'$this$args));
    }

    
/**
     * Triggers the PostEvent and return the result value.
     *
     * @param  string      $eventName
     * @param  ArrayObject $args
     * @param  mixed       $result
     * @return mixed
     */
    
protected function triggerPost($eventNameArrayObject $args, & $result)
    {
        
$postEvent = new PostEvent($eventName '.post'$this$args$result);
        
$eventRs   $this->getEventManager()->trigger($postEvent);
        if (
$eventRs->stopped()) {
            return 
$eventRs->last();
        }

        return 
$postEvent->getResult();
    }

    
/**
     * Trigger an exception event
     *
     * If the ExceptionEvent has the flag "throwException" enabled throw the
     * exception after trigger else return the result.
     *
     * @param  string      $eventName
     * @param  ArrayObject $args
     * @param  mixed       $result
     * @param  \Exception  $exception
     * @throws Exception\ExceptionInterface
     * @return mixed
     */
    
protected function triggerException($eventNameArrayObject $args, & $result, \Exception $exception)
    {
        
$exceptionEvent = new ExceptionEvent($eventName '.exception'$this$args$result$exception);
        
$eventRs        $this->getEventManager()->trigger($exceptionEvent);

        if (
$exceptionEvent->getThrowException()) {
            throw 
$exceptionEvent->getException();
        }

        if (
$eventRs->stopped()) {
            return 
$eventRs->last();
        }

        return 
$exceptionEvent->getResult();
    }

    
/**
     * Check if a plugin is registered
     *
     * @param  Plugin\PluginInterface $plugin
     * @return bool
     */
    
public function hasPlugin(Plugin\PluginInterface $plugin)
    {
        
$registry $this->getPluginRegistry();
        return 
$registry->contains($plugin);
    }

    
/**
     * Register a plugin
     *
     * @param  Plugin\PluginInterface $plugin
     * @param  int                    $priority
     * @return AbstractAdapter Fluent interface
     * @throws Exception\LogicException
     */
    
public function addPlugin(Plugin\PluginInterface $plugin$priority 1)
    {
        
$registry $this->getPluginRegistry();
        if (
$registry->contains($plugin)) {
            throw new 
Exception\LogicException(sprintf(
                
'Plugin of type "%s" already registered',
                
get_class($plugin)
            ));
        }

        
$plugin->attach($this->getEventManager(), $priority);
        
$registry->attach($plugin);

        return 
$this;
    }

    
/**
     * Unregister an already registered plugin
     *
     * @param  Plugin\PluginInterface $plugin
     * @return AbstractAdapter Fluent interface
     * @throws Exception\LogicException
     */
    
public function removePlugin(Plugin\PluginInterface $plugin)
    {
        
$registry $this->getPluginRegistry();
        if (
$registry->contains($plugin)) {
            
$plugin->detach($this->getEventManager());
            
$registry->detach($plugin);
        }
        return 
$this;
    }

    
/**
     * Return registry of plugins
     *
     * @return SplObjectStorage
     */
    
public function getPluginRegistry()
    {
        if (!
$this->pluginRegistry instanceof SplObjectStorage) {
            
$this->pluginRegistry = new SplObjectStorage();
        }
        return 
$this->pluginRegistry;
    }

    
/* reading */

    /**
     * Get an item.
     *
     * @param  string  $key
     * @param  bool $success
     * @param  mixed   $casToken
     * @return mixed Data on success, null on failure
     * @throws Exception\ExceptionInterface
     *
     * @triggers getItem.pre(PreEvent)
     * @triggers getItem.post(PostEvent)
     * @triggers getItem.exception(ExceptionEvent)
     */
    
public function getItem($key, & $success null, & $casToken null)
    {
        if (!
$this->getOptions()->getReadable()) {
            
$success false;
            return 
null;
        }

        
$this->normalizeKey($key);

        
$argn func_num_args();
        
$args = array(
            
'key' => & $key,
        );
        if (
$argn 1) {
            
$args['success'] = & $success;
        }
        if (
$argn 2) {
            
$args['casToken'] = & $casToken;
        }
        
$args = new ArrayObject($args);

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            if (
$args->offsetExists('success') && $args->offsetExists('casToken')) {
                
$result $this->internalGetItem($args['key'], $args['success'], $args['casToken']);
            } elseif (
$args->offsetExists('success')) {
                
$result $this->internalGetItem($args['key'], $args['success']);
            } else {
                
$result $this->internalGetItem($args['key']);
            }
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result false;
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to get an item.
     *
     * @param  string  $normalizedKey
     * @param  bool $success
     * @param  mixed   $casToken
     * @return mixed Data on success, null on failure
     * @throws Exception\ExceptionInterface
     */
    
abstract protected function internalGetItem(& $normalizedKey, & $success null, & $casToken null);

    
/**
     * Get multiple items.
     *
     * @param  array $keys
     * @return array Associative array of keys and values
     * @throws Exception\ExceptionInterface
     *
     * @triggers getItems.pre(PreEvent)
     * @triggers getItems.post(PostEvent)
     * @triggers getItems.exception(ExceptionEvent)
     */
    
public function getItems(array $keys)
    {
        if (!
$this->getOptions()->getReadable()) {
            return array();
        }

        
$this->normalizeKeys($keys);
        
$args = new ArrayObject(array(
            
'keys' => & $keys,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalGetItems($args['keys']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result = array();
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to get multiple items.
     *
     * @param  array $normalizedKeys
     * @return array Associative array of keys and values
     * @throws Exception\ExceptionInterface
     */
    
protected function internalGetItems(array & $normalizedKeys)
    {
        
$success null;
        
$result  = array();
        foreach (
$normalizedKeys as $normalizedKey) {
            
$value $this->internalGetItem($normalizedKey$success);
            if (
$success) {
                
$result[$normalizedKey] = $value;
            }
        }

        return 
$result;
    }

    
/**
     * Test if an item exists.
     *
     * @param  string $key
     * @return bool
     * @throws Exception\ExceptionInterface
     *
     * @triggers hasItem.pre(PreEvent)
     * @triggers hasItem.post(PostEvent)
     * @triggers hasItem.exception(ExceptionEvent)
     */
    
public function hasItem($key)
    {
        if (!
$this->getOptions()->getReadable()) {
            return 
false;
        }

        
$this->normalizeKey($key);
        
$args = new ArrayObject(array(
            
'key' => & $key,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalHasItem($args['key']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result false;
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to test if an item exists.
     *
     * @param  string $normalizedKey
     * @return bool
     * @throws Exception\ExceptionInterface
     */
    
protected function internalHasItem(& $normalizedKey)
    {
        
$success null;
        
$this->internalGetItem($normalizedKey$success);
        return 
$success;
    }

    
/**
     * Test multiple items.
     *
     * @param  array $keys
     * @return array Array of found keys
     * @throws Exception\ExceptionInterface
     *
     * @triggers hasItems.pre(PreEvent)
     * @triggers hasItems.post(PostEvent)
     * @triggers hasItems.exception(ExceptionEvent)
     */
    
public function hasItems(array $keys)
    {
        if (!
$this->getOptions()->getReadable()) {
            return array();
        }

        
$this->normalizeKeys($keys);
        
$args = new ArrayObject(array(
            
'keys' => & $keys,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalHasItems($args['keys']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result = array();
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to test multiple items.
     *
     * @param  array $normalizedKeys
     * @return array Array of found keys
     * @throws Exception\ExceptionInterface
     */
    
protected function internalHasItems(array & $normalizedKeys)
    {
        
$result = array();
        foreach (
$normalizedKeys as $normalizedKey) {
            if (
$this->internalHasItem($normalizedKey)) {
                
$result[] = $normalizedKey;
            }
        }
        return 
$result;
    }

    
/**
     * Get metadata of an item.
     *
     * @param  string $key
     * @return array|bool Metadata on success, false on failure
     * @throws Exception\ExceptionInterface
     *
     * @triggers getMetadata.pre(PreEvent)
     * @triggers getMetadata.post(PostEvent)
     * @triggers getMetadata.exception(ExceptionEvent)
     */
    
public function getMetadata($key)
    {
        if (!
$this->getOptions()->getReadable()) {
            return 
false;
        }

        
$this->normalizeKey($key);
        
$args = new ArrayObject(array(
            
'key' => & $key,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalGetMetadata($args['key']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result false;
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to get metadata of an item.
     *
     * @param  string $normalizedKey
     * @return array|bool Metadata on success, false on failure
     * @throws Exception\ExceptionInterface
     */
    
protected function internalGetMetadata(& $normalizedKey)
    {
        if (!
$this->internalHasItem($normalizedKey)) {
            return 
false;
        }

        return array();
    }

    
/**
     * Get multiple metadata
     *
     * @param  array $keys
     * @return array Associative array of keys and metadata
     * @throws Exception\ExceptionInterface
     *
     * @triggers getMetadatas.pre(PreEvent)
     * @triggers getMetadatas.post(PostEvent)
     * @triggers getMetadatas.exception(ExceptionEvent)
     */
    
public function getMetadatas(array $keys)
    {
        if (!
$this->getOptions()->getReadable()) {
            return array();
        }

        
$this->normalizeKeys($keys);
        
$args = new ArrayObject(array(
            
'keys' => & $keys,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalGetMetadatas($args['keys']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result = array();
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to get multiple metadata
     *
     * @param  array $normalizedKeys
     * @return array Associative array of keys and metadata
     * @throws Exception\ExceptionInterface
     */
    
protected function internalGetMetadatas(array & $normalizedKeys)
    {
        
$result = array();
        foreach (
$normalizedKeys as $normalizedKey) {
            
$metadata $this->internalGetMetadata($normalizedKey);
            if (
$metadata !== false) {
                
$result[$normalizedKey] = $metadata;
            }
        }
        return 
$result;
    }

    
/* writing */

    /**
     * Store an item.
     *
     * @param  string $key
     * @param  mixed  $value
     * @return bool
     * @throws Exception\ExceptionInterface
     *
     * @triggers setItem.pre(PreEvent)
     * @triggers setItem.post(PostEvent)
     * @triggers setItem.exception(ExceptionEvent)
     */
    
public function setItem($key$value)
    {
        if (!
$this->getOptions()->getWritable()) {
            return 
false;
        }

        
$this->normalizeKey($key);
        
$args = new ArrayObject(array(
            
'key'   => & $key,
            
'value' => & $value,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalSetItem($args['key'], $args['value']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result false;
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to store an item.
     *
     * @param  string $normalizedKey
     * @param  mixed  $value
     * @return bool
     * @throws Exception\ExceptionInterface
     */
    
abstract protected function internalSetItem(& $normalizedKey, & $value);

    
/**
     * Store multiple items.
     *
     * @param  array $keyValuePairs
     * @return array Array of not stored keys
     * @throws Exception\ExceptionInterface
     *
     * @triggers setItems.pre(PreEvent)
     * @triggers setItems.post(PostEvent)
     * @triggers setItems.exception(ExceptionEvent)
     */
    
public function setItems(array $keyValuePairs)
    {
        if (!
$this->getOptions()->getWritable()) {
            return 
array_keys($keyValuePairs);
        }

        
$this->normalizeKeyValuePairs($keyValuePairs);
        
$args = new ArrayObject(array(
            
'keyValuePairs' => & $keyValuePairs,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalSetItems($args['keyValuePairs']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result array_keys($keyValuePairs);
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to store multiple items.
     *
     * @param  array $normalizedKeyValuePairs
     * @return array Array of not stored keys
     * @throws Exception\ExceptionInterface
     */
    
protected function internalSetItems(array & $normalizedKeyValuePairs)
    {
        
$failedKeys = array();
        foreach (
$normalizedKeyValuePairs as $normalizedKey => $value) {
            if (!
$this->internalSetItem($normalizedKey$value)) {
                
$failedKeys[] = $normalizedKey;
            }
        }
        return 
$failedKeys;
    }

    
/**
     * Add an item.
     *
     * @param  string $key
     * @param  mixed  $value
     * @return bool
     * @throws Exception\ExceptionInterface
     *
     * @triggers addItem.pre(PreEvent)
     * @triggers addItem.post(PostEvent)
     * @triggers addItem.exception(ExceptionEvent)
     */
    
public function addItem($key$value)
    {
        if (!
$this->getOptions()->getWritable()) {
            return 
false;
        }

        
$this->normalizeKey($key);
        
$args = new ArrayObject(array(
            
'key'   => & $key,
            
'value' => & $value,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalAddItem($args['key'], $args['value']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result false;
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to add an item.
     *
     * @param  string $normalizedKey
     * @param  mixed  $value
     * @return bool
     * @throws Exception\ExceptionInterface
     */
    
protected function internalAddItem(& $normalizedKey, & $value)
    {
        if (
$this->internalHasItem($normalizedKey)) {
            return 
false;
        }
        return 
$this->internalSetItem($normalizedKey$value);
    }

    
/**
     * Add multiple items.
     *
     * @param  array $keyValuePairs
     * @return array Array of not stored keys
     * @throws Exception\ExceptionInterface
     *
     * @triggers addItems.pre(PreEvent)
     * @triggers addItems.post(PostEvent)
     * @triggers addItems.exception(ExceptionEvent)
     */
    
public function addItems(array $keyValuePairs)
    {
        if (!
$this->getOptions()->getWritable()) {
            return 
array_keys($keyValuePairs);
        }

        
$this->normalizeKeyValuePairs($keyValuePairs);
        
$args = new ArrayObject(array(
            
'keyValuePairs' => & $keyValuePairs,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalAddItems($args['keyValuePairs']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result array_keys($keyValuePairs);
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to add multiple items.
     *
     * @param  array $normalizedKeyValuePairs
     * @return array Array of not stored keys
     * @throws Exception\ExceptionInterface
     */
    
protected function internalAddItems(array & $normalizedKeyValuePairs)
    {
        
$result = array();
        foreach (
$normalizedKeyValuePairs as $normalizedKey => $value) {
            if (!
$this->internalAddItem($normalizedKey$value)) {
                
$result[] = $normalizedKey;
            }
        }
        return 
$result;
    }

    
/**
     * Replace an existing item.
     *
     * @param  string $key
     * @param  mixed  $value
     * @return bool
     * @throws Exception\ExceptionInterface
     *
     * @triggers replaceItem.pre(PreEvent)
     * @triggers replaceItem.post(PostEvent)
     * @triggers replaceItem.exception(ExceptionEvent)
     */
    
public function replaceItem($key$value)
    {
        if (!
$this->getOptions()->getWritable()) {
            return 
false;
        }

        
$this->normalizeKey($key);
        
$args = new ArrayObject(array(
            
'key'   => & $key,
            
'value' => & $value,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalReplaceItem($args['key'], $args['value']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result false;
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to replace an existing item.
     *
     * @param  string $normalizedKey
     * @param  mixed  $value
     * @return bool
     * @throws Exception\ExceptionInterface
     */
    
protected function internalReplaceItem(& $normalizedKey, & $value)
    {
        if (!
$this->internalhasItem($normalizedKey)) {
            return 
false;
        }

        return 
$this->internalSetItem($normalizedKey$value);
    }

    
/**
     * Replace multiple existing items.
     *
     * @param  array $keyValuePairs
     * @return array Array of not stored keys
     * @throws Exception\ExceptionInterface
     *
     * @triggers replaceItems.pre(PreEvent)
     * @triggers replaceItems.post(PostEvent)
     * @triggers replaceItems.exception(ExceptionEvent)
     */
    
public function replaceItems(array $keyValuePairs)
    {
        if (!
$this->getOptions()->getWritable()) {
            return 
array_keys($keyValuePairs);
        }

        
$this->normalizeKeyValuePairs($keyValuePairs);
        
$args = new ArrayObject(array(
            
'keyValuePairs' => & $keyValuePairs,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalReplaceItems($args['keyValuePairs']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result array_keys($keyValuePairs);
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to replace multiple existing items.
     *
     * @param  array $normalizedKeyValuePairs
     * @return array Array of not stored keys
     * @throws Exception\ExceptionInterface
     */
    
protected function internalReplaceItems(array & $normalizedKeyValuePairs)
    {
        
$result = array();
        foreach (
$normalizedKeyValuePairs as $normalizedKey => $value) {
            if (!
$this->internalReplaceItem($normalizedKey$value)) {
                
$result[] = $normalizedKey;
            }
        }
        return 
$result;
    }

    
/**
     * Set an item only if token matches
     *
     * It uses the token received from getItem() to check if the item has
     * changed before overwriting it.
     *
     * @param  mixed  $token
     * @param  string $key
     * @param  mixed  $value
     * @return bool
     * @throws Exception\ExceptionInterface
     * @see    getItem()
     * @see    setItem()
     */
    
public function checkAndSetItem($token$key$value)
    {
        if (!
$this->getOptions()->getWritable()) {
            return 
false;
        }

        
$this->normalizeKey($key);
        
$args = new ArrayObject(array(
            
'token' => & $token,
            
'key'   => & $key,
            
'value' => & $value,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalCheckAndSetItem($args['token'], $args['key'], $args['value']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result false;
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to set an item only if token matches
     *
     * @param  mixed  $token
     * @param  string $normalizedKey
     * @param  mixed  $value
     * @return bool
     * @throws Exception\ExceptionInterface
     * @see    getItem()
     * @see    setItem()
     */
    
protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value)
    {
        
$oldValue $this->internalGetItem($normalizedKey);
        if (
$oldValue !== $token) {
            return 
false;
        }

        return 
$this->internalSetItem($normalizedKey$value);
    }

    
/**
     * Reset lifetime of an item
     *
     * @param  string $key
     * @return bool
     * @throws Exception\ExceptionInterface
     *
     * @triggers touchItem.pre(PreEvent)
     * @triggers touchItem.post(PostEvent)
     * @triggers touchItem.exception(ExceptionEvent)
     */
    
public function touchItem($key)
    {
        if (!
$this->getOptions()->getWritable()) {
            return 
false;
        }

        
$this->normalizeKey($key);
        
$args = new ArrayObject(array(
            
'key' => & $key,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalTouchItem($args['key']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result false;
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to reset lifetime of an item
     *
     * @param  string $normalizedKey
     * @return bool
     * @throws Exception\ExceptionInterface
     */
    
protected function internalTouchItem(& $normalizedKey)
    {
        
$success null;
        
$value   $this->internalGetItem($normalizedKey$success);
        if (!
$success) {
            return 
false;
        }

        return 
$this->internalReplaceItem($normalizedKey$value);
    }

    
/**
     * Reset lifetime of multiple items.
     *
     * @param  array $keys
     * @return array Array of not updated keys
     * @throws Exception\ExceptionInterface
     *
     * @triggers touchItems.pre(PreEvent)
     * @triggers touchItems.post(PostEvent)
     * @triggers touchItems.exception(ExceptionEvent)
     */
    
public function touchItems(array $keys)
    {
        if (!
$this->getOptions()->getWritable()) {
            return 
$keys;
        }

        
$this->normalizeKeys($keys);
        
$args = new ArrayObject(array(
            
'keys' => & $keys,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalTouchItems($args['keys']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            return 
$this->triggerException(__FUNCTION__$args$keys$e);
        }
    }

    
/**
     * Internal method to reset lifetime of multiple items.
     *
     * @param  array $normalizedKeys
     * @return array Array of not updated keys
     * @throws Exception\ExceptionInterface
     */
    
protected function internalTouchItems(array & $normalizedKeys)
    {
        
$result = array();
        foreach (
$normalizedKeys as $normalizedKey) {
            if (!
$this->internalTouchItem($normalizedKey)) {
                
$result[] = $normalizedKey;
            }
        }
        return 
$result;
    }

    
/**
     * Remove an item.
     *
     * @param  string $key
     * @return bool
     * @throws Exception\ExceptionInterface
     *
     * @triggers removeItem.pre(PreEvent)
     * @triggers removeItem.post(PostEvent)
     * @triggers removeItem.exception(ExceptionEvent)
     */
    
public function removeItem($key)
    {
        if (!
$this->getOptions()->getWritable()) {
            return 
false;
        }

        
$this->normalizeKey($key);
        
$args = new ArrayObject(array(
            
'key' => & $key,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalRemoveItem($args['key']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result false;
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to remove an item.
     *
     * @param  string $normalizedKey
     * @return bool
     * @throws Exception\ExceptionInterface
     */
    
abstract protected function internalRemoveItem(& $normalizedKey);

    
/**
     * Remove multiple items.
     *
     * @param  array $keys
     * @return array Array of not removed keys
     * @throws Exception\ExceptionInterface
     *
     * @triggers removeItems.pre(PreEvent)
     * @triggers removeItems.post(PostEvent)
     * @triggers removeItems.exception(ExceptionEvent)
     */
    
public function removeItems(array $keys)
    {
        if (!
$this->getOptions()->getWritable()) {
            return 
$keys;
        }

        
$this->normalizeKeys($keys);
        
$args = new ArrayObject(array(
            
'keys' => & $keys,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalRemoveItems($args['keys']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            return 
$this->triggerException(__FUNCTION__$args$keys$e);
        }
    }

    
/**
     * Internal method to remove multiple items.
     *
     * @param  array $normalizedKeys
     * @return array Array of not removed keys
     * @throws Exception\ExceptionInterface
     */
    
protected function internalRemoveItems(array & $normalizedKeys)
    {
        
$result = array();
        foreach (
$normalizedKeys as $normalizedKey) {
            if (!
$this->internalRemoveItem($normalizedKey)) {
                
$result[] = $normalizedKey;
            }
        }
        return 
$result;
    }

    
/**
     * Increment an item.
     *
     * @param  string $key
     * @param  int    $value
     * @return int|bool The new value on success, false on failure
     * @throws Exception\ExceptionInterface
     *
     * @triggers incrementItem.pre(PreEvent)
     * @triggers incrementItem.post(PostEvent)
     * @triggers incrementItem.exception(ExceptionEvent)
     */
    
public function incrementItem($key$value)
    {
        if (!
$this->getOptions()->getWritable()) {
            return 
false;
        }

        
$this->normalizeKey($key);
        
$args = new ArrayObject(array(
            
'key'   => & $key,
            
'value' => & $value,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalIncrementItem($args['key'], $args['value']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result false;
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to increment an item.
     *
     * @param  string $normalizedKey
     * @param  int    $value
     * @return int|bool The new value on success, false on failure
     * @throws Exception\ExceptionInterface
     */
    
protected function internalIncrementItem(& $normalizedKey, & $value)
    {
        
$success  null;
        
$value    = (int) $value;
        
$get      = (int) $this->internalGetItem($normalizedKey$success);
        
$newValue $get $value;

        if (
$success) {
            
$this->internalReplaceItem($normalizedKey$newValue);
        } else {
            
$this->internalAddItem($normalizedKey$newValue);
        }

        return 
$newValue;
    }

    
/**
     * Increment multiple items.
     *
     * @param  array $keyValuePairs
     * @return array Associative array of keys and new values
     * @throws Exception\ExceptionInterface
     *
     * @triggers incrementItems.pre(PreEvent)
     * @triggers incrementItems.post(PostEvent)
     * @triggers incrementItems.exception(ExceptionEvent)
     */
    
public function incrementItems(array $keyValuePairs)
    {
        if (!
$this->getOptions()->getWritable()) {
            return array();
        }

        
$this->normalizeKeyValuePairs($keyValuePairs);
        
$args = new ArrayObject(array(
            
'keyValuePairs' => & $keyValuePairs,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalIncrementItems($args['keyValuePairs']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result = array();
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to increment multiple items.
     *
     * @param  array $normalizedKeyValuePairs
     * @return array Associative array of keys and new values
     * @throws Exception\ExceptionInterface
     */
    
protected function internalIncrementItems(array & $normalizedKeyValuePairs)
    {
        
$result = array();
        foreach (
$normalizedKeyValuePairs as $normalizedKey => $value) {
            
$newValue $this->internalIncrementItem($normalizedKey$value);
            if (
$newValue !== false) {
                
$result[$normalizedKey] = $newValue;
            }
        }
        return 
$result;
    }

    
/**
     * Decrement an item.
     *
     * @param  string $key
     * @param  int    $value
     * @return int|bool The new value on success, false on failure
     * @throws Exception\ExceptionInterface
     *
     * @triggers decrementItem.pre(PreEvent)
     * @triggers decrementItem.post(PostEvent)
     * @triggers decrementItem.exception(ExceptionEvent)
     */
    
public function decrementItem($key$value)
    {
        if (!
$this->getOptions()->getWritable()) {
            return 
false;
        }

        
$this->normalizeKey($key);
        
$args = new ArrayObject(array(
            
'key'   => & $key,
            
'value' => & $value,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalDecrementItem($args['key'], $args['value']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result false;
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to decrement an item.
     *
     * @param  string $normalizedKey
     * @param  int    $value
     * @return int|bool The new value on success, false on failure
     * @throws Exception\ExceptionInterface
     */
    
protected function internalDecrementItem(& $normalizedKey, & $value)
    {
        
$success  null;
        
$value    = (int) $value;
        
$get      = (int) $this->internalGetItem($normalizedKey$success);
        
$newValue $get $value;

        if (
$success) {
            
$this->internalReplaceItem($normalizedKey$newValue);
        } else {
            
$this->internalAddItem($normalizedKey$newValue);
        }

        return 
$newValue;
    }

    
/**
     * Decrement multiple items.
     *
     * @param  array $keyValuePairs
     * @return array Associative array of keys and new values
     * @throws Exception\ExceptionInterface
     *
     * @triggers incrementItems.pre(PreEvent)
     * @triggers incrementItems.post(PostEvent)
     * @triggers incrementItems.exception(ExceptionEvent)
     */
    
public function decrementItems(array $keyValuePairs)
    {
        if (!
$this->getOptions()->getWritable()) {
            return array();
        }

        
$this->normalizeKeyValuePairs($keyValuePairs);
        
$args = new ArrayObject(array(
            
'keyValuePairs' => & $keyValuePairs,
        ));

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalDecrementItems($args['keyValuePairs']);
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result = array();
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to decrement multiple items.
     *
     * @param  array $normalizedKeyValuePairs
     * @return array Associative array of keys and new values
     * @throws Exception\ExceptionInterface
     */
    
protected function internalDecrementItems(array & $normalizedKeyValuePairs)
    {
        
$result = array();
        foreach (
$normalizedKeyValuePairs as $normalizedKey => $value) {
            
$newValue $this->decrementItem($normalizedKey$value);
            if (
$newValue !== false) {
                
$result[$normalizedKey] = $newValue;
            }
        }
        return 
$result;
    }

    
/* status */

    /**
     * Get capabilities of this adapter
     *
     * @return Capabilities
     * @triggers getCapabilities.pre(PreEvent)
     * @triggers getCapabilities.post(PostEvent)
     * @triggers getCapabilities.exception(ExceptionEvent)
     */
    
public function getCapabilities()
    {
        
$args = new ArrayObject();

        try {
            
$eventRs $this->triggerPre(__FUNCTION__$args);
            if (
$eventRs->stopped()) {
                return 
$eventRs->last();
            }

            
$result $this->internalGetCapabilities();
            return 
$this->triggerPost(__FUNCTION__$args$result);
        } catch (\
Exception $e) {
            
$result false;
            return 
$this->triggerException(__FUNCTION__$args$result$e);
        }
    }

    
/**
     * Internal method to get capabilities of this adapter
     *
     * @return Capabilities
     */
    
protected function internalGetCapabilities()
    {
        if (
$this->capabilities === null) {
            
$this->capabilityMarker = new stdClass();
            
$this->capabilities     = new Capabilities($this$this->capabilityMarker);
        }
        return 
$this->capabilities;
    }

    
/* internal */

    /**
     * Validates and normalizes a key
     *
     * @param  string $key
     * @return void
     * @throws Exception\InvalidArgumentException On an invalid key
     */
    
protected function normalizeKey(& $key)
    {
        
$key = (string) $key;

        if (
$key === '') {
            throw new 
Exception\InvalidArgumentException(
                
"An empty key isn't allowed"
            
);
        } elseif ((
$p $this->getOptions()->getKeyPattern()) && !preg_match($p$key)) {
            throw new 
Exception\InvalidArgumentException(
                
"The key '{$key}' doesn't match agains pattern '{$p}'"
            
);
        }
    }

    
/**
     * Validates and normalizes multiple keys
     *
     * @param  array $keys
     * @return void
     * @throws Exception\InvalidArgumentException On an invalid key
     */
    
protected function normalizeKeys(array & $keys)
    {
        if (!
$keys) {
            throw new 
Exception\InvalidArgumentException(
                
"An empty list of keys isn't allowed"
            
);
        }

        
array_walk($keys, array($this'normalizeKey'));
        
$keys array_values(array_unique($keys));
    }

    
/**
     * Validates and normalizes an array of key-value pairs
     *
     * @param  array $keyValuePairs
     * @return void
     * @throws Exception\InvalidArgumentException On an invalid key
     */
    
protected function normalizeKeyValuePairs(array & $keyValuePairs)
    {
        
$normalizedKeyValuePairs = array();
        foreach (
$keyValuePairs as $key => $value) {
            
$this->normalizeKey($key);
            
$normalizedKeyValuePairs[$key] = $value;
        }
        
$keyValuePairs $normalizedKeyValuePairs;
    }
}