Skip to content

Laravel

Fundamental

Get the essentials right

  • Follow the official Laravel documentation.
  • Use the latest stable version. Update often. Don't use obsolete versions. Aim to bring your application to the latest Laravel version whenever possible.
  • Follow PHP best practices. You can refer to this guide for more details.

Naming convention

The standard for naming elements in Laravel is as follows:

ElementConventionExample
ClassPascalCasePostCreatedEvent
Class constantUPPER_SNAKE_CASESTATUS_COMPLETED
Class methodcamelCasepublishPost()
ModelPascalCase, singularPost
ControllerPascalCase, singularPostController
Routelowercase, plural, RESTful/posts, /posts/1, /posts/1/edit
Named routesnake_case, dottedposts.index, posts.publish_all
Viewfollows named routeposts/index.blade.php, posts/publish_all.blade.php
Model propertysnake_casesupport_email
Tablesnake_case, pluralposts, post_comments
Pivot tablesnake_case, singular, alphabeticalpost_user
Config keysnake_casecomment_relations.enabled
VariablecamelCase$postComments

Suffix class names with types

For classes that belong in standard types, such as commands, jobs, events, and so on, suffix their names with the types.

PostController
PostInterface
PostPolicy
PublishPostCommand
PublishPostJob
PostPublishedEvent

Use named routes

Name all your routes. Output the route using the route() function.

Use resource controllers

Resource controllers are controllers that have standard resourceful methods that handle well-defined RESTful routes for a model. This table below gives a good example of a resource controller that handles all RESTful routes for the Photo model:

URIMethodRoute Name
GET /photosindex()photos.index
GET /photos/createcreate()photos.create
POST /photosstore()photos.store
GET /photos/{photo}show()photos.show
GET /photos/{photo}/editedit()photos.edit
PATCH/PUT /photos/{photo}update()photos.update
DELETE /photos/{photo}destroy()photos.destroy

A good controller should only have these 7 resourceful methods.

There could be 1 or 2 additional methods when they handle closely related operations to these resourceful methods, and thus can belong in the same controller. When you start to have more methods in a controller, it's a good time to refactor these non-resourceful methods out to another controller.

Put all sensitive information in the .env file

And declare them in config, then use config() in logic to get the config data.

DANGER

Never commit the .env file in version control.

Don't read environment values directly in code

Never read data from the .env file directly. Reference the data with env() in a config file instead, then use config() to get the data in logic code.

Therefore, the only place where env() should be used is in config files.

Bad:

php
// In controller
PaymentGateway::init('ABCD1234'); // API key

Bad:

php
// In controller
PaymentGateway::init(env('PAYMENT_API_KEY')); // API key

Good:

# In .env
PAYMENT_API_KEY=ABCD1234
php
// In config/payment.php
[
    'api_key' => env('PAYMENT_API_KEY'),
]
php
// In controller
PaymentGateway::init(config('payment.api_key'));

Use separate config files

Keep Laravel's default config structure as is. When you want to introduce new config, make a separate file such as config/payment.php.

Keeping the default configs helps you upgrade Laravel later easily by simply replacing with the new version. You won't have to diff each file every time there is an update to see what changes and what needs to be kept, which creates a lot of work for no extra benefit.

Use migrations

Don't change your database directly, especially production. Use migrations for it.

Use seeders to quickly set up and reset data for your project

A good project should be able to run php artisan migrate:fresh --seed to have sample data for you to work on immediately from the latest codebase.

Don't put any logic in route files

Put logic in controllers and reference it from routes. As a rule of thumb, don't put any Closure in route files.

Use cast for boolean attributes of Eloquent models

Many database engines don't have a native boolean type, so most schema designs use integer for this. In fact, Laravel migrations define a boolean field as TINYINT for MySQL. To use an attribute as a boolean in PHP, you need to specify it in the Eloquent model's $cast property.

php
// In migration
Schema::table('users', function (Blueprint $table) {
    // ...
    $table->boolean('is_admin');
});

class User
{
    protected $casts = [
        'is_admin' => 'boolean',
    ];

    public function isAdmin(): bool
    {
        return $this->is_admin; // This will return true/false instead of 1/0    
    }
}

Use shorthand to check for existence with Eloquent

After a query, there are different ways to check if an Eloquent object exists, such as empty(), is_null, and === null. In fact, a simple Not Operator ! suffices. Laravel uses this itself.

php
$product = Product::where('code', $code)->first();

if (! $product) {
    Log::error('Product does not exist.');
}

When you want to check if an Eloquent collection is empty, use ->isEmpty().

php
$products = Product::where('name', $name)->get();

if ($products->isEmpty()) {
    Log::error('No products match name.');
}

Prevent Lazy Loading

Lazy loading is bad and harms performance. Disable it during development so Laravel will throw an exception noticing you about a lazy loading so you can fix it right away.

php
class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Model::preventLazyLoading(! $this->app->isProduction());
    }
}

Intermediate

Move request validation to Form Requests

Bad:

php
public function store()
{
    // Validate input
    request()->validate([
        'title' => ['required', 'string', 'max:255'],
        'body' => ['required', 'string'],
    ]);
    
    // Retrieve the validated input data
    $data = request()->only(['title', 'body']);
}

Good:

php
class PostRequest extends FormRequest
{
    public function rules()
    {
        return [
           'title' => ['required', 'string', 'max:255'],
           'body' => ['required', 'string'],
       ];
    }
}

public function store(PostRequest $request)
{
    // Retrieve the validated input data
    $data = $request->validated();
}

Declare middlewares in routes, not controllers

