From 48be2bb7d1f01252a57b24e653e067a18651c2b3 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Mon, 13 Jan 2014 12:54:18 +0200 Subject: [PATCH] MongoDB Session class added. --- extensions/mongodb/Session.php | 202 ++++++++++++++++++++++++++ tests/unit/extensions/mongodb/SessionTest.php | 140 ++++++++++++++++++ 2 files changed, 342 insertions(+) create mode 100644 extensions/mongodb/Session.php create mode 100644 tests/unit/extensions/mongodb/SessionTest.php diff --git a/extensions/mongodb/Session.php b/extensions/mongodb/Session.php new file mode 100644 index 0000000..bd06a04 --- /dev/null +++ b/extensions/mongodb/Session.php @@ -0,0 +1,202 @@ + [ + * 'class' => 'yii\mongodb\Session', + * // 'db' => 'mymongodb', + * // 'sessionCollection' => 'my_session', + * ] + * ~~~ + * + * @author Paul Klimov + * @since 2.0 + */ +class Session extends \yii\web\Session +{ + /** + * @var Connection|string the MongoDB connection object or the application component ID of the MongoDB connection. + * After the Session object is created, if you want to change this property, you should only assign it + * with a MongoDB connection object. + */ + public $db = 'mongodb'; + /** + * @var string|array the name of the MongoDB collection that stores the session data. + */ + public $sessionCollection = 'session'; + + /** + * Initializes the Session component. + * This method will initialize the [[db]] property to make sure it refers to a valid MongoDB connection. + * @throws InvalidConfigException if [[db]] is invalid. + */ + public function init() + { + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException($this->className() . "::db must be either a MongoDB connection instance or the application component ID of a MongoDB connection."); + } + parent::init(); + } + + /** + * Returns a value indicating whether to use custom session storage. + * This method overrides the parent implementation and always returns true. + * @return boolean whether to use custom storage. + */ + public function getUseCustomStorage() + { + return true; + } + + /** + * Updates the current session ID with a newly generated one. + * Please refer to for more details. + * @param boolean $deleteOldSession Whether to delete the old associated session file or not. + */ + public function regenerateID($deleteOldSession = false) + { + $oldID = session_id(); + + // if no session is started, there is nothing to regenerate + if (empty($oldID)) { + return; + } + + parent::regenerateID(false); + $newID = session_id(); + + $query = new Query; + $row = $query->from($this->sessionCollection) + ->where(['id' => $oldID]) + ->one($this->db); + if ($row !== false) { + if ($deleteOldSession) { + $this->db->getCollection($this->sessionCollection) + ->update(['_id' => $row['_id']], ['id' => $newID]); + } else { + unset($row['_id']); + $row['id'] = $newID; + $this->db->getCollection($this->sessionCollection) + ->insert($row); + } + } else { + // shouldn't reach here normally + $this->db->getCollection($this->sessionCollection) + ->insert([ + 'id' => $newID, + 'expire' => time() + $this->getTimeout(), + 'data' => '', + ]); + } + } + + /** + * Session read handler. + * Do not call this method directly. + * @param string $id session ID + * @return string the session data + */ + public function readSession($id) + { + $query = new Query; + $row = $query->select(['data']) + ->from($this->sessionCollection) + ->where([ + 'expire' => ['$gt' => time()], + 'id' => $id + ]) + ->one($this->db); + return $row === false ? '' : $row['data']; + } + + /** + * Session write handler. + * Do not call this method directly. + * @param string $id session ID + * @param string $data session data + * @return boolean whether session write is successful + */ + public function writeSession($id, $data) + { + // exception must be caught in session write handler + // http://us.php.net/manual/en/function.session-set-save-handler.php + try { + $expire = time() + $this->getTimeout(); + $query = new Query; + $exists = $query->select(['id']) + ->from($this->sessionCollection) + ->where(['id' => $id]) + ->one($this->db); + if ($exists === false) { + $this->db->getCollection($this->sessionCollection) + ->insert([ + 'id' => $id, + 'data' => $data, + 'expire' => $expire, + ]); + } else { + $this->db->getCollection($this->sessionCollection) + ->update(['id' => $id], ['data' => $data, 'expire' => $expire]); + } + } catch (\Exception $e) { + if (YII_DEBUG) { + echo $e->getMessage(); + } + // it is too late to log an error message here + return false; + } + return true; + } + + /** + * Session destroy handler. + * Do not call this method directly. + * @param string $id session ID + * @return boolean whether session is destroyed successfully + */ + public function destroySession($id) + { + $this->db->getCollection($this->sessionCollection) + ->remove(['id' => $id]); + return true; + } + + /** + * Session GC (garbage collection) handler. + * Do not call this method directly. + * @param integer $maxLifetime the number of seconds after which data will be seen as 'garbage' and cleaned up. + * @return boolean whether session is GCed successfully + */ + public function gcSession($maxLifetime) + { + $this->db->getCollection($this->sessionCollection) + ->remove([ + 'expire' => ['$lt' => time()] + ]); + return true; + } +} \ No newline at end of file diff --git a/tests/unit/extensions/mongodb/SessionTest.php b/tests/unit/extensions/mongodb/SessionTest.php new file mode 100644 index 0000000..ac0d49a --- /dev/null +++ b/tests/unit/extensions/mongodb/SessionTest.php @@ -0,0 +1,140 @@ +dropCollection(static::$sessionCollection); + parent::tearDown(); + } + + /** + * Creates test session instance. + * @return Session session instance. + */ + protected function createSession() + { + return Yii::createObject([ + 'class' => Session::className(), + 'db' => $this->getConnection(), + 'sessionCollection' => static::$sessionCollection, + ]); + } + + // Tests: + + public function testWriteSession() + { + $session = $this->createSession(); + + $id = uniqid(); + $data = [ + 'name' => 'value' + ]; + $dataSerialized = serialize($data); + $this->assertTrue($session->writeSession($id, $dataSerialized), 'Unable to write session!'); + + $collection = $session->db->getCollection($session->sessionCollection); + $rows = $this->findAll($collection); + $this->assertCount(1, $rows, 'No session record!'); + + $row = array_shift($rows); + $this->assertEquals($id, $row['id'], 'Wrong session id!'); + $this->assertEquals($dataSerialized, $row['data'], 'Wrong session data!'); + $this->assertTrue($row['expire'] > time(), 'Wrong session expire!'); + + $newData = [ + 'name' => 'new value' + ]; + $newDataSerialized = serialize($newData); + $this->assertTrue($session->writeSession($id, $newDataSerialized), 'Unable to update session!'); + + $rows = $this->findAll($collection); + $this->assertCount(1, $rows, 'Wrong session records after update!'); + $newRow = array_shift($rows); + $this->assertEquals($id, $newRow['id'], 'Wrong session id after update!'); + $this->assertEquals($newDataSerialized, $newRow['data'], 'Wrong session data after update!'); + $this->assertTrue($newRow['expire'] >= $row['expire'], 'Wrong session expire after update!'); + } + + /** + * @depends testWriteSession + */ + public function testDestroySession() + { + $session = $this->createSession(); + + $id = uniqid(); + $data = [ + 'name' => 'value' + ]; + $dataSerialized = serialize($data); + $session->writeSession($id, $dataSerialized); + + $this->assertTrue($session->destroySession($id), 'Unable to destroy session!'); + + $collection = $session->db->getCollection($session->sessionCollection); + $rows = $this->findAll($collection); + $this->assertEmpty($rows, 'Session record not deleted!'); + } + + /** + * @depends testWriteSession + */ + public function testReadSession() + { + $session = $this->createSession(); + + $id = uniqid(); + $data = [ + 'name' => 'value' + ]; + $dataSerialized = serialize($data); + $session->writeSession($id, $dataSerialized); + + $sessionData = $session->readSession($id); + $this->assertEquals($dataSerialized, $sessionData, 'Unable to read session!'); + + $collection = $session->db->getCollection($session->sessionCollection); + list($row) = $this->findAll($collection); + $newRow = $row; + $newRow['expire'] = time() - 1; + unset($newRow['_id']); + $collection->update(['_id' => $row['_id']], $newRow); + + $sessionData = $session->readSession($id); + $this->assertEquals('', $sessionData, 'Expired session read!'); + } + + public function testGcSession() + { + $session = $this->createSession(); + $collection = $session->db->getCollection($session->sessionCollection); + $collection->batchInsert([ + [ + 'id' => uniqid(), + 'expire' => time() + 10, + 'data' => 'actual', + ], + [ + 'id' => uniqid(), + 'expire' => time() - 10, + 'data' => 'expired', + ], + ]); + $this->assertTrue($session->gcSession(10), 'Unable to collection garbage session!'); + + $rows = $this->findAll($collection); + $this->assertCount(1, $rows, 'Wrong records count!'); + } +} \ No newline at end of file