Хранение PHP-сессий в базе данных

Подход, позволяющий хранить PHP-сессии в базе данных вместо файлов

Начну сразу с причин, по которым я пишу эту статью. Я периодически просматриваю лог запросов, по которому люди попадают сюда, и вот один из запросов — хранить сессию в .

Итак, как же хранить -сессии в базе данных?

На самом деле, в этом нет ничего сложного: в есть одна полезная функция — session_set_save_handler — которая и выполняет всю грязную работу:

session_set_save_handler() sets the user-level session storage functions which are used for storing and retrieving data associated with a session. This is most useful when a storage method other than those supplied by sessions is preferred. i.e. Storing the session data in a local database.

Эта функция принимает шесть аргументов типа сallback:

  • open — вызывается при открытии/создании сессии;
  • close — вызывается при закрытии сессии (например, чтобы приложение могло закрыть файл);
  • read — чтение сессии;
  • write — запись сессии;
  • destroy — уничтожение сессии (session_destroy());
  • gc — «сборка мусора»

Все функции (кроме read) должны возвращать true, если все прошло успешно и false в противном случае.

Для хранения сессии у нас есть такая таблица:

[-]
View Code MySQL
CREATE TABLE `session` (
    `id` CHARACTER(32) BINARY NOT NULL PRIMARY KEY, /* Session ID */
    `expires` INTEGER NOT NULL, /* Время истекания сессии */
    `session_data` TEXT NOT NULL, /* Данные, хранящиеся в сессии */

    KEY(`expires`)
)

Один маленький нюанс: если в делать таблицу сессий типа MEMORY (т.е. задать ей такой storage engine), то могут возникнуть проблемы с удалением записей при сборке мусора, ибо MEMORY storage engine не может использовать индексы для операций сравнения типа «больше»/«меньше».

Для абстракции от конкретного SQL-сервера, будем считать, что у нас имеется некий класс Persistent, обладающий способностями загружать/сохранять данные в базу данных.

Таким образом, реализация класса Session будет иметь следующий вид:

[-]
View Code PHP
<?php
    require_once('class.Persistent.php');

    class Session extends Persistent
    {
        /**
         * @var string
         */

        public $id;

        /**
         * @var int
         */

        public $expires;

        /**
         * @var string
         */

        public $session_data;

        /**
         * @var bool
         */

        public $m_new;

        function __contruct($data)
        {
            parent::__construct($data);
        }

        static function getTable()
        {
            return TABLE_SESSION; //константа, задающая имя таблицы сессий
        }
    }
?>

Пока все предельно просто. О переменной $m_new поговорим позже.

Теперь собственно реализация класса, управляющего сессиями:

[-]
View Code PHP
<?php
    require_once('class.Session.php');

    class SessionManager
    {
        /**
         * @var int
         */

        protected $life_time;

        /**
         * @var Session
         */

        protected $session;

        /**
         * @return SessionManager
         */

        public static function& instance($reinit = false)
        {
            static $self = null;

            if (true == is_null($self)) {
                $self = new SessionManager();
                $reinit = true;
            }

            if (true == $reinit) {
                session_set_save_handler(
                    array(&$self, "open"),
                    array(&$self, "close"),
                    array(&$self, "read"),
                    array(&$self, "write"),
                    array(&$self, "destroy"),
                    array(&$self, "gc")
                );

                register_shutdown_function('session_write_close');
            }

            return $self;
        }

        public function open($save_path, $sess_name)
        {
            $this->life_time = intval(get_cfg_var('session.gc_maxlifetime'));
            $this->session = new Session(
                array(
                    'id'           => (true == isset($_COOKIE[$sess_name])) ? $_COOKIE[$sess_name] : session_id(),
                    'expires'      => time() + $this->life_time,
                    'session_data' => '',
                )
            );

            $this->session->m_new = true;
            return true;
        }

        public function close()
        {
            return true;
        }

        public function read($sid)
        {
            $this->session = Session::load('Session', $sid, 3600);
            if (false == $this->session instanceof Session) {
                $this->session = new Session(
                    array(
                        'id'           => (true == isset($_COOKIE[session_name()])) ? $_COOKIE[session_name()] : session_id(),
                        'expires'      => time() + $this->life_time,
                        'session_data' => '',
                    )
                );

                $this->session->m_new = true;
            }
            else {
                $this->session->m_new = false;
            }

            return (string)$this->session->session_data; //Явное приведение типа позволит избежать трудноуловимых ошибок
        }

        public function write($sid, $data)
        {
            $this->session->m_new        |= ($sid != $this->session->id);
            $this->session->id            = $sid;
            $this->session->session_data  = $data;
            $this->session->expires       = time() + $this->life_time;

            $mode = (true == $this->session->m_new) ? SAVE_INSERT : SAVE_UPDATE;
            $this->session->save($mode);

            return true;
        }

        public function destroy($sid)
        {
            unset($_COOKIE[$sid]);
            Session::deleteMany('Session', new QueryCondition(array('id' => $sid)));
            return true;
        }

        public function gc($max_time)
        {
            Session::deleteMany('Session', new QueryCondition("`expires` < '" . time() . "'"));
            return true;
        }

        /**
         * @return Session
         */

        public function& getSession()
        {
            return $this->session;
        }
    }
