Статья
Версия для печати
Обсудить на форуме
Сессии PHP. Часть 4.


Хранилище. Создание собственного обработчика.

Опять возвращаюсь к хранению данных сессии.
В этот раз я расскажу, как сделать свой обработчик, как его активировать, и какие подводные камни я нашел.
Работа с данными сессии разделена на 6 разных этапов. Соответственно, и обработчик сессии должен перехватить все 6 функций. Вот их условные названия: open, read, write, delete, gc (garbage collection) и close. Я специально не поставил круглые скобки после имен функций, чтоб они в тексте не путались со встроенными функциями PHP.
Рассмотрим их работу подробнее:

  • open
    Назначение этой функции - подготовка ко всем остальным операциям. Функция вызывается автоматически сразу после старта сессии. В параметрах ей передаются путь к директории, где предполагается хранить файлы с данными сессий, и имя сессии.
    Путь берется из php.ini ("session.save_path") либо устанавливается в программе функцией session_save_path().
    Имя сессии может использоваться для составления имени файла с данными, а можно его просто игнорировать - вероятность повтора строки MD5 крайне мала.
    В литературе встречается мнение, что если данные сессии хранятся в базе данных, то в этой функции можно произвести подключение к БД. Я считаю, если БД есть и используется, то не только для хранения данных сессии, и подключение можно произвести раньше, в основной программе. Так проще и понятней.
  • read
    Назначение - считать данные сессии. Функция вызывается сразу после open. Единственный параметр - идентификатор сессии. Вернуть функция должна строку, соответствующую формату входного параметра функции unserialize(). Если данных нет, то она должна вернуть пустую строку.
    В этой функции также важно понять, есть ли источник данных, т.к. сессия могла быть ранее уничтожена сборщиком мусора (gc) или функцией delete. Также здесь можно проверять и устанавливать блокировки (об этом расскажу в последующих частях).
  • write
    Назначение - запись данных сессии. Функция автоматически вызывается при следующих обстоятельствах: завершение сессии по желанию программиста и по завершению программы (когда вывод в браузер окончен и сообщения об ошибках отобразить в браузере уже нельзя). Входных параметров два: идентификатор сессии и сохраняемые данные в сериализованном виде. Возвращаемое значение должно символизировать результат работы, но и отрицательный результат уже ни на что не повлияет, если вывод завершен (данные не сохранены, работа окончена, моем руки).
    Не стоит надеяться, что раз источник данных был доступен функции read, то в write не надо проверять его доступность. Файл (или запись в БД) вполне могли уже удалить. По крайней мере, есть такая вероятность, и проверки будут не лишними. Правда, если в read отрыть файл, сохранить его дескриптор и не закрывать, то в write файл будет гарантированно существовать (в *nix системах он может быть еще удален, но физически писать в него еще можно), если конечно не додуматься до хранения временных файлов на сетевом диске или съемном носителе. (? delete)
    Принудительная запись осуществляется функцией session_write_close(). Начиная с PHP 4.3.11, у нее есть более благозвучное имя - session_commit().
  • delete
    Назначение - удалить данные сессии. Вызывается автоматически некоторыми встроенными функциями. Например, session_destroy() (с версии 4.3.3), или session_regenerate_id() (с версии 5.1.0). Входной параметр один - идентификатор сессии. Так стоит проверить, существует ли объект для удаления, или, по крайней мере, подавить вывод ошибок - в их обработке смысла нет.
  • gc
    Назначение - "сборка мусора" (удаление ненужных данных). Разработчики PHP сделали предположение, что этот процесс может быть медленным, и поэтому эта функция вызывается не каждый раз. Периодичность запуска определяется настройками php.ini ("session.gc_divisor" и "session.gc_probability"): session.gc_probability/session.gc_divisor - это "вероятность" (chance) запуска сборщика мусора. Входной параметр - максимальное время жизни для сессии в секундах. Значение его берется из php.ini ("session.gc_maxlifetime"), либо может быть установлено в программе через ini_set(). Также можно игнорировать этот параметр и использовать свое значение.
    Если данные сессий хранятся в индивидуальных файлах, то рекомендуется в проверке использовать время изменения файла, т.к. в ОС Windows может быть неправильная обработка времени доступа. Например, как утверждается на сайте www.php.net, Windows98 не сохраняет время доступа, а только дату. Запускать web-сервер под Windows98 - это, на сегодняшний день, выглядит странно, но береженого Бог бережет - лучше использовать время модификации.
    С другой стороны, встроенный по умолчанию обработчик и так работает с файлами, и создавать свой аналогичный обработчик нет необходимости, если, конечно, он не выполняет каких-то особых действий, которые в штатном обработчике не предусмотрены.
  • close
    Назначение - завершающие операции. Это уникальная функция, по сравнению с остальными пятью. Параметров - нет. Условия работы - особые: вывод окончен, все объекты разобраны. Т.е., на эту роль подходит либо глобальная функция, либо статический метод класса, а метод объекта недопустим. И самое главное: она, в общем-то, не нужна. Не отрицаю, что возможно есть и ей применение, но два наиболее распространенных хранилища - файлы и СУБД MySQL - прекрасно обходятся без этой функции. Файлы, если их по какой-либо причине не закрыли, закроются автоматически по завершении программы. То же произойдет с подключением к серверу базы данных.

