Skip to content

Instantly share code, notes, and snippets.

@jpswade
Created August 17, 2022 15:54
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jpswade/4c57a7caf7b2e130109579255538960e to your computer and use it in GitHub Desktop.
Save jpswade/4c57a7caf7b2e130109579255538960e to your computer and use it in GitHub Desktop.
Laravel Best Practices

Laravel Best Practices

These are best practices, rather than coding standards.

Coding Standards should be guidelines that we should be aiming to stick to, relating to the way code is written and its readability.

Coding standards can be and should be defined by automation tools, such as phpCS and StyleCI.

Best Practices, however, are recommendations that may help with security, performance, or architectural patterns that are solutions to a commonly occurring problems.

These best practices exist to bring consistency throughout the codebase. What we're aiming for is that everyone in the team agrees what they are, and that it's consistent. It's really about saving time. It's not about being right or wrong.

This is a working document and can be changed as the codebase changes. It's a Request for Comments, if you don't agree with anything, it can be challenged. If you want to change or add anything, you can open a PR to contribute.

Think of this document as more of an explanation as to why those practices are best, rather than rules that are enforced.

The main focus is Laravel, however, this may also extend this to PHP and anything else related.

Quotes for strings

In PHP, for strings, we tend to favour single quotes over double quotes. Double quotes will look for variables, while single quotes are string literals, so the main reason for using single quotes over double quotes is that it makes it easier to communicate the intent, that being single quotes are string literals, while double quotes may contain variables.

If you need to format a string, sprintf is your friend here.

Note: There is very little difference between performance, and this would largely be a premature optimisation, optimise for whichever is most readable and consistent.

Type hints function arguments

Since PHP 5, type declarations can be added to function arguments, return values, and, as of PHP 7.4.0, class properties, so you can use them.

If the docblock is simply a repeat of these adding no new information, just annotations, then it's redundant, you don't need both and they can be safely removed.

You'll notice this is mirrored in the latest Laravel documents and code.

Use type hinting

Type hinting helps to make it easier to not only firm up exactly what you expect to happen logically, but it also helps make it easier to navigate the system as, if objects are type hinted, you will know what methods are available on that object, and so will your IDE.

Use dependency injection in controllers

You shouldn't need to use Auth::user() to get the user, you should be able to get this off the request by injecting it into the function. You can get the user from the Request object injected, for example, $request->user().

Also try to avoid as much as possible newing up objects (new MyObject()) if they can be injected. This helps decoupling classes and allows more flexibility when using Laravel's container

Use the language constructs instead of functions

  • Always use modern arrays [] over the legacy deprecated array().
  • Use $var === null rather than functions such as is_null(), they are quicker (because they are a language construct) and easier to read.
  • (int) $data['price'] can be used instead (reduces cognitive load, up to 6x times faster in PHP 5.x) vs intval()

Use model relationships to keep it simple

For example:

$existingBatch = SentLetterBatch::where('account_id', $accountId)
            ->where('user_id', $userId)
            ->where('id', $batchId)
            ->first();

Better is:

$existingBatch = $account->sentLetterBatch()
            ->where('user_id', $userId)
            ->where('id', $batchId)
            ->first();

Better still might be:

$existingBatch = $user->sentLetterBatch()
            ->where('id', $batchId)
            ->first();

Better still might be:

$existingBatch = $user->sentLetterBatch()->find($batchId);

If the query is large, use local scopes.

Local scopes allow you to define common sets of query constraints that you may easily re-use throughout your application.

You can use global scopes to add constraints to all queries for a given model, this can be used in conjunction with a trait to centralise the logic (similar to the SoftDelete trait).

You can use a dedicated query builder, when your model is too large and has too many scopes.

Avoid using Switch/Case

See: Avoid using switch-case statements - jpswade

Avoid using exceptions, except when exceptional

See: Exceptions are meant to be exceptional - jpswade

Avoid using bare exceptions

We should avoid using the default Exception as all other exceptions extend it, so in a genuine case (the exceptions that prove the rule) when you do need to catch an exception, you want to only catch the exception you're handling.

Avoid try/catch

A try/catch is quite expensive and is rarely needed. Try to think of a better way first.

All PHP files should end with a new line

This so you don't need the closing PHP tag. This is a PSR standard!

Use named queues

Avoid using the default queue, as it easily gets blocked up with lots of jobs, it'll be really difficult to debug if there's an issue and can cause unexpected behaviour in other parts of the system.

Handle methods should contain very little code

Both jobs and commands there are handle() functions. These should contain as little logic as possible, think of them as similar to controllers, they should be lightweight.

The logic should be abstracted away, so that it can be tested independently, or moved if needed.

Avoid fat controllers

In an MVC pattern, business logic does not belong in the controllers, it would belong in the model.

See: Taylor Otwell: "Thin" Controllers, "Fat" Models Approach

As per the most common architectures (hexagonal for example), logic is suggested to be added to the second layer (business layer), in this case services, and keep the models/repositories as data layers. Extracting the logic from the models allows us to use them as DTOs too by adding getters/setters and using return $this->attributes['my_property'] within those methods.

Validation

All user input coming from user land MUST have validation.

Avoid magic numbers

Set constants for magic numbers, this should give context to what that number is and why you chose it.

Use triple equals for exact type matching

The triple equals (===) comparison operator, is a type safe comparison match and will match the exact type.

You should always use this by default as there should be very few circumstances where you don't know the type.

Compared to the double equals (==) which is not type safe, this will match true == 1 as true, while triple equals won't.

Items in an array should end with a trailing comma

This means when you add to an array, you're only touching one line, rather than two.

There's fewer changes made and so less to review, and it'll decrease change volume.

Return early

Methods should have consistent return types

