Сессии

Дата обновления перевода 2023-07-10

Сессии

Компонент Symfony HttpFoundation имеет очень мощную и гибкую подсистему сессий, предназначенную для обеспечения управления сессиями, которую можно использовать для хранения информации о пользователе между запросами через понятный объектно-ориентированный интерфейс с использованием различных драйверов хранения сессий.

Сессии Symfony призваны заменить использование суперглобальной функции $_SESSION и нативных функций PHP, связанных с работой с сессиями, таких как session_start(),
session_regenerate_id(), session_id(), session_name(), и session_destroy().

Note

Сессии запускаются только если вы читаете или пишете в них.

Установка

Вам нужно установить компонент HttpFoundation, чтобы работать с сессиями:

1
$ composer require symfony/http-foundation

Базовое применение

Сессия доступна через объект Request и сервис RequestStack. сервис. Symfony внедряет сервис request_stack в сервисы и контроллеры, если вы добавляете подсказку аргумента RequestStack:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use Symfony\Component\HttpFoundation\RequestStack;

class SomeService
{
    private $requestStack;

    public function __construct(RequestStack $requestStack)
    {
        $this->requestStack = $requestStack;

        // Доступ к сессии в конструкторе *НЕ* рекомендуется, так как она
        // может быть еще недоступна или приведет к нежелательным побочным эффектам
        // $this->session = $requestStack->getSession();
    }

    public function someMethod()
    {
        $session = $this->requestStack->getSession();

        // ...
    }
}

Из контроллера Symfony вы также можете добавить подсказку к аргументу Request:

1
2
3
4
5
6
7
8
9
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

public function index(Request $request): Response
{
    $session = $request->getSession();

    // ...
}

Атрибуты сессий

Управление сессиями в PHP требует использования суперглобальной переменной $_SESSION. Однако это мешает тестируемости кода и инкапсуляции в парадигме ООП. Для преодоления этой проблемы в Symfony используются мешки сессий, связанные с сессией, для инкапсуляции определенного набора атрибутов.

Такой подход позволяет уменьшить загрязнение пространства имен внутри суперглобального $_SESSION, поскольку каждый мешок хранит все свои данные в уникальном пространстве имен. Это позволяет Symfony мирно сосуществовать с другими приложениями или библиотеками, которые могут использовать суперглобальный $_SESSION, при этом все данные остаются полностью совместимыми с управлением сессиями в Symfony.

Мешок сессий - это объект PHP, который работает как массив:

1
2
3
4
5
6
7
8
// хранит атрибут для повторного использования во время запроса пользователя позже
$session->set('attribute-name', 'attribute-value');

// получает атрибут по имени
$foo = $session->get('foo');

// второй аргумент - это значение, возвращённое, если атрибут не существует
$filters = $session->get('filters', []);

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

Tip

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

Флеш-сообщения

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

Например, представьте, что вы обрабатываете отправку формы:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
// ...

public function update(Request $request): Response
{
    // ...

    if ($form->isSubmitted() && $form->isValid()) {
        // do some sort of processing

        $this->addFlash(
            'notice',
            'Your changes were saved!'
        );
        // $this->addFlash() is equivalent to $request->getSession()->getFlashBag()->add()

        return $this->redirectToRoute(/* ... */);
    }

    return $this->render(/* ... */);
}

После обработки запроса контроллер устанавливает в сессии флеш-сообщение и затем осуществляет перенаправление. Ключ сообщения (в данном примере notice) может быть любым: вы будете использовать этот ключ для извлечения сообщения.