В завершение этой части я привожу пример обработчика, который, подобно штатному, хранит данные в индивидуальных файлах. Для разнообразия и примера я его сделал в виде класса.

Код: (PHP)
<?php
class session_std
{
    var $_path = '';
    var $_fd = false;

    function session_open($path, $name)
    {
        $this->_path = $path; // Сохраним путь - его нам больше не скажут
        return true;
    }

    function session_close()
    {
        return true; // А делать-то и нечего
    }

    function session_read($sid)
    {
        $file = "$this->_path/$sid";
        if (!file_exists($file)) // Файл был кем-то удален?
            return "";
        if (!($this->_fd = @fopen($file, 'rb+')))
            return ""; // Не удалось открыть
        $data = fread($this->_fd, filesize($file));
        return $data; // Ничего с данными делать не надо - просто возвращаем
    }

    function session_write($sid, $data)
    {
        if ($this->_fd === false) // Если в session_read() не удалось открыть
        {
            if ($this->_fd = @fopen($file, 'wb')) // попытка создать файл
                return false;
        }
        rewind($fd); // Мотаем на начало
        ftruncate($fd, 0); // очистить файл
        $res = fwrite($this->_fd, $data);
        fclose($this->_fd);
        return $res;
    }

    function session_delete($sid)
    {
        $file = "$this->_path/$sid";
        if (!is_file($file))
            return false;
        return unlink($file); // Просто удаляем
    }

    function session_gc($lifetime)
    {
        if (!($dir = opendir($this->_path)))
            return false;
        while($file = readdir($dir))
        {
            $filepath = "$this->_path/$file";
            if (!is_file($filepath)) // А вдруг это директория?
                continue;
            $time = filemtime($filepath); // Используем время изменения
            $now = time();
            if ($time + $lifetime < $now)
                unlink($filepath);
        }
        closedir($dir);
        return true; // Стало в доме меньше пыли
    }
}

$storage = new session_std;

session_set_save_handler(
    array(&$storage, 'session_open'),
    array('session_std', 'session_close'), // См. описание close
    array(&$storage, 'session_read'),
    array(&$storage, 'session_write'),
    array(&$storage, 'session_delete'),
    array(&$storage, 'session_gc')
    );

session_save_path("/tmp/my_session_dir/"); // Теперь мусорим сюда
session_name("MY_SESSION_ID"); // Имя у сессии будет такое

session_start();
?>

Пока все.
Продолжение - в следующей статье.

Роман Чернышов (RXL) 26.03.2006
Версия для печати
Обсудить на форуме