Pushing Laravel further — best tips & good practices for Laravel 5.7

This post has an audio version thanks to Miguel Piedrafita’s Blogcast app.

Laravel is already known by many PHP developers for writing clean, working and debugg-able code. It also has support for many many features, that sometimes aren’t listed in the docs, or they were, but they were removed for various reasons.

I have been working with Laravel in production-usage for 2 years and I have learned from writing bad code to better code and I have taken the advantage of Laravel since the first time I started wrote code with it. I am going to show you the mysterious tricks that might help you when writing code with Laravel.


Use local scopes when you need to query things

Laravel has a nice way to write queries for your database driver using Query Builder. Something like this:

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

This is pretty nice. This made me give up on SQL and focus on coding that is more approachable for me. But this bit of code can be better written if we use local scopes.

Local scopes allow us to create our own Query Builder methods we can chain when we try to retrieve data. For example, instead of ->where() statements, we can use ->delivered() and ->paid() in a cleaner way.

First, in our Order model, we should add some methods:

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

When declaring local scopes, you should use the scope[Something] exact naming. In this way, Laravel will know that this is a scope and will make use of it in your Query Builder. Make sure you include the first argument that is automatically injected by Laravel, and is the query builder instance.

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

For more dynamic retrieval, you can use dynamic local scopes. Each scope allows you to give parameters.

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

Later in this article, you’ll learn why you should use snake_case for database fields, but here is the first reason: Laravel uses by default where[Something] to replace the previously scope. So instead of the previously one, you can do:

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

Laravel will search for the snake_case version of Something from where[Something]. If you have status in your DB, you will use the previous example. If you have shipping_status, you can use:

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

It’s your choice!

Use Requests files when needed

Laravel gives you an eloquent way to validate your forms. Either it’s a POST request or a GET request, it won’t fail to validate it if you need it.

You can validate this way in your controller:

public function store(Request $request)
{
$validatedData = $request->validate([
'title' => 'required|unique:posts|max:255',
'body' => 'required',
]);

// The blog post is valid...
}

But when you have too much code in your controller methods, it can be pretty nasty. You want to reduce as much code as possible in your controller. At least, this is what is the first thing i think if i have to write really much logic.

Laravel gives you a *cute* way to validate requests by creating request classes and using them instead of the old-fashioned Request class. You just have to create your request:

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 Illuminate\Http\Request in your method, you should replace with the newly-created class:

use App\Http\Requests\StoreBlogPostRequest;
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 \Illuminate\Auth\Access\AuthorizationException) {
//
}
   return parent::render($request, $exception);
}

The missing method here, in the request class is the messages() function, that is an array containing the messages that will be returned in case the 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.',
...
];
}
}

To catch them in your controller, you can use the errors variable inside your blade files:

@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=new
User::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)

Have you ever used a ton of joins in a query just to get more info? It is pretty hard to write those SQL commands, even with Query Builder, but models already do that with Relationships. Maybe you won’t be familiar with at first, due to high-amount of information that the documentation provides, but this will help you understand better how the things work and how to make your application run smoother.

Check Relationships’ documentation here.

Use Jobs for time-consuming tasks

Laravel Jobs are a powerful must-to tool to run tasks in the background.

  • Do you want to send an email? Jobs.
  • Do you want to broadcast a message? Jobs.
  • Do you want to process images? Jobs.

Jobs help you give up on loading time for your users on consuming-time tasks like these. They can be put into named queues, they can be prioritized, and guess what — Laravel implemented queues almost everywhere where was possible: either processing some PHP in the background or sending notifications or broadcasting events, queues are there!

You can check Queues’ documentation here.

I love to use Laravel Horizon for queues since it’s easy to set up, it can be daemonized using Supervisor and through the configuration file, I’m able to tell Horizon how many processes I want for each queue.

Stick to database standards & Accessors

Laravel teaches you from the very beginning that your variables and methods should be $camelCase camelCase() while your database fields should be snake_case. Why? Because this helps us build better accessors.

Accessors are custom fields we can build right from our model. If our database contains first_name, last_name and age, we can add a custom field named name that concatenates the first_name and last_name. Don’t worry, this won’t be written in the DB by any means. It’s just a custom attribute this specific model has. 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 not 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 work 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:

  • Insde 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 they name is different. Check the Available Methods for more.

If you can filter data at the Query Builder level, do it! Do not rely on filtering when it comes to the Collection instance — you will use too much memory in some places and you don’t want to. Limit your results and use indexes at the DB level.

Use packages and don’t reinvent the wheel

Here are some packages I use:

Here are some packages I have written:

  • Befriended (like, follow and block like in social media)
  • Schedule (create timetables and check against hours and days)
  • Rating (rate models)
  • Guardian (permissions system, the easy way)

Too hard to understand? Reach me!

If you have more questions about Laravel, if you need help with any information related to DevOps or simply want to say a Thank you!, you can find me on Twitter @rennokki!