When you have a middleware that can be declared in both the controller and its associated route, declare on the route for better organization.

Don't execute queries in Blade templates

Instead, get and prepare all your data before passing to the view, such as in the controller.

Always use Dependency Injection

Laravel powerfully supports dependency injection, so use it whenever you can.

For example, in controller methods, declare Request $request and use the variable to gain access to the request.

php
public function update(Request $request, Post $post)
{
    $title = $request->input('title'); // Get an input named 'title'
    $user = $request->user(); // Get the current authenticated user, instead of `Auth::user()`
}

Using $request has the obvious benefits of having a real Request object to utilize in an OOP fashion and also enables convenient unit testing later on. If you need validation, promote the Request object into a FormRequest.

Prefer Helper Functions over Facades

When injected dependency is not an option, reach for Helper Functions next. Use Facades only when there is no equivalent helper function.

Another ideal place to use helper functions is in views, where Facades cannot make usage declarations and lose you on IDE discovery.

Here are the list of Helper Functions to gain access to Laravel functionalities as opposed to Facades:

FacadeHelper FunctionExample
Requestrequest()request('title') gets input named title
Responseresponse()response('yes') outputs yes in the HTTP response
Viewview()view('posts.index') displays the view template
Cachecache()cache('timeout') gets cache value of timeout
Configconfig()config('app.url') gets the value at app.url in config
Sessionsession()session()->flash('key', 'value') flashes a session value
Loglogger()logger('Job starts') writes to log at debug level
Appapp()app('router') gets the Router instance in the container
Validatorvalidator()validator($array, ['age' => 'numeric']) validates an array
Authauth()auth()->user() gets the authenticated user
Cookiecookie()cookie('name', 'Leo') sets a Laravel cookie

Specify relationships in migrations

This helps you use a database tool to easily visualize schema modeling later.

php
$table->foreignId('user_id');

Use the primary key id in models

Respect the primary key id for your models.

Sometimes, a table may feel like it doesn't need an id field. Perhaps it has a field that looks like a primary key, such as a unique code in a tickets table. Don't make it primary key. Have id as a dedicated primary key anyway.

id has been the primary key for models in most design conventions. It gives you an indexed table for free, and follows RESTful and other design patterns. There's a reason Laravel has id as the default primary key for models and tables.

In practice, just use id() in your table migrations and use foreignId() to declare foreign keys. This is the battle-tested method in the Laravel world.

If an integer id doesn't work for your application, make it a UUID string instead by using uuid(). There is likely no scenario where you need any other method.

Use Eloquent

Use Eloquent by default for all database operations when possible. Eloquent is the most powerful feature of Laravel. Most of your data operations can and should be done with Eloquent.

If you can't use Eloquent, such as in a complicated queries with joins, use Query Builder.

Using Eloquent and Query Builder give you absolute defense against SQL injection out of the box. It also allows you to swap out the database engine for testing and potential restructuring easily.

In the rare cases when you can't use Query Builder, use raw queries with great care.

Stream between Filesystems

When you need to copy a file from a Filesystem to another, stream instead of reading the whole content of the file.

Bad:

php
$fileContent = Storage::disk('s3')->get('from/file.jpg'); 
Storage::disk('ftp')->put('to/file.jpg', $fileContent);

Good:

php
Storage::disk('ftp')->writeStream(
    'to/file.jpg', Storage::disk('s3')->readStream('from/file.jpg')
);

Use each() on collections instead of foreach

When you have a collection of objects, use the each() method on the collection and type hint the variable.

Bad:

php
foreach ($users as $user) {
    $user->invite();
}

Good:

php
$users->each(function (User $user) {
    $user->invite();
});

Advanced

Thin models, thin controllers

Both your models and controllers should be thin, containing only necessary business logic specific to the class. Refactor complicated logic into modules such as jobs, commands, and services.

Models in Laravel are Eloquent models. They should contain only logic that pertains to Eloquent features, such as reserved properties, relationships, query scopes, mutators & accessors, and so on. For logic that goes beyond basic Eloquent features, separate the concern in another module like a trait, repository, or service.

Similarly, controllers should follow a resourceful pattern, where you will only need 8 resource methods. In general, there should be at most 2 more methods for directly related operations. Anything further should be packaged into another controller.

Don't instantiate objects in command and job constructors

Avoid creating new objects in the constructors of commands. All of them are carried when the application boots up from the CLI, thus creating unnecessary footprint.

Instead, place them in the handle() method, which will run only when the command is executed. You can also inject the dependencies by type hinting in the method's declaration.

Also, avoid creating new instances in the constructors of jobs. Laravel serializes all properties of a job when sending to queue. Less data is always good for efficient storage holding, deserialization, and performance.

Cache data

Cache all the data that are frequently queried, especially static data that don't change often.

You can also cache dynamic data related to models, and use Observers to refresh the cache.

TIP

Some developers even use the request URL as the key for caching.

Break a command down into jobs

If a command feels like it does too much, refactor its logic into jobs. Queuing jobs takes only milliseconds. Moving logic this way helps you manage execution on a queue (with Horizon, for example) and minimize the command's likelihood of hitting a timeout.

Bad:

php
// ProcessImagesCommand.php
public function handle()
{
    $this->images->each(function (Image as $image) {
        $image->process(); // Heavy operation    
    });
}

Good:

php
// ProcessImagesCommand.php
public function handle()
{
    $this->images->each(function (Image $image) {
        ProcessImageJob::dispatch($image); // Refactor into job    
    });
}

// ProcessImageJob.php
public function handle()
{
    $this->image->process();
}