Laravel 9 tips & best practices

 


There is a well-established reputation for writing clean, debuggable, and well-working code in Laravel. Furthermore, it contains a great deal of support for many features that may not be listed in the documentation, or may have been listed but removed for some reason.


I’ve been using Laravel in production-use for the past four years, and I’ve learned from writing bad code to writing better code, and I’ve been able to get the most out of it from the very first time I used it. In this post, I will teach you the tricks that might make your programming experience with Laravel more enjoyable.

When querying things, use local scopes

Laravel’s Query Builder makes it easy to create queries for your database. This would be something like:

$orders = Order::where('status', 'delivered')->where('paid', true)->get();

I think this is pretty cool. Because of this, I gave up on SQL and focused on coding that is more approachable. Using local scopes would allow us to write this part of the code better.
In order to retrieve data, we can chain local scopes together to create our Query Builder methods. Instead of using ->where() statements, we can use ->delivered() and ->paid().
We should add the following methods to our Order model:

class Order extends Model
{
...
public function scopeDelivered($query) {
return $query->where('status', 'delivered');
} public function scopePaid($query) {
return $query->where('paid', true);
}
}

When denoting a local scope, you should use the exact naming of the scope[Something]. This will allow Laravel to determine that this scope belongs to your query builder, so it will use it accordingly. Laravel automatically injects the query builder instance as the first argument which should be included.

$orders = Order::delivered()->paid()->get();

Using dynamic local scopes will allow you to retrieve data more dynamically. You can specify parameters for each scope.

class Order extends Model
{
...
public function scopeStatus($query, string $status) {
return $query->where('status', $status);
}
}$orders = Order::status('delivered')->paid()->get();

The reason for your first use of snake_case for database fields will be revealed later in this article, but for now, here is why: Laravel automatically replaces the previous scope with where[Something]. The following is an alternative to the previous one:

Order::whereStatus('delivered')->paid()->get();

Laravel will search for the snake_case version of Something from where[Something]. Using the previous example, you will use status if you have one in your database. You can use shipping_status if you have it:

Order::whereShippingStatus('delivered')->paid()->get();

It’s your choice!

Whenever possible, use the Requests files

You can validate forms with Laravel in a very elegant way. You don’t have to worry about it failing to validate a POST request or a GET request.
Using the following method, you can validate:

public function store(Request $request)
{
$validatedData = $request->validate([
'title' => ['required', 'unique:posts', 'max:255'],
'body' => 'required',
]); // The blog post is valid...
}

If your controller methods contain too much code, though, you’ll run into problems. The code in your controller should be as small as possible. It is at least what comes to mind first when I need to write a lot of logic.
By creating request classes instead of old-fashioned Request classes, Laravel provides a *cute* way to validate requests. To make a request, all you have to do is:

php artisan make:request StoreBlogPost

Inside the app/Http/Requests/ folder you’ll find your request file:

class StoreBlogPostRequest extends FormRequest
{
public function authorize()
{
return $this->user()->can('create.posts');
} public function rules()
{
return [
'title' => ['required', 'unique:posts', 'max:255'],
'body' => 'required',
];
}
}

Now, instead of your IlluminateHttpRequest in your method, you should replace with the newly-created class:

use AppHttpRequestsStoreBlogPostRequest;public function store(StoreBlogPostRequest $request)
{
// The blog post is valid...
}

The authorize() method should be a boolean. If it is false, it will throw a 403, so make sure you catch it in the app/Exceptions/Handler.php ‘s render() method:

public function render($request, Exception $exception)
{
if ($exception instanceof IlluminateAuthAccessAuthorizationException) {
//
}return parent::render($request, $exception);
}

Specifically, the messages() method is missing from the request class. This function is an array that contains the messages that are returned if validation fails:

class StoreBlogPostRequest extends FormRequest
{
public function authorize()
{
return $this->user()->can('create.posts');
} public function rules()
{
return [
'title' => ['required', 'unique:posts', 'max:255'],
'body' => 'required',
];
} public function messages()
{
return [
'title.required' => 'The title is required.',
'title.unique' => 'The post title already exists.',
...
];
}
}
@if ($errors->any())
@foreach ($errors->all() as $error)
{{ $error }}
@endforeach
@endif

In case you want to get a specific field’s validation message, you can do it like then (it will return a false-boolean entity if the validation passed for that field):

<input type="text" name="title" />
@if ($errors->has('title'))
<label class="error">{{ $errors->first('title') }}</label>
@endif

Magic scopes

When building things, you can use the magic scopes that are already embedded

  • Retrieve the results by created_at , descending:
User::latest()->get();
  • Retrieve the results by any field, descending:
User::latest('last_login_at')->get();
  • Retrieve results in random order:
User::inRandomOrder()->get();
  • Run a query method only if something’s true:
// Let's suppose the user is on news page, and wants to sort it by newest first
// mydomain.com/news?sort=newUser::when($request->query('sort'), function ($query, $sort) {
if ($sort == 'new') {
return $query->latest();
}
return $query;
})->get();

Instead of when() you can use unless, that is the opposite of when().

