├── README.md └── redis_session.php /README.md: -------------------------------------------------------------------------------- 1 | 2 | ### ATTENTION: 3 | 4 | Actually it is easier to just use the **Redis PECL extension** and leave **CakePHP** `Session.save` at `php`, 5 | which is the default. Just install the extension with: 6 | 7 | ``` 8 | sudo pecl install redis 9 | ``` 10 | 11 | Provided everything compiled fine, follow the instructions to load the extension and then 12 | just edit your `php.ini`. 13 | 14 | ```example 15 | session.save_handler = redis 16 | session.save_path = "tcp://192.168.0.1:6379" 17 | ``` 18 | 19 | Now restart your webserver and refresh your CakePHP app. It should work out of the box. 20 | 21 | To verify that it works you can use the `redis-cli`. When you are at the redis prompt 22 | type `monitor` and leave the window open. Refresh again and you should see the GET and 23 | SETEX calls managing the session. 24 | 25 | That is the same thing this wrapper would do. 26 | 27 | Hope this works for you. 28 | 29 | ---- 30 | 31 | # Redis Session Store for CakePHP 32 | 33 | This class can be used in [CakePHP](http://cakephp.org) and saves 34 | session data into [Redis](http://redis.io), an open source key-value store. 35 | 36 | ## Installation 37 | 38 | 1. Place the ```redis_session.php``` into ```[yourapp]/config/```. 39 | 2. Open ```[yourapp]/config/core.php``` and find ```'Session.save'```. 40 | 3. Change the value of the Configure call so that it looks like this: 41 | 42 | ```Configure::write('Session.save', 'redis_session');``` 43 | 44 | That's it. 45 | 46 | ## Usage 47 | 48 | ### Redis on localhost 49 | 50 | If you have a default Redis installation running on localhost you don't 51 | need to change anything. It will connect and take over. 52 | 53 | To confirm that it does indeed write to Redis you can use ```redis-cli monitor``` 54 | and refresh your browser. You should see some GET and SETEX calls. 55 | 56 | ### Redis on a foreign host 57 | 58 | If you run on a different host you have to add two config settings 59 | in your ```core.php``` to setup the connection. 60 | 61 | Configure::write('RedisSession.hostname', 'some.host.name'); 62 | Configure::write('RedisSession.port', 1337); 63 | 64 | ## About 65 | 66 | I've ran the core component tests with this store enabled (CakePHP 1.3.10). 67 | 68 | The class comes with [iRedis](https://github.com/dhorrigan/iRedis), a very 69 | lightweight Redis Library by Dan Horrigan. The library is embedded inside 70 | the source file, wrapped in a class_exists() condition. If you want to use 71 | your own version make sure to include it early or just delete the iRedis 72 | block. I leave that up to you. I wanted to keep this store small. 73 | 74 | Garbage collection is handled by Redis. I use SETEX (expire) when writing 75 | to the store. The key will delete itself when the session expires. You can 76 | verify the remaining time with ```TTL [key]```. 77 | 78 | I have no complex key-namespacing going on. The key however is prefixed with 79 | the session name ('Session.cookie' value) followed by the session_id() 80 | 81 | The cookie.path is hardcoded to '/' 82 | 83 | Enjoy! 84 | -------------------------------------------------------------------------------- /redis_session.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT License 8 | * @copyright 2011-2013, Kjell Bublitz 9 | * @package redis_session 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a copy 12 | * of this software and associated documentation files (the "Software"), to deal 13 | * in the Software without restriction, including without limitation the rights 14 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | * copies of the Software, and to permit persons to whom the Software is 16 | * furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included in 19 | * all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | * THE SOFTWARE. 28 | */ 29 | 30 | if (!class_exists('RedisSession')){ 31 | /** 32 | * Redis Session Store Class 33 | */ 34 | class RedisSession extends Object { 35 | 36 | static $store; 37 | static $timeout; 38 | static $prefix; 39 | 40 | public static function init() { 41 | $name = Configure::read('Session.cookie'); 42 | $level = Configure::read('Security.level'); 43 | $timeout = Configure::read('Session.timeout'); 44 | 45 | self::$timeout = $timeout * Security::inactiveMins(); 46 | self::$prefix = $name; 47 | 48 | if ($level == 'high') { 49 | $cookieLifeTime = 0; 50 | } else { 51 | $cookieLifeTime = Configure::read('Session.timeout') * (Security::inactiveMins() * 60); 52 | } 53 | 54 | if (empty($_SESSION) && function_exists('ini_set')) { 55 | ini_set('session.use_trans_sid', 0); 56 | ini_set('url_rewriter.tags', ''); 57 | ini_set('session.save_handler', 'user'); 58 | ini_set('session.serialize_handler', 'php'); 59 | ini_set('session.use_cookies', 1); 60 | ini_set('session.name', $name); 61 | ini_set('session.cookie_lifetime', $cookieLifeTime); 62 | ini_set('session.cookie_path', '/'); 63 | ini_set('session.auto_start', 0); 64 | } 65 | 66 | session_set_save_handler( 67 | array('RedisSession', '__open'), 68 | array('RedisSession', '__close'), 69 | array('RedisSession', '__read'), 70 | array('RedisSession', '__write'), 71 | array('RedisSession', '__destroy'), 72 | array('RedisSession', '__gc') 73 | ); 74 | 75 | } 76 | 77 | /** 78 | * OPEN 79 | * - Connect to Redis 80 | * - Calculate and set timeout for SETEX 81 | * - Set session_name as key prefix 82 | */ 83 | public static function __open($path, $name) { 84 | $hostname = Configure::read('RedisSession.hostname'); 85 | $port = Configure::read('RedisSession.port'); 86 | 87 | if ($hostname !== null && $port !== null) { 88 | $redis = new iRedisForRedisSession(compact('hostname', 'port')); 89 | } else { 90 | $redis = new iRedisForRedisSession(); 91 | } 92 | 93 | self::$store = $redis; 94 | } 95 | 96 | /** 97 | * CLOSE 98 | * - Disconnect from Redis 99 | */ 100 | public static function __close() { 101 | self::$store->disconnect(); 102 | return true; 103 | } 104 | 105 | /** 106 | * READ 107 | * - Make key from session_id and prefix 108 | * - Return whatever is stored in key 109 | */ 110 | public static function __read($id) { 111 | $key = self::$prefix. '_' . $id; 112 | return self::$store->get($key); 113 | } 114 | 115 | /** 116 | * WRITE 117 | * - Make key from session_id and prefix 118 | * - SETEX data with timeout calculated in open() 119 | */ 120 | public static function __write($id, $data) { 121 | $key = self::$prefix. '_' . $id; 122 | self::$store->setex($key, self::$timeout, $data); 123 | return true; 124 | } 125 | 126 | /** 127 | * DESTROY 128 | * - Make key from session_id and prefix 129 | * - DEL the key from store 130 | */ 131 | public static function __destroy($id) { 132 | $key = self::$prefix. '_' . $id; 133 | self::$store->del($key); 134 | return true; 135 | } 136 | 137 | /** 138 | * GARBAGE COLLECTION 139 | * not needed as SETEX automatically removes itself after timeout 140 | * ie. works like a cookie 141 | */ 142 | public static function __gc() { 143 | return true; 144 | } 145 | 146 | }// enddef RedisSession 147 | }// endif class-exists RedisSession 148 | 149 | if (!class_exists('iRedis')){ 150 | 151 | /** 152 | * iRedis 153 | * 154 | * @package iRedis 155 | * @version 1.0 156 | * @author Dan Horrigan 157 | * @license MIT License 158 | * @copyright 2010 Dan Horrigan 159 | * 160 | * Permission is hereby granted, free of charge, to any person obtaining a copy 161 | * of this software and associated documentation files (the "Software"), to deal 162 | * in the Software without restriction, including without limitation the rights 163 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 164 | * copies of the Software, and to permit persons to whom the Software is 165 | * furnished to do so, subject to the following conditions: 166 | * 167 | * The above copyright notice and this permission notice shall be included in 168 | * all copies or substantial portions of the Software. 169 | * 170 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 171 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 172 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 173 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 174 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 175 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 176 | * THE SOFTWARE. 177 | */ 178 | 179 | /** 180 | * This code is closely related to Redisent, a Redis interface for the modest. 181 | * Some code is from Redisent and has the following copyrights: 182 | * 183 | * @author Justin Poliey 184 | * @copyright 2009 Justin Poliey 185 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 186 | */ 187 | 188 | if ( ! defined('CRLF')) 189 | { 190 | define('CRLF', sprintf('%s%s', chr(13), chr(10))); 191 | } 192 | 193 | class iRedis { 194 | 195 | const ERROR = '-'; 196 | const INLINE = '+'; 197 | const BULK = '$'; 198 | const MULTIBULK = '*'; 199 | const INTEGER = ':'; 200 | 201 | protected $connection = false; 202 | 203 | /** 204 | * Open the connection to the Redis server. 205 | * 206 | * @param array $config the config array 207 | * @throws RedisException 208 | */ 209 | public function __construct(array $config = array()) 210 | { 211 | $config = array_merge(array('hostname' => 'localhost', 'port' => 6379), $config); 212 | $this->connection = @fsockopen($config['hostname'], $config['port'], $errno, $errstr); 213 | 214 | if ( ! $this->connection) 215 | { 216 | throw new RedisException($errstr, $errno); 217 | } 218 | } 219 | 220 | /** 221 | * Closes the connection to the Redis server. 222 | */ 223 | public function __destruct() 224 | { 225 | fclose($this->connection); 226 | } 227 | 228 | /** 229 | * Runs the given command ($name) with any number of arguments. 230 | * 231 | * @param string $name the method (command) 232 | * @param array $args the method (command) arguments 233 | * @return string the response 234 | * @throws RedisException 235 | */ 236 | public function __call($name, $args) 237 | { 238 | $cmd = $this->buildCommand($name, $args); 239 | $this->sendCommand($cmd); 240 | 241 | return $this->readReply(); 242 | } 243 | 244 | /** 245 | * Builds the given command with any number of arguments. 246 | * 247 | * @param string $cmd the command 248 | * @param array $args the arguments 249 | * @return string the full command 250 | */ 251 | public function buildCommand($cmd, $args) 252 | { 253 | // Start building the command 254 | $command = '*'.(count($args) + 1).CRLF; 255 | $command .= '$'.strlen($cmd).CRLF; 256 | $command .= strtoupper($cmd).CRLF; 257 | 258 | // Add all the arguments to the command 259 | foreach ($args as $arg) 260 | { 261 | $command .= '$'.strlen($arg).CRLF; 262 | $command .= $arg.CRLF; 263 | } 264 | 265 | return $command; 266 | } 267 | 268 | /** 269 | * Sends the given command to the Redis server. 270 | * 271 | * @param string $command the command to send 272 | * @throws RedisException 273 | */ 274 | public function sendCommand($command) 275 | { 276 | if ( ! $this->connection) 277 | { 278 | throw new RedisException('You must be connected to a Redis server to send a command.'); 279 | } 280 | 281 | fwrite($this->connection, $command); 282 | } 283 | 284 | /** 285 | * Reads in a reply from the Redis server. 286 | * 287 | * @return string the reply 288 | * @throws RedisException 289 | */ 290 | public function readReply() 291 | { 292 | if ( ! $this->connection) 293 | { 294 | throw new RedisException('You must be connected to a Redis server to send a command.'); 295 | } 296 | 297 | $reply = trim(fgets($this->connection, 512)); 298 | 299 | switch (substr($reply, 0, 1)) 300 | { 301 | case iRedis::ERROR: 302 | throw new RedisException(substr(trim($reply), 4)); 303 | break; 304 | 305 | case iRedis::INLINE: 306 | $response = substr(trim($reply), 1); 307 | break; 308 | 309 | case iRedis::BULK: 310 | if ($reply == '$-1') 311 | { 312 | return null; 313 | } 314 | $response = $this->readBulkReply($reply); 315 | break; 316 | 317 | case iRedis::MULTIBULK: 318 | $count = substr($reply, 1); 319 | if ($count == '-1') 320 | { 321 | return null; 322 | } 323 | 324 | $response = array(); 325 | for ($i = 0; $i < $count; $i++) 326 | { 327 | $bulk_head = trim(fgets($this->connection, 512)); 328 | $response[] = $this->readBulkReply($bulk_head); 329 | } 330 | break; 331 | 332 | case iRedis::INTEGER: 333 | $response = substr(trim($reply), 1); 334 | break; 335 | 336 | default: 337 | throw new RedisException("invalid server response: {$reply}"); 338 | break; 339 | } 340 | 341 | return $response; 342 | } 343 | 344 | /** 345 | * Reads in a bulk reply from the Redis server. 346 | * 347 | * @param string $reply the reply 348 | * @return string the bulk reply 349 | * @throws RedisException 350 | */ 351 | protected function readBulkReply($reply) 352 | { 353 | if ( ! $this->connection) 354 | { 355 | throw new RedisException('You must be connected to a Redis server to send a command.'); 356 | } 357 | 358 | $response = null; 359 | 360 | $read = 0; 361 | $size = substr($reply, 1); 362 | 363 | while ($read < $size) 364 | { 365 | // If the amount left to read is less than 1024 then just read the rest, else read 1024 366 | $block_size = ($size - $read) > 1024 ? 1024 : ($size - $read); 367 | $response .= fread($this->connection, $block_size); 368 | $read += $block_size; 369 | } 370 | // Get rid of the CRLF at the end 371 | fread($this->connection, 2); 372 | 373 | return $response; 374 | } 375 | } 376 | 377 | if (!class_exists('RedisException')){ 378 | class RedisException extends Exception { } 379 | } 380 | 381 | } // endif class-exists iRedis 382 | 383 | /** 384 | * Redis Session Store for CakePHP 385 | * 386 | * iRedisForRedisSession 387 | * Subclassed iRedis to change __destruct behavior. 388 | * 389 | * @package redis_session 390 | * @subpackage redis_session.support 391 | */ 392 | if (!class_exists('iRedisForRedisSession')){ 393 | class iRedisForRedisSession extends iRedis { 394 | function __destruct() { 395 | // don't disconnect yet 396 | } 397 | function disconnect() { 398 | parent::__destruct(); 399 | } 400 | } 401 | } 402 | 403 | // Setup RedisSession 404 | RedisSession::init(); 405 | ?> --------------------------------------------------------------------------------