# Расширенное использование

Хотя базовое использование является совершенно допустимым вариантом использования сессий Zend Framework, стоит рассмотреть другие возможности их использования. В этой секции обсуждаются тонкости работы с сессиями и показывается более продвинутое использование компонента Zend_Session.

## Запуск сессии

Если вы хотите, чтобы все запросы использовали сессии через Zend_Session, тогда запускайте сессию в файле инициализации (bootstrap):

Запуск глобальной сессии

    Zend_Session::start();

Запуская сессию в файле инициализации, вы исключаете вероятность её запуска после отправки заголовков браузеру, генерации исключения и возможного отображения испорченой страницы посетителю сайта. Некоторые расширенные возможности так же требуют вызова Zend_Session::start() до начала их использования (Подробнее о расширенных возможностях будет написано позднее.)

Существует четыре способа запустить сессию, используя Zend_Session. Два из них - неправильные.

1. Неправильно: Не устанавливайте параметр PHP конфигурации `session.auto_start`. Если у вас нет возможности отключить этот параметр в `php.ini`, вы используете mod_php(или аналог), тогда добавьте следующие строки в ваш `.htaccess` файл (Обычно находится в корневой директории для HTML-документов):

       php_value session.auto_start 0

2. Неправильно: Не используйте вызов PHP функции session_start() напрямую. Если вы вызываете session_start() напрямую и потом начинаете использовать Zend_Session_Namespace, Zend_Session::start() сгенерирует исключение ("session has already been started"). Если вы вызовете session_start()после использования Zend_Session_Namespace или вызова Zend_Session::start(), то будет сгенерирована ошибка уровня `E_NOTICE` и вызов будет проигнорирован.

3. Правильно: Используйте Zend_Session::start(). Если необходимо, чтобы все запросы имели и использовали сессии, то поместите вызов этой функции в коде инициализации близко к точке входа и без условной логики. При этом присутствуют издержки за счет сессий. Если сессии нужны только некоторым запросам, а остальным - нет, то:

   - Установите опцию `strict` в значение `TRUE` используя Zend_Session::setOptions() в файле инициализации.

   - Вызывайте Zend_Session::start() только для запросов, которым нужно использовать сессии, до создания любого объекта Zend_Session_Namespace.

   - Используйте "`new Zend_Session_Namespace()`" как обычно, там где требуется, но убедитесь, что Zend_Session::start() был вызван ранее.

   Опция `strict` предотвращает автоматический старт сессии с использованием Zend_Session::start() при вызове `new Zend_Session_Namespace()` Эта опция помогает разработчикам пользовательских приложений следовать принятому при проектировании решению не использовать сессии для определенных запросов, т.к. при установке этой опции и последующем инстанциировании Zend_Session_Namespace до явного вызова Zend_Session::start() будет сгенерировано исключение. Разработчики должны осторожно подходить к использованию Zend_Session::setOptions(), поскольку эти опции имеют глобальную область действия, вследствие их связи с лежащими в основе опциями расширения ext/session.

4. Правильно: Просто используйте Zend_Session_Namespace где необходимо, и PHP сессия будет запущена автоматически. Это наиболее простой вариант использования, подходящий для большинства случаев. Но необходимо следить зе тем, чтобы первый вызов `new Zend_Session_Namespace()` происходил до того, как PHP отправит клиенту любые данные(т.е. до того, как агенту пользователя отправлены HTTP-заголовки), если используется основанные на куки сессии (рекомендуемый вариант). Больше информации можно найти в этой секции.

## Блокировка пространств имен сессии

Можно применять блокировку к пространству имен для предотвращения изменения данных в нем. Используйте метод lock() для того, чтобы сделать определенное пространство имен доступным только для чтения, unLock() - чтобы сделать пространство имен доступным для чтения и изменений, а isLocked() - для проверки, не было ли пространство имен заблокировано ранее. Блокировка не сохраняется от запроса к запросу, не действует на методы установки(setter methods) в объектах, сохраненных в этом пространстве имен, но предотвращает использование методов установки для замены или удаления объектов, сохраненных непосредственно в пространстве имен. Аналогично, блокировка экземпляра Zend_Session_Namespace не препятствует использованию ссылок на те же данные (смотри PHP references).

Блокировка пространства имен сессии

    $userProfileNamespace = new Zend_Session_Namespace('userProfileNamespace');

    // помечаем сессию как заблокированную только для чтения
    $userProfileNamespace->lock();

    // снимаем блокировку
    if ($userProfileNamespace->isLocked()) {
        $userProfileNamespace->unLock();
    }

## Время жизни пространства имен

Время жизни может быть ограничено как у пространства имен в целом, так и у отдельных ключей. Общие случаи использованию включают в себя передачу временной информации между запросами и повышение защищенности от определенных угроз безопасности посредством удаления доступа к потенциально чувствительной информации по прошествии некоторого времени после аутентификации. Устаревание может быть основано на количестве секунд или на концепции "прыжков" (hops), в которой "прыжок" происходит при каждом успешном запросе.

