Хранение PHP-сессий в базе данных
Подход, позволяющий хранить PHP-сессии в базе данных вместо файлов
Начну сразу с причин, по которым я пишу эту статью. Я периодически просматриваю лог запросов, по которому люди попадают сюда, и вот один из запросов — хранить php сессию в mysql
.
Итак, как же хранить 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
в противном случае.
Для хранения сессии у нас есть такая таблица:
`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
будет иметь следующий вид:
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
поговорим позже.
Теперь собственно реализация класса, управляющего сессиями:
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;
}
}
?>
Пример использования:
$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()
, просто иногда бывают ситуации, когда сессию нужно восстановить вручную (подпатчить на лету).
При более глубоком изучении вопроса также будет полезна эта замечательная статья.
Оч нужная статья, но ничерта не ясно…
Dok, задавайте вопросы, будем разбираться… На самом деле всё не так страшно, как оно кажется
Не че не получилось. Будем разбираться сами!
Тем немениее спасибО!
Автору респект!
Собирался сам изобретать велосипед!:)
Еще одна подробная статья, выполненная на отличном уровне.
Сам в свое время разбирался в этом.
Очень хорошо, что теперь есть куда посмотреть и вспомнить если что
Спасибо вам!
С уважением, Савунов Василий (babr)
А какие приемущества хранить сессии в базе перед файлами?
Самые разные…
Например, если Вам нужно посчитать количество пользователей в онлайне. ID всех сессий априорно Вы знать не можете, а с БД такой запрос — пара пустяков:
SELECT COUNT(DISTINCT `user_id`) FROM `sessions` WHERE `user_id` != 0
.Прибить сессию (сессии) тоже проще через БД.
Или, допустим, сайт знакомств: Вам нужно найти пользователей, удовлетворяющих каким-то условиям и находящихся сейчас на сайте. С БД это проще.