Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

first commit

  • Loading branch information...
commit 22d9129410ec48461bfcc814aeb5ed1b7e40ce4b 0 parents
@phpnode authored
114 ARedisCache.php
@@ -0,0 +1,114 @@
+<?php
+/**
+ * A cache component that allows items to be cached using redis.
+ * @author Charles Pick
+ * @package packages.redis
+ */
+class ARedisCache extends CCache {
+ /**
+ * Holds the redis connection
+ * @var ARedisConnection
+ */
+ protected $_connection;
+
+
+ /**
+ * Sets the redis connection to use for caching
+ * @param ARedisConnection|string $connection the redis connection, if a string is provided, it is presumed to be a the name of an applciation component
+ */
+ public function setConnection($connection)
+ {
+ if (is_string($connection)) {
+ $connection = Yii::app()->{$connection};
+ }
+ $this->_connection = $connection;
+ }
+
+ /**
+ * Gets the redis connection to use for caching
+ * @return ARedisConnection
+ */
+ public function getConnection()
+ {
+ if ($this->_connection === null) {
+ if (!isset(Yii::app()->redis)) {
+ throw new CException("ARedisCache expects a 'redis' application component");
+ }
+ $this->_connection = Yii::app()->redis;
+ }
+ return $this->_connection;
+ }
+
+ /**
+ * Retrieves a value from cache with a specified key.
+ * This is the implementation of the method declared in the parent class.
+ * @param string a unique key identifying the cached value
+ * @return string the value stored in cache, false if the value is not in the cache or expired.
+ */
+ protected function getValue($key) {
+ return $this->getConnection()->getClient()->get($key);
+ }
+
+ /**
+ * Retrieves multiple values from cache with the specified keys.
+ * @param array a list of keys identifying the cached values
+ * @return array a list of cached values indexed by the keys
+ * @since 1.0.8
+ */
+ protected function getValues($keys) {
+ return $this->getConnection()->getClient()->mget($keys);
+ }
+
+ /**
+ * Stores a value identified by a key in cache.
+ * This is the implementation of the method declared in the parent class.
+ *
+ * @param string the key identifying the value to be cached
+ * @param string the value to be cached
+ * @param integer the number of seconds in which the cached value will expire. 0 means never expire.
+ * @return boolean true if the value is successfully stored into cache, false otherwise
+ */
+ protected function setValue($key,$value,$expire = 0) {
+
+ $this->getConnection()->getClient()->set($key,$value);
+ if ($expire) {
+ $this->getConnection()->getClient()->expire($key,$expire);
+ }
+ }
+
+ /**
+ * Stores a value identified by a key into cache if the cache does not contain this key.
+ * This is the implementation of the method declared in the parent class.
+ *
+ * @param string the key identifying the value to be cached
+ * @param string the value to be cached
+ * @param integer the number of seconds in which the cached value will expire. 0 means never expire.
+ * @return boolean true if the value is successfully stored into cache, false otherwise
+ */
+ protected function addValue($key,$value,$expire) {
+ if($expire>0)
+ $expire+=time();
+ else
+ $expire=0;
+
+ return $this->getConnection()->getClient()->add($key,$value,$expire);
+ }
+
+ /**
+ * Deletes a value with the specified key from cache
+ * This is the implementation of the method declared in the parent class.
+ * @param string the key of the value to be deleted
+ * @return boolean if no error happens during deletion
+ */
+ protected function deleteValue($key) {
+ return $this->getConnection()->getClient()->delete($key);
+ }
+
+ /**
+ * Deletes all values from cache.
+ * Be careful of performing this operation if the cache is shared by multiple applications.
+ */
+ public function flush() {
+ return $this->getConnection()->getClient()->flush();
+ }
+}
154 ARedisChannel.php
@@ -0,0 +1,154 @@
+<?php
+/**
+ * Represents a redis pub/sub channel.
+ *
+ * Publishing messages:
+ * <pre>
+ * $channel = new ARedisChannel("myChannel");
+ * $channel->publish("hello world"); // sends a message to the channel
+ * </pre>
+ *
+ * Subscribing to channels:
+ * <pre>
+ * $channel = new ARedisChannel("myChannel");
+ * $channel->onReceiveMessage = function($redis, $channel, $message) {
+ * echo "Message Received:".$message."\n";
+ * };
+ * $channel->subscribe(); // blocks, the callback is triggered when a message is received
+ * </pre>
+ * @author Charles Pick
+ * @package packages.redis
+ */
+class ARedisChannel extends ARedisIterableEntity {
+ /**
+ * Holds the data in the entity
+ * @var array
+ */
+ protected $_data = array();
+ /**
+ * Subscribes to the channel
+ * @return ARedisIterableChannel $this subscribed to the channel
+ */
+ public function subscribe() {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ $this->getConnection()->getClient()->subscribe(array($this->name),array($this,"receiveMessage"));
+ return $this;
+ }
+ /**
+ * Unsubscribes from the channel
+ * @return ARedisIterableChannel $this unsubscribed from the channel
+ */
+ public function unsubscribe() {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ $this->getConnection()->getClient()->unsubscribe(array($this->name));
+ return $this;
+ }
+
+ /**
+ * Publishes a message to the channel
+ * @param string $message The message to publish
+ * @return integer the number of clients that received the message
+ */
+ public function publish($message) {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ $this->_data[] = $message;
+ return $this->getConnection()->getClient()->publish($this->name,$message);
+ }
+ /**
+ * Receives a message from a subscribed channel
+ * @param Redis $redis the redis client instance
+ * @param string $channel the name of the channel
+ * @param string $message the message content
+ */
+ public function receiveMessage($redis, $channel, $message) {
+ $this->_data[] = $message;
+ $event=new CEvent($this);
+ $this->onReceiveMessage($event);
+ }
+ /**
+ * Gets the last received / sent message
+ * @return mixed the last message received, or null if no messages have been received yet
+ */
+ public function getLastMessage() {
+ $count = count($this->_data);
+ if (!$count) {
+ return null;
+ }
+ return $this->_data[$count - 1];
+ }
+
+ /**
+ * This event is raised after a message is received
+ * @param CEvent $event the event parameter
+ */
+ public function onReceiveMessage($event)
+ {
+ $this->raiseEvent('onReceiveMessage',$event);
+ }
+
+ /**
+ * Gets the number of items in the channel
+ * @return integer the number of items in the channel
+ */
+ public function getCount() {
+ return count($this->_data);
+ }
+ /**
+ * Gets all the members in the sorted set
+ * @param boolean $forceRefresh whether to force a refresh or not, IGNORED!
+ * @return array the members in the set
+ */
+ public function getData($forceRefresh = false) {
+ return $this->_data;
+ }
+
+ /**
+ * Returns whether there is an item at the specified offset.
+ * This method is required by the interface ArrayAccess.
+ * @param integer $offset the offset to check on
+ * @return boolean
+ */
+ public function offsetExists($offset)
+ {
+ return isset($this->data[$offset]);
+ }
+
+ /**
+ * Returns the item at the specified offset.
+ * This method is required by the interface ArrayAccess.
+ * @param integer $offset the offset to retrieve item.
+ * @return mixed the item at the offset
+ * @throws CException if the offset is invalid
+ */
+ public function offsetGet($offset)
+ {
+ return $this->_data[$offset];
+ }
+
+ /**
+ * Sets the item at the specified offset.
+ * This method is required by the interface ArrayAccess.
+ * @param integer $offset the offset to set item
+ * @param mixed $item the item value
+ */
+ public function offsetSet($offset,$item)
+ {
+ $this->_data[$offset] = $item;
+ }
+
+ /**
+ * Unsets the item at the specified offset.
+ * This method is required by the interface ArrayAccess.
+ * @param integer $offset the offset to unset item
+ */
+ public function offsetUnset($offset)
+ {
+ unset($this->_data[$offset]);
+ }
+}
154 ARedisConnection.php
@@ -0,0 +1,154 @@
+<?php
+/**
+ * Represents a redis connection.
+ *
+ * @author Charles Pick
+ * @package packages.redis
+ */
+class ARedisConnection extends CApplicationComponent {
+ /**
+ * The redis client
+ * @var Redis
+ */
+ protected $_client;
+
+ /**
+ * The redis server name
+ * @var string
+ */
+ public $hostname = "localhost";
+
+ /**
+ * The redis server port
+ * @var integer
+ */
+ public $port=6379;
+
+ /**
+ * The database to use, defaults to 1
+ * @var integer
+ */
+ public $database=1;
+
+ /**
+ * Sets the redis client to use with this connection
+ * @param Redis $client the redis client instance
+ */
+ public function setClient(Redis $client)
+ {
+ $this->_client = $client;
+ }
+
+ /**
+ * Gets the redis client
+ * @return Redis the redis client
+ */
+ public function getClient()
+ {
+ if ($this->_client === null) {
+ $this->_client = new Redis;
+ $this->_client->connect($this->hostname, $this->port);
+ }
+ return $this->_client;
+ }
+
+ /**
+ * Returns a property value based on its name.
+ * Do not call this method. This is a PHP magic method that we override
+ * to allow using the following syntax to read a property
+ * <pre>
+ * $value=$component->propertyName;
+ * </pre>
+ * @param string $name the property name
+ * @return mixed the property value
+ * @throws CException if the property is not defined
+ * @see __set
+ */
+ public function __get($name) {
+ $getter='get'.$name;
+ if (property_exists($this->getClient(),$name)) {
+ return $this->getClient()->{$name};
+ }
+ elseif(method_exists($this->getClient(),$getter)) {
+ return $this->$getter();
+ }
+ return parent::__get($name);
+ }
+
+ /**
+ * Sets value of a component property.
+ * Do not call this method. This is a PHP magic method that we override
+ * to allow using the following syntax to set a property
+ * <pre>
+ * $this->propertyName=$value;
+ * </pre>
+ * @param string $name the property name
+ * @param mixed $value the property value
+ * @return mixed
+ * @throws CException if the property is not defined or the property is read only.
+ * @see __get
+ */
+ public function __set($name,$value)
+ {
+ $setter='set'.$name;
+ if (property_exists($this->getClient(),$name)) {
+ return $this->getClient()->{$name} = $value;
+ }
+ elseif(method_exists($this->getClient(),$setter)) {
+ return $this->getClient()->{$setter}($value);
+ }
+ return parent::__set($name,$value);
+ }
+
+ /**
+ * Checks if a property value is null.
+ * Do not call this method. This is a PHP magic method that we override
+ * to allow using isset() to detect if a component property is set or not.
+ * @param string $name the property name
+ * @return boolean
+ */
+ public function __isset($name)
+ {
+ $getter='get'.$name;
+ if (property_exists($this->getClient(),$name)) {
+ return true;
+ }
+ elseif (method_exists($this->getClient(),$getter)) {
+ return true;
+ }
+ return parent::__isset($name);
+ }
+
+ /**
+ * Sets a component property to be null.
+ * Do not call this method. This is a PHP magic method that we override
+ * to allow using unset() to set a component property to be null.
+ * @param string $name the property name or the event name
+ * @throws CException if the property is read only.
+ * @return mixed
+ */
+ public function __unset($name)
+ {
+ $setter='set'.$name;
+ if (property_exists($this->getClient(),$name)) {
+ $this->getClient()->{$name} = null;
+ }
+ elseif(method_exists($this,$setter)) {
+ $this->$setter(null);
+ }
+ else {
+ parent::__unset($name);
+ }
+ }
+ /**
+ * Calls a method on the redis client with the given name.
+ * Do not call this method. This is a PHP magic method that we override to
+ * allow a facade in front of the redis object.
+ * @param string $name the name of the method to call
+ * @param array $parameters the parameters to pass to the method
+ * @return mixed the response from the redis client
+ */
+ public function __call($name, $parameters) {
+ return call_user_func_array(array($this->getClient(),$name),$parameters);
+ }
+}
75 ARedisCounter.php
@@ -0,0 +1,75 @@
+<?php
+/**
+ * Represents a redis counter that can be atomically incremented and decremented.
+ * <pre>
+ * $counter = new ARedisCounter("totalPageViews");
+ * $counter->increment();
+ * echo $counter->getValue();
+ * </pre>
+ * @author Charles Pick
+ * @package packages.redis
+ */
+class ARedisCounter extends ARedisEntity {
+
+ /**
+ * The value of the counter
+ * @var integer
+ */
+ protected $_value;
+
+ /**
+ * Removes all the items from the entity
+ * @return ARedisIterableEntity the current entity
+ */
+ public function clear() {
+ $this->_value = null;
+ $this->getConnection()->getClient()->delete($this->name);
+ return $this;
+ }
+
+ /**
+ * Gets the value of the counter
+ * @param boolean $forceRefresh whether to fetch the data from redis again or not
+ * @return integer the value of the counter
+ */
+ public function getValue($forceRefresh = false) {
+ if ($this->_value === null || $forceRefresh) {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ $this->_value = (int) $this->getConnection()->getClient()->get($this->name);
+ }
+ return $this->_value;
+ }
+ /**
+ * Increments the counter by the given amount
+ * @param integer $byAmount the amount to increment by, defaults to 1
+ * @return integer the new value of the counter
+ */
+ public function increment($byAmount = 1) {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ return $this->_value = (int) $this->getConnection()->getClient()->incrBy($this->name,$byAmount);
+ }
+
+ /**
+ * Decrements the counter by the given amount
+ * @param integer $byAmount the amount to decrement by, defaults to 1
+ * @return integer the new value of the counter
+ */
+ public function decrement($byAmount = 1) {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ return $this->_value = (int) $this->getConnection()->getClient()->decrBy($this->name,$byAmount);
+ }
+
+ /**
+ * Gets the value of the counter
+ * @return integer the value of the counter
+ */
+ public function __toString() {
+ return $this->getValue();
+ }
+}
110 ARedisEntity.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * A base class for redis entities.
+ * Extends CBehavior to allow entities to be attached to models and components.
+ * <pre>
+ * $user = User::model()->findByPk(1);
+ * $counter = new ARedisCounter("totalLogins");
+ * $user->attachBehavior("totalLogins", $counter);
+ * echo $user->totalLogins."\n"; // 0
+ * $user->totalLogins->increment();
+ * echo $user->totalLogins."\n"; // 1
+ *
+ * $friends = new ARedisSet("friendIds");
+ * $user->attachBehavior("friendIds",$friends);
+ * foreach($user->friendIds as $id) {
+ * echo "User ".$user->id." is friends with user ".$id."\n";
+ * }
+ *
+ * </pre>
+ *
+ */
+abstract class ARedisEntity extends CBehavior {
+ /**
+ * The name of the redis entity (key)
+ * @var string
+ */
+ public $name;
+
+ /**
+ * Holds the redis connection
+ * @var ARedisConnection
+ */
+ protected $_connection;
+
+ /**
+ * The old name of this entity
+ * @var string
+ */
+ protected $_oldName;
+ /**
+ * Constructor
+ * @param string $name the name of the entity
+ * @param ARedisConnection|string $connection the redis connection to use with this entity
+ */
+ public function __construct($name = null, $connection = null) {
+ $this->name = $name;
+ $this->setConnection($connection);
+ }
+ /**
+ * Attaches the entity to a component
+ * @throws CException if no name is set
+ * @param CComponent $owner the owner component
+ */
+ public function attach($owner) {
+ parent::attach($owner);
+ if ($this->name === null) {
+ throw new CException("No name specified for ".get_class($this));
+ }
+ if (method_exists($owner, "getPrimaryKey")) {
+ $this->_oldName = $this->name;
+
+ $pk = $owner->getPrimaryKey();
+ if (is_array($pk)) {
+ foreach($pk as $key => $value) {
+ $pk[$key] = $key.":".$value;
+ }
+ $pk = implode(":",$pk);
+ }
+ $this->name = get_class($owner).":".$pk.":".$this->name;
+ }
+ }
+ /**
+ * Detaches the entity from a component
+ * @param CComponent $owner the owner component
+ */
+ public function detach($owner) {
+ parent::detach($owner);
+ if (method_exists($owner, "getPrimaryKey")) {
+ $this->name = $this->_oldName;
+ }
+ }
+
+ /**
+ * Sets the redis connection to use for this entity
+ * @param ARedisConnection|string $connection the redis connection, if a string is provided, it is presumed to be a the name of an applciation component
+ */
+ public function setConnection($connection)
+ {
+ if (is_string($connection)) {
+ $connection = Yii::app()->{$connection};
+ }
+ $this->_connection = $connection;
+ }
+
+ /**
+ * Gets the redis connection to use for this entity
+ * @return ARedisConnection
+ */
+ public function getConnection()
+ {
+ if ($this->_connection === null) {
+ if (!isset(Yii::app()->redis)) {
+ throw new CException(get_class($this)." expects a 'redis' application component");
+ }
+ $this->_connection = Yii::app()->redis;
+ }
+ return $this->_connection;
+ }
+
+}
121 ARedisHash.php
@@ -0,0 +1,121 @@
+<?php
+
+class ARedisHash extends ARedisIterableEntity {
+
+ /**
+ * Adds an item to the hash
+ * @param string $key the hash key
+ * @param mixed $value the item to add
+ * @return boolean true if the item was added, otherwise false
+ */
+ public function add($key, $value) {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ if (!$this->getConnection()->getClient()->hset($this->name,$key, $value)) {
+ return false;
+ }
+ $this->_data = null;
+ $this->_count = null;
+ return true;
+ }
+ /**
+ * Removes an item from the hash
+ * @param string $key the hash key to remove
+ * @return boolean true if the item was removed, otherwise false
+ */
+ public function remove($key) {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ if (!$this->getConnection()->getClient()->hdel($this->name,$key)) {
+ return false;
+ }
+ $this->_data = null;
+ $this->_count = null;
+ return true;
+ }
+
+ /**
+ * Returns an iterator for traversing the items in the hash.
+ * This method is required by the interface IteratorAggregate.
+ * @return Iterator an iterator for traversing the items in the hash.
+ */
+ public function getIterator()
+ {
+ return new CMapIterator($this->getData());
+ }
+
+ /**
+ * Gets the number of items in the hash
+ * @return integer the number of items in the set
+ */
+ public function getCount() {
+ if ($this->_count === null) {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ $this->_count = $this->getConnection()->getClient()->hlen($this->name);
+ }
+ return $this->_count;
+ }
+ /**
+ * Gets all the members in the sorted set
+ * @param boolean $forceRefresh whether to force a refresh or not
+ * @return array the members in the set
+ */
+ public function getData($forceRefresh = false) {
+ if ($forceRefresh || $this->_data === null) {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ $this->_data = $this->getConnection()->getClient()->hgetall($this->name);
+ }
+ return $this->_data;
+ }
+
+
+ /**
+ * Returns whether there is an item at the specified offset.
+ * This method is required by the interface ArrayAccess.
+ * @param integer $offset the offset to check on
+ * @return boolean
+ */
+ public function offsetExists($offset)
+ {
+ return ($offset>=0 && $offset<$this->getCount());
+ }
+
+ /**
+ * Returns the item at the specified offset.
+ * This method is required by the interface ArrayAccess.
+ * @param integer $offset the offset to retrieve item.
+ * @return mixed the item at the offset
+ */
+ public function offsetGet($offset)
+ {
+ $data = $this->getData();
+ return $data[$offset];
+ }
+
+ /**
+ * Sets the item at the specified offset.
+ * This method is required by the interface ArrayAccess.
+ * @param integer $offset the offset to set item
+ * @param mixed $item the item value
+ */
+ public function offsetSet($offset,$item)
+ {
+ $this->add($offset,$item);
+ }
+
+ /**
+ * Unsets the item at the specified offset.
+ * This method is required by the interface ArrayAccess.
+ * @param integer $offset the offset to unset item
+ */
+ public function offsetUnset($offset)
+ {
+ $this->remove($offset);
+ }
+}
84 ARedisIterableEntity.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * A base class for iterable redis entities (lists, hashes, sets and sorted sets)
+ * @author Charles Pick
+ * @package packages.redis
+ */
+abstract class ARedisIterableEntity extends ARedisEntity implements IteratorAggregate,ArrayAccess,Countable {
+
+ /**
+ * The number of items in the entity
+ * @var integer
+ */
+ protected $_count;
+
+ /**
+ * Holds the data in the entity
+ * @var array
+ */
+ protected $_data;
+
+
+ /**
+ * Returns an iterator for traversing the items in the set.
+ * This method is required by the interface IteratorAggregate.
+ * @return Iterator an iterator for traversing the items in the set.
+ */
+ public function getIterator()
+ {
+ return new CListIterator($this->getData());
+ }
+
+ /**
+ * Returns the number of items in the set.
+ * This method is required by Countable interface.
+ * @return integer number of items in the set.
+ */
+ public function count()
+ {
+ return $this->getCount();
+ }
+
+ /**
+ * Gets a list of items in the set
+ * @return array the list of items in array
+ */
+ public function toArray()
+ {
+ return $this->getData();
+ }
+
+ /**
+ * Gets the number of items in the entity
+ * @return integer the number of items in the entity
+ */
+ abstract public function getCount();
+
+ /**
+ * Gets all the members in the entity
+ * @param boolean $forceRefresh whether to force a refresh or not
+ * @return array the members in the entity
+ */
+ abstract public function getData($forceRefresh = false);
+
+ /**
+ * Determines whether the item is contained in the entity
+ * @param mixed $item the item to check for
+ * @return boolean true if the item exists in the entity, otherwise false
+ */
+ public function contains($item) {
+ return in_array($item, $this->getData());
+ }
+
+ /**
+ * Removes all the items from the entity
+ * @return ARedisIterableEntity the current entity
+ */
+ public function clear() {
+ $this->_data = null;
+ $this->_count = null;
+ $this->getConnection()->getClient()->delete($this->name);
+ return $this;
+ }
+
+}
210 ARedisList.php
@@ -0,0 +1,210 @@
+<?php
+/**
+ * Represents a redis list.
+ *
+ * @author Charles Pick
+ * @package packages.redis
+ */
+class ARedisList extends ARedisIterableEntity {
+ /**
+ * Adds an item to the list
+ * @param mixed $item the item to add
+ * @return boolean true if the item was added, otherwise false
+ */
+ public function add($item) {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ if (!$this->getConnection()->getClient()->rpush($this->name,$item)) {
+ return false;
+ }
+ $this->_data = null;
+ $this->_count = null;
+ return true;
+ }
+
+ /**
+ * Removes an item from the list
+ * @param mixed $item the item to remove
+ * @return boolean true if the item was removed, otherwise false
+ */
+ public function remove($item) {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ if (!$this->getConnection()->getClient()->lrem($this->name,$item,1)) {
+ return false;
+ }
+ $this->_data = null;
+ $this->_count = null;
+ return true;
+ }
+
+ /**
+ * Adds an item to the end of the list
+ * @param mixed $item the item to add
+ * @return boolean true if the item was added, otherwise false
+ */
+ public function push($item) {
+ return $this->add($item);
+ }
+
+ /**
+ * Adds an item to the start of the list
+ * @param mixed $item the item to add
+ * @return boolean true if the item was added, otherwise false
+ */
+ public function unshift($item) {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ if (!$this->getConnection()->getClient()->lpush($this->name,$item)) {
+ return false;
+ }
+ $this->_data = null;
+ $this->_count = null;
+ return true;
+ }
+
+ /**
+ * Removes and returns the first item from the list
+ * @return mixed the item that was removed from the list
+ */
+ public function shift() {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ $item = $this->getConnection()->getClient()->lpop($this->name);
+ $this->_data = null;
+ $this->_count = null;
+ return $item;
+ }
+
+ /**
+ * Removes and returns the last item from the list
+ * @return mixed the item that was removed from the list
+ */
+ public function pop() {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ $item = $this->getConnection()->getClient()->rpop($this->name);
+ $this->_data = null;
+ $this->_count = null;
+ return $item;
+ }
+ /**
+ * Gets a range of items in the list
+ * @param integer $start the 0 based index to start from
+ * @param integer $stop the 0 based index to end at
+ * @return array the items in the range
+ */
+ public function range($start = 0, $stop = -1) {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ return $this->getConnection()->getClient()->lrange($this->name, $start, $stop);
+ }
+ /**
+ * Trims the list so that it will only contain the specified range of items
+ * @param integer $start the 0 based index to start from
+ * @param integer $stop the 0 based index to end at
+ * @return boolean true if the trim was successful
+ */
+ public function trim($start, $stop) {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ return $this->getConnection()->getClient()->ltrim($this->name, $start, $stop) ? true : false;
+ }
+
+ /**
+ * Gets the number of items in the list
+ * @return integer the number of items in the list
+ */
+ public function getCount() {
+ if ($this->_count === null) {
+ if ($this->name === null) {
+ throw new CException(get_class($this)." requires a name!");
+ }
+ $this->_count = (int) $this->getConnection()->getClient()->lSize($this->name);
+ }
+ return $this->_count;
+ }
+ /**
+ * Gets all the members in the list
+ * @param boolean $forceRefresh whether to force a refresh or not
+ * @return array the members in the list
+ */
+ public function getData($forceRefresh = false) {
+ if ($forceRefresh || $this->_data === null) {
+ $this->_data = $this->range(0,-1);
+ }
+ return $this->_data;
+ }
+
+ /**
+ * Copies iterable data into the list.
+ * Note, existing data in the list will be cleared first.
+ * @param mixed $data the data to be copied from, must be an array or object implementing Traversable
+ * @throws CException If data is neither an array nor a Traversable.
+ */
+ public function copyFrom($data)
+ {
+ if(is_array($data) || ($data instanceof Traversable))
+ {
+ if($this->_count>0)
+ $this->clear();
+ if($data instanceof CList)
+ $data=$data->_data;
+ foreach($data as $item) {
+ $this->add($item);
+ }
+ }
+ else if($data!==null)
+ throw new CException(Yii::t('yii','List data must be an array or an object implementing Traversable.'));
+ }
+
+ /**
+ * Returns whether there is an item at the specified offset.
+ * This method is required by the interface ArrayAccess.
+ * @param integer $offset the offset to check on
+ * @return boolean
+ */
+ public function offsetExists($offset)
+ {
+ return ($offset>=0 && $offset<$this->getCount());
+ }
+
+ /**
+ * Returns the item at the specified offset.
+ * This method is required by the interface ArrayAccess.
+ * @param integer $offset the offset to retrieve item.
+ * @return mixed the item at the offset
+ */
+ public function offsetGet($offset)
+ {
+ return $this->_data[$offset];
+ }
+
+ /**
+ * Sets the item at the specified offset.
+ * This method is required by the interface ArrayAccess.
+ * @param integer $offset the offset to set item
+ * @param mixed $item the item value
+ */
+ public function offsetSet($offset,$item)
+ {
+ $this->add($item);
+ }
+
+ /**
+ * Unsets the item at the specified offset.
+ * This method is required by the interface ArrayAccess.
+ * @param integer $offset the offset to unset item
+ */
+ public function offsetUnset($offset)
+ {
+ $this->remove($this->_data[$offset]);
+ }
+}
73 ARedisLogRoute.php
@@ -0,0 +1,73 @@
+<?php
+
+class ARedisLogRoute extends CLogRoute {
+ /**
+ * The name of the redis key to use when storing logs
+ * @var string
+ */
+ public $redisKey;
+
+ /**
+ * Holds the redis connection
+ * @var ARedisConnection
+ */
+ protected $_connection;
+
+
+ /**
+ * Sets the redis connection to use for caching
+ * @param ARedisConnection|string $connection the redis connection, if a string is provided, it is presumed to be a the name of an applciation component
+ */
+ public function setConnection($connection)
+ {
+ if (is_string($connection)) {
+ $connection = Yii::app()->{$connection};
+ }
+ $this->_connection = $connection;
+ }
+
+ /**
+ * Gets the redis connection to use for caching
+ * @return ARedisConnection
+ */
+ public function getConnection()
+ {
+ if ($this->_connection === null) {
+ if (!isset(Yii::app()->redis)) {
+ throw new CException(get_class($this)." expects a 'redis' application component");
+ }
+ $this->_connection = Yii::app()->redis;
+ }
+ return $this->_connection;
+ }
+
+ /**
+ * Stores log messages into database.
+ * @param array $logs list of log messages
+ */
+ protected function processLogs($logs)
+ {
+ $redis = $this->getConnection()->getClient();
+ if (function_exists("json_encode")) {
+ $useCJSON = false;
+ }
+ else {
+ $useCJSON = true;
+ }
+ foreach($logs as $log) {
+ $item = array(
+ "level" => $log[1],
+ "category" => $log[2],
+ "time" => $log[3],
+ "message" => $log[0],
+ );
+ if ($useCJSON) {
+ $json = CJSON::encode($item);
+ }
+ else {
+ $json = json_encode($item);
+ }
+ $redis->publish($this->redisKey, $json);
+ }
+ }
+}
192 ARedisMutex.php
@@ -0,0 +1,192 @@
+<?php
+/**
+ * Represents a redis mutex.
+ *
+ * Simple usage:
+ * <pre>
+ * $mutex = new ARedisMutex("someOperation");
+ * $mutex->block(); // blocks execution until the resource becomes available
+ * // do something
+ * $mutex->unlock(); // release the lock
+ * </pre>
+ *
+ * With events:
+ * <pre>
+ * $mutex = new ARedisMutex("someOperation");
+ * $mutex->afterLock = function(CEvent $event) {
+ * echo "Locked!\n";
+ * // do some processing here
+ * $event->sender->unlock(); // finally, unlock the mutex
+ * };
+ * $mutex->afterUnlock = function(CEvent $event) {
+ * echo "Unlocked!";
+ * }
+ * $mutex->block(); // triggers appropriate events when the resource becomes available
+ * </pre>
+ * @author Charles Pick
+ * @package packages.redis
+ */
+class ARedisMutex extends ARedisEntity {
+
+ /**
+ * The number of seconds before this mutex will automatically expire
+ * @var integer
+ */
+ public $expiresAfter = 10;
+
+ /**
+ * The number of micro seconds to sleep for between poll requests.
+ * Defaults to half a second.
+ * @var integer
+ */
+ public $pollDelay = 500000;
+
+ /**
+ * The time the mutex expires at
+ * @var integer
+ */
+ protected $_expiresAt;
+
+
+ /**
+ * Attempts to lock the mutex, returns true if successful or false if the mutex is locked by another process.
+ * @return boolean whether the lock was successful or not
+ */
+ public function lock() {
+ if (!$this->beforeLock()) {
+ return false;
+ }
+ $redis = $this->getConnection()->getClient();
+ if (!$redis->setnx($this->name, $this->getExpiresAt(true))) {
+ // see if this mutex has expired
+
+ $value = $redis->get($this->name);
+ if ($value > microtime(true)) {
+ return false;
+ }
+ }
+ $this->afterLock();
+ return true;
+ }
+ /**
+ * Attempts to unlock the mutex, returns true if successful, or false if the mutex is in use by another process
+ * @return boolean whether the unlock was successful or not
+ */
+ public function unlock() {
+ if (!$this->beforeUnlock()) {
+ return false;
+ }
+ $redis = $this->getConnection()->getClient();
+ $value = $redis->get($this->name);
+ $decimalPlaces = max(strlen(substr($value,strpos($value,"."))),strlen(substr($this->_expiresAt,strpos($this->_expiresAt,".")))) - 1;
+ if (bccomp($value,$this->_expiresAt,$decimalPlaces) == -1 && bccomp($value,microtime(true),$decimalPlaces) == 1) {
+ return false;
+ }
+
+
+ $redis->delete($this->name);
+ $this->afterUnlock();
+ return true;
+ }
+ /**
+ * Blocks program execution until the lock becomes available
+ * @return ARedisMutex $this after the lock is opened
+ */
+ public function block() {
+ while($this->lock() === false) {
+
+ usleep($this->pollDelay);
+ }
+ return $this;
+ }
+
+ /**
+ * Invoked before the mutex is locked.
+ * The default implementation raises the onBeforeLock event
+ * @return boolean true if the lock should continue
+ */
+ public function beforeLock() {
+ $event = new CModelEvent();
+ $event->sender = $this;
+ $this->onBeforeLock($event);
+ return $event->isValid;
+ }
+
+ /**
+ * Invoked after the mutex is locked.
+ * The default implementation raises the onAfterLock event
+ */
+ public function afterLock() {
+ $event = new CEvent;
+ $event->sender = $this;
+ $this->onAfterLock($event);
+ }
+
+ /**
+ * Invoked before the mutex is unlocked.
+ * The default implementation raises the onBeforeUnlock event
+ * @return boolean true if the unlock should continue
+ */
+ public function beforeUnlock() {
+ $event = new CModelEvent;
+ $event->sender = $this;
+ $this->onBeforeUnlock($event);
+ return $event->isValid;
+ }
+
+ /**
+ * Invoked after the mutex is unlocked.
+ * The default implementation raises the onAfterUnlock event
+ */
+ public function afterUnlock() {
+ $event = new CEvent;
+ $event->sender = $this;
+ $this->onAfterUnlock($event);
+ }
+
+ /**
+ * Raises the onBeforeLock event
+ * @param CEvent $event the event to raise
+ */
+ public function onBeforeLock($event) {
+ $this->raiseEvent("onBeforeLock",$event);
+ }
+
+ /**
+ * Raises the onAfterLock event
+ * @param CEvent $event the event to raise
+ */
+ public function onAfterLock($event) {
+ $this->raiseEvent("onAfterLock",$event);
+ }
+
+ /**
+ * Raises the onBeforeUnlock event
+ * @param CEvent $event the event to raise
+ */
+ public function onBeforeUnlock($event) {
+ $this->raiseEvent("onBeforeUnlock",$event);
+ }
+
+ /**
+ * Raises the onAfterUnlock event
+ * @param CEvent $event the event to raise
+ */
+ public function onAfterUnlock($event) {
+ $this->raiseEvent("onAfterUnlock",$event);
+ }
+
+
+ /**
+ * Gets the time the mutex expires
+ * @param boolean $forceRecalculate whether to force recalculation or not
+ * @return float the time the mutex expires
+ */
+ public function getExpiresAt($forceRecalculate = false)
+ {
+ if ($forceRecalculate || $this->_expiresAt === null) {
+ $this->_expiresAt = $this->expiresAfter + microtime(true);
+ }
+ return $this->_expiresAt;
+ }
+}
644 ARedisRecord.php
@@ -0,0 +1,644 @@
+<?php
+/**
+ * Allows access to redis records using the active record pattern.
+ * <pre>
+ * $record = ARedisRecord::model()->findByPk(1); // loads a record with a unique id of 1
+ * $record->name = "a test name"; // sets the name attribute on the record
+ * $record->save(); // saves the record to redis
+ * $record->delete(); // deletes the record from redis
+ * </pre>
+ * @author Charles Pick / PeoplePerHour.com
+ * @package packages.redis
+ */
+abstract class ARedisRecord extends CFormModel {
+
+ /**
+ * The redis connection
+ * @var ARedisConnection
+ */
+ public static $redis;
+ /**
+ * The record attributes.
+ * @var CAttributeCollection
+ */
+ protected $_attributes;
+
+ /**
+ * The connection to redis
+ * @var ARedisConnection
+ */
+ protected $_connection;
+
+ /**
+ * The redis set that represents the list of models of this type
+ * @var ARedisIterableSet
+ */
+ protected $_redisSet;
+
+ /**
+ * The redis hash that contains the values for this record
+ * @var ARedisIterableHash
+ */
+ protected $_redisHash;
+
+ /**
+ * The old primary key value
+ * @var mixed
+ */
+ private $_pk;
+
+ /**
+ * Whether this is a new record or not
+ * @var boolean
+ */
+ private $_new = true;
+
+ /**
+ * An array of static model instances, clas name => model
+ * @var array
+ */
+ private static $_models=array();
+
+ /**
+ * Constructor.
+ * @param string $scenario the scenario name
+ * See {@link CModel::scenario} on how scenario is used by models.
+ * @see getScenario
+ */
+ public function __construct($scenario = "insert")
+ {
+ if ($scenario === null) {
+ return;
+ }
+ $this->init();
+ $this->attachBehaviors($this->behaviors());
+ $this->afterConstruct();
+ }
+
+ /**
+ * Returns the static model of the specified redis record class.
+ * The model returned is a static instance of the redis record class.
+ * It is provided for invoking class-level methods (something similar to static class methods.)
+ *
+ * EVERY derived redis record class must override this method as follows,
+ * <pre>
+ * public static function model($className=__CLASS__)
+ * {
+ * return parent::model($className);
+ * }
+ * </pre>
+ *
+ * @param string $className redis record class name.
+ * @return ARedisRecord redis record model instance.
+ */
+ public static function model($className=__CLASS__)
+ {
+ if(isset(self::$_models[$className]))
+ return self::$_models[$className];
+ else
+ {
+ $model=self::$_models[$className]=new $className(null);
+ $model->attachBehaviors($model->behaviors());
+ return $model;
+ }
+ }
+
+ /**
+ * Returns the redis connection used by redis record.
+ * By default, the "redis" application component is used as the redis connection.
+ * You may override this method if you want to use a different redis connection.
+ * @return ARedisConnection the redis connection used by redis record.
+ */
+ public function getRedisConnection()
+ {
+ if ($this->_connection !== null) {
+ return $this->_connection;
+ }
+ elseif(self::$redis!==null) {
+ return self::$redis;
+ }
+ else
+ {
+ self::$redis=Yii::app()->redis;
+ if(self::$redis instanceof ARedisConnection)
+ return self::$redis;
+ else
+ throw new CException(Yii::t('yii','Redis Record requires a "redis" ARedisConnection application component.'));
+ }
+ }
+ /**
+ * Sets the redis connection used by this redis record
+ * @param ARedisConnection $connection the redis connection to use for this record
+ */
+ public function setRedisConnection(ARedisConnection $connection) {
+ $this->_connection = $connection;
+ }
+
+ /**
+ * Creates a redis record instance.
+ * This method is called by {@link populateRecord} and {@link populateRecords}.
+ * You may override this method if the instance being created
+ * depends the attributes that are to be populated to the record.
+ * For example, by creating a record based on the value of a column,
+ * you may implement the so-called single-table inheritance mapping.
+ * @param array $attributes list of attribute values for the redis record.
+ * @return ARedisRecord the active record
+ */
+ protected function instantiate($attributes)
+ {
+ $class=get_class($this);
+ $model=new $class(null);
+ return $model;
+ }
+
+ /**
+ * Creates a redis record with the given attributes.
+ * This method is internally used by the find methods.
+ * @param array $attributes attribute values (column name=>column value)
+ * @param boolean $callAfterFind whether to call {@link afterFind} after the record is populated.
+ * @return ARedisRecord the newly created redis record. The class of the object is the same as the model class.
+ * Null is returned if the input data is false.
+ */
+ public function populateRecord($attributes,$callAfterFind=true)
+ {
+ if($attributes!==false)
+ {
+ $record=$this->instantiate($attributes);
+ $record->setScenario('update');
+ $record->init();
+ foreach($attributes as $name=>$value) {
+ if (property_exists($record,$name)) {
+ $record->$name=$value;
+ }
+ }
+ $record->_pk=$record->getPrimaryKey();
+ $record->attachBehaviors($record->behaviors());
+ if($callAfterFind) {
+ $record->afterFind();
+ }
+ return $record;
+ }
+ else {
+ return null;
+ }
+ }
+
+ /**
+ * Creates a list of redis records based on the input data.
+ * This method is internally used by the find methods.
+ * @param array $data list of attribute values for the redis records.
+ * @param boolean $callAfterFind whether to call {@link afterFind} after each record is populated.
+ * @param string $index the name of the attribute whose value will be used as indexes of the query result array.
+ * If null, it means the array will be indexed by zero-based integers.
+ * @return array list of redis records.
+ */
+ public function populateRecords($data,$callAfterFind=true,$index=null)
+ {
+ $records=array();
+ foreach($data as $attributes)
+ {
+ if(($record=$this->populateRecord($attributes,$callAfterFind))!==null)
+ {
+ if($index===null)
+ $records[]=$record;
+ else
+ $records[$record->$index]=$record;
+ }
+ }
+ return $records;
+ }
+
+ /**
+ * Returns the name of the primary key of the associated redis index.
+ * Child classes should override this if the primary key is anything other than "id"
+ * @return mixed the primary key attribute name(s). Defaults to "id"
+ */
+ public function primaryKey()
+ {
+ return "id";
+ }
+ /**
+ * Gets the redis key used when storing the attributes for this model
+ * @param mixed $pk the primary key to create the redis key for
+ * @return string the redis key
+ */
+ public function getRedisKey($pk = null) {
+ if ($pk === null) {
+ $pk = $this->getPrimaryKey();
+ }
+ if (is_array($pk)) {
+ foreach($pk as $key => $value) {
+ $pk[$key] = $key.":".$value;
+ }
+ $pk = implode(":",$pk);
+ }
+ return get_class($this).":".$pk;
+ }
+
+
+ /**
+ * Returns the primary key value.
+ * @return mixed the primary key value. An array (column name=>column value) is returned if the primary key is composite.
+ * If primary key is not defined, null will be returned.
+ */
+ public function getPrimaryKey()
+ {
+ $attribute = $this->primaryKey();
+ if (!is_array($attribute)) {
+ return $this->{$attribute};
+ }
+ $pk = array();
+ foreach($attribute as $field) {
+ $pk[$field] = $this->{$attribute};
+ }
+ return $pk;
+ }
+
+ /**
+ * Sets the primary key value.
+ * After calling this method, the old primary key value can be obtained from {@link oldPrimaryKey}.
+ * @param mixed $value the new primary key value. If the primary key is composite, the new value
+ * should be provided as an array (column name=>column value).
+ */
+ public function setPrimaryKey($value)
+ {
+ $this->_pk=$this->getPrimaryKey();
+ $attribute = $this->primaryKey();
+ if (!is_array($attribute)) {
+ return $this->{$attribute} = $value;
+ }
+ foreach($value as $attribute => $attributeValue) {
+ $this->{$attribute} = $attributeValue;
+ }
+ return $value;
+ }
+ /**
+ * Returns the old primary key value.
+ * This refers to the primary key value that is populated into the record
+ * after executing a find method (e.g. find(), findAll()).
+ * The value remains unchanged even if the primary key attribute is manually assigned with a different value.
+ * @return mixed the old primary key value. An array (column name=>column value) is returned if the primary key is composite.
+ * If primary key is not defined, null will be returned.
+ * @since 1.1.0
+ */
+ public function getOldPrimaryKey()
+ {
+ return $this->_pk;
+ }
+
+ /**
+ * Sets the old primary key value.
+ * @param mixed $value the old primary key value.
+ * @since 1.1.3
+ */
+ public function setOldPrimaryKey($value)
+ {
+ $this->_pk=$value;
+ }
+
+
+ /**
+ * Saves the redis record
+ * @param boolean $runValidation whether to run validation or not, defaults to true
+ * @return boolean whether the save succeeded or not
+ */
+ public function save($runValidation = true) {
+ if ($runValidation && !$this->validate()) {
+ return false;
+ }
+ if (!$this->beforeSave()) {
+ return false;
+ }
+ if ($this->getPrimaryKey() === null) {
+ $count = $this->getRedisSet()->getCount();
+ $this->setPrimaryKey($count);
+ while (!$this->getRedisSet()->add($this->getRedisKey())) {
+ $count++;
+ $this->setPrimaryKey($count); // try again, this is suboptimal, need a better way to avoid collisions
+ }
+ }
+ elseif($this->getIsNewRecord() && !$this->getRedisSet()->add($this->getRedisKey())) {
+ $this->addError($this->primaryKey(),"A record with this id already exists");
+ return false;
+ }
+ $this->getRedisConnection()->getClient()->multi(); // enter transactional mode
+
+ $this->getRedisHash()->clear();
+ foreach($this->attributeNames() as $attribute) {
+ $this->getRedisHash()->add($attribute, $this->{$attribute});
+ }
+ $this->getRedisConnection()->getClient()->exec();
+ $this->afterSave();
+ return true;
+ }
+ /**
+ * Deletes the redis record
+ * @return boolean whether the delete succeeded or not
+ */
+ public function delete() {
+ if (!$this->beforeDelete()){
+ return false;
+ }
+ $this->getRedisSet()->remove($this->getRedisKey());
+ $this->getRedisHash()->clear();
+ $this->afterDelete();
+ return true;
+ }
+
+ /**
+ * Returns the number of records of this type
+ * @return integer the number of rows found
+ */
+ public function count()
+ {
+ Yii::trace(get_class($this).'.count()','packages.redis.ARedisRecord');
+ return $this->getRedisSet()->getCount();
+ }
+
+
+ /**
+ * Finds a single redis record with the specified primary key.
+ * @param mixed $pk primary key value(s). Use array for multiple primary keys. For composite key, each key value must be an array (column name=>column value).
+ * @return ARedisRecord the record found. Null if none is found.
+ */
+ public function findByPk($pk)
+ {
+ Yii::trace(get_class($this).'.findByPk()','packages.redis.ARedisRecord');
+ $this->beforeFind();
+ $hash = new ARedisHash($this->getRedisKey($pk),$this->getRedisConnection());
+ if ($hash->getCount() == 0) {
+ return null;
+ }
+ return $this->populateRecord($hash->toArray(),true);
+
+ }
+
+ /**
+ * Finds multiple redis records with the specified primary keys.
+ * @param array $pks primary key values.
+ * @return ARedisRecord[] the records found.
+ */
+ public function findAllByPk($pks)
+ {
+ Yii::trace(get_class($this).'.findAllByPk()','packages.redis.ARedisRecord');
+ $hashes = array();
+ $redis = $this->getRedisConnection()->getClient()->multi();
+ foreach($pks as $pk) {
+ $key = $this->getRedisKey($pk);
+ $redis->hGetAll($key);
+ }
+ $response = $redis->exec();
+ $rows = array();
+ foreach($response as $row) {
+ if (!$row || !count($row)) {
+ continue;
+ }
+ $rows[] = $row;
+ }
+ return $this->populateRecords($rows,true);
+
+ }
+
+ /**
+ * Returns if the current record is new.
+ * @return boolean whether the record is new and should be inserted when calling {@link save}.
+ * This property is automatically set in constructor and {@link populateRecord}.
+ * Defaults to false, but it will be set to true if the instance is created using
+ * the new operator.
+ */
+ public function getIsNewRecord()
+ {
+ return $this->_new;
+ }
+
+ /**
+ * Sets if the record is new.
+ * @param boolean $value whether the record is new and should be inserted when calling {@link save}.
+ * @see getIsNewRecord
+ */
+ public function setIsNewRecord($value)
+ {
+ $this->_new=$value;
+ }
+
+ /**
+ * This event is raised before the record is saved.
+ * By setting {@link CModelEvent::isValid} to be false, the normal {@link save()} process will be stopped.
+ * @param CModelEvent $event the event parameter
+ */
+ public function onBeforeSave($event)
+ {
+ $this->raiseEvent('onBeforeSave',$event);
+ }
+
+ /**
+ * This event is raised after the record is saved.
+ * @param CEvent $event the event parameter
+ */
+ public function onAfterSave($event)
+ {
+ $this->raiseEvent('onAfterSave',$event);
+ }
+
+ /**
+ * This event is raised before the record is deleted.
+ * By setting {@link CModelEvent::isValid} to be false, the normal {@link delete()} process will be stopped.
+ * @param CModelEvent $event the event parameter
+ */
+ public function onBeforeDelete($event)
+ {
+ $this->raiseEvent('onBeforeDelete',$event);
+ }
+
+ /**
+ * This event is raised after the record is deleted.
+ * @param CEvent $event the event parameter
+ */
+ public function onAfterDelete($event)
+ {
+ $this->raiseEvent('onAfterDelete',$event);
+ }
+
+ /**
+ * This event is raised before a find call.
+ * In this event, the {@link CModelEvent::criteria} property contains the query criteria
+ * passed as parameters to those find methods. If you want to access
+ * the query criteria specified in scopes, please use {@link getDbCriteria()}.
+ * You can modify either criteria to customize them based on needs.
+ * @param CModelEvent $event the event parameter
+ * @see beforeFind
+ */
+ public function onBeforeFind($event)
+ {
+ $this->raiseEvent('onBeforeFind',$event);
+ }
+
+ /**
+ * This event is raised after the record is instantiated by a find method.
+ * @param CEvent $event the event parameter
+ */
+ public function onAfterFind($event)
+ {
+ $this->raiseEvent('onAfterFind',$event);
+ }
+
+ /**
+ * This method is invoked before saving a record (after validation, if any).
+ * The default implementation raises the {@link onBeforeSave} event.
+ * You may override this method to do any preparation work for record saving.
+ * Use {@link isNewRecord} to determine whether the saving is
+ * for inserting or updating record.
+ * Make sure you call the parent implementation so that the event is raised properly.
+ * @return boolean whether the saving should be executed. Defaults to true.
+ */
+ protected function beforeSave()
+ {
+ if($this->hasEventHandler('onBeforeSave'))
+ {
+ $event=new CModelEvent($this);
+ $this->onBeforeSave($event);
+ return $event->isValid;
+ }
+ else
+ return true;
+ }
+
+ /**
+ * This method is invoked after saving a record successfully.
+ * The default implementation raises the {@link onAfterSave} event.
+ * You may override this method to do postprocessing after record saving.
+ * Make sure you call the parent implementation so that the event is raised properly.
+ */
+ protected function afterSave()
+ {
+ if($this->hasEventHandler('onAfterSave'))
+ $this->onAfterSave(new CEvent($this));
+ }
+
+ /**
+ * This method is invoked before deleting a record.
+ * The default implementation raises the {@link onBeforeDelete} event.
+ * You may override this method to do any preparation work for record deletion.
+ * Make sure you call the parent implementation so that the event is raised properly.
+ * @return boolean whether the record should be deleted. Defaults to true.
+ */
+ protected function beforeDelete()
+ {
+ if($this->hasEventHandler('onBeforeDelete'))
+ {
+ $event=new CModelEvent($this);
+ $this->onBeforeDelete($event);
+ return $event->isValid;
+ }
+ else
+ return true;
+ }
+
+ /**
+ * This method is invoked after deleting a record.
+ * The default implementation raises the {@link onAfterDelete} event.
+ * You may override this method to do postprocessing after the record is deleted.
+ * Make sure you call the parent implementation so that the event is raised properly.
+ */
+ protected function afterDelete()
+ {
+ if($this->hasEventHandler('onAfterDelete'))
+ $this->onAfterDelete(new CEvent($this));
+ }
+
+ /**
+ * This method is invoked before a find call.
+ * The find calls include {@link find}, {@link findAll}, {@link findByPk},
+ * {@link findAllByPk}, {@link findByAttributes} and {@link findAllByAttributes}.
+ * The default implementation raises the {@link onBeforeFind} event.
+ * If you override this method, make sure you call the parent implementation
+ * so that the event is raised properly.
+ */
+ protected function beforeFind()
+ {
+ if($this->hasEventHandler('onBeforeFind'))
+ {
+ $event=new CModelEvent($this);
+ // for backward compatibility
+ $event->criteria=func_num_args()>0 ? func_get_arg(0) : null;
+ $this->onBeforeFind($event);
+ }
+ }
+
+ /**
+ * This method is invoked after each record is instantiated by a find method.
+ * The default implementation raises the {@link onAfterFind} event.
+ * You may override this method to do postprocessing after each newly found record is instantiated.
+ * Make sure you call the parent implementation so that the event is raised properly.
+ */
+ protected function afterFind()
+ {
+ if($this->hasEventHandler('onAfterFind'))
+ $this->onAfterFind(new CEvent($this));
+ }
+
+ /**
+ * Calls {@link beforeFind}.
+ * This method is internally used.
+ * @since 1.0.11
+ */
+ public function beforeFindInternal()
+ {
+ $this->beforeFind();
+ }
+
+ /**
+ * Calls {@link afterFind}.
+ * This method is internally used.
+ * @since 1.0.3
+ */
+ public function afterFindInternal()
+ {
+ $this->afterFind();
+ }
+
+ /**
+ * Sets the redis hash to use with this record
+ * @param ARedisIterableHash $redisHash the redis hash
+ */
+ public function setRedisHash($redisHash)
+ {
+ $this->_redisHash = $redisHash;
+ }
+
+ /**
+ * Gets the redis hash to store the attributes for this record in
+ * @return ARedisIterableHash the redis hash
+ */
+ public function getRedisHash()
+ {
+ if ($this->_redisHash === null) {
+ $this->_redisHash = new ARedisHash($this->getRedisKey(), $this->getRedisConnection());
+ }
+ return $this->_redisHash;
+ }
+
+ /**
+ * Sets the redis set that contains the ids of the models of this type
+ * @param ARedisIterableSet $redisSet the redis set
+ */
+ public function setRedisSet($redisSet)
+ {
+ $this->_redisSet = $redisSet;
+ }
+
+ /**
+ * Gets the redis set that contains the ids of the models of this type
+ * @return ARedisIterableSet the redis set
+ */
+ public function getRedisSet()
+ {
+ if ($this->_redisSet === null) {
+ $this->_redisSet = new ARedisSet(get_class($this), $this->getRedisConnection());
+ }
+ return $this->_redisSet;
+ }
+
+
+}
+
329 ARedisSet.php
@@ -0,0 +1,329 @@
+<?php
+
+class ARedisSet extends ARedisIterableEntity {
+ /**
+ * Adds an item to the set
+ * @param mixed $item the item to add
+ * @return boolean true if the item was added, otherwise false
+ */
+ public function add($item) {
+ if (!$this->getConnection()->getClient()->sadd($this->name,$item)) {
+ return false;
+ }
+ $this->_data = null;
+ $this->_count = null;
+ return true;
+ }
+ /**
+ * Removes an item from the set
+ * @param mixed $item the item to remove
+ * @return boolean true if the item was removed, otherwise false
+ */
+ public function remove($item) {
+ if (!$this->getConnection()->getClient()->srem($this->name,$item)) {
+ return false;
+ }
+ $this->_data = null;
+ $this->_count = null;
+ return true;
+ }
+
+ /**
+ * Removes and returns a random item from the set
+ * @return mixed the item that was removed from the set
+ */
+ public function pop() {
+ $member = $this->getConnection()->getClient()->spop($this->name);
+ $this->_data = null;
+ $this->_count = null;
+ return $member;
+ }
+ /**
+ * Gets a random member of the set
+ * @return mixed a random member of the set
+ */
+ public function random() {
+ return $this->getConnection()->getClient()->srandmember($this->name);
+ }
+
+ /**
+ * Gets the difference between this set and the given set(s) and returns it
+ * @param mixed $set, $set2 The sets to compare to, either ARedisSet instances or their names
+ * @return array the difference between this set and the given sets
+ */
+ public function diff($set) {
+ if (is_array($set)) {
+ $parameters = $set;
+ }
+ else {
+ $parameters = func_get_args();
+ }
+ foreach($parameters as $n => $set) {
+ if ($set instanceof ARedisSet) {
+ $parameters[$n] = $set->name;
+ }
+ }
+ array_unshift($parameters,$this->name);
+ return call_user_func_array(array(
+ $this->getConnection()->getClient(),
+ "sdiff"
+ ),$parameters);
+ }
+
+ /**
+ * Gets the difference between this set and the given set(s), stores it in a new set and returns it
+ * @param mixed $set, $set2 The sets to compare to, either ARedisSet instances or their names
+ * @return ARedisSet a set that contains the difference between this set and the given sets
+ */
+ public function diffStore($destination, $set) {
+ if ($destination instanceof ARedisSet) {
+ $destination->_count = null;
+ $destination->_data = null;
+ }
+ else {
+ $destination = new ARedisSet($destination,$this->getConnection());
+ }
+ if (is_array($set)) {
+ $parameters = $set;
+ }
+ else {
+ $parameters = func_get_args();
+ array_shift($parameters);
+ }
+ foreach($parameters as $n => $set) {
+ if ($set instanceof ARedisSet) {
+ $parameters[$n] = $set->name;
+ }
+ }
+
+ array_unshift($parameters,$this->name);
+ array_unshift($parameters, $destination->name);
+ call_user_func_array(array(
+ $this->getConnection()->getClient(),
+ "sdiffstore"
+ ),$parameters);
+ return $destination;
+ }
+
+ /**
+ * Gets the intersection between this set and the given set(s) and returns it
+ * @param mixed $set, $set2 The sets to compare to, either ARedisSet instances or their names
+ * @return array the intersection between this set and the given sets
+ */
+ public function inter($set) {
+ if (is_array($set)) {
+ $parameters = $set;
+ }
+ else {
+ $parameters = func_get_args();
+ }
+ foreach($parameters as $n => $set) {
+ if ($set instanceof ARedisSet) {
+ $parameters[$n] = $set->name;
+ }
+ }
+ array_unshift($parameters,$this->name);
+ return call_user_func_array(array(
+ $this->getConnection()->getClient(),
+ "sinter"
+ ),$parameters);
+ }
+ /**
+ * Gets the intersection between this set and the given set(s), stores it in a new set and returns it
+ * @param mixed $set, $set2 The sets to compare to, either ARedisSet instances or their names
+ * @return ARedisSet a set that contains the intersection between this set and the given sets
+ */
+ public function interStore($destination, $set) {
+ if ($destination instanceof ARedisSet) {
+ $destination->_count = null;
+ $destination->_data = null;
+ }
+ else {
+ $destination = new ARedisSet($destination,$this->getConnection());
+ }
+ if (is_array($set)) {
+ $parameters = $set;
+ }
+ else {
+ $parameters = func_get_args();
+ array_shift($parameters);
+ }
+ foreach($parameters as $n => $set) {
+ if ($set instanceof ARedisSet) {
+ $parameters[$n] = $set->name;
+ }
+ }
+
+ array_unshift($parameters,$this->name);
+ array_unshift($parameters, $destination->name);
+ call_user_func_array(array(
+ $this->getConnection()->getClient(),
+ "sinterstore"
+ ),$parameters);
+ return $destination;
+ }
+
+ /**
+ * Gets the union of this set and the given set(s) and returns it
+ * @param mixed $set, $set2 The sets to compare to, either ARedisSet instances or their names
+ * @return array the union of this set and the given sets
+ */
+ public function union($set) {
+ if (is_array($set)) {
+ $parameters = $set;
+ }
+ else {
+ $parameters = func_get_args();
+ }
+ foreach($parameters as $n => $set) {
+ if ($set instanceof ARedisSet) {
+ $parameters[$n] = $set->name;
+ }
+ }
+ array_unshift($parameters,$this->name);
+ return call_user_func_array(array(
+ $this->getConnection()->getClient(),
+ "sunion"
+ ),$parameters);
+ }
+ /**
+ * Gets the union of this set and the given set(s), stores it in a new set and returns it
+ * @param mixed $set, $set2 The sets to compare to, either ARedisSet instances or their names
+ * @return ARedisSet a set that contains the union of this set and the given sets
+ */
+ public function unionStore($destination, $set) {
+ if ($destination instanceof ARedisSet) {
+ $destination->_count = null;
+ $destination->_data = null;
+ }
+ else {
+ $destination = new ARedisSet($destination,$this->getConnection());
+ }
+ if (is_array($set)) {
+ $parameters = $set;
+ }
+ else {
+ $parameters = func_get_args();
+ array_shift($parameters);
+ }
+ foreach($parameters as $n => $set) {
+ if ($set instanceof ARedisSet) {
+ $parameters[$n] = $set->name;
+ }
+ }
+
+ array_unshift($parameters,$this->name);
+ array_unshift($parameters, $destination->name);
+ call_user_func_array(array(
+ $this->getConnection()->getClient(),
+ "sunionstore"
+ ),$parameters);
+ return $destination;
+ }
+
+ /**
+ * Moves an item from this redis set to another
+ * @param ARedisIterableSet|string $destination the set to move the item to
+ * @param mixed $item the item to move
+ * @return boolean true if the item was moved successfully
+ */
+ public function move($destination, $item) {
+ if ($destination instanceof ARedisSet) {
+ $destination->_count = null;
+ $destination->_data = null;
+ $destination = $destination->name;
+ }
+ $this->_count = null;
+ $this->_data = null;
+ return $this->getConnection()->getClient()->smove($this->name, $destination, $item);
+ }
+
+
+
+ /**
+ * Gets the number of items in the set
+ * @return integer the number of items in the set
+ */
+ public function getCount() {
+ if ($this->_count === null) {
+ $this->_count = $this->getConnection()->getClient()->scard($this->name);
+ }
+ return $this->_count;
+ }
+ /**
+ * Gets all the members in the set
+ * @param boolean $forceRefresh whether to force a refresh or not
+ * @return array the members in the set
+ */
+ public function getData($forceRefresh = false) {
+ if ($forceRefresh || $this->_data === null) {
+ $this->_data = $this->getConnection()->getClient()->smembers($this->name);
+ }
+ return $this->_data;
+ }
+
+ /**
+ * Copies iterable data into the list.
+ * Note, existing data in the list will be cleared first.
+ * @param mixed $data the data to be copied from, must be an array or object implementing Traversable
+ * @throws CException If data is neither an array nor a Traversable.
+ */
+ public function copyFrom($data)
+ {
+ if(is_array($data) || ($data instanceof Traversable))
+ {
+ if($this->_count>0)
+ $this->clear();
+ if($data instanceof CList)
+ $data=$data->_data;
+ foreach($data as $item) {
+ $this->add($item);
+ }
+ }
+ else if($data!==null)
+ throw new CException(Yii::t('yii','List data must be an array or an object implementing Traversable.'));
+ }
+
+ /**
+ * Returns whether there is an item at the specified offset.
+ * This method is required by the interface ArrayAccess.
+ * @param integer $offset the offset to check on
+ * @return boolean
+ */
+ public function offsetExists($offset)
+ {
+ return ($offset>=0 && $offset<$this->getCount());
+ }
+
+ /**
+ * Returns the item at the specified offset.
+ * This method is required by the interface ArrayAccess.
+ * @param integer $offset the offset to retrieve item.
+ * @return mixed the item at the offset
+ */
+ public function offsetGet($offset)