Глубокое погружение в сессии Laravel

Источник: «A Deep Dive into Sessions in Laravel»
При создании приложений Laravel почти гарантированно придётся иметь дело с сессиями. Они являются фундаментальной частью веб-разработки.

В статье мы рассмотрим, что такое сессии, как они работают в Laravel, и как с ними взаимодействовать в приложениях Laravel.

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

В заключение рассмотрим, как тестировать данные сессии в Laravel.

Что такое сессии

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

В двух словах, сессии — безопасный способ хранения данных на протяжении нескольких запросов.

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

Данные сессии могут храниться в различных местах, например:

Как работают сессии в Laravel

Чтобы понять, что такое сессии, давайте рассмотрим, как они работают в Laravel.

Вот пример данных, которые можно найти внутри сессии в приложении Laravel:

[
'_token' => 'bKmSfoegonZLeIe8B6TWvSm1dKwftKsvcT40xaaW'
'_previous' => [
'url' => 'https://my-app.com/users'
]
'_flash' => [
'old' => [
'success',
],
'new' => []
]
'success' => 'User created successfully.'
'current_team_id' => 123
]

Давайте разберёмся, что представляет каждый из этих ключей.

Следующие ключи были добавлены самим фреймворком Laravel:

Следующие ключи были добавлены мной:

По умолчанию Laravel поддерживает следующие драйверы сессий:

У некоторых из этих драйверов есть требования к настройке. Поэтому перед их использованием необходимо ознакомиться с документацией Laravel, чтобы узнать, как их настроить.

Работа с сессиями в Laravel

Laravel делает работу с сессиями простой и удобной. Документация отлично объясняет, как с ними взаимодействовать. Но давайте быстро вспомним основы.

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

Чтобы не усложнять примеры, будем использовать хелпер session(). Но о доступе к данным сессии с помощью фасада Session или класса запроса поговорим позже.

Чтение данных из сессии

Чтобы прочитать данные из сессии, можно использовать метод get следующим образом:

$currentStep = session()->get(key: 'wizard:current_step');

Выполнение приведённого выше кода вернёт значение, хранящееся в сессии для ключа wizard:current_step. Если в сессии нет значения для этого ключа, то будет возвращён null.

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

$currentStep = session()->get(key: 'wizard:current_step', default: 1);

Выполнение приведённого выше кода вернёт значение, хранящееся в сессии для ключа wizard:current_step. Если в сессии нет значения для этого ключа, то будет возвращено 1.

Бывают случаи, когда нужно прочитать данные из сессии и одновременно удалить их (чтобы к ним нельзя было обратиться снова). Для этого можно использовать функцию pull:

$currentStep = session()->pull(key: 'wizard:current_step');

Выполнение приведённого выше кода вернёт значение, хранящееся в сессии для ключа wizard:current_step, а затем удалит его из сессии.

Запись данных в сессию

Для записи данных в сессию можно использовать функцию put:

session()->put(
key: 'wizard:step_one:form_data',
value: [
'name' => 'Ash Allen',
'email' => 'ash@example.com',
],
);

Выполнение приведённого выше кода сохранит массив (переданный во втором аргументе) в качестве значения для ключа wizard:step_one:form_data.

Передача данных в массив в сессии

Аналогичным образом можно переместить данные в массив в сессию с помощью метода push:

session()->push(
key: 'wizard:step_one:form_data:languages',
value: 'javascript',
);

Предположим, что ключ wizard:step_one:form_data:languages содержит следующие данные:

[
`php`,
]

Приведённый выше код (вызывающий метод push) обновит значение сессии до:

[
`php`,
`javascript`,
]

Если значение wizard:step_one:form_data:languages ещё не существовало в сессии, вызов push создаст ключ сессии и установит значение в массив с переданным значением.

Увеличение и уменьшение данных в сессии

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

Увеличить значение в сессии можно следующим образом:

session()->increment(key: 'wizard:current_step');

При выполнении приведённого выше кода, если значение сессии wizard:current_step было 3, то теперь оно увеличится до 4.

Также можно уменьшать значение в сессии следующим образом:

session()->decrement(key: 'wizard:current_step');

Если значение в сессии ещё не существует, оно будет считаться равным 0. Таким образом, вызов increment для пустого значения сессии установит значение 1. Вызов decrement для пустого значения сессии установит значение -1.

Оба этих метода позволяют также указать величину инкремента или декремента:

session()->increment(key: 'wizard:current_step', amount: 2);
session()->decrement(key: 'wizard:current_step', amount: 2);

Удаление данных из сессии

Кроме того, данные из сессии можно удалить с помощью метода forget:

session()->forget(keys: 'wizard:current_step');

Выполнение приведённого выше кода приведёт к удалению из сессии данных, относящихся к ключу wizard:current_step.

Если необходимо удалить сразу несколько ключей, можно передать функции forget массив ключей:

session()->forget(keys: [
'wizard:current_step',
'wizard:step_one:form_data',
]);

Или, если необходимо удалить все данные из сессии, можно воспользоваться функцией flush:

session()->flush();

Проверка существования данных в сессии

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

С помощью метода has можно проверить, существует ли ключ в сессии и его значение не равно null:

session()->has(key: 'wizard:current_step');

Если значение существует и не является null, то приведённый выше код вернёт true. Если значение равно null или ключ не существует, вернётся false.

Аналогично, можно использовать метод exists, чтобы проверить, существует ли ключ в сессии (независимо от того, равно ли его значение null):

session()->exists(key: 'wizard:current_step');

Также можно проверить, не существует ли ключ в сессии:

session()->missing(key: 'wizard:current_step');

Передача данных в сессию

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

Для этого можно использовать метод flash:

session()->flash(
key: 'success',
value: 'Your form has been submitted successfully!',
);

Если выполнить приведённый выше код, то в следующем запросе можно прочитать его значение из сессии (используя что-то вроде session()->get('success')) для отображения. Затем оно будет удалено, так что в последующем запросе оно будет недоступно.

Бывают случаи, когда есть некоторые флеш-данные ( добавленные в предыдущем запросе), и их необходимо сохранить для следующего запроса.

С помощью метода reflash можно обновить все флеш-данные:

session()->reflash();

Или, если необходимо сохранить только часть флеш-данных, можно воспользоваться методом keep:

session()->keep(keys: [
'success',
'error',
]);

Выполнение приведённого выше кода сохранит флэш-данные сессии success и error, но удалит все остальные.

Хелпер, фасад или класс Request

До сих пор в примерах использовался только хелпер session().

Но также с сессиями можно взаимодействовать, используя фасад Illuminate\Support\Facades\Session или класс Illuminate\Http\Request.

Независимо от того, какой из этих подходов используется, вы всё равно сможете применять те же методы, которые рассматривались ранее в статье. Эти подходы — просто разные способы взаимодействия с данными сессии.

Для использования фасада Session можно вызвать методы следующим образом:

use Illuminate\Support\Facades\Session;

Session::get('key');
Session::put('key', 'value');
Session::forget('key');

В качестве альтернативы, доступ к сессии можно получить, вызвав метод сессии на экземпляре Illuminate\Http\Request, который внедряется в методы контроллера. Допустим, есть метод контроллера вроде этого:

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class WizardController extends Controller
{
public function store(Request $request)
{
$request->session()->get('key');
$request->session()->put('key', 'value');
$request->session()->forget('key');

// Остальная часть метода...
}
}

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

Идём дальше

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

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

Опечатки в ключах сессии

Одна из распространённых ловушек, которую я вижу (да и сам не раз сталкивался), — это опечатки в ключах сессии.

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

session()->put(key: 'wizard:current_step', value: 1);

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

$currentStep = session()->get(key: 'wizard:step');

Вы заметили ошибку, которую я только что допустил? Я случайно пытаюсь прочитать ключ wizard:step вместо wizard:current_step.

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

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

Например, если ключ сессии статичен, можно определить константу (потенциально в классе сессии, который скоро рассмотрим) следующим образом:

class WizardSession
{
// ...

private const WIZARD_CURRENT_STEP = 'wizard:current_step';

public function currentStep(): int
{
return session()->get(key: self::WIZARD_CURRENT_STEP);
}

public function setCurrentStep(int $step): void
{
session()->put(key: self::WIZARD_CURRENT_STEP, value: $step);
}
}

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

Однако иногда может потребоваться динамическая генерация ключа сессии. Допустим, необходимо, чтобы ключ wizard:current_step включал поле ID команды. Можно создать метод для генерации ключа следующим образом:

use App\Models\Team;

class WizardSession
{
// ...

public function currentStep(Team $team): int
{
return session()->get(key: $this->currentStepKey($team));
}

public function setCurrentStep(Team $team, int $step): void
{
session()->put(key: $this->currentStepKey($team), value: $step);
}

private function currentStepKey(Team $team): string
{
return 'wizard:'.$team->id.':current_step';
}
}

Как видно из приведённого выше кода, ключ сессии генерируется динамически, чтобы его можно было использовать в различных методах. Например, если попытаться найти текущий шаг для команды с ID 1, ключ будет wizard:1:current_step.

Конфликты ключей сессии

Ещё один подводный камень, который я наблюдаю при работе над проектами, существующими уже долгое время, — это конфликты ключей сессий.

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

session()->put(key: 'wizard:current_step', value: 1);

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

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

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

О том, как добавлять эти префиксы в классы сессий, поговорим далее.

Неизвестные типы данных

Подскажите, какой тип данных хранится в этом значении сессии?

$formData = session()->get(key: 'wizard:step_one:form_data');

Если вы догадались, что это экземпляр App\DataTransferObjects\Wizards\FormData, то не ошиблись.

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

Можно добавить аннотацию или docblock в код, читающий данные из сессии. Но это лишь подсказка. Если аннотация не обновляется (если тип данных сессии изменился), то она не поможет и увеличит вероятность ошибок.

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

Для примера возьмём такой код:

use App\DataTransferObjects\Wizards\FormData;

class WizardSession
{
// ...

public function stepOneFormData(): FormData
{
return session()->get(key: 'wizard:step_one:form_data');
}
}

Теперь сразу видно, что метод stepOneFormData возвращает экземпляр App\DataTransferObjects\Wizards\FormData. Это позволяет понять, с каким типом данных мы работаем. Затем можно вызвать этот метод в контроллере следующим образом:

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class WizardController extends Controller
{
public function store(Request $request, WizardSession $wizardSession)
{
$stepOneFormData = $wizardSession->stepOneFormData();

// Остальная часть метода...
}
}

Обработка данных сессии в классах сессии

Как видно из нескольких последних разделов, при работе с сессиями в Laravel есть несколько легко устранимых (но распространённых) подводных камней.

Каждого из этих подводных камней можно избежать (или, по крайней мере, уменьшить их количество), используя «классы сессий». Мне нравится использовать классы сессий для инкапсуляции логики работы с данными сессии, связанными с одной функцией, в одном месте.

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

Используя классы сессий, можно:

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

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

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

declare(strict_types=1);

namespace App\Sessions\Users;

use App\DataTransferObjects\Wizards\Users\FormData;
use Illuminate\Contracts\Session\Session;

final class WizardSession
{
public function __construct(private Session $session)
{
//
}

public function getCurrentStep(): int
{
return $this->session->get(
key: $this->currentStepKey(),
default: 1
);
}

public function setCurrentStep(int $step): void
{
$this->session->put(
key: $this->currentStepKey($step),
value: $step,
);
}

public function setFormDataForStep(int $step, FormData $formData): void
{
$this->session->put(
key: $this->formDataForStepKey($step),
value: $formData,
);
}

public function getFormDataForStep(int $step): ?FormData
{
return $this->session->get(
key: $this->formDataForStepKey($step),
);
}

public function flush(): void
{
$this->session->forget([
$this->currentStepKey(),
$this->formDataForStepKey(1),
$this->formDataForStepKey(2),
$this->formDataForStepKey(3),
]);
}

private function currentStepKey(): string
{
return $this->sessionKey('current_step');
}

private function formDataForStepKey(int $step): string
{
return $this->sessionKey('step:'.$step.':form_data');
}

private function sessionKey(string $key): string
{
return 'new_user_wizard:'.$key;
}
}

В классе App\Sessions\Users\WizardSession выше мы начали с определения конструктора, принимающего экземпляр Illuminate\Contracts\Session\Session. Благодаря этому, когда мы разрешим класс App\Sessions\Users\WizardSession из сервис-контейнера, Laravel автоматически внедрит экземпляр сессии. Я покажу как сделать это в контроллерах.

Мы определили 5 основных публичных методов:

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

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

Теперь этот класс настроен, давайте посмотрим, как можно взаимодействовать с ним в контроллерах:

use App\Http\Controllers\Controller;
use App\Sessions\Users\WizardSession;
use Illuminate\Http\Request;

class WizardController extends Controller
{
public function store(Request $request, WizardSession $wizardSession)
{
// Получение текущего шага в визарде
$currentStep = $wizardSession->getCurrentStep();

// Обновление текущего шага в визарде
$wizardSession->setCurrentStep(2);

// Получение данных формы для заданного шага
$formData = $wizardSession->getFormDataForStep(step: $currentStep);

// Установка данных формы для заданного шага
$wizardSession->setFormDataForStep(
step: $currentStep,
formData: new FormData(
name: 'Ash Allen',
email: 'ash@example.com',
),
);

// Очистка всех данных визарда из сессии
$wizardSession->flush();
}
}

Как видите, в примере выше в метод контроллера внедряется класс App\Sessions\Users\WizardSession. Laravel автоматически разрешает экземпляр сессии для нас.

После этого с ним можно взаимодействовать так же, как с любым другим классом.

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

Тестирование сессий в Laravel

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

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

Например, можно написать несколько тестов для метода getFormDataForStep класса App\Sessions\Users\WizardSession. Вот метод код метода:

declare(strict_types=1);

namespace App\Sessions\Users;

use App\DataTransferObjects\Wizards\Users\FormData;
use Illuminate\Contracts\Session\Session;

final class WizardSession
{
public function __construct(private Session $session)
{
//
}

public function getFormDataForStep(int $step): ?FormData
{
return $this->session->get(
key: $this->formDataForStepKey($step),
);
}

private function formDataForStepKey(int $step): string
{
return $this->sessionKey('step:'.$step.':form_data');
}

private function sessionKey(string $key): string
{
return 'new_user_wizard:'.$key;
}
}

Есть несколько сценариев, которые можно протестировать:

Класс теста может выглядеть следующим образом:

declare(strict_types=1);

namespace Tests\Feature\Sessions\Users\WizardSession;

use App\DataTransferObjects\Wizards\Users\FormData;
use App\Sessions\Users\WizardSession;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

final class GetFormDataForStepTest extends TestCase
{
#[Test]
public function form_data_can_be_returned_for_a_step(): void
{
// Пропишите ключ сессии, чтобы быть уверенными, что он правильно
// генерируется/используется в классе сессии.
$sessionKey = 'new_user_wizard:step:1:form_data';

$formData = new FormData(
name: 'Ash Allen',
email: 'ash@example.com',
);

// Сохранение данных формы в сессии.
session()->put(key: $sessionKey, value: $formData);

// Сохраните случайные данные для другого шага, чтобы убедиться,
// что получаем данные только для того шага, который запрашиваем.
session()->put(
key: 'new_user_wizard:step:2:form_data',
value: 'dummy data',
);

// Чтение данных из сессии
$formData = app(WizardSession::class)->getFormDataForStep(1);

// Проверка корректности данных
$this->assertInstanceOf(FormData::class, $formData);
$this->assertSame('Ash Allen', $formData->name);
$this->assertSame('ash@example.com', $formData->email);
}

#[Test]
public function null_is_returned_if_no_form_data_exists_for_a_step(): void
{
// Сохраняем случайные данные для другого шага, чтобы убедиться,
// что получим данные только для того шага, который запрашиваем.
session()->put(
key: 'new_user_wizard:step:2:form_data',
value: 'dummy data',
);

// Чтение данных из сессии и сравнивание с null.
$this->assertNull(
app(WizardSession::class)->getFormDataForStep(1)
);
}
}

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

