PHP: пишем свою реализацию сессий для обработки мертвой сессии перед зачисткой

  Веб-разработка, PHP

Репост моей оооочень старой статьи на хабре исключительно на память

Представим ситуацию: есть корзина покупок на сайте, при добавлении в корзину мы ставим на товар т.н. lock, исключающий его из списка доступных для покупки товаров. Когда клиент удаляет товар из корзины - lock снимается. Но что делать, если пользователь просто закрыл браузер? В таком случае сессия будет удалена сборщиком мусора, а локи так и останутся.

Когда я столкнулся с такой ситуацией, первое что мне пришло в голову - хранить локи и дату доступа в БД и периодически дергать её кроном. Но костыльность этого решения очевидна. Я вообще недолюбливаю web-программирование на PHP за часто-встречающуюся костыльность многих решений. А ещё за бред, с которым я столкнулся при решении сабжа: для сериализации и десереализации сессий используются функции и формат, отличные от функций serialize и unserialize. Приходится делать велосипеды для ансериализации сессии.

Ближе к телу: как решил проблему я...
Тут надо сделать замечание, что идею для такого решения мне подал хабраюзер rigid, а помогли решить пару около-сабжевых проблем в конференции php@conference.jabber.ru.

PHP позволяет определять свои функции для обработки сессий. Отвечает за это функция session_set_save_handler. В качестве параметров она принимает список функций, который будут вызываться для работы с сессиями. В мануале есть даже пример, который реализует стандартный механизм работы с сессиями. Его-то мы и возьмем, изменив только функцию gc, которая занимается сборкой мусора, т.е. удалением файлов мертвых сессий.

Пример функции gc:

/* Функция принимает в качестве параметра время жизни сессии */
function gc($maxlifetime)
  {
        global $sess_save_path; /* путь, где лежат сессии */
        foreach (glob("$sess_save_path/sess_*") as $filename)
      {
                /* Проверяем не пора ли убить сессию */
                if (filemtime($filename) + $maxlifetime < time())
                  {
                        /* $tmp_sess у нас теперь аналогична $_SESSION той сессии */
                        $tmp_sess=unserializesession(file_get_contents($filename));
                        /* Обрабатываем данные, например снимаем локи из этой сессии */
                        @unlink($filename); /* Удаляем сессию */
                  }
          }
        return true;
  }
  

Код функции unserializesession, взятый откуда-то из интернета (скорее всего из комментариев к функции в мануале PHP):

function unserializesession($data) {
  $vars=preg_split('/([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff^|]*)\|/',
                   $data,-1,PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
  for($i=0; $vars[$i]; $i++) $result[$vars[$i++]]=unserialize($vars[$i]);
  return $result;
}

Теперь подключаем это в наш проект:

session_set_save_handler("open", "close", "read", "write", "destroy", "gc");
/* Вероятность чистки мусора на каждый session_start() примерно равна 30%,
другими словами - чистка мертвых сессий будет производится при тридцати вызовах session_start() из ста */
ini_set("session.gc_probability", 30); /* Можно настроить на 100%, если у вас там нет никакого медленного кода */
ini_set("session.gc_divisor", 100);
ini_set("session.gc_maxlifetime", 1800); /* Время жизни сессии в секундах (то самое, которое передается в функцию gc) */
session_start();

Есть одно но: в Debian/Ubuntu свой механизм очистки сессий, который выполняется кроном, а у PHP нет возможности удалять файлы сессий. Честно говоря, это "черезжопа". Решить проблему можно задав собственный каталог для файлов сессии и закрыв его в .htaccess (если он находится в document_root).