Примеры установки времени жизни

    $s = new Zend_Session_Namespace('expireAll');
    $s->a = 'apple';
    $s->p = 'pear';
    $s->o = 'orange';

    // время жизни установлено в 5 секунд только для ключа "a"
    $s->setExpirationSeconds(5, 'a');

    // установить время жизни всему пространству имен в 5 "прыжков"
    $s->setExpirationHops(5);

    $s->setExpirationSeconds(60);
    // пространство имен "expireAll" будет помечено как истекшее по
    // прошествии 60 секунд при ближайшем запросе
    // или через 5 "прыжков", в зависимости от того, что произойдет раньше.

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

## Инкапсуляция сессий и контроллеры

Пространство имен может быть использовано для разделения доступа контроллеров к сессии, для защиты переменных от повреждения. К примеру, контроллер аутентификации может хранить свои данные в сессии отдельно от всех остальных контроллеров для достижения требований безопасности.

Пространства имен сессий с автоматическим устареванием для контроллеров

Следующий код, как часть контроллера, отображающего вопрос в тесте, создает переменную с булевым значением для обозначения, должен ли быть принят ответ на вопрос теста. В данном случае пользователю приложения дается 300 секунд на ответ для отображаемого вопроса.

    // ...
    // контроллер для вывода вопроса
    $testSpace = new Zend_Session_Namespace('testSpace');
    // установка времени жизни только для этой переменной
    $testSpace->setExpirationSeconds(300, 'accept_answer');
    $testSpace->accept_answer = true;
    //...

Ниже показан контроллер, обрабатывающий ответы на вопросы теста. Он определяет, принимать ли ответ, на основе того, был ли он отправлен в отведенное время:

    // ...
    // контроллер обработки ответа
    $testSpace = new Zend_Session_Namespace('testSpace');
    if ($testSpace->accept_answer === true) {
        // в отведенное время
    }
    else {
        // больше отведенного времени
    }
    // ...

## Ограничение количества экземпляров на каждое пространство имен

Хотя блокировка сессии предоставляет неплохой уровень защиты против непреднамеренного использования данных сессии в кокретном пространстве имен, Zend_Session_Namespace так же предоставляет возможность предотвратить создание множества экземпляров для одного пространства имен.

Для включения этой возможности, передайте `TRUE` вторым аргументом конструктора при создании последнего разрешенного экземпляра Zend_Session_Namespace. Любая последующая попытка инстанциации этого пространства имен сгенерирует исключение.

Ограничение доступа к пространству имен сессии одним экземпляром

    // создаем экземпляр для пространства имен
    $authSpaceAccessor1 = new Zend_Session_Namespace('Zend_Auth');

    // Создание второго экземпляра для пространства имен,
    // и запрет на создание новых
    $authSpaceAccessor2 = new Zend_Session_Namespace('Zend_Auth', true);

    // Создание ссылок на существующие возможно
    $authSpaceAccessor3 = $authSpaceAccessor2;

    $authSpaceAccessor1->foo = 'bar';

    assert($authSpaceAccessor2->foo, 'bar');

    try {
        $aNamespaceObject = new Zend_Session_Namespace('Zend_Auth');
    } catch (Zend_Session_Exception $e) {
        echo 'Cannot instantiate this namespace since ' .
             '$authSpaceAccessor2 was created\n';
    }

Второй параметр в конструкторе выше говорит Zend_Session_Namespace, что любые новые экземпляры с пространством имен "Zend_Auth" не разрешены. Попытка создания такого экземпляра вызовет генерацию исключения конструктором. Разработчик сам отвечает за хранение ссылки на экземпляры объектов(`$authSpaceAccessor1`, `$authSpaceAccessor2`, или `$authSpaceAccessor3` в примере выше), если в дальнейшем при обработке того же запроса необходим доступ к этому пространству имен сессии. Например, вы можете сохранять экземпляр в статической переменной или передавать его реестру (смотри Zend_Registry), или передавать его другим методам, которым нужен доступ к данному пространству имен.

## Работа с массивами

Всвязи с историей реализаций магических методов в PHP, изменение массивов внутри пространства имен может не работать при версиях PHP меньше 5.2.1. Если вы будете работать только с версиями PHP 5.2.1 или более поздними, тогда вы можете перейти к следующей секции.

Изменение данных массивов с помощью Zend_Session_Namespace

Далее показано, как эта проблема может быть воспроизведена:

    $sessionNamespace = new Zend_Session_Namespace();
    $sessionNamespace->array = array();

    // может не работать, как ожидается, в версиях PHP до 5.2.1
    $sessionNamespace->array['testKey'] = 1;
    echo $sessionNamespace->array['testKey'];

Формирование массива до сохранения в сессию

Если возможно, всецело избегайте данной проблемы, сохраняя массивы в пространство имен сессии только после того, как все необходимые значения были установлены.

    $sessionNamespace = new Zend_Session_Namespace('Foo');
    $sessionNamespace->array = array('a', 'b', 'c');

Если вы используете подверженную проблеме версию PHP и необходимо изменить массив после его сохранения в пространстве имен сессии, вы можете использовать оба или один из следующих обходных путей:

Обходной путь: Пересохранение измененного массива

В последующем коде создается копия сохраненного массива, изменяется и сохраняется обратно, перезаписывая оригинал.

    $sessionNamespace = new Zend_Session_Namespace();

    // устанавливаем исходный массив
    $sessionNamespace->array = array('tree' => 'apple');

    // делаем копию массива
    $tmp = $sessionNamespace->array;

    // изменяем копию массива
    $tmp['fruit'] = 'peach';

    // устанавливаем копию массива обратно в пространство имен сессии
    $sessionNamespace->array = $tmp;

    echo $sessionNamespace->array['fruit']; // выводит "peach"

Обходной путь: Сохранение массива, содержащего ссылку

В качестве альтернативы, сохраниение массива, содержащего ссылку на интересующий массив, с его последующим непрямым изменением:

    $myNamespace = new Zend_Session_Namespace('myNamespace');
    $a = array(1, 2, 3);
    $myNamespace->someArray = array( &$a );
    $a['foo'] = 'bar';
    echo $myNamespace->someArray['foo']; // выводит "bar"

## Использование сессии с объектами

Если вы планируете хранить объекты в PHP сессии, имейте в виду, что они будут сериализованы для сохранения. Таким образом, любые объекты, сохраненные в PHP сессии, должны быть десериализованы при извлечении из хранилища. Подразумевается что разработчик должен обеспечить определение классов для сохраненных объектов до десериализации объекта из сессионного хранилища. Если класс десериализуемого объекта не определен, он становится экземпляром `stdClass`.

## Использование сессии с юнит тестами

Zend Framework опирается на PHPUnit для облегчения своего тестирования. Многие разработчики расширяют существующий набор юнит тестов для покрытия кода в их приложении. Если во время выполнения юнит тестирования после закрытия сессии происходит попытка использования пишущих в сессию методов, генерируется исключение "Zend_Session is currently marked as read-only (Zend_Session в данный момент помечен как только для чтения)". Однако Zend_Session требует особое внимание, так как закрытие (Zend_Session::writeClose()), или уничтожение (Zend_Session::destroy()) сессии предотвращает дальнейшую установку или удаление ключей в любом экземпляре Zend_Session_Namespace. Это поведение - прямой результат используемого механизма ext/session, методов session_destroy() и session_write_close() в PHP, которые не имеют механизма "отменить" для выполнения настройки/сброса юнит теста.

Для обхода этой проблемы, смотри метод testSetExpirationSeconds() юнит тестов в `SessionTest.php` и `SessionTestHelper.php`. Оба находятся в `tests/Zend/Session`. Они используют функцию PHP - exec() для запуска отдельного процесса. Новый процесс более точно симулирует второй, последующий, запрос от браузера. Отдельный процесс стартует с "чистой" сессией, как любое другое выполнение PHP скрипта по web запросу. Также, любые изменения в `$_SESSION` сделанные в вызывающем процессе становятся доступны дочернему процессу, при условии что родитель закрыл сессию до вызова exec().

PHPUnit тестирование кода, зависимого от Zend_Session

    // тестирование setExpirationSeconds()
    $script = 'SessionTestHelper.php';
    $s = new Zend_Session_Namespace('space');
    $s->a = 'apple';
    $s->o = 'orange';
    $s->setExpirationSeconds(5);

    Zend_Session::regenerateId();
    $id = Zend_Session::getId();
    session_write_close(); // освобождаем сессию для процесса ниже
    sleep(4); // недостаточно долго для истекания сессии
    exec($script . "expireAll $id expireAll", $result);
    $result = $this->sortResult($result);
    $expect = ';a === apple;o === orange;p === pear';
    $this->assertTrue($result === $expect,
        "iteration over default Zend_Session namespace failed; " .
        "expecting result === '$expect', but got '$result'");

    sleep(2); // достаточно для истекания сессии(всего 6 секунд
              // ожидания, но истекла за 5)
    exec($script . "expireAll $id expireAll", $result);
    $result = array_pop($result);
    $this->assertTrue($result === '',
        "iteration over default Zend_Session namespace failed; " .
        "expecting result === '', but got '$result')");
    session_start(); // Восстанавливаем искусственно остановленную сессию

    // Мы можем выделить это в отдельный тест, но на самом деле, если
    // какие-либо остатки от теста выше загрязняют тест ниже, это тоже баг,
    // о котором мы хотели бы знать.
    $s = new Zend_Session_Namespace('expireGuava');
    $s->setExpirationSeconds(5, 'g'); // теперь устанавливаем время жизни только
                                      // одному ключу в пространстве имен сессии
    $s->g = 'guava';
    $s->p = 'peach';
    $s->p = 'plum';

    session_write_close(); // освобождаем сессию для процесса ниже
    sleep(6); // недостаточно долго для истекания сессии
    exec($script . "expireAll $id expireGuava", $result);
    $result = $this->sortResult($result);
    session_start(); // Восстанавливаем искусственно остановленную сессию
    $this->assertTrue($result === ';p === plum',
        "iteration over named Zend_Session namespace failed (result=$result)");