# Laravel

# Fundamental

# Follow the official Laravel documentation

Laravel has very good documentation that you can use to reference on developing a Laravel application.

# Use the latest stable version

Be mindful of the Laravel release and support cycles. Update often. Don't use obsolete versions. Ideally, bring your application to the latest version along with the right PHP version.

# Follow PHP Best Practices

When you develop a Laravel application, you use PHP. Therefore, follow PHP Best Practices.

# Use Composer

Use Composer to install Laravel itself and get other packages. It also helps you update packages and manage dependencies smoothly.

# Basic

# Naming convention

The standard for naming elements in Laravel is as follows:

Element Convention Example
Class PascalCase PostCreatedEvent
Class constant UPPER_SNAKE_CASE CREATED_AT
Class method camelCase publishPost()
Model PascalCase, singular Post
Controller PascalCase, singular PostController
Route lowercase, plural, RESTful posts, posts/1, posts/1/edit
Named route snake_case, dotted posts.index, posts.publish_all
View follows named route posts/index.blade.php, posts/publish_all.blade.php
Model properties snake_case updated_at
Table snake_case, plural posts, post_comments
Pivot table snake_case, singular, alphabetical post_user
Config key snake_case comment_relations.enabled
Variable camelCase $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 by 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:

URI Method Route Name
GET /photos index() photos.index
GET /photos/create create() photos.create
POST /photos store() photos.store
GET /photos/{photo} show() photos.show
GET /photos/{photo}/edit edit() 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.

WARNING

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.

TIP

The only place where env() should be used is in config files.

Bad:

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

Bad:

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

Good:

# In .env file
PAYMENT_API_KEY=ABCD1234
// In config/payment.php config file
[
    'api_key' => env('PAYMENT_API_KEY'),
]
// In a 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 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, especially production, directly. 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 immediately work on from the last point. A good first example is a user seeder.

# Don't put any logic in route files

Extract all logic handling in controllers and reference them in routes. As a general rule of thumb, don't ever have any Closure in route files. This also helps enable route caching which makes your application extremely fast in handling routes.

# Use cast for boolean attributes of Eloquent models

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

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

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

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

# Use shorthand to check for existence with Eloquent

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

$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().

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

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

# Intermediate

# Create the Models directory

Create a Models directory under the app directory, then put all Eloquent models in it. This adheres to the principle of separation of concerns and will make your code much more maintainable in the long run.

# Move request validation to Form Requests

Bad:

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:

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 or the route group 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.

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 namespace and usage declarations, thus you will lose out on IDE discovery.

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

Facade Helper Function Example
Request request() request('title') gets input named title
Response response() response('yes') outputs yes in the HTTP response
View view() view('posts.index') displays the view template
Cache cache() cache('timeout') gets cache value of timeout
Config config() config('app.url') gets the value at app.url in config
Session session() session()->flash('key', 'value') flashes a session value
Log logger() logger('Job starts') writes to log at debug level
App app() app('router') gets the Router instance in the container
Validator validator() validator($array, ['age' => 'numeric']) validates an array
Auth auth() auth()->user() gets the authenticated user
Cookie cookie() cookie('name', 'Leo') sets a Laravel cookie

# Specify relationships in migrations

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

$table->foreign('user_id')
	->references('id')
    ->on('users');

// Laravel 7
$table->foreignId('user_id');

# Respect the primary key id in models

Always try have the primary key id for your models.

Sometimes, you might think you don't need an id field, or you have another field that looks temptingly like it could be a primary key, like a unique code in a tickets table. Don't fall for it. Have id as a dedicated primary key anyway.

id has been the primary key for models in most design conventions. Having it helps you automatically have an indexed table for free and follow RESTful and other design patterns easily. 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 a big 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 restructuring or testing anytime.

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

# Stream between Filesystems

When you need to copy a file from an origin Filesystem to a target Filesystem, stream instead of reading the whole content of the file with get().

Bad:

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

Good:

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. It gives you type hinting.

Bad:

foreach ($emails as $email) {
    $email->send();
}

Good:

$emails->each(function (Email $email) {
    $email->send();
});

# Advanced

# Thin models, thin controllers

Both your models and controllers should be thin, containing only necessary high-level business logic. Refactor complicated logic into other 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 Service, Repository, or Trait.

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 extraordinarily 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 run no matter what 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, if you can. Laravel serializes all properties of a Job when sending to queues, and having less data is always good for a faster storage holding, deserialization, and performance.

# Cache data

Cache all the data that are frequently queried, especially static data.

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

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

# When you can, break a Command down and refactor logic into Jobs

If a Command feels like it does too much, refactor 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:

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

Good:

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

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