If you're only dealing with one return type, then it makes it much easier to reason about.

For example:

  • When returning early, use an empty version of the same type or null, rather than false.
  • When you're returning a string, you can use an empty string ('')
  • When using arrays, return an empty array ([]).
  • With objects, you can return a null, but there's no need to return a boolean false, as this would be inconsistent typing.

Sometimes, you don’t need to return anything, you could consider an exception, if it's an exceptional situation.

Because it is dynamically typed, PHP does not enforce a return type on a function. This means that different paths through a function can return different types of values, which can be very confusing to the user and significantly harder to maintain.

In particular, it is consequently also possible to mix empty return statements (implicitly returning null) with some returning an expression. This rule verifies that all the return statements from a function are consistent.

Consistently returning the same type means that we can always trust the response of a function or method. It also means that if mistakes are made and the response is treated as truthy, even when it’s not, the application will often fail gracefully.

Remove unused code

Unused code is a code smell. It means there's no tests. It means it's not being used and may drift. If it's not being used we should get rid of it. You ain't gonna need it! (YAGNI)

Employ TDD practices

Write the test (red), make the test pass (green), refactor the code to make it better.

When you have a bug, write code that will set up the test to reflect the same conditions. Make improvements until the test passes. This will help to build confidence that you've fixed the issue, and that it won't regress.

Keep tables and models extensible

Avoid a table/model for each purpose, instead leave it open to extension and extensible. For example, you don't need to make a table for each type of user, instead just add a type column to the table.

Add soft deletes by default

It should be very rare that we would want to permanently delete something straight away. Users expect to be able to “undo” all of their actions and keeping data gives us a paper trail, so defaulting to always using soft deletes is a good idea.

Consistent variable names and naming conventions

Form field names, tables names, column names, url argument names should always be snake-case, eg: snake_case. This is because http queries and mysql fields aren't case sensitive.

In PHP, variables should be camel case, eg: $camelCase, this is because PHP variables are case sensitive and is the standard.

In CSS classes and ids should be kebab-case (eg: dash-separated-case).

Use eloquent each

When working with a database query, using eloquent each uses cursors so it's quicker. It's quicker because you're not collecting the whole result set into memory then iterating using a foreach.

Don't use env() outside of config files

As per the Laravel documentation:

If you execute the config:cache command during your deployment process, you should be sure that you are only calling the env function from within your configuration files. Once the configuration has been cached, the .env file will not be loaded; therefore, the env function will only return external, system level environment variables.

Avoid using the “not” logical operator on it's own

Take this example:

If (!$value) { ... }

Although this is quite common in PHP, from the beginning, in larger codebases it has become less common because it does not communicate intent very well.

It isn't type safe, and this would return true for if $value was false, null, 0, an empty string (‘') etc.

That's why it's much better to be strict and type safe, or use empty, as it demonstrates intent.

Better, when you know the return type is a boolean:

if ($value === false) { ... }

Better, when you know the return type is numeric:

if ($value > 0) { ... }

Better, when you know the return type can be an object or null:

if ($value === null) { ... }

Better, when you don't know what type you're expecting back, and you want to include things like empty arrays or 0 strings (“0”) as empty too:

if (empty($value) === true) { ... }

Note: You can use the built-in Laravel functions such as blank or filled.

Don't be afraid to create helpers functions

When you have functionality that is commonly used across the system and is immutable in nature, then it's a prime candidate for a helper.

Avoid using database transactions

Database transactions are great and really powerful but use them carefully and sparingly to avoid locking tables.

As a Rule of Thumb, do not use Database Transactions, unless you have to. It should be the exception.

Also see

Always add declare(strict_types = 1) at the beginning of the file

This will help to enforce your classes to keep types consistent and unique when type hinted. For example, this will fail if the string is not cast before using it:

public function doSomething(int $x): int { /* ... */ }
$this->doSomething('1');

Naming conventions for methods

There's two problems, naming is hard and people think it doesn't matter, that it's just semantics or bike shedding when actually it's probably one of the most important things.

Naming things correctly is as important as well written code itself, it's all about communication.

Method names should be contextual to the class and namespace they are in. For example, if you have a Person class, then the method to get the person should be simply “get”, not getPerson.

Following the single-responsibility principle (SRP), having “and” in a method name would signal a code smell and should be avoided.

See also:

Don't needlessly access relationships from models

You don't need to load the whole relationship just to get the id, for example:

Where we'd get an account ID for a user, it would be better to use $user->account_id rather than $user->account->id.

as that would load in that relationship from the database, making an extra, needless call and putting load on the database that's not necessary.

Use eager loading

When a query is obviously slow, when it's looping through to load many of the relationships, to save calling each of them as separate queries, you can use “with” to do the query up front and eager load the relationships.

Use integers not floats for currency and money

When it comes to money and currency, as a rule of thumb it's better to use integers rather than floats.

floating point arithmetic can cause rounding errors

using floats to handle money can lead to unexpected results

You'll find that large companies, including Stripe and Google use integers for currencies for this reason.

They tend to use the "currency’s smallest unit" or "micros".

Avoid talking directly to the database

As a Rule of Thumb, always use the Eloquent Models to talk to the database and avoid talking directly to the database tables.

Laravel's Eloquent Models are designed to make talking to the database easier.

Laravel includes Eloquent, an object-relational mapper (ORM) that makes it enjoyable to interact with your database. When using Eloquent, each database table has a corresponding "Model" that is used to interact with that table. In addition to retrieving records from the database table, Eloquent models allow you to insert, update, and delete records from the table as well.

It's always more desirable to use the ORM pattern when talking to the database, as it will help to ensure that we talk to the database in a consistent manner.

It'll also ensure that relationships are easy to work with. This allows you to use model relationships to keep it simple!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment