A Deep Dive into Sessions in Laravel
Last updated on by Ashley Allen
When building Laravel applications, it's almost guaranteed you'll need to deal with sessions at some point. They are a fundamental part of web development.
In this article, we will quickly cover what sessions are, how they work in Laravel, and how you can work with them in your Laravel applications.
We're then going to take it further and delve into how you can interact with sessions using "session classes" to avoid common pitfalls that I often see when working on Laravel apps.
Finally, we will look at how you can test session data in Laravel.
What are Sessions?
By default, web applications are stateless, meaning that requests are generally not aware of each other. So, we need a way to store data between the requests. For example, when a user logs into a website, we need to remember that they're logged in for the duration of their visit. This is where sessions come in.
In a nutshell, sessions are a secure way to persist data across multiple requests.
The session data might be used to store things like:
- User authentication status.
- Temporary data that can be accessed on another page.
- Flash messages that are displayed to the user.
The session data may be stored in a variety of places, such as:
- Cookies
- Databases
- Cache stores (such as Redis)
How do Sessions Work in Laravel?
To understand what sessions are, let's take a look at how they work in Laravel.
Here is some example data you might find inside a session in a Laravel app:
[ '_token' => 'bKmSfoegonZLeIe8B6TWvSm1dKwftKsvcT40xaaW' '_previous' => [ 'url' => 'https://my-app.com/users' ] '_flash' => [ 'old' => [ 'success', ], 'new' => [] ] 'success' => 'User created successfully.' 'current_team_id' => 123]
Let's break down what each of these keys might represent.
The following keys were added by the Laravel framework itself:
- The
_token
value is used to protect against CSRF attacks. - The
_previous.url
value is used to store the URL of the previous request. - The
_flash.old
value is used to store the keys of the flashed session data from the previous request. In this case, it's stating that thesuccess
value was flashed in the previous request. - The
_flash.new
value is used to store the keys of the flashed session data for the current request.
The following keys were added by me:
- The
success
value is used to store a success message that might be displayed to the user. - The
current_team_id
value is used to store the ID of the current team the user is viewing.
By default, Laravel supports the following session drivers:
-
cookie
- session data is stored in secure and encrypted cookies. -
database
- sessions are stored in your database (such as MySQL, PostgreSQL, SQLite). -
memcached
/redis
- sessions data is stored in these fast cache stores. -
dynamodb
- session data is stored in AWS DynamoDB. -
file
- session data is stored instorage/framework/sessions
. -
array
- session data is stored in an in-memory PHP array and will not be persisted.
Some of these drivers have setup requirements. So it's important to check the Laravel documentation to see how to set them up before using them.
Working with Sessions in Laravel
Laravel makes working with sessions nice and simple. The documentation does a great job of explaining how to interact with sessions. But, let's quickly recap the basics.
For our examples, we'll make the assumption we're building a step-by-step wizard that spans multiple pages. We'll store the current step and the data entered on each step in the session. This way, we can read all the submitted data at the end of the wizard when the user has completed all the steps.
To keep the examples simple, we'll also use the session()
helper function. But we'll discuss accessing the session data using the Session
facade or a request class later on.
Reading Data from the Session
To read data from the session, you can use the get
method like so:
$currentStep = session()->get(key: 'wizard:current_step');
Running the above code would return the value stored in the session for the wizard:current_step
key. If there is no value stored in the session for that key, it will return null
.
This method also allows you to define a default value to return if the key doesn't exist:
$currentStep = session()->get(key: 'wizard:current_step', default: 1);
Running the above code would return the value stored in the session for the wizard:current_step
key. If there is no value stored in the session for that key, it will return 1
.
There may also be times when you want to read the data from the session and remove it at the same time (so it can't be accessed again). You can use the pull
function for this:
$currentStep = session()->pull(key: 'wizard:current_step');
Running the above code would return the value stored in the session for the wizard:current_step
key and then remove it from the session.
Writing Data to the Session
To write data to the session, you can use the put
function like so:
session()->put( key: 'wizard:step_one:form_data', value: [ 'name' => 'Ash Allen', 'email' => 'ash@example.com', ],);
Running the above code would store the array (passed in the second argument) as the value for the key wizard:step_one:form_data
.
Pushing Data to an Array in the Session
Similarly, you can also push data onto an array in the session using the push
method:
session()->push( key: 'wizard:step_one:form_data:languages', value: 'javascript',);
Assuming that the wizard:step_one:form_data:languages
key had the following data:
[ `php`,]
The above code (that is calling the push
method) would update the session value to:
[ `php`, `javascript`,]
If the wizard:step_one:form_data:languages
value didn't already exist in the session, using push
would create the session key and set the value to an array with the value you passed in.
Incrementing and Decrementing Data in the Session
Laravel also provides some convenient helper methods that allow you to increment and decrement values in the session:
You can increment a value in the session like so:
session()->increment(key: 'wizard:current_step');
When we run the code above, if the wizard:current_step
session value was 3, it would now be incremented to 4.
You can also decrement a value in the session like so:
session()->decrement(key: 'wizard:current_step');
If the values don't already exist in the session, they'll be treated as if they were 0. So calling increment
on an empty session value will set the value to 1. Calling decrement
on an empty session value will set the value to -1.
Both of these methods also allow you to specify the amount to increment or decrement by:
session()->increment(key: 'wizard:current_step', amount: 2);session()->decrement(key: 'wizard:current_step', amount: 2);
Removing Data from the Session
You can also remove data from the session using the forget
methods:
session()->forget(keys: 'wizard:current_step');
Running the code above would remove the data belonging to the wizard:current_step
key from the session.
If you want to remove multiple keys at once, you can pass an array of keys to the forget
function:
session()->forget(keys: [ 'wizard:current_step', 'wizard:step_one:form_data',]);
Or, if you'd prefer to remove all data from the session, you can use the flush
function:
session()->flush();
Checking if Data Exists in the Session
Laravel also provides some handy helper functions for checking whether data exists in the session.
You can use the has
method to check if a key exists in the session and that its value is not null
:
session()->has(key: 'wizard:current_step');
If the value exists and is not null
, the above code will return true
. If the value is null
or the key doesn't exist, it will return false
.
Similarly, you can also use the exists
method to check if a key exists in the session (regardless of if the value is null):
session()->exists(key: 'wizard:current_step');
You can also check whether the session does not exist in the session at all:
session()->missing(key: 'wizard:current_step');
Flashing Data to the Session
There may be times when you want to persist some data in the session, but only for the next request. For example, you might want to display a success notification to the user after they've submitted a form.
To do this, you can use the flash
method:
session()->flash( key: 'success', value: 'Your form has been submitted successfully!',);
If you were to run the above code, in the next request, you could read the value from the session (using something like session()->get('success')
) for displaying. Then it would be removed so it's not available in the next request.
There may be times when you have some flashed data (that was added in the previous request) and you want to keep it for the next request.
You can refresh all the flashed data using the reflash
method:
session()->reflash();
Or, if you only want to keep some of the flashed data, you can use the keep
method:
session()->keep(keys: [ 'success', 'error',]);
Running the above code would keep the success
and error
flashed session values, but remove any other flashed data for the next request.
Helper, Facade, or Request Class?
So far, we've only used the session()
helper in our examples.
But you can also interact with sessions using the Illuminate\Support\Facades\Session
facade or the Illuminate\Http\Request
class.
No matter which of these approaches you use, you'll still be able to use the same methods we've covered earlier in this article. These approaches are just different ways of interacting with the session data.
To use the Session
facade, you can call the methods like so:
use Illuminate\Support\Facades\Session; Session::get('key');Session::put('key', 'value');Session::forget('key');
Alternatively, you can access the session by calling the session
method on the Illuminate\Http\Request
instance that is injected into your controller methods. Let's say you have a controller method like so:
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'); // The rest of the method... }}
Each of these approaches is totally valid, so it's up to you to decide which one you and your team prefer.
Taking it Further
For smaller projects, interacting with the session using the approaches we've discussed is completely fine. But as your Laravel project grows, you might run into some issues that can cause bugs and make your code harder to maintain.
So we're now going to cover some of the potential pitfalls and how you can avoid them.
Typos in Session Keys
One of the common pitfalls I see (and have experienced many times myself) is typos in session keys.
Sticking with our wizard example, let's say we want to store the current step in the session. So we might have the following code:
session()->put(key: 'wizard:current_step', value: 1);
Then later on, in a different part of the codebase, we might want to read the current step from the session:
$currentStep = session()->get(key: 'wizard:step');
Did you see the error I just made? I'm accidentally trying to read the wizard:step
key instead of the wizard:current_step
key.
This is a simple example, but in a large codebase, it can be easy to make these kinds of mistakes. And these types of mistakes that are looking you right in the face that can also be the hardest to spot.
So a useful way to avoid these typos is to make use of constants or methods to generate your session keys.
For example, if the session key is static, you could define a constant (potentially in a session class which we'll cover soon) like so:
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); }}
This means we're reducing the number of raw strings being used in our codebase, which can help reduce the number of typos.
However, sometimes you might need to generate the session key dynamically. For example, let's say we want our wizard:current_step
key to include a team ID field. We could create a method to generate the key like so:
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'; }}
As we can see in the code above, we're generating the session key dynamically so it can be used in the different methods. For example, if we were trying to find the current step for a team with an ID of 1, the key would be wizard:1:current_step
.
Session Key Clashes
Another pitfall I see when working on projects that have been around for a while is session key clashes.
For example, imagine you built a wizard for creating a new user account a few years ago. So you might be storing your session data like so:
session()->put(key: 'wizard:current_step', value: 1);
Now you've been tasked with building a new feature that also has a wizard and you've completely forgotten about the old wizard and the naming convention you used. You might accidentally use the same keys for the new wizard, causing the data to clash and introducing potential bugs.
To avoid this, I like to prefix my session keys with the feature name. So for holding the wizard data for creating a new user, I might have the following keys:
-
new_user_wizard:current_step
-
new_user_wizard:step_one:form_data
-
new_user_wizard:step_two:form_data
- And so on...
Then in my new wizard that's being used for creating a new team, I might have the following keys:
-
new_team_wizard:current_step
-
new_team_wizard:step_one:form_data
-
new_team_wizard:step_two:form_data
- And so on...
We'll cover how to add these prefixes in your session classes later in this article.
Unknown Data Types
Can you tell me what data type is being stored in this session value?
$formData = session()->get(key: 'wizard:step_one:form_data');
If you guessed it was an instance of App\DataTransferObjects\Wizards\FormData
, then you'd be correct.
All joking aside, the point I'm trying to make is that it's not always immediately obvious what type of data you're working with when you're reading it from the session. You end up having to look at the code that writes the data to the session to figure out what it is. This can be distracting and time-consuming, as well as potentially leading to bugs.
You can add an annotation or docblock to the code that reads the data from the session. But this is only a hint. If the annotation isn't kept up to date (if the session data type changes), then it's not helpful and will increase the likelihood of bugs.
An alternative approach I like to use is to read the session data inside a method and add a return type to that method. This way you can be sure that the data you're working with is of the correct type. It will also help your IDE and the person reading the code.
For example, let's take this code:
use App\DataTransferObjects\Wizards\FormData; class WizardSession{ // ... public function stepOneFormData(): FormData { return session()->get(key: 'wizard:step_one:form_data'); }}
We can now immediately see that the stepOneFormData
method returns an instance of App\DataTransferObjects\Wizards\FormData
. This makes it clear what type of data we're working with. We can then call this method like so in our controller:
use App\Http\Controllers\Controller;use Illuminate\Http\Request; class WizardController extends Controller{ public function store(Request $request, WizardSession $wizardSession) { $stepOneFormData = $wizardSession->stepOneFormData(); // The rest of the method... }}
Handling Session Data in Session Classes
As we've seen in the last few sections, there are some easily avoidable (yet common) pitfalls when working with sessions in Laravel.
Each of these pitfalls can be avoided (or at least reduced) by using "session classes". I like to use session classes to encapsulate the logic for working with session data that are related to a single feature in one place.
For example, say we have a wizard for creating users and another wizard for creating teams. I'd have a session class for each of these wizards:
-
App\Sessions\Users\NewUserWizardSession
-
App\Sessions\Teams\NewTeamWizardSession
By using the session classes, you can:
- Automatically prefix all your keys with the feature name.
- Add type hints and return types to the methods.
- Reduce the number of raw strings being used in your codebase.
- Make it easier to refactor the session data structure.
- Make it easier to test the session data.
- Know exactly where to go if you need to make any changes to the session data for a given feature.
Using this class-based approach for dealing with the session data has saved me numerous times when working on larger Laravel projects. It's a simple approach that can make a big difference.
In the previous examples, I've already been hinting towards using session classes. But let's take a more in-depth look at how I like to structure these classes.
Say we have the following session class for the new user wizard. It might look a little overwhelming at first, but let's check out the code and then break it down:
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; }}
In the App\Sessions\Users\WizardSession
class above, we've started by defining the constructor that accepts an instance of Illuminate\Contracts\Session\Session
. By doing this, when we resolve an App\Sessions\Users\WizardSession
class from the service container, Laravel will automatically inject the session instance for us. I'll show you how to do this in a moment in your controllers.
We've then defined 5 basic public methods:
-
getCurrentStep
- returns the current step in the wizard. If no step is set, it defaults to1
. -
setCurrentStep
- sets the current step in the wizard. -
setFormDataForStep
- sets the form data for a given step in the wizard. This method takes the step number and an instance ofApp\DataTransferObjects\Wizards\Users\FormData
. -
getFormDataForStep
- gets the form data for a given step in the wizard. This method takes the step number and returns an instance ofApp\DataTransferObjects\Wizards\Users\FormData
ornull
if the data doesn't exist. -
flush
- removes all the data related to the wizard from the session. You might want to call this if the wizard has been completed or cancelled.
You may have noticed that all the keys are generated inside methods. I like to do this to reduce the number of raw strings that are used (and reduce the chance of typos). It also means that if we want to add another method that accesses a particular key, we can easily do so.
An added bonus of using these key generation methods is that we can easily add prefixes to the keys to avoid clashes. In this example, we've prefixed all the keys with new_user_wizard:
by using the sessionKey
method.
Now this class is set up, let's see how we could interact with it in our controllers:
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) { // Get the current step in the wizard $currentStep = $wizardSession->getCurrentStep(); // Update the current step in the wizard $wizardSession->setCurrentStep(2); // Get the form data for a given step $formData = $wizardSession->getFormDataForStep(step: $currentStep); // Set the form data for a given step $wizardSession->setFormDataForStep( step: $currentStep, formData: new FormData( name: 'Ash Allen', email: 'ash@example.com', ), ); // Clear all the wizard data from the session $wizardSession->flush(); }}
As we can see, in the example above, we're injecting the App\Sessions\Users\WizardSession
class into our controller method. Laravel will automatically resolve the session instance for us.
We're then able to interact with it like we would with any other class.
At first, this might feel like over-abstraction and more code to maintain. But as your projects grow, the type hints, return types, key generation methods, and even the naming of the methods (to make your actions more descriptive) can be extremely helpful.
Testing Sessions in Laravel
Just like any other part of your codebase, you should make sure you have coverage of your session data to make sure the correct fields are being read and written.
One of the big benefits of using a session class is that you can easily write focused unit-style tests for each of the methods in the class.
For example, we could write some tests for the getFormDataForStep
method of our App\Sessions\Users\WizardSession
class. Here's the method as a reminder:
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; }}
There are several scenarios we can test here:
- An
App\DataTransferObjects\Wizards\Users\FormData
object is returned for a step. -
null
is returned if the form data doesn't exist for a step.
Our test class might look something like so:
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 { // Hardcode the session key so we can be sure it's being // generated/used correctly in the session class. $sessionKey = 'new_user_wizard:step:1:form_data'; $formData = new FormData( name: 'Ash Allen', email: 'ash@example.com', ); // Store the form data in the session. session()->put(key: $sessionKey, value: $formData); // Store some random data for another step to ensure we're // only getting the data for the step we're asking for. session()->put( key: 'new_user_wizard:step:2:form_data', value: 'dummy data', ); // Read the data from the session. $formData = app(WizardSession::class)->getFormDataForStep(1); // Assert the data is correct. $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 { // Store some random data for another step to ensure we're // only getting the data for the step we're asking for. session()->put( key: 'new_user_wizard:step:2:form_data', value: 'dummy data', ); // Read the data from the session and assert it's null. $this->assertNull( app(WizardSession::class)->getFormDataForStep(1) ); }}
In the test class above, we have two tests that cover both scenarios we mentioned earlier.
These unit-style tests are great for ensuring that your session class is configured correctly to read and write data to the session. But they don't necessarily give you confidence that they're being used correctly in the rest of your codebase. For example, you might be calling getFormDataForStep(1)
when you were supposed to be calling getFormDataForStep(2)
.
For this reason, you might want to consider also asserting the session data in your feature tests (that you'd typically write for your controllers).
For example, let's imagine you have the following basic method in your controller that goes to the next step in the wizard:
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'); }}
In the method above, we're first reading the current step from the session. We're then storing the form data for the current step in the session. Finally, we're incrementing the current step and redirecting to the next step in the wizard.
We'll assume our App\Http\Requests\Users\Wizard\NextStepRequest
class is responsible for validating the form data and returning an instance of App\DataTransferObjects\Wizards\Users\FormData
when we call the toDto
method.
We'll also assume the nextStep
controller method is available via a POST request to the /users/wizard/next-step
route (named users.wizard.next-step
).
We might want to write a test like so to ensure that the form data is being stored in the session correctly:
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); // You can use `assertSessionHas` to read the session data. // Alternatively, we can read the session data directly. $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); } // Other tests...}
In the test above, we're making a POST request to the /users/wizard/next-step
route with some form data. You might notice that we're using withSession
. This method allows us to set the session data so we can assert that it's being read correctly.
We're then asserting that the user is redirected to the next step in the wizard and that the current step in the session is set to 3
. We're also asserting that the form data for step 2
is stored in the session correctly.
As we can see in the test, we're also reading from the session in two ways:
- Using the
assertSessionHas
method to check that the session data is set correctly. - Using the
session()
helper to read the session data directly.
Both of these approaches are valid, so it's up to you to decide which one you prefer. I used both in the test above to show you that you have options.
Conclusion
Hopefully, this article has given you a good understanding of what sessions are and how they work in Laravel. I'm also hoping they've given you some ideas on how you can use a class-based approach to interacting with session data to avoid some common pitfalls.
I am a freelance Laravel web developer who loves contributing to open-source projects, building exciting systems, and helping others learn about web development.