?>

Пример использования:

[-]
View Code PHP
<?php
    $sm = &SessionManager::instance();
    session_start();
?>

Вкратце о $m_new. Дело в том, что ID сессии при ее открытии может не совпадать с ID при закрытии (иными словами, измениться в ходе выполнения скрипта). Один из способов — это использование функции session_regenerate_id(). Поэтому при сохранении сессии важно знать, является ID новым или нет (если ID новый, то для сохранения будет использоваться INSERT, если существующий — то UPDATE; по большому счету можно обойтись одним REPLACE, однако это не особо эффективное решение). Для этого при открытии сессии получаем текущий идентификатор сессии и сравниваем его с тем, который получаем при сохранении сессии (конечно, в этом простом случае можно было обойтись без лишней переменной, но в более сложных проектах она может понадобиться); в зависимости от их равенства/неравенства используем тот или иной метод сохранения данных.

Строка register_shutdown_function('session_write_close') гарантирует, что сессия будет сохранена (без нее в PHP4 у меня были случаи, когда сессия не сохранялась).

О том, как прочитать данные из Session::session_data, можно прочитать в этой статье. Сразу отмечу, что самостоятельно выполняет восстановление данных, которые ему передаются функцией SessionManager::read(), просто иногда бывают ситуации, когда сессию нужно восстановить вручную (подпатчить на лету).

При более глубоком изучении вопроса также будет полезна эта замечательная статья.

Добавить в закладки

Связанные записи

Автор: Vladimir; опубликовано в: MySQL, PHP; метки: MySQL, PHP, база данных, сессия
19
Март
2008

RSS Комментарии к статье «Хранение PHP-сессий в базе данных» (19)  »

  1. Astral

    Vladimir не могли бы вы оставить свою аську, или почту чтобы связаться?
    Или стукнуть мне?

  2. [...] Продолжение статьи «Хранение PHP-сессий в базе данных». [...]

  3. Marat

    в PHP5 session_write_close следует вызывать из деструктора. В PHP4 – деструкторов еще не было, конечно

  4. Nikita

    Все понял, очень понравилось. Но в упор не понимаю, что должны делать методы load() и save(),точнее какие данные получать и как. Можно пример кода класса Persistent?

    • load() должен загружать данные сессии из таблицы, используя идентификатор сессии ($sid) в качестве ключа (SELECT session_data FROM session WHERE session_id = '{$sid}').
      save(), соответственно, сохранять эти данные в базу.

      PS — того кода, к сожалению, не осталось :-(

  5. neo

    Поэтому при сохранении сессии важно знать, является ID новым или нет (если ID новый, то для сохранения будет использоваться INSERT, если существующий — то UPDATE; по большому счету можно обойтись одним REPLACE, однако это не особо эффективное решение).
    А почему бы здесь использовать завернутый в специально обученную функцию INSERT … ON DUPLICATE KEY UPDATE? Понятно, что такой синтаксис только для mysql, но другие СУБД не всем нужны и там такой функционал тоже вполне реализуем, если не ошибаюсь.

Оставить комментарий к записи «Хранение PHP-сессий в базе данных»

Вы должны быть авторизованы, чтобы иметь возможность оставить комментарий.

Подписаться, не комментируя