<?php

// Require Rediska
require_once dirname(__FILE__) . '/../../Rediska.php';

/**
 * Rediska PubSub channel
 *
 * @author Ivan Shumkov
 * @package Rediska
 * @subpackage PublishSubscribe
 * @version @package_version@
 * @link http://rediska.geometria-lab.net
 * @license http://www.opensource.org/licenses/bsd-license.php
 */
class Rediska_PubSub_Channel extends Rediska_Options_RediskaInstance implements Iterator, ArrayAccess
{
    const SUBSCRIBE     = 'subscribe';
    const UNSUBSCRIBE   = 'unsubscribe';
    const MESSAGE       = 'message';

    /**
     * Subscriptions
     *
     * @var array
     */
    protected $_subscriptions = array();

    /**
     * The pool of subscription connections
     *
     * @var Rediska_PubSub_Connections
     */
    protected $_connections;

    /**
     * Timeout in seconds
     *
     * @var int
     */
    protected $_timeout;

    /**
     * The unix timestamp when started getting a message
     *
     * @var int
     */
    protected $_timeStart;

    /**
     * Need start time?
     *
     * @var boolean
     */
    protected $_needStart = true;

    /**
     * Server alias or connection object
     *
     * @var string|Rediska_Connection
     */
    protected $_serverAlias;

    /**
     * Current channel for iterator
     *
     * @var string
     */
    protected $_currentChannel;

    /**
     * Current message for iterator
     *
     * @var string
     */
    protected $_currentMessage;

    /**
     * Message buffer
     *
     * @var array
     */
    static protected $_messages = array();

    /**
     * Exception class name for options
     *
     * @var string
     */
    protected $_optionsException = 'Rediska_PubSub_Exception';

    /**
     * Constructor
     *
     * @param string|array    $nameOrNames Channel name or array of names
     * @param array[optional] $options     Options:
     *                                         timeout     - Timeout in seconds. This can be a float to allow sub-second accuracy (For example 0.2 for 200 msec)
     *                                         serverAlias - Server alias or connection object
     *                                         rediska     - Rediska instance name, Rediska object or Rediska options for new instance
     */
    public function __construct($nameOrNames, $options = array())
    {
        $this->setOptions($options);

        $this->_throwIfNotSupported();

        $this->_connections = new Rediska_PubSub_Connections($this);

        $this->subscribe($nameOrNames);
    }

    /**
     * Subscribe to channel or channels
     *
     * @param string|array $channelOrChannels Channel name or names
     * @return Rediska_PubSub_Channel
     */
    public function subscribe($channelOrChannels)
    {
        if (!is_array($channelOrChannels)) {
            $channels = array($channelOrChannels);
        } else {
            $channels = $channelOrChannels;
        }

        $this->_execCommand(self::SUBSCRIBE, $channels);

        return $this;
    }

    /**
     * Unsubscribe from channel or channels
     * If $keys is passed - we unsubscribe from all channels
     *
     * @param string|array[optional] $channelOrChannels
     * @return Rediska_PubSub_Channel
     */
    public function unsubscribe($channelOrChannels = null)
    {
        if (is_null($channelOrChannels)) {
            $channels = $this->_subscriptions;
        } elseif (!is_array($channelOrChannels)) {
            $channels = array($channelOrChannels);
        } else {
            $channels = $channelOrChannels;
        }

        $this->_execCommand(self::UNSUBSCRIBE, $channels);

        return $this;
    }

    /**
     * Publish a message to channel
     *
     * @param $message
     * @return int
     */
    public function publish($message)
    {
        $rediska = $this->getRediska();

        if ($this->getServerAlias() !== null) {
            $rediska = $rediska->on($this->getServerAlias());
        }

        return $rediska->publish($this->_subscriptions, $message);
    }

    /**
     * Has subscriptions?
     *
     * @return boolean
     */
    public function hasSubscriptions()
    {
        return !empty($this->_subscriptions);
    }

    /**
     * Get subscriptions
     *
     * @return array
     */
    public function getSubscriptions()
    {
        return $this->_subscriptions;
    }

    /**
     * Get message
     *
     * @param float[optional] Timeout The timeout in seconds. Since this is a float it allows sub-second accuracy (For example 0.2 for 200 msec)
     * @return Rediska_PubSub_Response_Message|null
     */
    public function getMessage($timeout = null)
    {
        // Get default timeout
        if (!$timeout && $this->getTimeout()) {
            $timeout = $this->getTimeout();
        }

        // Start timer if not started from iterator
        if ($timeout && $this->_needStart) {
            $this->_timeStart = microtime(true);
        }

        // Get message from connections
        while (true) {
            // Try to get message from buffer
            if (!empty(self::$_messages)) {
                /* @var $connection Rediska_Connection */
                foreach($this->_connections as $connection) {
                    $channels = $this->_connections->getChannelsByConnection($connection);
                    foreach($channels as $channel) {
                        $key = "{$connection->getAlias()}-$channel";
                        if (isset(self::$_messages[$key])) {
                            $message = array_shift(self::$_messages[$key]);
                            if (empty(self::$_messages[$key])) {
                                unset(self::$_messages[$key]);
                            }

                            return $message;
                        }
                    }
                }
            }

            if (empty($this->_subscriptions)) {
                return null;
            }

            /* @var $connection Rediska_Connection */
            foreach ($this->_connections as $connection) {
                if ($timeout) {
                    $timeLeft = ($this->_timeStart + $timeout) - microtime(true);

                    if ($timeLeft <= 0) {
                        // Reset timeStart if time started from this method
                        if ($this->_needStart) {
                            $this->_timeStart = 0;
                        }

                        return null;
                    }

                    $connection->setReadTimeout($timeLeft);
                } else {
                    $connection->setReadTimeout(600);
                }

                try {
                    $response = $this->_getResponseFromConnection($connection);

                    if ($response === null) {
                        // Sleep before next connection check
                        usleep(10000); // 0.01 sec
                        continue;
                    }

                    if (!in_array($response->getChannel(), $this->_subscriptions)) {
                        $this->_addMessageToBuffer($response);
                        continue;
                    }

                    // Reset timeStart if time started from this method
                    if ($this->_needStart) {
                        $this->_timeStart = 0;
                    }

                    return $response;
                } catch (Rediska_Connection_TimeoutException $e) {
                    if (!$timeout) {
                        continue;
                    }

                    // Reset timeStart if time started from this method
                    if ($this->_needStart) {
                        $this->_timeStart = 0;
                    }

                    return null;
                }
            }
        }
    }

    /**
     * Set timeout in seconds
     *
     * @return Rediska_PubSub_Channel
     */
    public function setTimeout($timeout)
    {
        $this->_timeout = (int)$timeout;

        return $this;
    }

    /**
     * Get timeout
     *
     * @return int
     */
    public function getTimeout()
    {
        return $this->_timeout;
    }

    /**
     * Set server alias
     *
     * @param $serverAlias
     * @return Rediska_PubSub_Channel
     */
    public function setServerAlias($serverAlias)
    {
        $this->_serverAlias = $serverAlias;

        return $this;
    }

    /**
     * Get server alias
     *
     * @return Rediska_Connection|string
     */
    public function getServerAlias()
    {
        return $this->_serverAlias;
    }

    /* Iterator implementation */

    public function rewind()
    {
        if ($this->_timeout) {
            $this->_timeStart = microtime(true);
        }
    }

    public function next()
    {

    }

    public function valid()
    {
        $this->_needStart = false;
        $message = $this->getMessage();
        $this->_needStart = true;

        if ($message) {
            $this->_currentChannel = $message->getChannel();
            $this->_currentMessage = $message->getMessage();

            return true;
        } else {
            $this->_currentChannel = null;
            $this->_currentMessage = null;

            $this->_timeStart = 0;

            return false;
        }
    }

    public function key()
    {
        return $this->_currentChannel;
    }

    public function current()
    {
        return $this->_currentMessage;
    }

    /* ArrayAccess implementation */

    public function offsetSet($offset, $value)
    {
        if (!is_null($offset)) {
            throw new Rediska_PubSub_Exception('Offset is not allowed in Rediska_PubSub_Channel');
        }

        $this->publish($value);

        return $value;
    }

    public function offsetExists($value)
    {
        throw new Rediska_PubSub_Exception('Offset is not allowed in Rediska_PubSub_Channel');
    }

    public function offsetUnset($value)
    {
        throw new Rediska_PubSub_Exception('Offset is not allowed in Rediska_PubSub_Channel');
    }

    public function offsetGet($value)
    {
        throw new Rediska_PubSub_Exception('Offset is not allowed in Rediska_PubSub_Channel');
    }

    /**
     * Execute subscribe or unsubscribe command
     *
     * @param string $command
     * @param array $channels
     */
    protected function _execCommand($command, $channels)
    {
        // Group channels by connections
        $channelsByConnections = array();
        foreach($channels as $channel) {
            $hasSubscription = in_array($channel, $this->_subscriptions);

            if ($command == self::SUBSCRIBE) {
                if ($hasSubscription) {
                    throw new Rediska_PubSub_Exception("You already subscribed to $channel");
                } else {
                    $this->_subscriptions[] = $channel;
                }
            }

            if ($command == self::UNSUBSCRIBE) {
                if (!$hasSubscription) {
                    throw new Rediska_PubSub_Exception("You not subscribed to $channel");
                } else {
                    $key = array_search($channel, $this->_subscriptions);
                    unset($this->_subscriptions[$key]);
                }
            }

            $hasChannel = $this->_connections->hasChannel($channel);

            if (($command == self::SUBSCRIBE && !$hasChannel) || ($command == self::UNSUBSCRIBE && $hasChannel)) {
                switch ($command) {
                    case self::SUBSCRIBE:
                        $connection = $this->_connections->addChannel($channel);
                        break;
                    case self::UNSUBSCRIBE:
                        $connection = $this->_connections->removeChannel($channel);
                        break;
                }

                $connectionAlias = $connection->getAlias();
                if (!array_key_exists($connectionAlias, $channelsByConnections)) {
                    $channelsByConnections[$connectionAlias] = array();
                }
                $channelsByConnections[$connectionAlias][] = $channel;
            }
        }

        // Write commands to connections
        foreach($channelsByConnections as $connectionAlias => $channels) {
            $execCommand = array($command);
            foreach($channels as $channel) {
                $execCommand[] = $this->getRediska()->getOption('namespace') . $channel;
            }
            $connection = $this->_connections->getConnectionByAlias($connectionAlias);
            $exec = new Rediska_Connection_Exec($connection, $execCommand);
            $exec->write();
        }

        // Get responses from connections
        while (!empty($channelsByConnections)) {
            foreach ($channelsByConnections as $connectionAlias => $channels) {
                $connection = $this->_connections->getConnectionByAlias($connectionAlias);

                foreach($channels as $channel) {
                    $response = $this->_getResponseFromConnection($connection);

                    // TODO: May be timeout? Not data or server die()
                    if ($response === null) {
                        continue;
                    }

                    $channel = $response->getChannel();

                    if (($command == self::SUBSCRIBE && $response instanceof Rediska_PubSub_Response_Subscribe)
                     || ($command == self::UNSUBSCRIBE && $response instanceof Rediska_PubSub_Response_Unsubscribe)) {
                        $key = array_search($channel, $channelsByConnections[$connectionAlias]);

                        unset($channelsByConnections[$connectionAlias][$key]);

                        if (empty($channelsByConnections[$connectionAlias])) {
                            unset($channelsByConnections[$connectionAlias]);
                        }
                    } else if ($response instanceof Rediska_PubSub_Response_Message) {
                        $this->_addMessageToBuffer($response);
                    }
                }
            }
        }
    }