В шаблоне следующей страницы (или, что еще лучше, в шаблоне базового макета), прочитайте все флеш-сообщения из сессии, используя метод flashes(), предоставленный глобальной переменной приложения Twig :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{# templates/base.html.twig #}

{# прочитать и отобразить только один тип флеш-сообщения #}
{% for message in app.flashes('notice') %}
    <div class="flash-notice">
        {{ message }}
    </div>
{% endfor %}

{# прочитать и отобразить несколько типов флеш-сообщений #}
{% for label, messages in app.flashes(['success', 'warning']) %}
    {% for message in messages %}
        <div class="flash-{{ label }}">
            {{ message }}
        </div>
    {% endfor %}
{% endfor %}

{# прочитать и отобразить все флеш-сообщения #}
{% for label, messages in app.flashes %}
    {% for message in messages %}
        <div class="flash-{{ label }}">
            {{ message }}
        </div>
    {% endfor %}
{% endfor %}

В качестве ключей различных типов флеш-сообщений принято использовать notice, warning и error, но вы можете использовать любой ключ, соответствующий вашим потребностям.

Tip

Вместо этого вы можете использовать метод peek(), чтобы извлекать сообщения, оставляя их в мешке.

Конфигурация

В фреймворке Symfony сессии включены по умолчанию. Хранение сессий и другая конфигурация может управляться с помощью конфигурации framework.session в config/packages/framework.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
# config/packages/framework.yaml
framework:
    # Включает поддержку сессии. Отметьте, что сессия будет запущена ТОЛЬКО при чтении или записи в нее.
    # Удалите или прокомментируйте этот раздел, чтобы ясно отключить поддержку сессий.
    session:
        # ID сервиса, используемого для хранения сессии
        # NULL означает, что Symfony использует механизм сессий PHP по умолчанию
        handler_id: null
        # улучшает безопасность куки, используемых для сессий
        cookie_secure: auto
        cookie_samesite: lax
        storage_factory_id: session.storage.factory.native

Установка опции конфигурации handler_id в значение null означает, что Symfony будет использовать нативный механизм сессий PHP. Файлы метаданных сессии будут храниться вне приложения Symfony, в каталоге, управляемом PHP. Хотя это обычно упрощает работу, некоторые опции, связанные с истечением срока действия сессии, могут работать не так, как ожидается, если другие приложения, пишущие в тот же каталог, имеют короткие значения максимального времени жизни.

При желании можно использовать сервис session.handler.native_file в качестве handler_id, чтобы позволить Symfony самой управлять сессиями. Еще одной полезной опцией является save_path, которая определяет каталог, в котором Symfony будет хранить файлы метаданных сессий:

1
2
3
4
5
6
# config/packages/framework.yaml
framework:
    session:
        # ...
        handler_id: 'session.handler.native_file'
        save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'

Просмотрите справочник конфигурации Symfony, чтобы узнать больше о других доступных опциях конфигурации Session .

Caution

Сессии Symfony несовместимы с директивой php.ini. session.auto_start = 1 - эта директива должна быть отключена в php.ini, в директивах веб-сервера или в .htaccess.

Время простоя/поддержки жизни сессии

Часто возникают обстоятельства, когда необходимо защитить или минимизировать несанкционированное использование сессии, когда пользователь отходит от терминала во время нахождения в системе, путем уничтожения сессии после определенного периода простоя. Например, в банковских приложениях принято выводить пользователя из системы после 5 - 10 минут бездействия. Задавать время жизни куки здесь нецелесообразно, потому что клиент может манипулировать этим параметром, поэтому мы должны устанавливать срок действия на стороне сервера. Проще всего реализовать это через сбор мусора, который выполняется достаточно часто. cookie_lifetime должно быть установлено в относительно большое значение, а сбор мусора gc_maxlifetime будет настроен на уничтожение сессий при желаемом времени простоя.

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

Symfony записывает некоторые метаданные о каждой сессии, чтобы предоставить вам возможность тонкого контроля над настройками безопасности:

1
2
$session->getMetadataBag()->getCreated();
$session->getMetadataBag()->getLastUsed();

Оба метода возвращают временную отметку Unix (относительно сервера).

This metadata can be used to explicitly expire a session on access:

1
2
3
4
5
$session->start();
if (time() - $session->getMetadataBag()->getLastUsed() > $maxIdleTime) {
    $session->invalidate();
    throw new SessionExpired(); // перенаправление на страницу истекшей сессии
}

Также возможно сказать, какое cookie_lifetime было установлено для конкретного куки, прочитав метод getLifetime():

1
$session->getMetadataBag()->getLifetime();

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

Храните сессии в базе данных

По умолчанию Symfony хранит сессии в файлах. Если ваше приложение выдается несколькими серверами, то для обеспечения работы сессий на разных серверах необходимо использовать базу данных.

Symfony может хранить сессии во всех типах баз данных (реляционных, NoSQL и ключ-значение), но для достижения наилучшей производительности рекомендуется использовать базы данных ключ-значение, например Redis.

Храните сессии в базе данных ключ-значение (Redis)

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

У вас есть два варианта использования Redis для хранения сессий:

Первый вариант, основанный на PHP, заключается в конфигурации обработчика сессий Redis непосредственно в файле сервера php.ini:

1
2
3
; php.ini
session.save_handler = redis
session.save_path = "tcp://192.168.0.178:6379?auth=REDIS_PASSWORD"

Второй вариант заключается в конфигурации сессий Redis в Symfony. Сначала определите сервис Symfony для подключения к серверу Redis:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <!-- вы также можете использовать классы \RedisArray, \RedisCluster или \Predis\Client -->
        <service id="Redis" class="Redis">
            <call method="connect">
                <argument>%env(REDIS_HOST)%</argument>
                <argument>%env(int:REDIS_PORT)%</argument>
            </call>

            <!-- раскомментируйте следующее, если ваш сервер Redis требует пароль:
            <call method="auth">
                <argument>%env(REDIS_PASSWORD)%</argument>
            </call> -->

            <!-- раскомментируйте следующее, если ваш сервер Redis требует пользователя и пароль (если пользователь не по умолчанию):
            <call method="auth">
                <argument>%env(REDIS_USER)%</argument>
                <argument>%env(REDIS_PASSWORD)%</argument>
            </call> -->
        </service>

        <service id="Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler">
            <argument type="service" id="Redis"/>
            <!-- вы можете по желанию передать массив опций. Единственными опциями являются 'prefix' и 'ttl',
                 которые определяют префикс для использования с ключами, чтобы избежать коллизий на сервере Redis
                 и время истечения срока действия для любой заданной записи (в секундах), по умолчанию - 'sf_s' и null:
            <argument type="collection">
                <argument key="prefix">my_prefix</argument>
                <argument key="ttl">600</argument>
            </argument> -->
        </service>
    </services>
</container>

Далее, используйте опцию конфигурации handler_id , чтобы указать Symfony использовать этот сервис как обработчик сессии:

1
2
3
4
5
# config/packages/framework.yaml
framework:
    # ...
    session:
        handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler

Теперь Symfony будет использовать ваш сервер Redis для чтения и записи данных сессии. Основным недостатком этого решения является то, что Redis не выполняет блокировку сессий, поэтому при доступе к сессиям можно столкнуться с условиями гонки. Например, вы можете увидеть ошибку "Невалидный CSRF-токен", поскольку два запроса были сделаны параллельно и только первый из них сохранил CSRF-токен в сессии.

See also

Если вместо Redis вы используете Memcached, то действуйте аналогичным образом, но замените RedisSessionHandler на MemcachedSessionHandler.

Храните сессии в реляционной базе данных (MariaDB, MySQL, PostgreSQL)

Symfony включает в себя PdoSessionHandler для хранения сессий в реляционных базах данных, таких как MariaDB, MySQL и PostgreSQL. Чтобы использовать его, сначала зарегистрируйте новый сервис-обработчик с учетными данными вашей базы данных:

1
2
3
4
5
6
7
8
9
10
11
# config/services.yaml
services:
    # ...

    Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
        arguments:
            - '%env(DATABASE_URL)%'

            # вы также можете использовать конфигурацию PDO, но это требует передачи двух аргументов
            # - 'mysql:dbname=mydatabase; host=myhost; port=myport'
            # - { db_username: myuser, db_password: mypassword }

Tip

При использовании MySQL в качестве базы данных, DSN, определённое в DATABASE_URL, может содержать опции charset и unix_socket как параметры строки запроса.

Далее, используйте опцию конфигурации handler_id , чтобы сообщить Symfony использовать этот сервис как обработчик сессии:

1
2
3
4
5
# config/packages/framework.yaml
framework:
    session:
        # ...
        handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler

Конфигурация имён таблицы и столбцов сессии

Таблица, используемая для хранения сессий, по умолчанию называется sessions и определяет имена некоторых столбцов. Вы можете сконфигурировать эти значения с помощью второго аргумента, переданного сервису PdoSessionHandler:

1
2
3
4
5
6
7
8
# config/services.yaml
services:
    # ...

    Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
        arguments:
            - '%env(DATABASE_URL)%'
            - { db_table: 'customer_session', db_id_col: 'guid' }

Вот параметры, которые вы можете сконфигурировать:

db_table (по умолчанию sessions):
Имя таблицы сессии в вашей базе данных;
db_username: (по умолчанию: '')
Имя пользователя, используемое для подключения при использовании конфигурации PDO (при использовании подключения на основе переменной окружения DATABASE_URL, она переопределяет имя пользователя, определенное в переменной окружения).
db_password: (по умолчанию: '')
Пароль, используемый для подключения при использовании конфигурации PDO (при использовании подключения на основе переменной окружения DATABASE_URL, она переопределяет пароль, определенный в переменной окружения).
db_id_col (по умолчанию: sess_id):
Имя столбца, где хранить ID сессии (тип столбца: VARCHAR(128));
db_data_col (по умолчанию: sess_data):
Имя столбца, где хранить данные сессии (тип столбца: BLOB);
db_time_col (по умолчанию: sess_time):
Имя столбца, где хранить временную отметку создания сессии (тип столбца: INTEGER);
db_lifetime_col (по умолчанию: sess_lifetime):
Имя столбца, где хранить время жизни сессии (тип столбца: INTEGER);
db_connection_options (default: [])
Массив опций подключения, специфичных для драйвера;
lock_mode (по умолчанию: LOCK_TRANSACTIONAL)
Стратегия блокировки базы данных для предотвращения состояния гонки. Возможные значения: LOCK_NONE (отсутствие блокировки), LOCK_ADVISORY (блокировка на уровне приложения) и LOCK_TRANSACTIONAL (блокировка на уровне рядов).

Подготовка базы данных к хранению сессий

Перед сохранением сессий в базе данных необходимо создать таблицу, в которой будет храниться информация. Обработчик сессий предоставляет метод под названием createTable() для создания таблицы в соответствии с используемым движком базы данных:

1
2
3
4
5
try {
    $sessionHandlerService->createTable();
} catch (\PDOException $exception) {
    // таблица не могла быть создана по какой-то причине
}

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

1
$ php bin/console doctrine:migrations:generate

Затем найдите ниже подходящий SQL для вашей базы данных, добавьте его в файл миграции и запустите миграцию с помощью следующей команды:

1
$ php bin/console doctrine:migrations:migrate
MariaDB/MySQL
1
2
3
4
5
6
7
CREATE TABLE `sessions` (
    `sess_id` VARBINARY(128) NOT NULL PRIMARY KEY,
    `sess_data` BLOB NOT NULL,
    `sess_lifetime` INTEGER UNSIGNED NOT NULL,
    `sess_time` INTEGER UNSIGNED NOT NULL,
    INDEX `sessions_sess_lifetime_idx` (`sess_lifetime`)
) COLLATE utf8mb4_bin, ENGINE = InnoDB;

Note

Тип столбца BLOB (который используется по умолчанию в createTable()) хранит до 64 кб. Если данные сессии пользователя превысят этот размер, то может быть вызвано исключение или сессия будет тихо сброшена. Рассмотрите возможность использования MEDIUMBLOB, если вам требуется больше места.

PostgreSQL
1
2
3
4
5
6
7
CREATE TABLE sessions (
    sess_id VARCHAR(128) NOT NULL PRIMARY KEY,
    sess_data BYTEA NOT NULL,
    sess_lifetime INTEGER NOT NULL,
    sess_time INTEGER NOT NULL
);
CREATE INDEX sessions_sess_lifetime_idx ON sessions (sess_lifetime);
Сервер Microsoft SQL
1
2
3
4
5
6
7
CREATE TABLE sessions (
    sess_id VARCHAR(128) NOT NULL PRIMARY KEY,
    sess_data NVARCHAR(MAX) NOT NULL,
    sess_lifetime INTEGER NOT NULL,
    sess_time INTEGER NOT NULL,
    INDEX sessions_sess_lifetime_idx (sess_lifetime)
);

Храните сессии в базе данных NoSQL (MongoDB)

Symfony включает в себя MongoDbSessionHandler для хранения сессий в NoSQL базе данных MongoDB. Прежде всего, убедитесь, что в вашем приложении Symfony есть рабочее соединение с MongoDB, как описано в статье Конфигурация DoctrineMongoDBBundle.

Затем, зарегистрируйте новый сервис-обработчик для MongoDbSessionHandler и передайте его соединению MongoDB как аргумент:

1
2
3
4
5
6
7
# config/services.yaml
services:
    # ...

    Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler:
        arguments:
            - '@doctrine_mongodb.odm.default_connection'

Далее, используйте опцию конфигурации handler_id , чтобы указать Symfony использовать этот сервис как обработчик сессии:

1
2
3
4
5
# config/packages/framework.yaml
framework:
    session:
        # ...
        handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler

Note

MongoDB ODM 1.x работает только с наследуемым драйвером, который больше не поддерживается классом сессии Symfony. Установите пакет alcaeus/mongo-php-adapter для получения основоположного объекта \MongoDB\Client или обновите пакет до версии MongoDB ODM 2.0.

Вот и все! Теперь Symfony будет использовать ваш сервер MongoDB для чтения и записи данных сессии. Вам не нужно ничего делать для инициализации вашей коллекции сессий. Однако для повышения производительности сбора мусора может потребоваться добавить индекс. Запустите это из оболочки MongoDB:

1
2
use session_db
db.session.createIndex( { "expires_at": 1 }, { expireAfterSeconds: 0 } )

Конфигурация имён полей сессии

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

1
2
3
4
5
6
7
8
# config/services.yaml
services:
    # ...

    Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler:
        arguments:
            - '@doctrine_mongodb.odm.default_connection'
            - { id_field: '_guid', 'expiry_field': 'eol' }

Вот параметры, которые вы можете сконфигурировать:

id_field (по умолчанию: _id):
Имя поля, где хранить ID сессии;
data_field (по умолчанию: data):
Имя поля, где хранить данные сессии;
time_field (по умолчанию: time):
Имя поля, где хранить временную отметку создания сессии;
expiry_field (по умолчанию: expires_at):
Имя поля, где хранить время жизни сессии.
.. index::
single: Сессии, сохранение локали

Миграция между обработчиками сессий

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

Вот рекомендуемый процесс миграции:

  1. Переключитесь на обработчик миграции, при этом ваш новый обработчик будет работать только для записи. Старый обработчик ведет себя как обычно, а сессии записываются в новый:

    1
    $sessionStorage = new MigratingSessionHandler($oldSessionStorage, $newSessionStorage);
  2. После окончания периода gc сессии проверьте правильность данных в новом обработчике.
  3. Обновите обработчик миграции, чтобы использовать старый обработчик только для записи, чтобы теперь сессии считывались из нового обработчика. Этот шаг позволяет упростить откаты:

    1
    $sessionStorage = new MigratingSessionHandler($newSessionStorage, $oldSessionStorage);
  4. Убедившись, что сессии в вашем приложении работают, переключитесь обработчика миграции на новый.

Конфигурация TTL сессии

Symfony по умолчанию будет использовать ini-настройку PHP session.gc_maxlifetime в качестве времени жизни сессии. Если вы храните сессии в базе данных, вы также можете настроить свой собственный TTL в конфигурации фреймворка или даже во время прогона.

Note

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

Сконфигурируйте TTL

Вам нужно передать TTL в массиве опций обработчика сессий, который вы используете:

1
2
3
4
5
6
7
# config/services.yaml
services:
    # ...
    Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler:
        arguments:
            - '@Redis'
            - { 'ttl': 600 }

Конфигурируйте TTL динамически во время прогона

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

1
2
3
4
5
6
7
8
9
10
11
12
13
# config/services.yaml
services:
    # ...
    Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler:
        arguments:
            - '@Redis'
            - { 'ttl': !closure '@my.ttl.handler' }

    my.ttl.handler:
        class: Some\InvokableClass # какой-то класс с методом an __invoke()
        arguments:
            # Внедрите нужные вам зависимости, чтобы иметь возможность разрешить TTL для текущей сессии
            - '@security'

Как сделать локаль "липкой" во время сессии пользователя

Symfony хранит настройку локали в запросе, что означает, что эта настройка не сохраняется автоматически (не "липкая") между запросами. Однако вы можете сохранить локаль в сессии, чтобы она использовалась при последующих запросах.

Создание LocaleSubscriber

Создайте нового подписчика событий . Обычно в качестве параметра маршрутизации используется _locale, чтобы указать локаль. Однако, вы можете определить правильную локаль любым способом:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// src/EventSubscriber/LocaleSubscriber.php
namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class LocaleSubscriber implements EventSubscriberInterface
{
    private $defaultLocale;

    public function __construct(string $defaultLocale = 'en')
    {
        $this->defaultLocale = $defaultLocale;
    }

    public function onKernelRequest(RequestEvent $event)
    {
        $request = $event->getRequest();
        if (!$request->hasPreviousSession()) {
            return;
        }

        // попробуйте увидеть, была ли локаль установлена как параметр маршрутизации _locale
        if ($locale = $request->attributes->get('_locale')) {
            $request->getSession()->set('_locale', $locale);
        } else {
            // если в этом запросе не было установлено четкой локали, используйте одну из сессии
            $request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
        }
    }

    public static function getSubscribedEvents()
    {
        return [
            // должно быть зарегистрировано ранее (т.е. с большим приоритетом, чем слушатель локали по умолчанию)
            KernelEvents::REQUEST => [['onKernelRequest', 20]],
        ];
    }
}

Если вы используете конфигурацию services.yaml по умолчанию , то все готово! Symfony автоматически узнает о подписчике событий и вызовет метод onKernelRequest. в каждом запросе.

Чтобы убедиться в его работоспособности, либо установите ключ _locale в сессии вручную (например, через какой-нибудь маршрут и контроллер "Change Locale"), либо создайте маршрут с
_locale по умолчанию .

Вы также можете ясно сконфигурировать его, чтобы передать в default_locale :

1
2
3
4
5
6
7
8
# config/services.yaml
services:
    # ...

    App\EventSubscriber\LocaleSubscriber:
        arguments: ['%kernel.default_locale%']
        # раскомментируйте следующую строку, если вы не используете автоконфигурацию
        # tags: [kernel.event_subscriber]

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

Помните, чтобы получить локаль пользователя, всегда используйте метод Request::getLocale:

1
2
3
4
5
6
7
// из контроллера...
use Symfony\Component\HttpFoundation\Request;

public function index(Request $request)
{
    $locale = $request->getLocale();
}

Установка локали, основываясь на предпочтениях пользователя

Возможно, вы захотите усовершенствовать эту технику еще больше и определить локаль на основе сущности пользователя, вошедшего в систему. Однако, поскольку LocaleSubscriber вызывается перед FirewallListener, который отвечает за обработку аутентификации и установку токена пользователя в TokenStorage, вы не имеете доступа к пользователю, который вошел в систему.

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

Для этого необходимо создать подписчика события в событии security.interactive_login.:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// src/EventSubscriber/UserLocaleSubscriber.php
namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;

/**
 * Сохраняет локаль пользователя в сессии посе входа в систему.
 * Это может быть использовано позже LocaleSubscriber.
 */
class UserLocaleSubscriber implements EventSubscriberInterface
{
    private $requestStack;

    public function __construct(RequestStack $requestStack)
    {
        $this->requestStack = $requestStack;
    }

    public function onInteractiveLogin(InteractiveLoginEvent $event)
    {
        $user = $event->getAuthenticationToken()->getUser();

        if (null !== $user->getLocale()) {
            $this->requestStack->getSession()->set('_locale', $user->getLocale());
        }
    }

    public static function getSubscribedEvents()
    {
        return [
            SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin',
        ];
    }
}

Caution

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

Прокси сессии

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

Затем определите класс как сервис . Если вы используете конфигурацию services.yaml по умолчанию , то это произойдет автоматически.

Наконец, используйте опцию конфигурации framework.session.handler_id, чтобы указать Symfony использовать ваш обработчик сессий вместо стандартного:

1
2
3
4
5
# config/packages/framework.yaml
framework:
    session:
        # ...
        handler_id: App\Session\CustomSessionHandler

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

Шифрование данных сессии

Если вы хотите зашифровать данные сессии, вы можете использовать прокси для шифрования и расшифровки сессии по необходимости. В следующем примере используется библиотека php-encryption, но вы можете адаптировать его к любой другой библиотеке, которую вы используете:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// src/Session/EncryptedSessionProxy.php
namespace App\Session;

use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;

class EncryptedSessionProxy extends SessionHandlerProxy
{
    private $key;

    public function __construct(\SessionHandlerInterface $handler, Key $key)
    {
        $this->key = $key;

        parent::__construct($handler);
    }

    public function read($id)
    {
        $data = parent::read($id);

        return Crypto::decrypt($data, $this->key);
    }

    public function write($id, $data)
    {
        $data = Crypto::encrypt($data, $this->key);

        return parent::write($id, $data);
    }
}

Гостевые сессии только для чтения

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// src/Session/ReadOnlySessionProxy.php
namespace App\Session;

use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;

class ReadOnlySessionProxy extends SessionHandlerProxy
{
    private $security;

    public function __construct(\SessionHandlerInterface $handler, Security $security)
    {
        $this->security = $security;

        parent::__construct($handler);
    }

    public function write($id, $data)
    {
        if ($this->getUser() && $this->getUser()->isGuest()) {
            return;
        }

        return parent::write($id, $data);
    }

    private function getUser()
    {
        $user = $this->security->getUser();
        if (is_object($user)) {
            return $user;
        }
    }
}

Интеграция с наследуемыми приложениями

Если вы интегрируете полный стек фреймворка Symfony в наследуемое приложение, которое запускает сессию с помощью session_start(), вы все еще можете использовать управление сессиями Symfony, используя сессию PHP Bridge.

Если приложение имеет собственный обработчик сохранения PHP, то можно указать null для handler_id:

1
2
3
4
5
# config/packages/framework.yaml
framework:
    session:
        storage_factory_id: session.storage.factory.php_bridge
        handler_id: ~

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

1
2
3
4
5
# config/packages/framework.yaml
framework:
    session:
        storage_factory_id: session.storage.factory.php_bridge
        handler_id: session.handler.native_file

Note

Если унаследованное приложение требует собственного обработчика сохранения сессий, не следует переопределять его. Вместо этого установите handler_id: ~. Обратите внимание, что обработчик сохранения не может быть изменен после запуска сессии. Если приложение запускает сессию до инициализации Symfony, то обработчик сохранения будет уже установлен. В этом случае потребуется handler_id: ~. Переопределяйте обработчик сохранения только в том случае, если вы уверены, что унаследованное приложение может использовать обработчик сохранения Symfony без побочных эффектов и что сессия не была запущена до инициализации Symfony.