Сесії

Дата оновлення перекладу 2024-06-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
use Symfony\Component\HttpFoundation\RequestStack;

class SomeService
{
    public function __construct(
        private RequestStack $requestStack,
    ) {
        // Доступ до сесії у конструкторі *НЕ* рекомендовано, так як
        // вона може бути ще недоступна або це призведе до небажаних побічних ефектів        public function __construct(RequestStack $requestStack)
        // $this->session = $requestStack->getSession();
    }

    public function someMethod(): void
    {
        $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()) {
        // провести якусь обробку

        $this->addFlash(
            'notice',
            'Your changes were saved!'
        );
        // $this->addFlash() еквівалентно $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. Хоча це зазвичай все спрощує, деякі опції, пов'язані із завершенням дії сесії, можуть працювати не так, як очікувалося, якщо інші додатки, які пишуть у той самий каталог, мають короткі налаштування максимального часу життя.

Якщо ви бажаєте, ви можете використовувати сервіс сесія.обробник.рідний_файл як 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, щоб дізнатися білье про інші доступні опції конфігурації Сесії .

Caution

Сесії Symfony несумісні з директивою php.ini ession.auto_start = 1. Цю директиву слід вимкнути в php.ini, в директивах веб-сервера або в .htaccess.

Куки сесії також доступні в об'єкті Response . Це корисно для отримання цього файлу куки в контексті CLI або при використанні PHP-бігунів таких як Roadrunner або Swoole.

Час простою/підтримки життя сесії

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

Інший варіант полягає у конкретній перевірці того, чи завершилася сесія після її запуску. Сесія може бути знищена за необхідності. Цей метод обробки може дозволити інтегрувати завершення строку дії сесій у досвід користувача, наприклад, шляхом відображення повідомлення.

Symfony записує деякі метадані про кожну сесію, щоб надати вам потужний контроль над налаштуваннями безпеки:

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

Обидва методи повертають часову відмітку Unix (відносну до сервера).

Ці метадані можуть бути використані, щоб чітко завершити строк дії сесії при отриманні доступу до неї:

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();

Термін дії куки можна визначити, додавши створену відмітку часу та термін дії.

Конфігурація колекції сміття

Коли відкривається сесія, PHP викликає обробник gc рандомно відповідно до ймовірності, заданої session.gc_probability / session.gc_divisor. Наприклад, якщо ці значення були встановлені як 5/100 відповідно, це означатиме ймовірність у 5%. Аналогічно, 3/4 означатиме ймовірність виклику 3 з 4, тобто 75%.

Якщо викликається обробник збору сміття, PHP передасть значення, що зберігається у директів php.ini.
session.gc_maxlifetime. В даному контексті це означає, що

що будь- яка збережена сесія, яка була збережена більше, ніж gc_maxlifetime тому, повинна бути видалена. Це дозволяє видаляти записи на основі часу простою.

Втім, деякі операційні системи (наприклад, Debian) виконують власну обробку сесій і встановлюють змінну session.gc_probability як 0, щоб PHP не виконував збірку сміття. збір сміття. Ось чому Symfony тепер перезаписує це значення як 1.

Якщо ви хочете використовувати оригінальне значення, встановлене у вашому php.ini, додайте наступну конфігурацію:

1
2
3
4
5
# config/packages/framework.yaml
framework:
    session:
        # ...
        gc_probability: null

Ви можете сконфігурувати ці параметри, передавши gc_probability, gc_divisor та gc_maxlifetime у масиві до конструктора NativeSessionStorage або до метода :method:Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage::setOptions`.

Зберігайте сесії у базі даних

За замовчуванням 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
# config/services.yaml
services:
    # ...
    Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler:
        arguments:
            - '@Redis'
            # ви можете за бажанням передати масив опцій. Єдиними опціями є 'prefix' та 'ttl',
            # що визначають префікс для використання з ключами, щоб уникнути суперечностей на сервері Redis
            # та час закінчення строку дії для будь-якого заданого запису (в секундах), за замовчуванням - 'sf_s' та null:
            # - { 'prefix': 'my_prefix', 'ttl': 600 }

    Redis:
        # you can also use \RedisArray, \RedisCluster or \Predis\Client classes
        class: Redis
        calls:
            - connect:
                - '%env(REDIS_HOST)%'
                - '%env(int:REDIS_PORT)%'

            # розкоментуйте наступне, якщо вашому серверу Redis потрібен пароль
            # - auth:
            #     - '%env(REDIS_PASSWORD)%'

            # розкоментуйте наступне, якщо вашому серверу Redis потрібен користувач та пароль (коли користувач не за замовчуванням)
            # - auth:
            #     - ['%env(REDIS_USER)%','%env(REDIS_PASSWORD)%']

Далі, використайте опцію конфігурації 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

Якщо ви використовуєте Memcached замість Redis, дотримуйтесь схожого підходу, але замініть RedisSessionHandler на MemcachedSessionHandler.

Tip

При використанні Redis з DSN в опції конфігурації handler_id , ви можете додати опції prefix і ttl як параметри рядка запиту в DSN.

Зберігайте сесії у реляційній базі даних (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

Конфігурація імен таблиці та стовпців сесії

Таблиця, що використовується для зберігання сесій, за замовчуванням називається сесії і визначає певні назви стовпців. Ви можете налаштувати ці значення за допомогою другого аргументу, переданого сервісу 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 (за замовчуванням: [])
Масив опцій зʼєднання, повʼязаних з драйверами;
lock_mode (за замовчуванням: LOCK_TRANSACTIONAL)
Стратегія блокування бази даних, щоб уникнути стану гонитви. Можливі значення - LOCK_NONE (без блокування), LOCK_ADVISORY (блокування на рівні додатку) та LOCK_TRANSACTIONAL (бокування на рівні рядку).

Підготовка бази даних до збереження сесій

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

Якщо Doctrine встановлено, таблиця сесій буде автоматично згенерована, коли ви запускаєте команду make:migration, якщо база даних, на яку спрямовано Doctrine, ідентична до тієї, що використовується цим компонентом.

Або якщо ви бажаєте створити таблицю самостійно, а таблицю ще не було створено, обробник сесії надає метод, який називається 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

При необхідності ви також можете додати цю таблицю до вашої схеми, викликавши метод configureSchema() у вашому коді.

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 для зберігання сесій в базі даних MongoDB NoSQL. По-перше, переконайтеся, що у вас є робоче з'єднання з MongoDB у вашому додатку Symfony, як описано у статті конфігурація DoctrineMongoDBBundle.

Потім зареєструйте новий сервіс-обробник для MongoDbSessionHandler і передайте йому з'єднання з MongoDB як аргумент, та обовʼязкові параметри:

database:
Імʼя бази даних
collection:
Імʼя колекції
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'
            - { database: '%env(MONGODB_DB)%', collection: 'sessions' }

Далі, використайте опцію конфігурації handler_id , щоб вказати Symfony використовувати цей сервіс як обробник сесії:

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

Це все! Тепер 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
9
10
11
12
# config/services.yaml
services:
    # ...

    Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler:
        arguments:
            - '@doctrine_mongodb.odm.default_connection'
            -
                database: '%env(MONGODB_DB)%'
                collection: 'sessions'
                id_field: '_guid'
                expiry_field: 'eol'

Ось параметри, які ви можете сконфігурувати:

id_field (за замовчуванням: _id):
Імʼя поля, де зберігати ID сесії;
data_field (за замовчуванням: data):
Імʼя поля, де зберігати дані сесії;
time_field (за замовчуванням: time):
Імʼя поля, де зберігати часову відмітку створення сесії;
expiry_field (за замовчуванням: expires_at):
Імʼя поля, де зберігати час життя сесії.

Міграція між обробниками сесій

Якщо ваш додаток змінює спосіб зберігання сесій, використовуйте 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
// 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
{
    public function __construct(
        private string $defaultLocale = 'en',
    ) {
    }

    public function onKernelRequest(RequestEvent $event): void
    {
        $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
// from a controller...
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
// src/EventSubscriber/UserLocaleSubscriber.php
namespace App\EventSubscriber;

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

/**
 * Зберігає локаль користувача в сесії після входу в систему.
 * Це може бути використано LocaleSubscriber пізніше.
 */
class UserLocaleSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private RequestStack $requestStack,
    ) {
    }

    public function onLoginSuccess(LoginSuccessEvent $event): void
    {
        $user = $event->getUser();

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

    public static function getSubscribedEvents(): array
    {
        return [
            LoginSuccessEvent::class => 'onLoginSuccess',
        ];
    }
}

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
// 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
{
    public function __construct(
        private \SessionHandlerInterface $handler,
        private Key $key
    ) {
        parent::__construct($handler);
    }

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

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

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

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

Ще однією можливістю зашифрувати дані сесії є декорування сервісу session.marshaller, який вказує на MarshallingSessionHandler. Ви можете декорувати цей обробник маршалером, який використовує шифрування, на зразок SodiumMarshaller.

По-перше, вам потрібно згенерувати безпечний ключ і додати його до вашого сховища секретів як SESSION_DECRYPTION_FILE:

1
$ php -r 'echo base64_encode(sodium_crypto_box_keypair());'

Потім зареєструйте сервіс SodiumMarshaller за допомогою цього ключа:

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

    # ...
    Symfony\Component\Cache\Marshaller\SodiumMarshaller:
        decorates: 'session.marshaller'
        arguments:
            - ['%env(file:resolve:SESSION_DECRYPTION_FILE)%']
            - '@.inner'

Danger

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

Гостьові сесії тільки для читання

Існують додатки, у яких сесія потрібна для гостьових користувачів, але де немає особливої потреби у збереженні сесії. У цьому випадку ви можете перехопити сесію до її запису:

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
{
    public function __construct(
        private \SessionHandlerInterface $handler,
        private Security $security
    ) {
        parent::__construct($handler);
    }

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

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

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

        return null;
    }
}

Інтеграція з успадкованими додатками

Якщо ви інтегруєте повностековий фреймворк 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.