    /**
     * Add message response to buffer
     *
     * @param Rediska_PubSub_Response_Message $message
     */
    protected function _addMessageToBuffer(Rediska_PubSub_Response_Message $message)
    {
        $key = "{$message->getConnection()->getAlias()}-{$message->getChannel()}";

        if (!isset(self::$_messages[$key])) {
            self::$_messages[$key] = array();
        }

        self::$_messages[$key][] = $message;
    }

    /**
     * Get response from connection
     *
     * @param Rediska_Connection $connection
     * @return Rediska_PubSub_Response_Abstract
     */
    protected function _getResponseFromConnection(Rediska_Connection $connection)
    {
        $response = Rediska_Connection_Exec::readResponseFromConnection($connection);

        if ($response === null || $response === true) {
            return null;
        }

        list($type, $channel, $body) = $response;

        if ($this->getRediska()->getOption('namespace') !== '' && strpos($channel, $this->getRediska()->getOption('namespace')) === 0) {
            $channel = substr($channel, strlen($this->getRediska()->getOption('namespace')));
        }

        switch ($type) {
            case self::SUBSCRIBE:
                return new Rediska_PubSub_Response_Subscribe($connection, $channel);

            case self::UNSUBSCRIBE:
                return new Rediska_PubSub_Response_Unsubscribe($connection, $channel);

            case self::MESSAGE:
                $message = $this->getRediska()->getSerializer()->unserialize($body);

                return new Rediska_PubSub_Response_Message($connection, $channel, $message);

            default:
                throw new Rediska_PubSub_Response_Exception('Unknown reponse type: ' . $type);
        }
    }

    /**
     * Throw if PubSub not supported by Redis
     */
    protected function _throwIfNotSupported()
    {
        $version = '1.3.8';
        $redisVersion = $this->getRediska()->getOption('redisVersion');
        if (version_compare($version, $this->getRediska()->getOption('redisVersion')) == 1) {
            throw new Rediska_PubSub_Exception("Publish/Subscribe requires {$version}+ version of Redis server. Current version is {$redisVersion}. To change it specify 'redisVersion' option.");
        }
    }
}


<?php

/**
 * Set + Expire atomic command
 *
 * @author Ivan Shumkov
 * @package Rediska
 * @subpackage Commands
 * @version @package_version@
 * @link http://rediska.geometria-lab.net
 * @license http://www.opensource.org/licenses/bsd-license.php
 */
class Rediska_Command_SetAndExpire extends Rediska_Command_Abstract
{
    /**
     * Supported version
     *
     * @var string
     */
    protected $_version = '2.0';

    /**
     * Create command
     *
     * @param string  $key      Key name
     * @param mixed   $value    Value
     * @param integer $seconds  Expire time in seconds
     * @return Rediska_Connection_Exec
     */
    public function create($key, $value, $seconds)
    {
        $connection = $this->getRediska()->getConnectionByKeyName($key);

        $command = array('SETEX',
                         $this->getRediska()->getOption('namespace') . $key,
                         $seconds, $this->getRediska()->getSerializer()->serialize($value));

        return new Rediska_Connection_Exec($connection, $command);
    }
}


<?php

/**
 * Get members from sorted set by min and max score
 *
 * @author Ivan Shumkov
 * @package Rediska
 * @subpackage Commands
 * @version @package_version@
 * @link http://rediska.geometria-lab.net
 * @license http://www.opensource.org/licenses/bsd-license.php
 */
class Rediska_Command_GetFromSortedSetByScore extends Rediska_Command_Abstract
{
    /**
     * Supported version
     *
     * @var string
     */
    protected $_version = '1.1';

    /**
     * Create command
     *
     * @param string            $key        Key name
     * @param number            $min        Min score
     * @param number            $max        Max score
     * @param boolean[optional] $withScores Get with scores. For default is false
     * @param integer[optional] $limit      Limit. For default is no limit
     * @param integer[optional] $offset     Offset. For default is no offset
     * @param boolean[optional] $revert     Revert. For default is false
     * @return Rediska_Connection_Exec
     */
    public function create($key, $min, $max, $withScores = false, $limit = null, $offset = null, $revert = false)
    {
        if ($revert) {
            $this->_throwExceptionIfNotSupported('2.1.6');
        }

        $connection = $this->_rediska->getConnectionByKeyName($key);

        $command = array($revert ? 'ZREVRANGEBYSCORE' : 'ZRANGEBYSCORE',
                         $this->_rediska->getOption('namespace') . $key,
                         $revert ? $max : $min,
                         $revert ? $min : $max);

        if (!is_null($limit)) {
            if (is_null($offset)) {
                $offset = 0;
            }
            $command[] = 'LIMIT';
            $command[] = $offset;
            $command[] = $limit;
        }

        if ($withScores) {
            $this->_throwExceptionIfNotSupported('1.3.4');

            $command[] = 'WITHSCORES';
        }

        return new Rediska_Connection_Exec($connection, $command);
    }

    /**
     * Parse response
     *
     * @param array $response
     * @return array
     */
    public function parseResponse($response)
    {
        $values = $response;

        if ($this->withScores) {
            $values = Rediska_Command_Response_ValueAndScore::combine($this->_rediska, $values);
        } else {
            $values = array_map(array($this->_rediska->getSerializer(), 'unserialize'), $values);
        }

        return $values;
    }
}


<?php

/**
 * Default PHP serializer adapter
 *
 * @author Ivan Shumkov
 * @package Rediska
 * @subpackage Serializer
 * @version @package_version@
 * @link http://rediska.geometria-lab.net
 * @license http://www.opensource.org/licenses/bsd-license.php
 */
class Rediska_Serializer_Adapter_PhpSerialize implements Rediska_Serializer_Adapter_Interface
{
    protected $_unserialized = true;

    /**
     * Serialize value
     *
     * @param mixed $value
     * @return string
     */
    public function serialize($value)
    {
        return serialize($value);
    }

    /**
     * Unserialize value
     *
     * @throws Rediska_Serializer_Exception
     * @param string $value
     * @return mixed
     */
    public function unserialize($value)
    {
        set_error_handler(array($this, 'catchUnserializeNotice'));

        $unserializedValue = @unserialize($value);

        restore_error_handler();

        if (!$this->_unserialized) {
            $this->_unserialized = true;
            throw new Rediska_Serializer_Adapter_Exception("Can't unserialize value");
        }

        return $unserializedValue;
    }

    /**
     * Catch unserialize notice
     */
    public function catchUnserializeNotice($errno, $errstr, $errfile, $errline, $errcontext)
    {
        if (!error_reporting() && strpos($errstr, 'unserialize()') !== false) {
            $this->_unserialized = false;
            return true;
        } else {
            return false;
        }
    }
}


<?php

/**
 * Return and remove the last element of the List at key
 *
 * @author Ivan Shumkov
 * @package Rediska
 * @subpackage Commands
 * @version @package_version@
 * @link http://rediska.geometria-lab.net
 * @license http://www.opensource.org/licenses/bsd-license.php
 */
class Rediska_Command_PopFromList extends Rediska_Command_Abstract
{
    /**
     * Create command
     *
     * @param string           $name       Key name
     * @param string[optional] $pushToName If not null - push value to another key.
     * @return Rediska_Connection_Exec
     */
    public function create($key, $pushToKey = null)
    {
        $connection = $this->_rediska->getConnectionByKeyName($key);

        if (is_null($pushToKey)) {
            $command = array('RPOP',
                             $this->_rediska->getOption('namespace') . $key);
        } else {
            $toConnection = $this->_rediska->getConnectionByKeyName($pushToKey);

            if ($connection->getAlias() == $toConnection->getAlias()) {
                $this->_throwExceptionIfNotSupported('1.1');

                $command = array('RPOPLPUSH',
                                 $this->_rediska->getOption('namespace') . $key,
                                 $this->_rediska->getOption('namespace') . $pushToKey);
            } else {
                $this->setAtomic(false);

                $command = array('RPOP',
                                 $this->_rediska->getOption('namespace') . $key);
            }
        }

        return new Rediska_Connection_Exec($connection, $command);
    }

    /**
     * Parse response
     *
     * @param string $response
     * @return mixed
     */
    public function parseResponse($response)
    {
        if (!$this->isAtomic()) {
            $value = $this->_rediska->getSerializer()->unserialize($response);

            $this->_rediska->prependToList($this->pushToKey, $value);

            return $value;
        } else {
            return $this->_rediska->getSerializer()->unserialize($response);
        }
    }
}


<?php

/**
 * Abstract class for union, intersection and diff of sets
 *
 * @author Ivan Shumkov
 * @package Rediska
 * @subpackage Commands
 * @version @package_version@
 * @link http://rediska.geometria-lab.net
 * @license http://www.opensource.org/licenses/bsd-license.php
 */
abstract class Rediska_Command_CompareSets extends Rediska_Command_Abstract
{
    protected $_storeConnection;

    protected $_command;
    protected $_storeCommand;

