The essential news about content management systems and mobile technology. Powered by Perfect Publisher and XT Search for Algolia.
The News Site publishes posts to the following channels: Facebook, Instagram, Twitter, Telegram, Web Push, Tumblr, and Blogger.
The Laravel team released v11.24 this week, with new shorthands
for Process fakes, a nullOnUpdate()
method for foreign
key definitions, support for retrying multiple batch IDs when
retrying jobs, and more.
A quick note that as of the time of writing, auto-discovery of
console commands in the app/Console/Commands
path.
This issue likely gets patched quickly, but if you want to update
to 11.24.0, you can add the following to
bootstrap/app.php
:
Application::configure(basePath: dirname(__DIR__))
// ...
->withCommands([
__DIR__.'/../app/Console/Commands'
]);
I'd recommend waiting for a patch update, but this could confuse you if you update and application console commands are missing.
prependLocation()
to View FactoryNoboru Shiroiwa added the
prependLocation()
method to the View factory, which
enables you to use the method with the View
facade:
// Before
View::getFinder()->addLocation($path); // ✅
View::getFinder()->prependLocation($path); // ✅
View::addLocation($path); // ✅
View::prependLocation($path); // ❌
// After
View::prependLocation($path); // ✅
nullOnUpdate()
Method to
ForeignKeyDefinitionGisu Nasrollahi contributed a
nullOnUpdate()
method to the
ForeignKeyDefinition
schema class, which indicates
that updates on the referenced model should set the foreign key
value to null.
$table->foreign('user_id')
->references('id')
->on('users')
->nullOnUpdate();
BackedEnum
to be Used With
Route::can()
@Omegadela contributed support for backed enums in the
Route::can()
method:
// Before
Route::post('/request', function () {...})->can(Permissions::CAN_REQUEST->value);
// After
Route::post('/request', function () {...})->can(Permissions::CAN_REQUEST);
Process
FakesJason McCreary contributed two new result
shorthands for Process
fake results in tests. The
first example is a shorthand to set the exit code. The second
example is a shorthand for throwing an exception.
Process::fake([
'php -l script.php' => 255,
'cat README.txt' => new \RuntimeException('fake exception message'),
]);
Here's an example of the fake in action from the Pull Request's tests:
// Exit code example
$factory = new Factory;
$factory->fake(['ls -la' => 1]);
$result = $factory->run('ls -la');
$this->assertSame(1, $result->exitCode());
$this->assertFalse($result->successful());
// Exception example
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('fake exception message');
$factory = new Factory;
$factory->fake(['cat me' => new \RuntimeException('fake exception message')]);
$factory->run('cat me');
Sean Kegel updated the queue:retry-batch
command to accept multiple IDs so that multiple jobs can be retried
at once:
php artisan queue:retry-batch \
'9d0ed667-8c3a-4385-8a07-aff118a21e5c' \
'9d0ed6f1-b8dd-486b-8761-f89de0fb68e3'
You can see the complete list of new features and updates below and the diff between 11.23.0 and 11.24.0 on GitHub. The following release notes are directly from the changelog:
$name
variable in non base
config file becomes it's key by @rojtjo in https://github.com/laravel/framework/pull/52738BackedEnum
to be passed to
Route::can()
by @Omegadela in
https://github.com/laravel/framework/pull/52792Concurrency
Configuration
Index Name by @devajmeireles in https://github.com/laravel/framework/pull/52788withoutPretending
method properly
resets state after callback execution by @xurshudyan in
https://github.com/laravel/framework/pull/52794ProcessDriver[@defer](https://github.com/defer)()
to
ProcessDriver[@run](https://github.com/run)()
method
by @rodrigopedra in https://github.com/laravel/framework/pull/52807Concurrency\ProcessDriver
by @rodrigopedra
in https://github.com/laravel/framework/pull/52813Illuminate\Support\defer
function by @crynobone
in https://github.com/laravel/framework/pull/52801laravel/serializable-closure
on
Database component by @crynobone in
https://github.com/laravel/framework/pull/52835Process
fakes by @jasonmccreary in https://github.com/laravel/framework/pull/52840laravel/prompts
v0.2 by @crynobone in https://github.com/laravel/framework/pull/52849[@throws](https://github.com/throws)
section to Concurrency manager doc block by @rnambaale in
https://github.com/laravel/framework/pull/52856routes/console.php
by @SamuelNitsche in https://github.com/laravel/framework/pull/52867touchesParents()
for chaperoned models by @samlev in https://github.com/laravel/framework/pull/52883Cache::flexible
improvements by @timacdonald in https://github.com/laravel/framework/pull/52891\DateTimeInterface
and
\DateInterval
to type for
Cache::flexible()
by @bram-pkg in
https://github.com/laravel/framework/pull/52888The post Laravel 11.24 Released appeared first on Laravel News.
Join the Laravel Newsletter to get all the latest Laravel articles like this directly in your inbox.
Read more https://laravel-news.com/laravel-11-24-0
The WordPress.com team is always working on new design ideas to bring your website to life. Check out the latest themes in our library, including great options for brats, foodies, and beauty bloggers. Take a look below.
All WordPress.com ThemesTap into your brat summer vibes all year round with our brand new Partygurl theme. Inspired by the now-iconic bright green and simple typography from Charli XCX’s Brat album, this styling will immediately identify you as someone who’s done with the “clean and polished” aesthetic of sameness that you see across the web.
Click here to view a demo of this theme.
RecipeBook is a warm, vibrant theme made for foodie content creators who want their recipes to shine. Inspired by the charm of old-school cookbooks, RecipeBook pairs eye-catching colors with bold typography. The homepage invites discovery with a handy category list and flexible Query Loop block, making it easy to showcase your culinary creations. Whether you’re sharing recipes or creating food collections, RecipeBook offers a playful yet functional design to help you dish out your passion for cooking.
Click here to view a demo of this theme.
Goodskin is a great theme for beauty bloggers and skincare enthusiasts. With its light, calming aesthetic and clean layout, Goodskin provides an elegant space for sharing your routines, product reviews, and more. The theme includes thoughtful features like a sidebar for easy navigation and a product rating pattern to highlight your favorite finds. Available in three soothing color variations—Jojoba, Blush, and Eyeshadow—Goodskin offers a serene, sophisticated platform that allows your content to glow.
Click here to view a demo of this theme.
Luxus is the perfect look for beauty salons wanting to make a sleek, no-fuss impression. Designed with simplicity in mind, Luxus gives the essentials—services, location, hours, and contact information—right away on the homepage, making it easy for clients to find what they need.
Optimized for mobile right out of the box with a clean, single-column layout, Luxus offers a seamless experience for your busy, on-the-go clientele. With the luxurious Ojuju font for headings and the classic Hanken Grotesk for body text, this theme exudes elegance while keeping the focus on what matters most.
Click here to view a demo of this theme.
To install any of the above themes, click the name of the theme you like, which brings you right to the installation page. Then click the “Activate this design” button. You can also click “Open live demo,” which brings up a clickable, scrollable version of the theme for you to preview.
Premium themes are available to use at no extra charge for customers on the Personal plan or above. Partner themes are third-party products that can be purchased for $99/year each on the Business plan and above.
You can explore all of our themes by navigating to the “Themes” page, which is found under “Appearance” in the left-side menu of your WordPress.com dashboard. Or you can click below:
All WordPress.com ThemesRead more https://wordpress.com/blog/2024/09/24/new-wordpress-themes-september-2024/
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.
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:
The session data may be stored in a variety of places, such as:
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:
_token
value is used to protect against CSRF
attacks._previous.url
value is used to store the URL
of the previous request._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 the success
value was flashed in the
previous request._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:
success
value is used to store a success
message that might be displayed to the user.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 in
storage/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.
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.
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.
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' => This email address is being protected from spambots. You need JavaScript enabled to view it. ',
],
);
Running the above code would store the array (passed in the
second argument) as the value for the key
wizard:step_one:form_data
.
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.
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);
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();
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');
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.
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.
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.
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
.
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
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
We'll cover how to add these prefixes in your session classes later in this article.
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...
}
}
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:
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 to 1
.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 of
App\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 of
App\DataTransferObjects\Wizards\Users\FormData
or
null
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: This email address is being protected from spambots. You need JavaScript enabled to view it. ',
),
);
// 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.
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:
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: This email address is being protected from spambots. You need JavaScript enabled to view it. ',
);
// 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(This email address is being protected from spambots. You need JavaScript enabled to view it. ', $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' => This email address is being protected from spambots. You need JavaScript enabled to view it. '
])
->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(This email address is being protected from spambots. You need JavaScript enabled to view it. ', $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:
assertSessionHas
method to check that
the session data is set correctly.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.
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.
The post A Deep Dive into Sessions in Laravel appeared first on Laravel News.
Join the Laravel Newsletter to get all the latest Laravel articles like this directly in your inbox.
Read more https://laravel-news.com/laravel-sessions
The next Laravel Worldwide Meetup is today, Tuesday, September 24th, at 16:00 UTC. It features speakers Marty Friedel and Max Brokman.
Marty Friedel will talk about building maintainable websites with Statamic. Second, Max Brokman will discuss building software in a team environment in his talk Reducing the Cost of Action: Ideas for Effective Teams.
The Laravel Worldwide Meetup is the official Laravel meetup in the cloud, where you can learn from familiar faces and new speakers. Today's meetup begins live on YouTube at 16:00 UTC:
To find out when the Laravel Worldwide Meetup starts in your timezone, check this Every Time Zone link. See the official Laravel Worldwide meetup page for complete details and to submit your talk for a future meetup!
The post The September 2024 Laravel Worldwide Meetup is Today appeared first on Laravel News.
Join the Laravel Newsletter to get all the latest Laravel articles like this directly in your inbox.
Read more https://laravel-news.com/laravel-worldwide-meetup-september-2024
Modern web applications have become more complex over the years as user requirements continue to push for more performance, less latency, and more features. As a developer, choosing Laravel is easy. But when you get beyond the application platform, you have to be thinking about data.
Does the database fit my needs? Will it scale? Can I report from it? And most systems require a search feature. What if the database you chose was AWS DynamoDB? The Laravel ecosystem has libraries for using Eloquent to store data in DynamoDB. But since it's a simple key/value datastore, searching is limited to indexes and expensive filters.
This is where a purpose-built search database like Typesense really shines. It handles those tough searches, ranks, and scores the results, and abstracts that complexity into an API for a developer to use. But how would you populate this secondary store? Traditional approaches would say to use Scout. But in this article, I want to demonstrate a more AWS native way by using DynamoDB's Stream capabilities and AWS Lambda to keep our primary DynamoDB table in sync with a Typesense collection.
But before we begin and for disclosure, Typesense sponsored me to experiment with their product and report my findings. They have rented my attention, but not my opinion. Here is my unbiased view of my experience as a developer when integrating search into a Laravel Application that includes AWS DynamoDB and Typesense.
Before diving into the code, let's have a look at a macro view
of our solution. Think of a very traditional Laravel application
which has some Blade views, Controllers, and a simple Todo model.
The user can list the Todos, create new ones, and then search on
the name
and description
. For storage, we
are going to use DynamoDB and Typesense's Cloud option.
Let's dive in!
Let's jump in with our base level Todo model. I find that working from the bottom up helps stitch things together until I've got the full picture like in the architecture diagram above.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use BaoPham\DynamoDb\DynamoDbModel;
class Todo extends DynamoDbModel
{
use HasFactory;
protected $primaryKey = 'id'; // required
}
Notice that I'm extending my model from
DynamoDbModel
. This comes from the widely popular and
well-maintained library in this GitHub repository..
What's nice about this library is that I can use the same
Laravel Eloquent methods that I'm used to. In the
TodoController
, the save looks like it normally would
even though I'm using DynamoDB.
public function store(request $request): redirectresponse
{
$todo = new todo;
$todo->id = Uuid::uuid4()->toString();
$todo->name = $request->name;
$todo->description = $request->description;
$todo->save();
return redirect('/todos')->with('status', 'todo data has been inserted');
}
Before looking at connecting DynamoDB to Typesense, I want to show you how easy and powerful using the Typesense Cloud offering is. You could 100% run Typesense yourself and if you are self-hosting or VM-hosting your Laravel app, that might be the approach you want to take. But if you are deploying on AWS and using DynamoDB, a Cloud-hosted Typesense makes a ton of sense.
What I really enjoy about the cloud offering is that I can manage the cluster at an abstracted level. I set things like memory, nodes, whether I want high-availability, and high disk performance and Typesense takes care of the rest. All that I need to do from there is generate some API keys, attach permissions, and my clients are free to perform the operations needed.
Cluster configuration ends up looking like the below image.
With the cluster in place, other Typesense operations are opened up. I must define a Collection, with a document schema, and then I can begin inserting documents. For instance, here's how searching our Todos Collection shows up.
With our Typesense Cloud Cluster established, let's talk about syncing with DynamoDB. When building systems with Cloud native technologies like DynamoDB, sometimes it makes more sense to stay in that ecosystem to do additional processing like syncing data with DynamoDB and Typesense.
If you aren't familiar with AWS, DynamoDB offers a set of capabilities to respond to changes in real time via Streams. A DynamoDB stream can be read from AWS Kinesis or a Lambda function. For this example, a Lambda function is the route I'm going to take. It's simple, isolated, and I don't need the extensibility that Kinesis provides.
DynamoDB offers the option to read changes in a few varieties. I can read just Key changes, New Images, or New and Old Images. I've chosen to just read New Images so that I get the full DynamoDB item vs having to go back and do a double read to hydrate the item later. For this implementation, I'm using Golang to read from the stream and populate Typesense. I find Golang to be a solid choice when doing event-driven work in AWS Lambda.
Without diving into how it all works (there's a repository link at the end), here are the Typesense specific bits. Another solid plus for Typesense is that there are plenty of options when working with their API from an SDK standpoint.
// New Client
client = typesense.NewClient(
typesense.WithServer(url),
typesense.WithAPIKey(apiKey))
// Write a Document (Upsert)
result, err := client.Collection("todos").Documents().Upsert(ctx, typesenseTodo)
All the above has to be triggered by a user action. When the user creates a new Todo like in the UI image below, the Controller will create a new model and save it to the database.
Again, note that I'm using Eloquent to perform the save.
public function store(request $request): redirectresponse
{
$todo = new todo;
$todo->id = Uuid::uuid4()->toString();
$todo->name = $request->name;
$todo->description = $request->description;
$todo->save();
return redirect('/todos')->with('status', 'todo data has been inserted');
}
There are some great resources for syncing Typesense when using Scout. However, what I love about building things is that there are usually multiple ways of solving a problem. And what I really enjoy with the DynamoDB pattern is that everything happens in its own isolated space. The write operation to DynamoDB happens, and the user is returned control from the API call. The synchronization process happens asynchronously from the user's action. By having the Stream trigger the Lambda function, that workload is also isolated.
With that said, using the Scout integration, my
Todo
model would expose search functions. That isn't
what I'm going to show below. I need to use the same Typesense API
that I used in the Lambda function, but instead of Golang, I'm
going to use good ole PHP.
To use the client in a consistent and reliable fashion, let's
create a Provider
so that I can inject it into my
TodoController
.
php artisan make:provider SearchProvider
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\HttpClient\HttplugClient;
use Typesense\Client;
class SearchProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(Client::class, function ($app) {
$client = new Client(
[
'api_key' => '<API Key>',
'nodes' => [
[
'host' => '<Typesense Host>',
'port' => '443',
'protocol' => 'https',
],
],
'client' => new HttplugClient(),
]
);
return $client;
});
}
}
With a SearchProvider
established that exposes the
Typesense Client, I can now begin to handle search operations from
my Web App. I start by injecting the Client into my controller.
private Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
Once configured, I can then handle search operations in a controller method.
public function search(request $request): view
{
$search = '';
if ($request->search) {
$search = $request->search;
}
$values = $this->client->collections["todos"]->documents->search(
[
'q' => $search,
'query_by' => 'name,description',
'sort_by' => 'created_at:desc',
]
);
$searched = [];
foreach ($values['hits'] as $todo) {
$item = $todo['document'];
$t = new Todo;
$t->id = $item['id'];
$t->name = $item['name'];
$t->description = $item['description'];
$t->created_at = $item['created_at'];
$t->updated_at = $item['updated_at'];
array_push($searched, $t);
}
log::debug($searched);
return view('todo')->with(['todos' => $searched]);
}
Notice in the query_by
property of the search
criteria that I'm going across multiple fields. Doing something
like this in DynamoDB would be tough to accomplish but with
Typesense, it's extremely simple. The search parameters allow for
even further customization such as supplying the search criteria
and the sort order. What I like about using Typesense is that I'm
not having to prepend or append %
to make some kind of
wildcard syntax. Typesense handles that for me. It also handles
things like typographical errors which I'll show you below.
In addition, you get what hit, the score, and some other useful items in the result. That extra data gives you the ability to produce richer UIs and user experiences.
When searching for the word Demonstration
I
correctly see the one record that I created a few paragraphs ago.
That all makes sense. I could also have used Demo
or
stration
or any other contains
type
clause.
However, what about when a user makes a mistake. They meant to
search for Demo
, but they typed Deom
.
Wouldn't it be nice if your search provider could make that
correction? Well Typesense does!
These types of features are the reason that you choose a purpose-built provider when storing your data.
And that wraps up the code and the user experience. To quickly recap what we've built:
First off, I started building for the web in the late 90s when PHP was competing with Perl's CGI. I'm a huge fan of the community and ecosystem, and it continues to get better and better each year. Laravel adds something magical to the full application experience. I love that I can mix and match components and attach just the things I want based upon my user requirements.
Which is precisely why including Typesense into a project is both so simple and so powerful. Laravel gives me the power through Eloquent to save my data where I want, and then I can make decisions about what to do from there.
My thoughts on using Laravel with DynamoDB and Typesense can be summarized like this:
The one thing that I wished the Typesense cloud offered was a purely serverless and consumption-based model. With DynamoDB and Lambda, I only pay for the resources I consume and that's either per data written/read or time spent computing. As opposed to the Typesense cloud where I pay for capacity upfront, which might leave unused capacity that I'm wasting during my billing period. It's on me as the builder to manage that and effectively using the resources correctly.
With all that said, that isn't a reason I wouldn't use the Typesense cloud. It works amazingly well and much easier than running the service myself.
Thanks so much for reading about an alternative approach to saving and synchronizing a Laravel-based web application with a Typesense search datastore. I've run through a good bit of code and configuration of which I can't possibly demonstrate in an article.
So here are the repositories that I worked from. Feel free to clone and run for yourself. If you've got any feedback, please leave them in the repositories.
Building software for customers is my life's work. I enjoy seeing delighted users that don't have to worry or think about the complexity that is lurking just under the surface. Choosing Laravel, AWS, and Typesense are great foundational decisions from which to start a build and accomplish those abstractions.
They come with rock solid platforms, libraries, and outstanding communities. It's never been a better time to be a part of the PHP and Laravel community!
Thanks again for reading and happy building!
The post Serverless Search using Laravel and the Typesense Cloud appeared first on Laravel News.
Join the Laravel Newsletter to get all the latest Laravel articles like this directly in your inbox.
Read more https://laravel-news.com/serverless-search-laravel-typesense
Page 8 of 1359