diff --git a/composer.json b/composer.json index aafa267..9ae84d0 100644 --- a/composer.json +++ b/composer.json @@ -28,12 +28,12 @@ }, "autoload": { "psr-4": { - "mirzaev\\yii2\\arangodb\\sessions\\": "mirzaev/yii2-arangodb-sessions" + "mirzaev\\yii2\\arangodb\\sessions\\": "mirzaev/yii2/arangodb/sessions" } }, "autoload-dev": { "psr-4": { - "mirzaev\\yii2\\arangodb\\sessions\\tests\\": "mirzaev/yii2-arangodb-sessions/tests" + "mirzaev\\yii2\\arangodb\\sessions\\tests\\": "mirzaev/yii2/arangodb/sessions/tests" } } } \ No newline at end of file diff --git a/mirzaev/yii2-arangodb-sessions/ArangoDbSession.php b/mirzaev/yii2-arangodb-sessions/ArangoDbSession.php deleted file mode 100644 index ff1cc18..0000000 --- a/mirzaev/yii2-arangodb-sessions/ArangoDbSession.php +++ /dev/null @@ -1,288 +0,0 @@ - - */ -final class ArangoDbSession extends DbSession { - - /** - * Идентификатор компонента (Component ID) - */ - public Connection|array|string $db = 'arangodb'; - - /** - * Название документа для хранения данных сеанса (Document name for storing session data) - */ - public string $sessions = 'sessions'; - - /** - * Буфер данных для записи в документ сессий (Data buffer for write to session document) - */ - protected array $buffer = []; - - - /** - * Инициализация компонента (Component initialization) - * - * @throws InvalidConfigException if $this->db is invalid. - */ - public function init(): self - { - parent::init(); - - return $this->db = Instance::ensure($this->db, Connection::class); - } - - /** - * Открыть сессию (Open session) - * - * @param string $path session save path - * @param string $name session name - * - * @return bool Статус открытия сессии (Session opening status) - * - * @internal Do not call this method directly - */ - public function openSession($path, $name): bool - // public function openSession(string $save, string $session_name): bool - { - if ($this->getUseStrictMode()) { - $id = $this->getId(); - if (!$this->getReadQuery($id)->exists()) { - // This session id does not exist, mark it for forced regeneration - $this->_forceRegenerateId = $id; - } - } - - return parent::openSession($path, $name); - } - - /** - * Метод не создаст эффект пока сессия неактивна (getIsActive()). Убедитесь, что открыли (open()) перед вызовом - * This method has no effect when session is not [[getIsActive()|active]]. Make sure to call [[open()]] before calling it. - * - * @param bool $delete Удалить прошлую сессию (Delete old session) - * - * @see $this->open() - * @see $this->getIsActive() - * @see https://secure.php.net/session_regenerate_id - */ - public function regenerateID($delete = false): void - // public function regenerateID(bool $delete = false): void - { - $oldID = session_id(); - - // if no session is started, there is nothing to regenerate - if (empty($oldID)) { - return; - } - - parent::regenerateID(false); - - $newID = session_id(); - - // if session id regeneration failed, no need to create/update it. - if (empty($newID)) { - Yii::warning('Failed to generate new session ID', __METHOD__); - return; - } - - $row = $this->db->useMaster(function() use ($oldID) { - return (new Query())->from($this->sessionTable) - ->where(['id' => $oldID]) - ->createCommand($this->db) - ->queryOne(); - }); - - if ($row !== false) { - if ($delete) { - $this->db->createCommand() - ->update($this->sessionTable, ['id' => $newID], ['id' => $oldID]) - ->execute(); - } else { - $row['id'] = $newID; - $this->db->createCommand() - ->insert($this->sessionTable, $row) - ->execute(); - } - } else { - // shouldn't reach here normally - $this->db->createCommand() - ->insert($this->sessionTable, $this->composeFields($newID, '')) - ->execute(); - } - } - - /** - * Закрыть сессию (Close session) - * - * Закрывает текущую сессию и сохраняет данные - * Ends the current session and store session data - */ - public function close(): void - { - if ($this->getIsActive()) { - // prepare writeCallback fields before session closes - $this->fields = $this->composeFields(); - YII_DEBUG ? session_write_close() : @session_write_close(); - } - } - - /** - * Прочитать сессию (Read session) - * - * @param string $id Идентификатор сессии (Session ID) - * - * @return string Данные сессии (The session data) - * - * @internal Do not call this method directly - */ - public function readSession($id): string - // public function readSession(string $id): string - { - $query = $this->getReadQuery($id); - - if ($this->readCallback !== null) { - $fields = $query->one($this->db); - return $fields === false ? '' : $this->extractData($fields); - } - - $data = $query->select(['data'])->scalar($this->db); - return $data === false ? '' : $data; - } - - /** - * Записать сессию (Write session) - * - * @param string $id Идентификатор сессии (Session ID) - * @param string $data Данные сессии (Session data) - * - * @return bool Статус записи сессии (Session writing status) - * - * @internal Do not call this method directly - */ - public function writeSession($id, $data): bool - // public function writeSession(string $id, string $data): bool - { - if ($this->getUseStrictMode() && $id === $this->_forceRegenerateId) { - //Ignore write when forceRegenerate is active for this id - return true; - } - - // exception must be caught in session write handler - // https://secure.php.net/manual/en/function.session-set-save-handler.php#refsect1-function.session-set-save-handler-notes - try { - // ensure backwards compatability (fixed #9438) - if ($this->writeCallback && !$this->fields) { - $this->fields = $this->composeFields(); - } - // ensure data consistency - if (!isset($this->fields['data'])) { - $this->fields['data'] = $data; - } else { - $_SESSION = $this->fields['data']; - } - // ensure 'id' and 'expire' are never affected by [[writeCallback]] - $this->fields = array_merge($this->fields, [ - 'id' => $id, - 'expire' => time() + $this->getTimeout(), - ]); - $this->fields = $this->typecastFields($this->fields); - $this->db->createCommand()->upsert($this->sessionTable, $this->fields)->execute(); - $this->fields = []; - } catch (Exception $e) { - Yii::$app->errorHandler->handleException($e); - return false; - } - - return true; - } - - /** - * Удалить сессию (Delete session) - * - * @param string $id Идентификатор сессии (Session ID) - * - * @return bool Статус удаления сессии (Session deleting status) - * - * @internal Do not call this method directly - */ - public function destroySession($id) - { - $this->db->createCommand() - ->delete($this->sessionTable, ['id' => $id]) - ->execute(); - - return true; - } - - /** - * Удалить неиспользуемые данные (Delete garbage) - *. - * @param int $max Периодичность очистки в секундах (Cleaning frequency in seconds) - * - * @return bool Статус очистки неиспользуемых данных сессии (Session garbage deleting status) - * - * @internal Do not call this method directly - */ - public function gcSession($max): bool - // public function gcSession(int $max): bool - { - $this->db->createCommand() - ->delete($this->sessionTable, '[[expire]]<:expire', [':expire' => time()]) - ->execute(); - - return true; - } - - /** - * Генерация запроса для чтения сеанса (Generating a query to read a session) - * - * @param string $id Идентификатор сессии (Session ID) - */ - protected function getReadQuery($id): Query - // protected function getReadQuery(string $id): Query - { - return (new Query()) - ->from($this->sessionTable) - ->where('[[expire]]>:expire AND [[id]]=:id', [':expire' => time(), ':id' => $id]); - } - - /** - * Конвертация для отправки в PDO (Convertation for sending to PDO) - * - * Default implementation casts field `data` to `\PDO::PARAM_LOB`. - * You can override this method in case you need special type casting. - * - * @param array $data Данные для конвертации (Data for convertation) [name => value] - */ - protected function typecastFields($data): array - // protected function typecastFields(array $data): array - { - // if (isset($data['data']) && !is_array($data['data']) && !is_object($data['data'])) { - // $data['data'] = new PdoValue($data['data'], \PDO::PARAM_LOB); - // } - - // return $data; - - return []; - } -} \ No newline at end of file diff --git a/mirzaev/yii2/arangodb/sessions/ArangoDbSession.php b/mirzaev/yii2/arangodb/sessions/ArangoDbSession.php new file mode 100644 index 0000000..ffeec20 --- /dev/null +++ b/mirzaev/yii2/arangodb/sessions/ArangoDbSession.php @@ -0,0 +1,381 @@ +gcSession() Do not call this method directly + * @uses $this->openSession() Do not call this method directly + * @uses $this->readSession() Do not call this method directly + * @uses $this->writeSession() Do not call this method directly + * @uses $this->destroySession() Do not call this method directly + * + * @see yii\web\DbSession Наследует, копирует (Inherit, copy) + * + * @package yii2\ArangoDB + * @author Arsen Mirzaev Tatyano-Muradovich + */ +final class ArangoDbSession extends MultiFieldSession +{ + + /** + * Идентификатор компонента (Component ID) + */ + public Connection|array|string $database = 'arangodb'; + + /** + * Название документа для хранения данных сеанса (Document name for storing session data) + */ + public string $document = 'sessions'; + + /** + * Буфер данных для записи в документ сессии (Data buffer for write to session document) + */ + protected array $buffer = []; + + + /** + * Инициализация компонента (Component initialization) + * + * @throws InvalidConfigException if $this->database is invalid. + */ + public function init(): Connection + { + return $this->database = Instance::ensure($this->database, Connection::class); + } + + /** + * Открыть сессию (Open session) + * + * @param string $path Путь для сохранения сессии (Path to save session) + * @param string $name Имя сессии (Session name) + * + * @return bool Статус открытия сессии (Session opening status) + */ + public function openSession($path, $name): bool + // public function openSession(string $path, string $name): bool + { + if ($this->getUseStrictMode()) { + + $id = $this->getId(); + + if (!$this->getReadQuery($id)->exists()) { + // This session id does not exist, mark it for forced regeneration + $this->_forceRegenerateId = $id; + } + } + + return parent::openSession($path, $name); + } + + /** + * Закрыть сессию (Close session) + * + * Закрывает текущую сессию и сохраняет данные + * Ends the current session and store session data + */ + public function close(): void + { + if ($this->getIsActive()) { + // prepare writeCallback fields before session closes + + $this->buffer = $this->composeFields(); + + YII_DEBUG ? session_write_close() : @session_write_close(); + } + } + + /** + * Обновление сессии (Update session) + * + * Метод не создаст эффект пока сессия неактивна (getIsActive()). Убедитесь, что открыли (open()) перед вызовом + * This method has no effect when session is not [[getIsActive()|active]]. Make sure to call [[open()]] before calling it. + * + * @param bool $rewrite Перезаписать прошлую сессию (Rewrite old session) + * + * @see $this->open() + * @see $this->getIsActive() + * + * @tutorial https://secure.php.net/session_regenerate_id + */ + public function regenerateID($rewrite = false): void + // public function regenerateID(bool $rewrite = false): void + { + // Инициализация + $oldID = session_id(); + + if (empty($oldID)) { + // if no session is started, there is nothing to regenerate + + return; + } + + // parent::regenerateID(false); + + $newID = session_id(); + + // if session id regeneration failed, no need to create/update it. + if (empty($newID)) { + yii::warning('Failed to generate new session ID', __METHOD__); + + return; + } + + $row = (new Query()) + ->in($this->document) + ->where(['hash' => $oldID]) + ->one($this->database); + + if ($row) { + if ($rewrite) { + // Обновить + + (new Query()) + ->in($this->document) + ->where(['hash' => $newID]) + ->update(['hash' => $oldID], db: $this->database); + } else { + // Создать + + (new Query()) + ->in($this->document) + ->insert(['hash' => $newID], db: $this->database); + } + } else { + // shouldn't reach here normally + (new Query()) + ->in($this->document) + ->insert($this->composeFields($newID, ''), db: $this->database); + } + } + + /** + * Прочитать сессию (Read session) + * + * @param string $id Идентификатор сессии (Session ID) + * + * @return string Данные сессии (The session data) + */ + public function readSession($id): string + // public function readSession(string $id): string + { + $query = $this->getReadQuery($id); + + + if ($this->readCallback !== null) { + $data = $query->one($this->database); + + return $data ? $this->extractData($data) : ''; + } + + $data = $query->select(['data' => 'data'])->one($this->database); + + return $data['data'] ?? ''; + } + + /** + * Найти сессию (Search session) + * + * @param string $id Идентификатор сессии (Session ID) + * + * @return array|null Сессия, если найдена (Session, if founded) + */ + public function searchSession(string $id): ?array + { + $session = (new Query()) + ->in($this->document) + ->where(['hash' => $id]) + ->one($this->database); + + return $session ? $session : null; + } + + /** + * Инициализировать сессию (Initialize session) + * + * @param string $id Идентификатор сессии (Session ID) + * + * @return array Сессия (Session) + */ + public function initSession(string $id): array + { + return $this->searchSession($id) ?? (new Query()) + ->in($this->document) + ->insert($this->buffer, db: $this->database); + } + + /** + * Записать сессию (Write session) + * + * @param string $id Идентификатор сессии (Session ID) + * @param string $data Данные сессии (Session data) + * + * @return bool Статус записи сессии (Session writing status) + */ + public function writeSession($id, $data): bool + // public function writeSession(string $id, string $data): bool + { + if ($this->getUseStrictMode() && $id === $this->_forceRegenerateId) { + //Ignore write when forceRegenerate is active for this id + return true; + } + + // exception must be caught in session write handler + // https://secure.php.net/manual/en/function.session-set-save-handler.php#refsect1-function.session-set-save-handler-notes + try { + // ensure backwards compatability (fixed #9438) + if ($this->writeCallback && !$this->buffer) { + $this->buffer = $this->composeFields(); + } + + // ensure data consistency + if (isset($this->buffer['data'])) { + $_SESSION = $this->buffer['data']; + } else { + $this->buffer['data'] = $data; + } + + $this->buffer = array_merge($this->buffer, [ + 'hash' => $id, + 'from' => time(), + 'to' => time() + $this->getTimeout(), + ]); + + // Конвертация или сериализация, не пойму + $this->buffer = $this->typecastFields($this->buffer); + + // Инициализация сессии + $this->initSession($id); + + // Деинициализация буфера данных сессии + $this->buffer = []; + } catch (Exception $e) { + yii::$app->errorHandler->handleException($e); + + return false; + } + + return true; + } + + /** + * Удалить сессию (Delete session) + * + * @param string $id Идентификатор сессии (Session ID) + * + * @return bool Статус удаления сессии (Session deleting status) + */ + public function destroySession($id): bool + // public function destroySession(string $id): bool + { + return (new Query()) + ->in($this->document) + ->where([ + 'hash' => $id + ]) + ->remove(db: $this->database); + } + + /** + * Удалить неиспользуемые данные (Delete garbage) + * + * Очищает от сессий чей срок хранения истёк + * Clears sessions whose retention period has expired + *. + * @param int $max Периодичность очистки в секундах (Cleaning frequency in seconds) + * + * @return bool Статус удаления устаревших сессий (Deletion status of obsolete sessions) + */ + public function gcSession($max): bool + // public function gcSession(int $max): bool + { + return (new Query()) + ->in($this->document) + ->where([ + [ + [ + 'to' => time() + ], + 'operator' => '>=' + ] + ]) + ->remove(db: $this->database); + } + + /** + * Генерация запроса для чтения сеанса (Generating a query to read a session) + * + * @param string $id Идентификатор сессии (Session ID) + * + * @return Query Инстанция запроса на чтение (Read query instance) + */ + protected function getReadQuery(string $id): Query + { + return (new Query()) + ->in($this->document) + ->where([ + [ + [ + 'to' => time() + ], + 'operator' => '<=' + ] + ]) + ->where([ + [ + 'to' => null + ] + ], 'OR') + ->where([ + [ + 'hash' => $id, + ], + ]); + } + + /** + * Конвертация для отправки в PDO (Convertation for sending to PDO) + * + * Default implementation casts field `data` to `\PDO::PARAM_LOB`. + * You can override this method in case you need special type casting. + * + * @param array $data Данные для конвертации (Data for convertation) [name => value] + * + * @return array Конвертированные данные (Converted data) + */ + protected function typecastFields(array $data): array + { + return $data; + } +} diff --git a/mirzaev/yii2/arangodb/sessions/Migrations/create_sessions_collection.php b/mirzaev/yii2/arangodb/sessions/Migrations/create_sessions_collection.php new file mode 100644 index 0000000..67fac6e --- /dev/null +++ b/mirzaev/yii2/arangodb/sessions/Migrations/create_sessions_collection.php @@ -0,0 +1,20 @@ +createCollection('sessions', ['type' => 2]); + } + + public function down() + { + $this->dropCollection('sessions'); + } +} \ No newline at end of file diff --git a/mirzaev/yii2/arangodb/sessions/MultiFieldSession.php b/mirzaev/yii2/arangodb/sessions/MultiFieldSession.php new file mode 100644 index 0000000..4c60528 --- /dev/null +++ b/mirzaev/yii2/arangodb/sessions/MultiFieldSession.php @@ -0,0 +1,134 @@ + + * @author Paul Klimov + */ +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. + * + * 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 bool whether to use custom storage. + */ + public function getUseCustomStorage(): bool + { + return true; + } + + /** + * Composes storage field set for session writing. + * + * @param string $id Optional session id + * @param string $data Optional session data + * + * @return array storage fields + */ + protected function composeFields($id = null, $data = null): array + { + $buffer = $this->writeCallback ? call_user_func($this->writeCallback, $this) : []; + + if ($id !== null) { + $buffer['hash'] = $id; + } + + if ($data !== null) { + $buffer['data'] = $data; + } + + return $buffer; + } + + /** + * Extracts session data from storage field set. + * + * @param array $buffer storage buffer. + * + * @return string session data. + */ + protected function extractData(array $buffer): string + { + if ($this->readCallback !== null) { + if (!isset($buffer['data'])) { + $buffer['data'] = ''; + } + + $extraData = call_user_func($this->readCallback, $buffer); + + if (!empty($extraData)) { + session_decode($buffer['data']); + + $_SESSION = array_merge((array) $_SESSION, (array) $extraData); + + return session_encode(); + } + + return $buffer['data']; + } + + return isset($buffer['data']) ? $buffer['data'] : ''; + } +}