    /**
     * Create command
     *
     * @param array            $keys     Array of key names
     * @param string[optional] $storeKey Store to set with key name
     * @return Rediska_Connection_Exec
     */
    public function create(array $keys, $storeKey = null)
    {
        if (empty($keys)) {
            throw new Rediska_Command_Exception('You must specify sets');
        }

        $connections = array();
        $keysByConnections = array();
        foreach ($keys as $key) {
            $connection = $this->_rediska->getConnectionByKeyName($key);
            $connectionAlias = $connection->getAlias();
            if (!array_key_exists($connectionAlias, $connections)) {
                $connections[$connectionAlias] = $connection;
                $keysByConnections[$connectionAlias] = array();
            }
            $keysByConnections[$connectionAlias][] = $key;
        }

        if (count($connections) == 1) {
            $connectionValues = array_values($connections);
            $connection = $connectionValues[0];

            if (!is_null($storeKey)) {
                $storeConnection = $this->_rediska->getConnectionByKeyName($storeKey);
                if ($storeConnection === $connection) {
                    $command = array($this->_storeCommand,
                                     $this->_rediska->getOption('namespace') . $storeKey);
                } else {
                    $this->setAtomic(false);
                    $this->_storeConnection = $storeConnection;
                    $command = array($this->_command);
                }
            } else {
                $command = array($this->_command);
            }

            $connectionKeys = array_keys($connections);
            $connectionAlias = $connectionKeys[0];

            foreach($keysByConnections[$connectionAlias] as $key) {
                $command[] = $this->_rediska->getOption('namespace') . $key;
            }

            return new Rediska_Connection_Exec($connection, $command);
        } else {
            $this->setAtomic(false);

            $commands = array();
            foreach($keysByConnections as $connectionAlias => $keys) {
                foreach ($keys as $key) {
                    $command = array('SMEMBERS',
                                     $this->_rediska->getOption('namespace') . $key);
                    $commands[] = new Rediska_Connection_Exec($connections[$connectionAlias], $command);
                }
            }

            return $commands;
        }
    }

    /**
     * Parse responses
     *
     * @param array $responses
     * @return boolean|array
     */
    public function parseResponses($responses)
    {
        if (!$this->isAtomic()) {
            if ($this->_storeConnection) {
                $values = $responses[0];
            } else {
                $values = array_values($this->_compareSets($responses));
            }

            $unserializedValues = array_map(array($this->_rediska->getSerializer(), 'unserialize'), $values);

            if (is_null($this->storeKey)) {
                return $unserializedValues;
            } else {
                $this->_rediska->delete($this->storeKey);
                foreach($unserializedValues as $value) {
                    $this->_rediska->addToSet($this->storeKey, $value);
                }
                return true;
            }
        } else {
            $reply = $responses[0];
            if (is_null($this->storeKey)) {
                $reply = array_map(array($this->_rediska->getSerializer(), 'unserialize'), $reply);
            } else {
                $reply = (boolean)$reply;
            }

            return $reply;
        }
    }

    abstract protected function _compareSets($sets);
}


<?php

/**
 * JSON adpter
 *
 * @author Ivan Shumkov
 * @package Rediska
 * @subpackage Serializer
 * @version @package_version@
 * @link http://rediska.geometria-lab.net
 * @license http://www.opensource.org/licenses/bsd-license.php
 */
class Rediska_Serializer_Adapter_Json extends Rediska_Options implements Rediska_Serializer_Adapter_Interface
{
    const DECODE_TYPE_ARRAY = true;
    const DECODE_TYPE_OBJECT = false;

    /**
     * Options
     *
     * Encode options:
     *
     * encodeAsObject       - Force encode as object
     * encodeHexQuote       - Encode quote to hex
     * encodeHexTag         - Encode tag to hex
     * encodeHexAmp         - Encode amp to hex
     * encodeHexApos        - Encode apos to hex
     *
     *
     * Decode options:
     *
     * decodeType           - Return associative array or object. Array by default.
     * decodeDepth          - Depth of returned objects. 512 by default.
     * decodeBigintAsString - Convert bigint to strings in returned objects.
     */
    protected $_options = array(
        'encodeasobject'       => false,
        'encodehexquote'       => false,
        'encodehextag'         => false,
        'encodehexamp'         => false,
        'encodehexapos'        => false,
        'decodetype'           => self::DECODE_TYPE_ARRAY,
        'decodedepth'          => 512,
        'decodebigintasstring' => false,
    );

    /**
     * Serialize value
     *
     * @param mixed $value
     * @return string
     */
    public function serialize($value)
    {
        $options = 0;

        if ($this->_options['encodeasobject']) {
            $options = $options | JSON_FORCE_OBJECT;
        }
        if ($this->_options['encodehexquote']) {
            $options = $options | JSON_HEX_QUOT;
        }
        if ($this->_options['encodehextag']) {
            $options = $options | JSON_HEX_TAG;
        }
        if ($this->_options['encodehexamp']) {
            $options = $options | JSON_HEX_AMP;
        }
        if ($this->_options['encodehexapos']) {
            $options = $options | JSON_HEX_APOS;
        }

        $serializedValue = json_encode($value);

        if (json_last_error() != JSON_ERROR_NONE) {
            throw new Rediska_Serializer_Adapter_Exception("Can't serialize value");
        }

        return $serializedValue;
    }

    /**
     * Unserialize value
     *
     * @param mixed $value
     * @return string
     */
    public function unserialize($value)
    {
        $decodedValue = json_decode($value);

        if ($decodedValue === null && $value !== 'null') {
            throw new Rediska_Serializer_Adapter_Exception("Can't unserialize value");
        }

        return $decodedValue;
    }
}


<?php

// Require Rediska
require_once dirname(__FILE__) . '/../../../../../Rediska.php';

/**
 * Caching layer for queue adapter to avoid subsequent identical queries on existing queues set.
 *
 * @author Maxim Ivanov
 * @package Rediska
 * @subpackage ZendFrameworkIntegration
 * @version @package_version@
 * @link http://rediska.geometria-lab.net
 * @license http://www.opensource.org/licenses/bsd-license.php
 */
class Rediska_Zend_Queue_Adapter_Redis_Queues
{
    /**
     * Queues set
     *
     * @var Rediska_Key_Set
     */
    protected $_queuesSet;

    /**
     * Cached queues set array
     *
     * @var array
     */
    protected $_queuesCache;

    /**
     * Constructor
     *
     * @param Rediska_Key_Set $queuesSet
     */
    public function __construct(Rediska_Key_Set $queuesSet)
    {
        $this->_queuesSet = $queuesSet;
    }

    /**
     * Does a queue already exist?
     *
     * @param string $name
     * @return bool
     */
    public function exists($name)
    {
        $queuesCache = $this->_getQueuesCache();

        return in_array($name, $queuesCache);
    }

    /**
     * Create a new queue in the queues set
     *
     * @param string $name
     * @return bool
     */
    public function add($name)
    {
        $result = $this->_queuesSet->add($name);

        if ($result) {
            $queuesCache = $this->_getQueuesCache();
            $queuesCache[] = $name;
            $this->_setQueuesCache($queuesCache);
        }

        return $result;
    }

    /**
     * Delete a queue from the queues set
     *
     * @param $name
     * @return bool
     */
    public function remove($name)
    {
        $result = $this->_queuesSet->remove($name);

        if ($result) {
            $queuesCache = $this->_getQueuesCache();

            $key = array_search($name, $queuesCache);
            unset($queuesCache[$key]);

            $this->_setQueuesCache($queuesCache);
        }

        return $result;
    }

    /**
     * Get existing queues names
     *
     * @return array
     */
    public function toArray()
    {
        return $this->_getQueuesCache();
    }

    /**
     * Get queues cache. Fetches data from redis on first call.
     *
     * @return array
     */
    protected function _getQueuesCache()
    {
        if ($this->_queuesCache === null) {
            $this->_queuesCache = $this->_queuesSet->toArray();
        }

        return $this->_queuesCache;
    }

    /**
     * Set queues cache
     *
     * @param array $queuesCache
     */
    protected function _setQueuesCache(array $queuesCache)
    {
        $this->_queuesCache = $queuesCache;
    }
}


<?php

/**
 * Rediska transaction
 *
 * @author Ivan Shumkov
 * @package Rediska
 * @version @package_version@
 * @link http://rediska.geometria-lab.net
 * @license http://www.opensource.org/licenses/bsd-license.php
 */
class Rediska_Transaction
{

    /**
     * @var string
     */
    const TRANSACTION_PREAMBLE = 'Transaction: ';

    /**
     * @var string
     */
    const TRANSACTION_EMPTY = 'Empty transaction';
    /**
     * Rediska instance
     *
     * @var Rediska
     */
    protected $_rediska;

    /**
     * Rediska specified connection instance
     *
     * @var Rediska_Connection
     */
    protected $_connection;

    /**
     * Rediska specified connection instance
     *
     * @var Rediska_Connection_Specified
     */
    protected $_specifiedConnection;

    /**
     * Commands buffer
     *
     * @var array
     */
    protected $_commands = array();

    /**
     * If transaction watched something
     *
     * @var bool
     */
    protected $_isWatched = false;

    /**
     * Constructor
     *
     * @param Rediska            $rediska    Rediska instance
     * @param Rediska_Connection $connection Trancation connection
     */
    public function __construct(Rediska $rediska, Rediska_Connection_Specified $specifiedConnection, Rediska_Connection $connection)
    {
        $this->_rediska             = $rediska;
        $this->_specifiedConnection = $specifiedConnection;
        $this->_connection          = $connection;

        $this->_throwIfNotSupported('Transactions', '1.3.8');
    }

    /**
     * Marks the given keys to be watched for conditional execution of a transaction.
     *
     * @throws Rediska_Transaction_Exception
     * @param  $keyOrKeys Key or array of keys
     * @return Rediska_Transaction
     */
    public function watch($keyOrKeys)
    {
        $this->_throwIfNotSupported('Watch', '2.1');

        $command = array('WATCH');

        if (!is_array($keyOrKeys)) {
            $keys = array($keyOrKeys);
        } else {
            $keys = $keyOrKeys;
        }

        foreach($keys as $key) {
            $command[] = $this->_rediska->getOption('namespace') . $key;
        }

        $exec = new Rediska_Connection_Exec($this->_connection, $command);
        $exec->execute();

        $this->_isWatched = true;

        return $this;
    }

