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

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

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

Итак, как же хранить 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 PHP 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`)
)

Один маленький нюанс: если в MySQL делать таблицу сессий типа 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, можно прочитать в этой статье. Сразу отмечу, что PHP самостоятельно выполняет восстановление данных, которые ему передаются функцией SessionManager::read(), просто иногда бывают ситуации, когда сессию нужно восстановить вручную (подпатчить на лету).

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

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

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

  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, но другие СУБД не всем нужны и там такой функционал тоже вполне реализуем, если не ошибаюсь.

  6. Sheva

    Не хватает только реализации класса Persistent для MySQL.

Пожалуйста, не используйте эту форму для комментирования! Данная форма предназначена исключительно для ботов.

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Оставляя комментарий, вы выражаете своё согласие с Правилами комментирования.

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

गते गते पारगते पारसंगते बोधि स्वाहा