Use Relationships to avoid big queries (or bad-written ones)

When searching for information, did you ever use a huge number of joins? Models already support this with relational relationships, so writing those SQL commands is not so difficult, even with Query Builder. This may be difficult to understand at first, due to the amount of information you will find in the documentation, but as you learn more and improve your application, you will begin to feel more comfortable.

Use Jobs for time-consuming tasks

Keeping your tasks running in the background with Laravel Jobs is a must-have tool.

  • Sending an email is what you want? Jobs.
  • Send a message if you wish? Jobs.
  • Are you interested in processing images? Jobs.

When users perform consuming-time tasks like these, jobs reduce your users’ loading times. There’s a way to name queues, they can be prioritized, and guess what – Laravel has implemented queues pretty much everywhere where it was possible. Whether the queue is used to handle background PHP processing, sending notifications, or broadcasting events, queues are available!
In Laravel Horizon, I can specify how many processes I want for each queue in the configuration file. Horizon is easy to setup, it can be daemonized with Supervisor, and I can specify how many processes I want for each queue by configuring the configuration file.

Stick to database standards & Accessors

During your learning of Laravel, you are taught to put your variables and methods as $camelCase camelCase(), while your database fields as snake_case. What’s the reason? Our accessors will be better because of this.
We can create custom fields right from our model with the help of accessors. We can add a custom field named name to our database that concatenates first_name and last_name if our database contains first_name, last_name, and age. Please be assured that this will not be stored in any DB. There’s nothing special about this specific attribute.
All accessors, like scopes, have a custom naming syntax: getSomethingAttribute:

class User extends Model
{
...
public function getNameAttribute(): string
{
return $this->first_name.' '.$this->last_name;
}
}

When using $user->name, it will return the concatenation.

By default, the name attribute is not shown if we dd($user), but we can make this generally available by using the $appends variable:

class User extends Model
{
protected $appends = [
'name',
];
... public function getNameAttribute(): string
{
return $this->first_name.' '.$this->last_name;
}
}

Now, each time we dd($user) , we will see that the variable is there (but still, this is not present in the database)

Be careful, however: if you already have a name field, the things are a bit different: the name inside $appends is no longer needed, and the attribute function waits for one parameter, which is the already stored variable (we will no longer use $this).

For the same example, we might want to ucfirst() the names:

class User extends Model
{
protected $appends = [
//
];
... public function getFirstNameAttribute($firstName): string
{
return ucfirst($firstName);
} public function getLastNameAttribute($lastName): string
{
return ucfirst($lastName);
}
}

Now, when we use $user->first_name, it will return an uppercase-first string.

Due to this feature, it’s good to use snake_case for your database fields.

Do not store model-related static data in configs

What I love to do is to store model-related static data inside the model. Let me show you.

Instead of this:

BettingOdds.php

class BettingOdds extends Model
{
...
}

config/bettingOdds.php

return [
'sports' => [
'soccer' => 'sport:1',
'tennis' => 'sport:2',
'basketball' => 'sport:3',
...
],
];

And accessing them using:

config(’bettingOdds.sports.soccer’);

I prefer doing this:

BettingOdds.php

class BettingOdds extends Model
{
protected static $sports = [
'soccer' => 'sport:1',
'tennis' => 'sport:2',
'basketball' => 'sport:3',
...
];
}

And access them using:

BettingOdds::$sports['soccer'];

Why? Because it’s easier to be used in further operations:

class BettingOdds extends Model
{
protected static $sports = [
'soccer' => 'sport:1',
'tennis' => 'sport:2',
'basketball' => 'sport:3',
...
];public function scopeSport($query, string $sport)
{
if (! isset(self::$sports[$sport])) {
return $query;
}
return $query->where('sport_id', self::$sports[$sport]);
}
}

Now we can enjoy scopes:

BettingOdds::sport('soccer')->get();

Use collections instead of raw-array processing

Back in the days, we were used to working with arrays in a raw way:

$fruits = ['apple', 'pear', 'banana', 'strawberry'];foreach ($fruits as $fruit) {
echo 'I have '. $fruit;
}

Now, we can use advanced methods that will help us process the data within arrays. We can filter, transform, iterate and modify data inside an array:

$fruits = collect($fruits);$fruits = $fruits->reject(function ($fruit) {
return $fruit === 'apple';
})->toArray();['pear', 'banana', 'strawberry']

For more details, check the extensive documentation on Collections.

When working with Query Builders, the ->get() method returns a Collection instance. But be careful to not confuse Collection with a Query builder:

  • Inside the Query Builder, we did not retrieve any data. We have a lot of query-related methods: orderBy()where(), etc.
  • After we hit ->get(), the data is retrieved, the memory has been consumed, it returns a Collection instance. Some Query Builder methods are not available, or they are, but their name is different. Check the Available Methods for more.

Query Builder allows you to filter data at the query level. The Collection instance should not be filtered – it will consume too much memory in certain areas, and you do not want that. Make sure that your results are limited and that your database has indexes.

Use packages and don’t reinvent the wheel

Here are some useful packages you can use:

*

Post a Comment (0)
Previous Post Next Post