    /**
     * Flushes all the previously watched keys for a transaction.
     *
     * @return Rediska_Transaction
     */
    public function unwatch()
    {
        $this->_throwIfNotSupported('Unwatch', '2.1');

        $command = 'UNWATCH';

        $exec = new Rediska_Connection_Exec($this->_connection, $command);
        $exec->execute();

        $this->_isWatched = false;

        return $this;
    }

    /**
     * Execute transaction
     *
     * @return array
     */
    public function execute()
    {
        $results = array();

        $this->_rediska->getProfiler()->start($this);

        $multi = new Rediska_Connection_Exec($this->_connection, 'MULTI');
        $multi->execute();

        foreach($this->_commands as $command) {
            $command->execute();
        }

        $exec = new Rediska_Connection_Exec($this->_connection, 'EXEC');
        $responses = $exec->execute();

        $this->_rediska->getProfiler()->stop();

        if (!$responses) {
            throw new Rediska_Transaction_AbortedException('Transaction has been aborted by server');
        }

        foreach($this->_commands as $i => $command) {
            $results[] = $command->parseResponses(array($responses[$i]));
        }

        $this->_reset();

        return $results;
    }

    /**
     * Magic method for execute
     *
     * @return array
     */
    public function __invoke()
    {
        return $this->execute();
    }

    /**
     * Discard transaction
     *
     * @return boolean
     */
    public function discard()
    {
        if ($this->_isWatched) {
            $this->unwatch();
        }

        $this->_reset();

        return true;
    }

    /**
     * Magic method for adding command to transaction
     *
     * @param $name
     * @param $args
     * @return Rediska_Transaction
     */
    public function __call($name, $args)
    {
        if (in_array(strtolower($name), array('on', 'pipeline', 'monitor', 'config', 'subscribe'))) {
            throw new Rediska_Transaction_Exception("You can't use '$name' in transaction");
        }

        return $this->_addCommand($name, $args);
    }

    /**
     * Add command to transaction
     *
     * @param string $name Command name
     * @param array  $args Arguments
     * @return Rediska_Transaction
     */
    protected function _addCommand($name, $args = array())
    {
        $this->_specifiedConnection->setConnection($this->_connection);

        $command = Rediska_Commands::get($this->_rediska, $name, $args);
        $command->initialize();

        if (!$command->isAtomic()) {
            throw new Rediska_Exception("Command '$name' doesn't work properly (not atomic) in transaction on multiple servers");
        }

        $this->_commands[] = $command;

        $this->_specifiedConnection->resetConnection();

        return $this;
    }

    /**
     * Throw if transaction not supported by Redis
     */
    protected function _throwIfNotSupported($title, $version)
    {
        $redisVersion = $this->_rediska->getOption('redisVersion');
        if (version_compare($version, $this->_rediska->getOption('redisVersion')) == 1) {
            throw new Rediska_Transaction_Exception("$title requires {$version}+ version of Redis server. Current version is {$redisVersion}. To change it specify 'redisVersion' option.");
        }
    }

    public function  __toString()
    {
        if (empty($this->_commands)) {
            return self::TRANSACTION_EMPTY;
        } else {
            return self::TRANSACTION_PREAMBLE . implode(', ', $this->_commands);
        }
    }

    protected function _reset()
    {
        $this->_commands = array();
        $this->_isWatched = false;
    }

    /**
     * Generated command methods by 'scripts/add_command_methods.php'
     */

    /**
     * Ask the server to silently close the connection.
     *
     * @return Rediska_Transaction
     */
    public function quit() { $args = func_get_args(); return $this->_addCommand('quit', $args); }

    /**
     * Test if a key exists
     *
     * @param string $key Key name
     * @return Rediska_Transaction
     */
    public function exists($key) { $args = func_get_args(); return $this->_addCommand('exists', $args); }

    /**
     * Delete a key or keys
     *
     * @param string|array $keyOrKeys Key name or array of key names
     * @return Rediska_Transaction
     */
    public function delete($keyOrKeys) { $args = func_get_args(); return $this->_addCommand('delete', $args); }

    /**
     * Get key type
     *
     * @param string $key Key name
     * @return Rediska_Transaction
     */
    public function getType($key) { $args = func_get_args(); return $this->_addCommand('getType', $args); }

    /**
     * Returns all the keys matching the glob-style pattern
     * Glob style patterns examples:
     *   h?llo will match hello hallo hhllo
     *   h*llo will match hllo heeeello
     *   h[ae]llo will match hello and hallo, but not hillo
     *
     * @param string $pattern Pattern
     * @return Rediska_Transaction
     */
    public function getKeysByPattern($pattern) { $args = func_get_args(); return $this->_addCommand('getKeysByPattern', $args); }

    /**
     * Return a random key from the key space
     *
     * @return Rediska_Transaction
     */
    public function getRandomKey() { $args = func_get_args(); return $this->_addCommand('getRandomKey', $args); }

    /**
     * Rename the old key in the new one
     *
     * @param string            $oldKey    Old key name
     * @param string            $newKey    New key name
     * @param boolean[optional] $overwrite Overwrite the new name key if it already exists. For default is false.
     * @return Rediska_Transaction
     */
    public function rename($oldKey, $newKey, $overwrite = true) { $args = func_get_args(); return $this->_addCommand('rename', $args); }

    /**
     * Get the number of keys
     *
     * @return Rediska_Transaction
     */
    public function getKeysCount() { $args = func_get_args(); return $this->_addCommand('getKeysCount', $args); }

    /**
     * Set a time to live in seconds or timestamp on a key
     *
     * @param string  $key                   Key name
     * @param integer $secondsOrTimestamp    Time in seconds or timestamp
     * @param boolean $isTimestamp[optional] Time is timestamp. For default is false.
     * @return Rediska_Transaction
     */
    public function expire($key, $secondsOrTimestamp, $isTimestamp = false) { $args = func_get_args(); return $this->_addCommand('expire', $args); }

    /**
     * Get key lifetime
     *
     * @param string $key Key name
     * @return Rediska_Transaction
     */
    public function getLifetime($key) { $args = func_get_args(); return $this->_addCommand('getLifetime', $args); }

    /**
     * Select the DB having the specified index
     *
     * @param integer $index Db index
     * @return Rediska_Transaction
     */
    public function selectDb($index) { $args = func_get_args(); return $this->_addCommand('selectDb', $args); }

    /**
     * Move the key from the currently selected DB to the DB having as index dbindex
     *
     * @param string  $key     Key name
     * @param integer $dbIndex Redis DB index
     * @return Rediska_Transaction
     */
    public function moveToDb($key, $dbIndex) { $args = func_get_args(); return $this->_addCommand('moveToDb', $args); }

    /**
     * Remove all the keys of the currently selected DB
     *
     * @param boolean[optional] $all Remove from all Db. For default is false.
     * @return Rediska_Transaction
     */
    public function flushDb($all = false) { $args = func_get_args(); return $this->_addCommand('flushDb', $args); }

    /**
     * Set value to a key or muliple values to multiple keys
     *
     * @param string|array $keyOrData                  Key name or array with key => value.
     * @param mixed        $valueOrOverwrite[optional] Value or overwrite property for array of values. For default true.
     * @param boolean      $overwrite[optional]        Overwrite for single value (if false don't set and return false if key already exist). For default true.
     * @return Rediska_Transaction
     */
    public function set($keyOrData, $valueOrOverwrite = null, $overwrite = true) { $args = func_get_args(); return $this->_addCommand('set', $args); }

    /**
     * Set + Expire atomic command
     *
     * @param string  $key      Key name
     * @param mixed   $value    Value
     * @param integer $seconds  Expire time in seconds
     * @return Rediska_Transaction
     */
    public function setAndExpire($key, $value, $seconds) { $args = func_get_args(); return $this->_addCommand('setAndExpire', $args); }

    /**
     * Atomic set value and return old
     *
     * @param string $key   Key name
     * @param mixed  $value Value
     * @return Rediska_Transaction
     */
    public function setAndGet($key, $value) { $args = func_get_args(); return $this->_addCommand('setAndGet', $args); }

    /**
     * Get value of key or array of values by array of keys
     *
     * @param string|array $keyOrKeys Key name or array of names
     * @return Rediska_Transaction
     */
    public function get($keyOrKeys) { $args = func_get_args(); return $this->_addCommand('get', $args); }

    /**
     * Append value to a end of string key
     *
     * @param string $key    Key name
     * @param mixed  $value  Value
     * @return Rediska_Transaction
     */
    public function append($key, $value) { $args = func_get_args(); return $this->_addCommand('append', $args); }

    /**
     * Increment the number value of key by integer
     *
     * @param string            $key    Key name
     * @param integer[optional] $amount Amount to increment. One for default
     * @return Rediska_Transaction
     */
    public function increment($key, $amount = 1) { $args = func_get_args(); return $this->_addCommand('increment', $args); }

    /**
     * Decrement the number value of key by integer
     *
     * @param string            $key    Key name
     * @param integer[optional] $amount Amount to decrement. One for default
     * @return Rediska_Transaction
     */
    public function decrement($key, $amount = 1) { $args = func_get_args(); return $this->_addCommand('decrement', $args); }

    /**
     * Overwrite part of a string at key starting at the specified offset
     *
     * @param string  $key    Key name
     * @param integer $offset Offset
     * @param integer $value  Value
     * @return Rediska_Transaction
     */
    public function setRange($key, $offset, $value) { $args = func_get_args(); return $this->_addCommand('setRange', $args); }

    /**
     * Return a subset of the string from offset start to offset end (both offsets are inclusive)
     *
     * @param string            $key   Key name
     * @param integer           $start Start
     * @param integer[optional] $end   End. If end is omitted, the substring starting from $start until the end of the string will be returned. For default end of string
     * @return Rediska_Transaction
     */
    public function getRange($key, $start, $end = -1) { $args = func_get_args(); return $this->_addCommand('getRange', $args); }

    /**
     * Returns the bit value at offset in the string value stored at key
     *
     * @param string  $key    Key name
     * @param integer $offset Offset
     * @param integer $bit    Bit (0 or 1)
     * @return Rediska_Transaction
     */
    public function setBit($key, $offset, $bit) { $args = func_get_args(); return $this->_addCommand('setBit', $args); }

    /**
     * Returns the bit value at offset in the string value stored at key
     *
     * @param string  $key    Key name
     * @param integer $offset Offset
     * @return Rediska_Transaction
     */
    public function getBit($key, $offset) { $args = func_get_args(); return $this->_addCommand('getBit', $args); }