Эти тесты в стиле unit-тестов отлично подходят, чтобы убедиться, что класс сессии правильно настроен для чтения и записи данных в сессию. Но они не всегда дают уверенность в том, что правильно используются в остальной части кодовой базы. Например, можно вызвать getFormDataForStep(1), когда нужно было вызвать getFormDataForStep(2).

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

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

declare(strict_types=1);

namespace App\Http\Controllers\Users;

use App\Http\Controllers\Controller;
use App\Http\Requests\Users\Wizard\NextStepRequest;
use App\Sessions\Users\WizardSession;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

final class WizardController extends Controller
{
public function nextStep(
NextStepRequest $request,
WizardSession $wizardSession
): RedirectResponse {
$currentStep = $wizardSession->getCurrentStep();

$wizardSession->setFormDataForStep(
step: $currentStep,
formData: $request->toDto(),
);

$wizardSession->setCurrentStep($currentStep + 1);

return redirect()->route('users.wizard.step');
}
}

В приведённом выше методе сначала считывается текущий шаг из сессии. Затем сохраняются данные формы для текущего шага в сессии. Наконец, увеличиваем текущий шаг и перенаправляем на следующий шаг визарда.

Предположим, что класс App\Http\Requests\Users\Wizard\NextStepRequest отвечает за валидацию данных формы и возвращает экземпляр App\DataTransferObjects\Wizards\Users\FormData, когда вызывается метод toDto.

Предположим также, что метод контроллера nextStep доступен через POST-запрос по маршруту /users/wizard/next-step (с именем users.wizard.next-step).

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

declare(strict_types=1);

namespace Tests\Feature\Http\Controllers\Users\WizardController;

use App\DataTransferObjects\Wizards\Users\FormData;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

final class NextStepTest extends TestCase
{
#[Test]
public function user_is_redirected_to_the_next_step_in_the_wizard(): void
{
$this->withSession([
'new_user_wizard:current_step' => 2,
])
->post(route('users.wizard.next-step'), [
'name' => 'Ash Allen',
'email' => 'ash@example.com'
])
->assertRedirect(route('users.wizard.step'))
->assertSessionHas('new_user_wizard:current_step', 3);

// Можно использовать `assertSessionHas` для чтения данных сессии.
// В качестве альтернативы можно прочитать данные сессии напрямую.
$formData = session()->get('new_user_wizard:step:2:form_data');

$this->assertInstanceOf(
expected: FormData::class,
actual: $formData,
);
$this->assertSame('Ash Allen', $formData->name);
$this->assertSame('ash@example.com', $formData->email);
}

// Другие тесты...
}

В приведённом выше тесте выполняется POST-запрос по маршруту /users/wizard/next-step с данными формы. Возможно, вы заметили, что используется метод withSession. Этот метод позволяет установить данные сессии, чтобы можно было убедиться, в правильности их считывания.

Мы утверждаем, что пользователь перенаправлен на следующий шаг визарда и текущий шаг в сессии установлен на 3. Также утверждаем, что данные формы для шага 2 сохранены в сессии правильно.

Как видно из теста, чтение из сессии происходит двумя способами:

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

Заключение

Надеюсь, эта статья дала представление, что такое сессии и как они работают в Laravel. Также надеюсь, что она дала несколько идей о том, как использовать основанный на классах подход к взаимодействию с данными сессии, чтобы избежать распространённых подводных камней.

Комментарии


Дополнительные материалы

Предыдущая Статья

Новое в Symfony 7.2: Улучшения в Mime

Следующая Статья

Руководство по пагинации в Laravel 11