diff --git a/framework/web/DbSession.php b/framework/web/DbSession.php index d567b9a700..56a9234e8c 100644 --- a/framework/web/DbSession.php +++ b/framework/web/DbSession.php @@ -30,12 +30,13 @@ use yii\di\Instance; * ] * ~~~ * - * @property boolean $useCustomStorage Whether to use custom storage. This property is read-only. + * DbSession extends [[MultiFieldSession]], thus it allows saving extra fields into the [[sessionTable]]. + * Refer to [[MultiFieldSession]] for more details. * * @author Qiang Xue * @since 2.0 */ -class DbSession extends Session +class DbSession extends MultiFieldSession { /** * @var Connection|array|string the DB connection object or the application component ID of the DB connection. @@ -85,16 +86,6 @@ class DbSession extends Session $this->db = Instance::ensure($this->db, Connection::className()); } - /** - * 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. @@ -112,7 +103,7 @@ class DbSession extends Session parent::regenerateID(false); $newID = session_id(); - $query = new Query; + $query = new Query(); $row = $query->from($this->sessionTable) ->where(['id' => $oldID]) ->createCommand($this->db) @@ -131,10 +122,8 @@ class DbSession extends Session } else { // shouldn't reach here normally $this->db->createCommand() - ->insert($this->sessionTable, [ - 'id' => $newID, - 'expire' => time() + $this->getTimeout(), - ])->execute(); + ->insert($this->sessionTable, $this->composeFields($newID, '')) + ->execute(); } } @@ -146,13 +135,16 @@ class DbSession extends Session */ public function readSession($id) { - $query = new Query; - $data = $query->select(['data']) - ->from($this->sessionTable) - ->where('[[expire]]>:expire AND [[id]]=:id', [':expire' => time(), ':id' => $id]) - ->createCommand($this->db) - ->queryScalar(); + $query = new Query(); + $query->from($this->sessionTable) + ->where('[[expire]]>:expire AND [[id]]=:id', [':expire' => time(), ':id' => $id]); + if (isset($this->readCallback)) { + $fields = $query->one($this->db); + return $fields === false ? '' : $this->extractData($fields); + } + + $data = $query->select(['data'])->scalar($this->db); return $data === false ? '' : $data; } @@ -168,23 +160,21 @@ class DbSession extends Session // 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->sessionTable) ->where(['id' => $id]) ->createCommand($this->db) ->queryScalar(); + $fields = $this->composeFields($id, $data); if ($exists === false) { $this->db->createCommand() - ->insert($this->sessionTable, [ - 'id' => $id, - 'data' => $data, - 'expire' => $expire, - ])->execute(); + ->insert($this->sessionTable, $fields) + ->execute(); } else { + unset($fields['id']); $this->db->createCommand() - ->update($this->sessionTable, ['data' => $data, 'expire' => $expire], ['id' => $id]) + ->update($this->sessionTable, $fields, ['id' => $id]) ->execute(); } } catch (\Exception $e) { diff --git a/framework/web/MultiFieldSession.php b/framework/web/MultiFieldSession.php new file mode 100644 index 0000000000..760b919b9d --- /dev/null +++ b/framework/web/MultiFieldSession.php @@ -0,0 +1,140 @@ + + * @since 2.0.5 + */ +abstract class MultiFieldSession extends Session +{ + /** + * @var callable a callback that will be called during session data reading. + * The signature of the callback should be as follows: + * + * ``` + * function ($fields) + * ``` + * + * where `$fields` is the storage field set for read session and `$session` is this session instance. + * If callback returns an array, it will be merged into the session data. + * + * For example: + * + * ```php + * function ($fields) { + * return [ + * 'expireDate' => Yii::$app->formatter->asDate($fields['expire']), + * ]; + * } + * ``` + */ + public $readCallback; + /** + * @var callable a callback that will be called during session data writing. + * The signature of the callback should be as follows: + * + * ``` + * function ($session) + * ``` + * + * where `$session` is this session instance, this variable can be used to retrieve session data. + * Callback should return the actual fields set, which should be saved into the session storage. + * + * For example: + * + * ```php + * function ($session) { + * return [ + * 'user_id' => Yii::$app->user->id, + * 'ip' => $_SERVER['REMOTE_ADDR'], + * 'is_trusted' => $session->get('is_trusted', false), + * ]; + * } + * ``` + */ + public $writeCallback; + + + /** + * 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; + } + + /** + * Composes storage field set for session writing. + * @param string $id session id + * @param string $data session data + * @return array storage fields + */ + protected function composeFields($id, $data) + { + $fields = [ + 'data' => $data, + ]; + if (isset($this->writeCallback)) { + $fields = array_merge( + $fields, + call_user_func($this->writeCallback, $this) + ); + if (!is_string($fields['data'])) { + $_SESSION = $fields['data']; + $fields['data'] = session_encode(); + } + } + // ensure 'id' and 'expire' are never affected by [[writeCallback]] + $fields = array_merge($fields, [ + 'id' => $id, + 'expire' => time() + $this->getTimeout(), + ]); + return $fields; + } + + /** + * Extracts session data from storage field set. + * @param array $fields storage fields. + * @return string session data. + */ + protected function extractData($fields) + { + if (isset($this->readCallback)) { + if (!isset($fields['data'])) { + $fields['data'] = ''; + } + $extraData = call_user_func($this->readCallback, $fields); + if (!empty($extraData)) { + session_decode($fields['data']); + $_SESSION = array_merge((array)$_SESSION, (array)$extraData); + return session_encode(); + } + return $fields['data']; + } else { + return isset($fields['data']) ? $fields['data'] : ''; + } + } +} \ No newline at end of file