    /**
     * Returns the length of the string value stored at key
     *
     * @param string  $key Key name
     * @return Rediska_Transaction
     */
    public function getLength($key) { $args = func_get_args(); return $this->_addCommand('getLength', $args); }

    /**
     * Append value to the end of List
     *
     * @param string            $key                Key name
     * @param mixed             $value              Element value
     * @param boolean[optional] $createIfNotExists  Create list if not exists
     * @return Rediska_Transaction
     */
    public function appendToList($key, $value, $createIfNotExists = true) { $args = func_get_args(); return $this->_addCommand('appendToList', $args); }

    /**
     * Append value to the head of List
     *
     * @param string            $key                Key name
     * @param mixed             $value              Element value
     * @param boolean[optional] $createIfNotExists  Create list if not exists
     * @return Rediska_Transaction
     */
    public function prependToList($key, $value, $createIfNotExists = true) { $args = func_get_args(); return $this->_addCommand('prependToList', $args); }

    /**
     * Return the length of the List value at key
     *
     * @param string $key Key name
     * @return Rediska_Transaction
     */
    public function getListLength($key) { $args = func_get_args(); return $this->_addCommand('getListLength', $args); }

    /**
     * Get List by key
     *
     * @param string  $key                         Key name
     * @param integer $start[optional]             Start index. For default is begin of list
     * @param integer $end[optional]               End index. For default is end of list
     * @param boolean $responseIterator[optional]  If true - command return iterator which read from socket buffer.
     *                                             Important: new connection will be created
     * @return Rediska_Transaction
     */
    public function getList($key, $start = 0, $end = -1, $responseIterator = false) { $args = func_get_args(); return $this->_addCommand('getList', $args); }

    /**
     * Trim the list at key to the specified range of elements
     *
     * @param string  $key   Key name
     * @param integer $start Start index
     * @param integer $end   End index
     * @return Rediska_Transaction
     */
    public function truncateList($key, $start, $end) { $args = func_get_args(); return $this->_addCommand('truncateList', $args); }

    /**
     * Return element of List by index at key
     *
     * @param string  $key   Key name
     * @param integer $index Index
     * @return Rediska_Transaction
     */
    public function getFromList($key, $index) { $args = func_get_args(); return $this->_addCommand('getFromList', $args); }

    /**
     * Set a new value as the element at index position of the List at key
     *
     * @param string  $key   Key name
     * @param mixed   $value Value
     * @param integer $index Index
     * @return Rediska_Transaction
     */
    public function setToList($key, $index, $member) { $args = func_get_args(); return $this->_addCommand('setToList', $args); }

    /**
     * Delete element from list by member at key
     *
     * @param $key             Key name
     * @param $value           Element value
     * @param $count[optional] Limit of deleted items. For default no limit.
     * @return Rediska_Transaction
     */
    public function deleteFromList($key, $value, $count = 0) { $args = func_get_args(); return $this->_addCommand('deleteFromList', $args); }

    /**
     * Return and remove the first element of the List at key
     *
     * @param string $key Key name
     * @return Rediska_Transaction
     */
    public function shiftFromList($key) { $args = func_get_args(); return $this->_addCommand('shiftFromList', $args); }

    /**
     * Return and remove the first element of the List at key and block if list is empty or not exists
     *
     * @param string $keyOrKeys   Key name or array of names
     * @param string $timeout     Blocking timeout in seconds. Timeout disabled for default.
     * @return Rediska_Transaction
     */
    public function shiftFromListBlocking($keyOrKeys, $timeout = 0) { $args = func_get_args(); return $this->_addCommand('shiftFromListBlocking', $args); }

    /**
     * Return and remove the last element of the List at key
     *
     * @param string           $name       Key name
     * @param string[optional] $pushToName If not null - push value to another key.
     * @return Rediska_Transaction
     */
    public function popFromList($key, $pushToKey = null) { $args = func_get_args(); return $this->_addCommand('popFromList', $args); }

    /**
     * Return and remove the last element of the List at key and block if list is empty or not exists
     *
     * @param string|array $keyOrKeys           Key name or array of names
     * @param integer      $timeout[optional]   Timeout. 0 for default - timeout is disabled.
     * @param string       $pushToKey[optional] If not null - push value to another list.
     * @return Rediska_Transaction
     */
    public function popFromListBlocking($keyOrKeys, $timeout = 0, $pushToKey = null) { $args = func_get_args(); return $this->_addCommand('popFromListBlocking', $args); }

    /**
     * Insert a new value as the element before or after the reference value
     *
     * @param string  $key            Key name
     * @param string  $position       BEFORE or AFTER
     * @param mixed   $referenceValue Reference value
     * @param mixed   $value          Value
     * @return Rediska_Transaction
     */
    public function insertToList($key, $position, $referenceValue, $value) { $args = func_get_args(); return $this->_addCommand('insertToList', $args); }

    /**
     * Insert a new value as the element after the reference value
     *
     * @param string  $key            Key name
     * @param mixed   $referenceValue Reference value
     * @param mixed   $value          Value
     * @return Rediska_Transaction
     */
    public function insertToListAfter($key, $referenceValue, $value) { $args = func_get_args(); return $this->_addCommand('insertToListAfter', $args); }

    /**
     * Insert a new value as the element before the reference value
     *
     * @param string  $key            Key name
     * @param mixed   $referenceValue Reference value
     * @param mixed   $value          Value
     * @return Rediska_Transaction
     */
    public function insertToListBefore($key, $referenceValue, $value) { $args = func_get_args(); return $this->_addCommand('insertToListBefore', $args); }

    /**
     * Add the specified member to the Set value at key
     *
     * @param string $key    Key name
     * @param mixed  $member Member
     * @return Rediska_Transaction
     */
    public function addToSet($key, $member) { $args = func_get_args(); return $this->_addCommand('addToSet', $args); }

    /**
     * Remove the specified member from the Set value at key
     *
     * @param string $key    Key name
     * @param mixed  $member Member
     * @return Rediska_Transaction
     */
    public function deleteFromSet($key, $member) { $args = func_get_args(); return $this->_addCommand('deleteFromSet', $args); }

    /**
     * Get random element from the Set value at key
     *
     * @param string            $key  Key name
     * @param boolean[optional] $pop  If true - pop value from the set. For default is false
     * @return Rediska_Transaction
     */
    public function getRandomFromSet($key, $pop = false) { $args = func_get_args(); return $this->_addCommand('getRandomFromSet', $args); }

    /**
     * Return the number of elements (the cardinality) of the Set at key
     *
     * @param string $key Key name
     * @return Rediska_Transaction
     */
    public function getSetLength($key) { $args = func_get_args(); return $this->_addCommand('getSetLength', $args); }

    /**
     * Test if the specified value is a member of the Set at key
     *
     * @param string $key    Key value
     * @param mixed  $member Member
     * @return Rediska_Transaction
     */
    public function existsInSet($key, $member) { $args = func_get_args(); return $this->_addCommand('existsInSet', $args); }

    /**
     * Return the intersection between the Sets stored at key1, key2, ..., keyN
     *
     * @param array            $keys     Array of key names
     * @param string[optional] $storeKey Store to set with key name
     * @return Rediska_Transaction
     */
    public function intersectSets(array $keys, $storeKey = null) { $args = func_get_args(); return $this->_addCommand('intersectSets', $args); }

    /**
     * Return the union between the Sets stored at key1, key2, ..., keyN
     *
     * @param array            $keys     Array of key names
     * @param string[optional] $storeKey Store to set with key name
     * @return Rediska_Transaction
     */
    public function unionSets(array $keys, $storeKey = null) { $args = func_get_args(); return $this->_addCommand('unionSets', $args); }

    /**
     * Return the difference between the Set stored at key1 and all the Sets key2, ..., keyN
     *
     * @param array            $keys     Array of key names
     * @param string[optional] $storeKey Store to set with key name
     * @return Rediska_Transaction
     */
    public function diffSets(array $keys, $storeKey = null) { $args = func_get_args(); return $this->_addCommand('diffSets', $args); }

    /**
     * Return all the members of the Set value at key
     *
     * @param string  $key Key name
     * @param boolean $responseIterator[optional]  If true - command return iterator which read from socket buffer.
     *                                             Important: new connection will be created
     * @return Rediska_Transaction
     */
    public function getSet($key, $responseIterator = false) { $args = func_get_args(); return $this->_addCommand('getSet', $args); }

    /**
     * Move the specified member from one Set to another atomically
     *
     * @param string $fromKey From key name
     * @param string $toKey   To key name
     * @param mixed  $member  Member
     * @return Rediska_Transaction
     */
    public function moveToSet($fromKey, $toKey, $member) { $args = func_get_args(); return $this->_addCommand('moveToSet', $args); }

    /**
     * Add member to sorted set
     *
     * @param string $key    Key name
     * @param mixed  $member Member
     * @param number $score  Score of member
     * @return Rediska_Transaction
     */
    public function addToSortedSet($key, $member, $score) { $args = func_get_args(); return $this->_addCommand('addToSortedSet', $args); }

    /**
     * Delete the specified member from the sorted set by value
     *
     * @param string $key    Key name
     * @param mixed  $member Member
     * @return Rediska_Transaction
     */
    public function deleteFromSortedSet($key, $member) { $args = func_get_args(); return $this->_addCommand('deleteFromSortedSet', $args); }

    /**
     * Get all the members of the Sorted Set value at key
     *
     * @param string  $key                         Key name
     * @param integer $withScores[optional]        Return values with scores. For default is false.
     * @param integer $start[optional]             Start index. For default is begin of set.
     * @param integer $end[optional]               End index. For default is end of set.
     * @param boolean $revert[optional]            Revert elements (not used in sorting). For default is false
     * @param boolean $responseIterator[optional]  If true - command return iterator which read from socket buffer.
     *                                             Important: new connection will be created
     * @return Rediska_Transaction
     */
    public function getSortedSet($key, $withScores = false, $start = 0, $end = -1, $revert = false, $responseIterator = false) { $args = func_get_args(); return $this->_addCommand('getSortedSet', $args); }

    /**
     * Get members from sorted set by min and max score
     *
     * @param string            $key        Key name
     * @param number            $min        Min score
     * @param number            $max        Max score
     * @param boolean[optional] $withScores Get with scores. For default is false
     * @param integer[optional] $limit      Limit. For default is no limit
     * @param integer[optional] $offset     Offset. For default is no offset
     * @param boolean[optional] $revert     Revert. For default is false
     * @return Rediska_Transaction
     */
    public function getFromSortedSetByScore($key, $min, $max, $withScores = false, $limit = null, $offset = null, $revert = false) { $args = func_get_args(); return $this->_addCommand('getFromSortedSetByScore', $args); }

    /**
     * Get length of Sorted Set
     *
     * @param string $key Key name
     * @return Rediska_Transaction
     */
    public function getSortedSetLength($key) { $args = func_get_args(); return $this->_addCommand('getSortedSetLength', $args); }

    /**
     * Get count of members from sorted set by min and max score
     *
     * @param string $key Key name
     * @param number $min Min score
     * @param number $max Max score
     * @return Rediska_Transaction
     */
    public function getSortedSetLengthByScore($key, $min, $max) { $args = func_get_args(); return $this->_addCommand('getSortedSetLengthByScore', $args); }

    /**
     * Increment score of sorted set element
     *
     * @param string        $key   Key name
     * @param mixed         $value Member
     * @param integer|float $score Score to increment
     * @return Rediska_Transaction
     */
    public function incrementScoreInSortedSet($key, $value, $score) { $args = func_get_args(); return $this->_addCommand('incrementScoreInSortedSet', $args); }

    /**
     * Remove all the elements in the sorted set at key with a score between min and max (including elements with score equal to min or max).
     *
     * @param string  $key   Key name
     * @param numeric $min   Min value
     * @param numeric $max   Max value
     * @return Rediska_Transaction
     */
    public function deleteFromSortedSetByScore($key, $min, $max) { $args = func_get_args(); return $this->_addCommand('deleteFromSortedSetByScore', $args); }

    /**
     * Remove all elements in the sorted set at key with rank between start  and end
     *
     * @param string  $key   Key name
     * @param numeric $start Start position
     * @param numeric $end   End position
     * @return Rediska_Transaction
     */
    public function deleteFromSortedSetByRank($key, $start, $end) { $args = func_get_args(); return $this->_addCommand('deleteFromSortedSetByRank', $args); }

    /**
     * Get member score from Sorted Set
     *
     * @param string $key    Key name
     * @param mixed  $member Member value
     * @return Rediska_Transaction
     */
    public function getScoreFromSortedSet($key, $member) { $args = func_get_args(); return $this->_addCommand('getScoreFromSortedSet', $args); }

    /**
     * Get rank of member from sorted set
     *
     * @param string  $key              Key name
     * @param integer $member           Member value
     * @param boolean $revert[optional] Revert elements (not used in sorting). For default is false
     * @return Rediska_Transaction
     */
    public function getRankFromSortedSet($key, $member, $revert = false) { $args = func_get_args(); return $this->_addCommand('getRankFromSortedSet', $args); }

    /**
     * Store to key union between the sorted sets
     *
     * @param array  $keys       Array of key names or associative array with weights
     * @param string $storeKey   Result sorted set key name
     * @param string $aggregation Aggregation method: SUM (for default), MIN, MAX.
     * @return Rediska_Transaction
     */
    public function unionSortedSets(array $keys, $storeKey, $aggregation = Rediska_Command_UnionSortedSets::SUM) { $args = func_get_args(); return $this->_addCommand('unionSortedSets', $args); }

    /**
     * Store to key intersection between sorted sets
     *
     * @param array  $keys       Array of key names or associative array with weights
     * @param string $storeKey   Result sorted set key name
     * @param string $aggregation Aggregation method: SUM (for default), MIN, MAX.
     * @return Rediska_Transaction
     */
    public function intersectSortedSets(array $keys, $storeKey, $aggregation = Rediska_Command_IntersectSortedSets::SUM) { $args = func_get_args(); return $this->_addCommand('intersectSortedSets', $args); }

    /**
     * Set value to a hash field or fields
     *
     * @param string        $key          Key name
     * @param array|string  $fieldOrData  Field or array of many fields and values: field => value
     * @param mixed         $value        Value for single field
     * @param boolean       $overwrite    Overwrite for single field (if false don't set and return false if key already exist). For default true.
     * @return Rediska_Transaction
     */
    public function setToHash($key, $fieldOrData, $value = null, $overwrite = true) { $args = func_get_args(); return $this->_addCommand('setToHash', $args); }

    /**
     * Get value from hash field or fields
     *
     * @param string       $key           Key name
     * @param string|array $fieldOrFields Field or fields
     * @return Rediska_Transaction
     */
    public function getFromHash($key, $fieldOrFields) { $args = func_get_args(); return $this->_addCommand('getFromHash', $args); }

    /**
     * Increment field value in hash
     *
     * @param string $key              Key name
     * @param mixed  $field            Field
     * @param number $amount[optional] Increment amount. One for default
     * @return Rediska_Transaction
     */
    public function incrementInHash($key, $field, $amount = 1) { $args = func_get_args(); return $this->_addCommand('incrementInHash', $args); }

    /**
     * Test if field is present in hash
     *
     * @param string $key   Key name
     * @param mixed  $field Field
     * @return Rediska_Transaction
     */
    public function existsInHash($key, $field) { $args = func_get_args(); return $this->_addCommand('existsInHash', $args); }

    /**
     * Delete field from hash
     *
     * @param string $key   Key name
     * @param mixed  $field Field
     * @return Rediska_Transaction
     */
    public function deleteFromHash($key, $field) { $args = func_get_args(); return $this->_addCommand('deleteFromHash', $args); }

    /**
     * Return the number of fields in hash
     *
     * @param string $key Key name
     * @return Rediska_Transaction
     */
    public function getHashLength($key) { $args = func_get_args(); return $this->_addCommand('getHashLength', $args); }

    /**
     * Get hash fields and values
     *
     * @param string $key Key name
     * @return Rediska_Transaction
     */
    public function getHash($key) { $args = func_get_args(); return $this->_addCommand('getHash', $args); }

    /**
     * Get hash fields
     *
     * @param string $key Key name
     * @return Rediska_Transaction
     */
    public function getHashFields($key) { $args = func_get_args(); return $this->_addCommand('getHashFields', $args); }

    /**
     * Get hash values
     *
     * @param string $key Key name
     * @return Rediska_Transaction
     */
    public function getHashValues($key) { $args = func_get_args(); return $this->_addCommand('getHashValues', $args); }

    /**
     * Get sorted elements contained in the List, Set, or Sorted Set value at key.
     *
     * @param string        $key   Key name
     * @param array         $value Options:
     *                               * order
     *                               * limit
     *                               * offset
     *                               * alpha
     *                               * get
     *                               * by
     *                               * store
     *
     *                              See more: http://code.google.com/p/redis/wiki/SortCommand

     *                              If you use more then one connection to Redis servers,
     *                              it will choose by key name, and key by you pattern's may not present on it.
     *
     * @return Rediska_Transaction
     */
    public function sort($key, array $options = array()) { $args = func_get_args(); return $this->_addCommand('sort', $args); }

    /**
     * Publish message to pubsub channel
     *
     * @param array|string $channelOrChannels Channel or array of channels
     * @param mixed        $message           Message
     * @return Rediska_Transaction
     */
    public function publish($channelOrChannels, $message) { $args = func_get_args(); return $this->_addCommand('publish', $args); }

    /**
     * Save the DB on disk
     *
     * @param boolean[optional] $background Save asynchronously. For default is false
     * @return Rediska_Transaction
     */
    public function save($background = false) { $args = func_get_args(); return $this->_addCommand('save', $args); }

    /**
     * Return the UNIX time stamp of the last successfully saving of the dataset on disk
     *
     * @return Rediska_Transaction
     */
    public function getLastSaveTime() { $args = func_get_args(); return $this->_addCommand('getLastSaveTime', $args); }

    /**
     * Stop all the clients, save the DB, then quit the server
     *
     * @return Rediska_Transaction
     */
    public function shutdown() { $args = func_get_args(); return $this->_addCommand('shutdown', $args); }

    /**
     * Rewrite the Append Only File in background when it gets too big
     *
     * @return Rediska_Transaction
     */
    public function rewriteAppendOnlyFile() { $args = func_get_args(); return $this->_addCommand('rewriteAppendOnlyFile', $args); }

    /**
     * Provide information and statistics about the server
     *
     * @return Rediska_Transaction
     */
    public function info() { $args = func_get_args(); return $this->_addCommand('info', $args); }

    /**
     * This command is often used to test if a connection is still alive, or to
     * measure latency.
     *
     * @return Rediska_Transaction
     */
    public function ping() { $args = func_get_args(); return $this->_addCommand('ping', $args); }

    /**
     * Change the replication settings of a slave on the fly
     *
     * @param string|Rediska_Connection|false $aliasOrConnection Server alias, Rediska_Connection object or false if not slave
     * @return Rediska_Transaction
     */
    public function slaveOf($aliasOrConnection) { $args = func_get_args(); return $this->_addCommand('slaveOf', $args); }

}


<?php

/**
 * Abstract class for union and intersection of sorted sets
 *
 * @author Ivan Shumkov
 * @package Rediska
 * @subpackage Commands
 * @version @package_version@
 * @link http://rediska.geometria-lab.net
 * @license http://www.opensource.org/licenses/bsd-license.php
 */
abstract class Rediska_Command_CompareSortedSets extends Rediska_Command_Abstract
{
    const SUM = 'sum';
    const MAX = 'max';
    const MIN = 'min';

    /**
     * Supported version
     *
     * @var string
     */
    protected $_version = '1.3.12';

    protected $_command;

    protected $_storeConnection;
    protected $_keys   = array();
    protected $_weights = array();

    /**
     * Create command
     *
     * @param array  $keys       Array of key names or associative array with weights
     * @param string $storeKey   Result sorted set key name
     * @param string $aggregation Aggregation method: SUM (for default), MIN, MAX.
     * @return Rediska_Connection_Exec
     */
    public function create(array $keys, $storeKey, $aggregation = self::SUM)
    {
        if (empty($keys)) {
            throw new Rediska_Command_Exception('You must specify sorted sets');
        }

        // With weights?
        $withWeights = false;
        foreach($keys as $keyOrIndex => $keyOrWeight) {
            if (is_string($keyOrIndex)) {
                $this->_weights = $keys;
                $keys = array_keys($keys);
                $withWeights = true;
                break;
            }
        }

        $connections = array();
        $keysByConnections = array();
        foreach ($keys as $key) {
            $connection = $this->_rediska->getConnectionByKeyName($key);
            $connectionAlias = $connection->getAlias();
            if (!array_key_exists($connectionAlias, $connections)) {
                $connections[$connectionAlias] = $connection;
                $keysByConnections[$connectionAlias] = array();
            }
            $keysByConnections[$connectionAlias][] = $key;
        }

        // If only one connection, compare by redis
        if (count($connections) == 1) {
            $connectionValues = array_values($connections);
            $connection = $connectionValues[0];
            $storeConnection = $this->_rediska->getConnectionByKeyName($storeKey);

            if ($storeConnection === $connection) {
                $command = array($this->_command,
                                 $this->_rediska->getOption('namespace') . $storeKey,
                                 count($keys));

                foreach($keys as $key) {
                    $command[] = $this->_rediska->getOption('namespace') . $key;
                }

                if ($withWeights) {
                    $command[] = 'WEIGHTS';
                    $command = array_merge($command, $this->_weights);
                }

                if (strtolower($aggregation) != self::SUM) {
                    $command[] = 'AGGREGATE';
                    $command[] = strtoupper($aggregation);
                }

                return new Rediska_Connection_Exec($connection, $command);
            }
        }

        // Compare by hand

        // Set default weights
        if (!$withWeights) {
            $this->_weights = array_fill_keys($keys, 1);
        }

        $this->setAtomic(false);
        $commands = array();
        foreach($keysByConnections as $connectionAlias => $keys) {
            foreach($keys as $key) {
                $this->_keys[] = $key;
                $command = array('ZRANGE',
                                 $this->_rediska->getOption('namespace') . $key,
                                 0,
                                 -1,
                                 'WITHSCORES');
                $commands[] = new Rediska_Connection_Exec($connections[$connectionAlias], $command);
            }
        }

        return $commands;
    }

    /**
     * Parse responses
     *
     * @param array $responses
     * @return integer
     */
    public function parseResponses($responses)
    {
        if ($this->isAtomic()) {
           return $responses[0];
        } else {
            $sets = array();
            $valuesWithScores = array();
            foreach ($this->_keys as $key) {
                $sets[$key] = array();
                $response = current($responses);
                next($responses);
                $isValue = true;
                foreach ($response as $valueOrScore) {
                    if ($isValue) {
                        $value = $valueOrScore;
                        $sets[$key][] = $value;
                        if (!isset($valuesWithScores[$value])) {
                            $valuesWithScores[$value] = array();
                        }
                    } else {
                        $score = $valueOrScore;
                        $valuesWithScores[$value][] = $score * $this->_weights[$key];
                    }
                    $isValue = !$isValue;
                }
            }

            $aggregation = strtolower($this->aggregation);

            $pipeline = $this->_rediska->pipeline();

            $count = 0;
            foreach($this->_compareSets($sets) as $value) {
                $scores = $valuesWithScores[$value];
                switch ($aggregation) {
                    case self::SUM:
                        $score = array_sum($scores);
                        break;
                    case self::MIN:
                        $score = min($scores);
                        break;
                    case self::MAX:
                        $score = max($scores);
                        break;
                    default:
                        throw new Rediska_Command_Exception('Unknown aggregation method ' . $this->aggregation);
                }

                $value = $this->_rediska->getSerializer()->unserialize($value);

                $pipeline->addToSortedSet($this->storeKey, $value, $score);

                $count++;
            }

            $pipeline->execute();

            return $count;
        }
    }

    abstract protected function _compareSets($sets);
}


<?php

class PostController extends Zend_Controller_Action
{
    public function init()
    {
        parent::init();

        $auth = Zend_Auth::getInstance();
        if (!$auth->hasIdentity()) {
            throw new Zend_Auth_Exception("You're not authorized to see this page");
        }
    }

    /**
     * Create new post
     */
    public function newAction()
    {
        $currentUser = Zend_Auth::getInstance()->getStorage()->read();

        $form = new Form_Post;

        if ($this->getRequest()->isPost() && $form->isValid($this->getRequest()->getPost())) {

            $postData = $form->getValues();
            $postData['id'] = Post::fetchNextId();
            $postData['userId'] = $currentUser['id'];

            // save post
            $post = new Post($postData['id']);
            $post->setValue($postData);

            $userPosts = new UserPosts($currentUser['id']);
            $userPosts->add($postData['id']);

            // save post in the follower feeds
            $followers = new Followers($currentUser['id']);
            foreach ($followers as $followerId) {
                $feed = new Feed($followerId);
                $feed->prepend($postData['id']);
            }

            $this->_redirect('/post/my');
        }

        $this->view->form = $form;
    }

    /**
     * Read your posts
     */
    public function indexAction()
    {
        $currentUser = Zend_Auth::getInstance()->getStorage()->read();

        $feed = new Feed($currentUser['id']);

        $this->view->posts = array();

        // just for example, better use multiget
        foreach ($feed as $postId) {
            $post = new Post($postId);
            $postData = $post->getValue();

            $user = new User($postData['userId']);
            $userData = $user->getValue();

            $this->view->posts[] = array('post' => $postData, 'user' => $userData);
        }
    }

    public function myAction()
    {
        $currentUser = Zend_Auth::getInstance()->getStorage()->read();

        $userPosts = new UserPosts($currentUser['id']);

        $this->view->posts = array();

        foreach ($userPosts as $postId) {
            $post = new Post($postId);
            $postData = $post->getValue();

            $user = new User($postData['userId']);
            $userData = $user->getValue();

            $this->view->posts[] = array('post' => $postData, 'user' => $userData);
        }
    }
}


<?php

/**
 * Get value of key or array of values by array of keys
 *
 * @author Ivan Shumkov
 * @package Rediska
 * @subpackage Commands
 * @version @package_version@
 * @link http://rediska.geometria-lab.net
 * @license http://www.opensource.org/licenses/bsd-license.php
 */
class Rediska_Command_Get extends Rediska_Command_Abstract
{
    protected $_keys = array();
    protected $_keysByConnections = array();

    /**
     * Create command
     *
     * @param string|array $keyOrKeys Key name or array of names
     * @return Rediska_Connection_Exec
     */
    public function create($keyOrKeys)
    {
        if (is_array($keyOrKeys)) {
            $this->_multi = true;
            $keys = $keyOrKeys;

            if (empty($keys)) {
                throw new Rediska_Command_Exception('Not present keys for get');
            }

            $sortedResult = array();
            $this->_keys = $keys;
            $connections = array();
            $keysByConnections = array();
            foreach ($keys as $key) {
                $connection = $this->_rediska->getConnectionByKeyName($key);
                $connectionAlias = $connection->getAlias();
                if (!array_key_exists($connectionAlias, $connections)) {
                    $connections[$connectionAlias] = $connection;
                    $keysByConnections[$connectionAlias] = array();
                }
                $keysByConnections[$connectionAlias][] = $key;
            }

            $result = array();
            $commands = array();
            foreach($keysByConnections as $connectionAlias => $keys) {
                $command = array('MGET');
                foreach($keys as $key) {
                    $command[] = $this->_rediska->getOption('namespace') . $key;
                    $this->_keysByConnections[] = $key;
                }
                $commands[] = new Rediska_Connection_Exec($connections[$connectionAlias], $command);
            }

            return $commands;
        } else {
            $key = $keyOrKeys;

            $connection = $this->_rediska->getConnectionByKeyName($key);

            $command = array('GET',
                             $this->_rediska->getOption('namespace') . $key);

            return new Rediska_Connection_Exec($connection, $command);
        }
    }

    /**
     * Parse responses
     *
     * @param array $responses
     * @return mixed
     */
    public function parseResponses($responses)
    {
        if (is_array($this->keyOrKeys)) {
            $result = array();
            if (!empty($responses)) {
                $mergedResponses = array();
                foreach($responses as $response) {
                    $mergedResponses = array_merge($mergedResponses, $response);
                }
                $unsortedResult = array();
                foreach($this->_keysByConnections as $i => $key) {
                    $unsortedResult[$key] = $mergedResponses[$i];
                }
                foreach($this->_keys as $key) {
                    if (isset($unsortedResult[$key])) {
                        $result[$key] = $this->_rediska->getSerializer()->unserialize($unsortedResult[$key]);
                    }
                }
            }

            return $result;
        } else {
            return $this->_rediska->getSerializer()->unserialize($responses[0]);
        }
    }
}


<?php

// Require Rediska
require_once dirname(__FILE__) . '/../../../../Rediska.php';

/**
 * @see Zend_Log_Writer_Abstract
 */
require_once 'Zend/Log/Writer/Abstract.php';

/**
 * Redis writer for Zend_Log
 *
 * @author Ivan Shumkov
 * @package Rediska
 * @subpackage ZendFrameworkIntegration
 * @version @package_version@
 * @link http://rediska.geometria-lab.net
 * @license http://www.opensource.org/licenses/bsd-license.php
 */
class Rediska_Zend_Log_Writer_Redis extends Zend_Log_Writer_Abstract
{
    /**
     * Log list
     *
     * @var Rediska_Key_List
     */
    protected $_list;

    /**
     * Writer constructor
     *
     * @param string $keyName Log key name
     * @param mixed  $rediska Rediska instance name, Rediska object or array of options
     */
    public function __construct($keyName, $rediska = Rediska::DEFAULT_NAME)
    {
        if ($rediska instanceof Zend_Config) {
            $rediska = $rediska->toArray();
        }

        $this->_list = new Rediska_Key_List($keyName, array('rediska' => $rediska));
    }

    /**
     * Formatting is not possible on this writer
     */
    public function setFormatter(Zend_Log_Formatter_Interface $formatter)
    {
        require_once 'Zend/Log/Exception.php';
        throw new Zend_Log_Exception(get_class() . ' does not support formatting');
    }

    /**
     * Remove reference to list
     *
     * @return void
     */
    public function shutdown()
    {
        $this->_list = null;
    }

    /**
     * Create a new instance of Rediska_Zend_Log_Writer_Redis
     *
     * @param  array|Zend_Config $config
     * @return Zend_Log_Writer_Mock
     * @throws Zend_Log_Exception
     */
    static public function factory($config)
    {
        $config = self::_parseConfig($config);

        if (!isset($config['keyName'])) {
            throw new Zend_Log_Exception('keyName not present');
        }

        if (!isset($config['rediska'])) {
            $config['rediska'] = Rediska::DEFAULT_NAME;
        }

        return new self($config['keyName'], $config['rediska']);
    }

    /**
     * Write a message to the log.
     *
     * @param  array  $event  event data
     * @return void
     */
    protected function _write($event)
    {
        $this->_list[] = $event;
    }
}


<?php

// Require Rediska
require_once dirname(__FILE__) . '/../../Rediska.php';

/**
 * Rediska Sorted set key
 *
 * @author Ivan Shumkov
 * @package Rediska
 * @subpackage Key objects
 * @version @package_version@
 * @link http://rediska.geometria-lab.net
 * @license http://www.opensource.org/licenses/bsd-license.php
 */
class Rediska_Key_SortedSet extends Rediska_Key_Abstract implements IteratorAggregate, ArrayAccess, Countable
{
    /**
     * Add the specified member to the Sorted set
     *
     * @param mixed $value Value
     * @param numeric $score Score
     * @return boolean
     */
    public function add($value, $score)
    {
        $result = $this->_getRediskaOn()->addToSortedSet($this->getName(), $value, $score);

        if (!is_null($this->getExpire()) && $result) {
            $this->expire($this->getExpire(), $this->isExpireTimestamp());
        }

        return $result;
    }

    /**
     * Remove the specified member from the Sorted set
     *
     * @param mixed $value Value
     * @return boolean
     */
    public function remove($value)
    {
        $result = $this->_getRediskaOn()->deleteFromSortedSet($this->getName(), $value);

        if (!is_null($this->getExpire()) && $result) {
            $this->expire($this->getExpire(), $this->isExpireTimestamp());
        }

        return $result;
    }

    /**
     * Get Sorted set length
     *
     * @return integer
     */
    public function getLength()
    {
        return $this->_getRediskaOn()->getSortedSetLength($this->getName());
    }

    /**
     * Get count of members from sorted set by min and max score
     *
     * @param integer $min Min score
     * @param integer $max Max score
     * @return integer
     */
    public function getLengthByScore($min, $max)
    {
        return $this->_getRediskaOn()->getSortedSetLengthByScore($this->getName(), $min, $max);
    }

    /**
     * Get Sorted set by score
     *
     * @param number  $min        Min score
     * @param number  $max        Max score
     * @param boolean $withScores Get with scores
     * @param integer $limit      Limit
     * @param integer $offset     Offset
     * @param boolean $revert     Is revert result
     * @return array
     */
    public function getByScore($min, $max, $withScores = false, $limit = null, $offset = null, $revert = false)
    {
        return $this->_getRediskaOn()->getFromSortedSetByScore($this->getName(), $min, $max, $withScores, $limit, $offset, $revert);
    }

    /**
     * Remove members from sorted set by score
     *
     * @param $min Min score
     * @param $max Max score
     * @return integer
     */
    public function removeByScore($min, $max)
    {
        return $this->_getRediskaOn()->DeleteFromSortedSetByScore($this->getName(), $min, $max);
    }

    /**
     * Get member score from Sorted Set
     *
     * @param mixed $value
     * @return numeric
     */
    public function getScore($value)
    {
        return $this->_getRediskaOn()->getScoreFromSortedSet($this->getName(), $value);
    }

    /**
     * Increment score of element
     *
     * @param $value
     * @return integer
     */
    public function incrementScore($value, $score)
    {
        return $this->_getRediskaOn()->incrementScoreInSortedSet($this->getName(), $value, $score);
    }

    /**
     * Get Sorted set by Rank
     *
     * @param boolean $withScores Get with scores
     * @param integer $start      Start index
     * @param integer $end        End index
     * @param boolean $revert     Revert elements (not used in sorting)
     * @param boolean $responseIterator[optional]  If true - command return iterator which read from socket buffer.
     *                                             Important: new connection will be created
     *
     * @return array
     */
    public function getByRank($withScores = false, $start = 0, $end = -1, $revert = false, $responseIterator = false)
    {
        return $this->_getRediskaOn()->getSortedSet($this->getName(), $withScores, $start, $end, $revert, $responseIterator);
    }

    /**
     * Remove all elements in the sorted set at key with rank between start and end
     *
     * @param numeric $start Start position
     * @param numeric $end   End position
     * @return integer
     */
    public function removeByRank($start, $end)
    {
        return $this->_getRediskaOn()->deleteFromSortedSetByRank($this->getName(), $start, $end);
    }

    /**
     * Get rank of member
     *
     * @param integer $value  Member value
     * @param boolean $revert Revert elements (not used in sorting)
     * @return integer
     */
    public function getRank($value, $revert = false)
    {
        return $this->_getRediskaOn()->getRankFromSortedSet($this->getName(), $value, $revert);
    }

    /**
     * Store to key union between the sorted sets
     *
     * @param string|Rediska_Key_SortedSet|array  $setOrSets    Sorted set key name or object, or array of its
     * @param string                              $storeKeyName Result sorted set key name
     * @param string                              $aggregation  Aggregation method: SUM (for default), MIN, MAX.
     * @return integer
     */
    public function union($setOrSets, $storeKeyName, $aggregation = 'sum')
    {
        $sets = $this->_prepareSetsForComapre($setOrSets);

        return $this->_getRediskaOn()->unionSortedSets($sets, $storeKeyName, $aggregation);
    }

    /**
     * Store to key intersection between sorted sets
     *
     * @param string|Rediska_Key_SortedSet|array  $setOrSets    Sorted set key name or object, or array of its
     * @param string                              $storeKeyName Result sorted set key name
     * @param string                              $aggregation  Aggregation method: SUM (for default), MIN, MAX.
     * @return integer
     */
    public function intersect($setOrSets, $storeKeyName, $aggregation = 'sum')
    {
        $sets = $this->_prepareSetsForComapre($setOrSets);

        return $this->_getRediskaOn()->intersectSortedSets($sets, $storeKeyName, $aggregation);
    }

    /**
     * Get sorted the elements
     *
     * @param string|array  $value Options or SORT query string (http://code.google.com/p/redis/wiki/SortCommand).
     *                             Important notes for SORT query string:
     *                                 1. If you set Rediska namespace option don't forget add it to key names.
     *                                 2. If you use more then one connection to Redis servers, it will choose by key name,
     *                                    and key by you pattern's may not present on it.
     * @return array
     */
    public function sort($options = array())
    {
        return $this->_getRediskaOn()->sort($this->getName(), $options);
    }

    /**
     * Get Sorted set values
     *
     * @param integer $withScores  Return values with scores
     * @param integer $start       Start index
     * @param integer $end         End index
     * @param boolean $revert      Revert elements (not used in sorting)
     * @param boolean $responseIterator[optional]  If true - command return iterator which read from socket buffer.
     *                                             Important: new connection will be created
     *
     * @return array
     */
    public function toArray($withScores = false, $start = 0, $end = -1, $revert = false, $responseIterator = false)
    {
        return $this->getByRank($withScores, $start, $end, $revert, $responseIterator);
    }

    /**
     * Add array to Sorted set
     *
     * @param array $array
     */
    public function fromArray(array $array)
    {
        $pipeline = $this->_getRediskaOn()->pipeline();

        foreach($array as $score => $value) {
            $pipeline->addToSortedSet($this->getName(), $value, $score);
        }

        if (!is_null($this->getExpire())) {
            $pipeline->expire($this->getName(), $this->getExpire(), $this->isExpireTimestamp());
        }

        $pipeline->execute();

        return true;
    }

    /**
     * Implement intrefaces
     */

    public function count()
    {
        return $this->getLength();
    }

    public function getIterator()
    {
        return new ArrayObject($this->toArray());
    }

    public function offsetSet($score, $value)
    {
        if (is_null($score)) {
            throw new Rediska_Key_Exception('Score must be present');
        }

        $this->add($value, $score);

        return $value;
    }

    public function offsetExists($score)
    {
        return (boolean)$this->offsetGet($score);
    }

    public function offsetUnset($score)
    {
        $value = $this->offsetGet($score);
        if (!is_null($value)) {
            $this->remove($value);

            return true;
        } else {
            return false;
        }
    }

    public function offsetGet($score)
    {
        $values = $this->getByScore($score, $score);

        if (!empty($values)) {
            return $values[0];
        }
    }

    protected function _prepareSetsForComapre($setOrSets)
    {
        if (!is_array($setOrSets)) {
            $sets = array($setOrSets);
        } else {
            $sets = $setOrSets;
        }

        // With weights?
        $withWeights = false;
        foreach($sets as $nameOrIndex => $weightOrName) {
            if (is_string($nameOrIndex)) {
                $withWeights = true;
                break;
            }
        }

        if ($withWeights) {
            if (!array_key_exists($this->getName(), $sets)) {
                $sets[$this->getName()] = 1;
            }
        } else {
            foreach($sets as &$set) {
                if ($set instanceof Rediska_Key_SortedSet) {
                    $set = $set->getName();
                }
            }

            if (!in_array($this->getName(), $sets)) {
                $sets[] = $this->getName();
            }
        }

        return $sets;
    }
}


<?php

/**
 * Get List by key
 *
 * @throws Rediska_Command_Exception

 * @return array
 *
 * @author Ivan Shumkov
 * @package Rediska
 * @subpackage Commands
 * @version @package_version@
 * @link http://rediska.geometria-lab.net
 * @license http://www.opensource.org/licenses/bsd-license.php
 */
class Rediska_Command_GetList extends Rediska_Command_Abstract
{
    /**
     * Create command
     *
     * @param string  $key                         Key name
     * @param integer $start[optional]             Start index. For default is begin of list
     * @param integer $end[optional]               End index. For default is end of list
     * @param boolean $responseIterator[optional]  If true - command return iterator which read from socket buffer.
     *                                             Important: new connection will be created
     * @return Rediska_Connection_Exec
     */
    public function create($key, $start = 0, $end = -1, $responseIterator = false)
    {
        $connection = $this->_rediska->getConnectionByKeyName($key);

        $command = array('LRANGE',
                         $this->_rediska->getOption('namespace') . $key,
                         $start,
                         $end);

        $exec = new Rediska_Connection_Exec($connection, $command);

        if ($responseIterator) {
            $exec->setResponseIterator(true);
            $exec->setResponseCallback(array($this->getRediska()->getSerializer(), 'unserialize'));
        }

        return $exec;
    }

    /**
     * Parse responses
     *
     * @param array $response
     * @return array
     */
    public function parseResponse($response)
    {
        return array_map(array($this->_rediska->getSerializer(), 'unserialize'), $response);
    }
}