laravel-best-practices README

The project's README from github.com/alexeymezenin/laravel-best-practices. As one markdown file: README.md.

Laravel best practices

You might also want to check out the real-world Laravel example application and Eloquent SQL reference

Translations:

Nederlands (by Protoqol)

Indonesia (by P0rguy, Doni Ahmad)

한국어 (by cherrypick)

日本語 (by 2bo)

简体中文 (by xiaoyi)

繁體中文 (by woeichern)

ภาษาไทย (by kongvut sangkla)

বাংলা (by Anowar Hossain)

فارسی (by amirhossein baghaie)

Português (by jonaselan)

Українська (by Tenevyk)

Русский

Tiếng Việt (by Chung Nguyễn)

Español (by César Escudero)

Français (by Mikayil S.)

Polski (by Karol Pietruszka)

Română (by als698)

Türkçe (by Burak)

Turkmen (by Artyom Osepyan)

Deutsch (by Sujal Patel)

Italiana (by Sujal Patel)

Azərbaycanca (by Maharramoff)

العربية (by ahmedsaoud31)

اردو (by RizwanAshraf1)

မြန်မာဘာသာ (by Kaung Zay Yan)

Laravel example app

Contents

Single responsibility principle

Methods should do just one thing

Fat models, skinny controllers

Validation

Business logic should be in service class

Don't repeat yourself (DRY)

Prefer to use Eloquent over using Query Builder and raw SQL queries. Prefer collections over arrays

Mass assignment

Do not execute queries in Blade templates and use eager loading (N + 1 problem)

Chunk data for data-heavy tasks

Comment your code, but prefer descriptive method and variable names over comments

Do not put JS and CSS in Blade templates and do not put any HTML in PHP classes

Use config and language files, constants instead of text in the code

Use standard Laravel tools accepted by community

Follow Laravel naming conventions

Convention over configuration

Use shorter and more readable syntax where possible

Use IoC / Service container instead of new Class

Do not get data from the .env file directly

Store dates in the standard format. Use accessors and mutators to modify date format

Do not use DocBlocks

Other good practices

Single responsibility principle

A class should have only one responsibility.

Bad:

public function update(Request $request): string
{
    $validated = $request->validate([
        'title' => 'required|max:255',
        'events' => 'required|array:date,type'
    ]);

    foreach ($request->events as $event) {
        $date = $this->carbon->parse($event['date'])->toString();

        $this->logger->log('Update event ' . $date . ' :: ' . $);
    }

    $this->event->updateGeneralEvent($request->validated());

    return back();
}

Good:

public function update(UpdateRequest $request): string
{
    $this->logService->logEvents($request->events);

    $this->event->updateGeneralEvent($request->validated());

    return back();
}

🔝 Back to contents

Methods should do just one thing

A function should do just one thing and do it well.

Bad:

public function getFullNameAttribute(): string
{
    if (auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified()) {
        return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
    } else {
        return $this->first_name[0] . '. ' . $this->last_name;
    }
}

Good:

public function getFullNameAttribute(): string
{
    return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort();
}

public function isVerifiedClient(): bool
{
    return auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified();
}

public function getFullNameLong(): string
{
    return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
}

public function getFullNameShort(): string
{
    return $this->first_name[0] . '. ' . $this->last_name;
}

🔝 Back to contents

Fat models, skinny controllers

Put all DB related logic into Eloquent models.

Bad:

public function index()
{
    $clients = Client::verified()
        ->with(['orders' => function ($q) {
            $q->where('created_at', '>', Carbon::today()->subWeek());
        }])
        ->get();

    return view('index', ['clients' => $clients]);
}

Good:

public function index()
{
    return view('index', ['clients' => $this->client->getWithNewOrders()]);
}

class Client extends Model
{
    public function getWithNewOrders(): Collection
    {
        return $this->verified()
            ->with(['orders' => function ($q) {
                $q->where('created_at', '>', Carbon::today()->subWeek());
            }])
            ->get();
    }
}

🔝 Back to contents

Validation

Move validation from controllers to Request classes.

Bad:

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

    ...
}

Good:

public function store(PostRequest $request)
{
    ...
}

class PostRequest extends Request
{
    public function rules(): array
    {
        return [
            'title' => 'required|unique:posts|max:255',
            'body' => 'required',
            'publish_at' => 'nullable|date',
        ];
    }
}

🔝 Back to contents

Business logic should be in service class

A controller must have only one responsibility, so move business logic from controllers to service classes.

Bad:

public function store(Request $request)
{
    if ($request->hasFile('image')) {
        $request->file('image')->move(public_path('images') . 'temp');
    }
    
    ...
}

Good:

public function store(Request $request)
{
    $this->articleService->handleUploadedImage($request->file('image'));

    ...
}

class ArticleService
{
    public function handleUploadedImage($image): void
    {
        if (!is_null($image)) {
            $image->move(public_path('images') . 'temp');
        }
    }
}

🔝 Back to contents

Don't repeat yourself (DRY)

Reuse code when you can. SRP is helping you to avoid duplication. Also, reuse Blade templates, use Eloquent scopes etc.

Bad:

public function getActive()
{
    return $this->where('verified', 1)->whereNotNull('deleted_at')->get();
}

public function getArticles()
{
    return $this->whereHas('user', function ($q) {
            $q->where('verified', 1)->whereNotNull('deleted_at');
        })->get();
}

Good:

public function scopeActive($q)
{
    return $q->where('verified', true)->whereNotNull('deleted_at');
}

public function getActive(): Collection
{
    return $this->active()->get();
}

public function getArticles(): Collection
{
    return $this->whereHas('user', function ($q) {
            $q->active();
        })->get();
}

🔝 Back to contents

Prefer to use Eloquent over using Query Builder and raw SQL queries. Prefer collections over arrays

Eloquent allows you to write readable and maintainable code. Also, Eloquent has great built-in tools like soft deletes, events, scopes etc. You may want to check out Eloquent to SQL reference

Bad:

SELECT *
FROM `articles`
WHERE EXISTS (SELECT *
              FROM `users`
              WHERE `articles`.`user_id` = `users`.`id`
              AND EXISTS (SELECT *
                          FROM `profiles`
                          WHERE `profiles`.`user_id` = `users`.`id`) 
              AND `users`.`deleted_at` IS NULL)
AND `verified` = '1'
AND `active` = '1'
ORDER BY `created_at` DESC

Good:

Article::has('user.profile')->verified()->latest()->get();

🔝 Back to contents

Mass assignment

Bad:

$article = new Article;
$article->title = $request->title;
$article->content = $request->content;
$article->verified = $request->verified;

// Add category to article
$article->category_id = $category->id;
$article->save();

Good:

$category->article()->create($request->validated());

🔝 Back to contents

Do not execute queries in Blade templates and use eager loading (N + 1 problem)

Bad (for 100 users, 101 DB queries will be executed):

@foreach (User::all() as $user)
    {{ $user->profile->name }}
@endforeach

Good (for 100 users, 2 DB queries will be executed):

$users = User::with('profile')->get();

@foreach ($users as $user)
    {{ $user->profile->name }}
@endforeach

🔝 Back to contents

Chunk data for data-heavy tasks

Bad:

$users = $this->get();

foreach ($users as $user) {
    ...
}

Good:

$this->chunk(500, function ($users) {
    foreach ($users as $user) {
        ...
    }
});

🔝 Back to contents

Prefer descriptive method and variable names over comments

Bad:

// Determine if there are any joins
if (count((array) $builder->getQuery()->joins) > 0)

Good:

if ($this->hasJoins())

🔝 Back to contents

Do not put JS and CSS in Blade templates and do not put any HTML in PHP classes

Bad:

let article = `{{ json_encode($article) }}`;

Better:

<input id="article" type="hidden" value='@json($article)'>

Or

<button class="js-fav-article" data-article='@json($article)'>{{ $article->name }}<button>

In a Javascript file:

let article = $('#article').val();

The best way is to use specialized PHP to JS package to transfer the data.

🔝 Back to contents

Use config and language files, constants instead of text in the code

Bad:

public function isNormal(): bool
{
    return $article->type === 'normal';
}

return back()->with('message', 'Your article has been added!');

Good:

public function isNormal()
{
    return $article->type === Article::TYPE_NORMAL;
}

return back()->with('message', __('app.article_added'));

🔝 Back to contents

Use standard Laravel tools accepted by community

Prefer to use built-in Laravel functionality and community packages instead of using 3rd party packages and tools. Any developer who will work with your app in the future will need to learn new tools. Also, chances to get help from the Laravel community are significantly lower when you're using a 3rd party package or tool. Do not make your client pay for that.

Task Standard tools 3rd party tools
Authorization Policies Entrust, Sentinel and other packages
Compiling assets Laravel Mix, Vite Grunt, Gulp, 3rd party packages
Development Environment Laravel Sail, Homestead Docker
Deployment Laravel Forge Deployer and other solutions
Unit testing PHPUnit, Mockery Phpspec, Pest
Browser testing Laravel Dusk Codeception
DB Eloquent SQL, Doctrine
Templates Blade Twig
Working with data Laravel collections Arrays
Form validation Request classes 3rd party packages, validation in controller
Authentication Built-in 3rd party packages, your own solution
API authentication Laravel Passport, Laravel Sanctum 3rd party JWT and OAuth packages
Creating API Built-in Dingo API and similar packages
Working with DB structure Migrations Working with DB structure directly
Localization Built-in 3rd party packages
Realtime user interfaces Laravel Echo, Pusher 3rd party packages and working with WebSockets directly
Generating testing data Seeder classes, Model Factories, Faker Creating testing data manually
Task scheduling Laravel Task Scheduler Scripts and 3rd party packages
DB MySQL, PostgreSQL, SQLite, SQL Server MongoDB

🔝 Back to contents

Follow Laravel naming conventions

Follow PSR standards.

Also, follow naming conventions accepted by Laravel community:

What How Good Bad
Controller singular ArticleController ArticlesController
Route plural articles/1 article/1
Route name snake_case with dot notation users.show_active users.show-active, show-active-users
Model singular User Users
hasOne or belongsTo relationship singular articleComment articleComments, article_comment
All other relationships plural articleComments articleComment, article_comments
Table plural article_comments article_comment, articleComments
Pivot table singular model names in alphabetical order article_user user_article, articles_users
Table column snake_case without model name meta_title MetaTitle; article_meta_title
Model property snake_case $model->created_at $model->createdAt
Foreign key singular model name with _id suffix article_id ArticleId, id_article, articles_id
Primary key - id custom_id
Migration - 2017_01_01_000000_create_articles_table 2017_01_01_000000_articles
Method camelCase getAll get_all
Method in resource controller table store saveArticle
Method in test class camelCase testGuestCannotSeeArticle test_guest_cannot_see_article
Variable camelCase $articlesWithAuthor $articles_with_author
Collection descriptive, plural $activeUsers = User::active()->get() $active, $data
Object descriptive, singular $activeUser = User::active()->first() $users, $obj
Config and language files index snake_case articles_enabled ArticlesEnabled; articles-enabled
View kebab-case show-filtered.blade.php showFiltered.blade.php, show_filtered.blade.php
Config snake_case google_calendar.php googleCalendar.php, google-calendar.php
Contract (interface) adjective or noun AuthenticationInterface Authenticatable, IAuthentication
Trait adjective Notifiable NotificationTrait
Trait (PSR) adjective NotifiableTrait Notification
Enum singular UserType UserTypes, UserTypeEnum
FormRequest singular UpdateUserRequest UpdateUserFormRequest, UserFormRequest, UserRequest
Seeder singular UserSeeder UsersSeeder

🔝 Back to contents

Convention over configuration

As long as you follow certain conventions, you do not need to add additional configuration.

Bad:

// Table name 'Customer'
// Primary key 'customer_id'
class Customer extends Model
{
    const CREATED_AT = 'created_at';
    const UPDATED_AT = 'updated_at';

    protected $table = 'Customer';
    protected $primaryKey = 'customer_id';

    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class, 'role_customer', 'customer_id', 'role_id');
    }
}

Good:

// Table name 'customers'
// Primary key 'id'
class Customer extends Model
{
    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class);
    }
}

🔝 Back to contents

Use shorter and more readable syntax where possible

Bad:

$request->session()->get('cart');
$request->input('name');

Good:

session('cart');
$request->name;

More examples:

Common syntax Shorter and more readable syntax
Session::get('cart') session('cart')
$request->session()->get('cart') session('cart')
Session::put('cart', $data) session(['cart' => $data])
$request->input('name'), Request::get('name') $request->name, request('name')
return Redirect::back() return back()
is_null($object->relation) ? null : $object->relation->id optional($object->relation)->id (in PHP 8: $object->relation?->id)
return view('index')->with('title', $title)->with('client', $client) return view('index', compact('title', 'client'))
$request->has('value') ? $request->value : 'default'; $request->get('value', 'default')
Carbon::now(), Carbon::today() now(), today()
App::make('Class') app('Class')
->where('column', '=', 1) ->where('column', 1)
->orderBy('created_at', 'desc') ->latest()
->orderBy('age', 'desc') ->latest('age')
->orderBy('created_at', 'asc') ->oldest()
->select('id', 'name')->get() ->get(['id', 'name'])
->first()->name ->value('name')

🔝 Back to contents

Use IoC / Service container instead of new Class

new Class syntax creates tight coupling between classes and complicates testing. Use IoC container or facades instead.

Bad:

$user = new User;
$user->create($request->validated());

Good:

public function __construct(protected User $user) {}

...

$this->user->create($request->validated());

🔝 Back to contents

Do not get data from the .env file directly

Pass the data to config files instead and then use the config() helper function to use the data in an application.

Bad:

$apiKey = env('API_KEY');

Good:

// config/api.php
'key' => env('API_KEY'),

// Use the data
$apiKey = config('api.key');

🔝 Back to contents

Store dates in the standard format. Use accessors and mutators to modify date format

A date as a string is less reliable than an object instance,

laravel-best-practices (alexeymezenin/laravel-best-practices)

Searching Database…

这是什么?一份广受欢迎的社区指南(12k+ star),收录了编写 Laravel 应用的最佳实践。每条实践都配有一个反面代码示例和一个正面代码示例,并且这份指南已被翻译成 25 种语言。
这是要加到我项目里的包,还是一份供阅读的指南?只是一份指南——单个 README 文档,没有任何东西需要安装。你阅读这些实践并应用到自己的代码中;每条规则都用一个反面示例搭配一个正面示例来展示。
我应该怎么用它——从头到尾通读吗?它组织成一个带链接的目录,每一项都是一小节,包含一组正反示例,所以大多数人会浏览目录后直接跳到与自己当前代码相关的部分。完整读一遍用不了一个小时。
它是 Laravel 官方规范,还是某个开发者的一家之言?这是一份由一位作者(alexeymezenin)维护、由志愿者翻译的社区指南——并非 Laravel 官方文档。这些规则带有鲜明的个人倾向,比如不用 DocBlocks、用描述性命名代替注释,但每条都用一组正反示例来论证,你可以自行判断。
我是 Laravel 新手——这是给新手看的还是给有经验的开发者看的?主要面向已经在写 Laravel 代码的开发者,因为每个示例都默认你了解 Eloquent、Blade 和数据库迁移。在你做出第一个应用、想把它理顺的时候,这份指南的收益最大。
为什么这份指南一直推崇 Eloquent?因为它能让代码保持易读、易维护,而且自带软删除、事件、作用域等内置工具。指南建议在大多数情况下优先使用它,而不是 Query Builder 和原生 SQL。
它推荐了哪些能立刻上手的语法技巧?有:用 session('cart') 代替 Session::get('cart'),用 back() 代替 Redirect::back(),用 ->latest() 代替 orderBy('created_at', 'desc'),以及通过 IoC 容器或 app() 辅助函数来解析类,而不是用 new Class——后者会把类耦合在一起,也让测试变复杂。
它有没有指向值得一读的相关资源?两个:一个真实场景的 Laravel 示例应用,以及一份 Eloquent 到 SQL 的对照参考,展示你的 ORM 调用背后实际执行的查询。
Size
1 MB
Stars
12.3K
Updated
CommitMay 10
Age
8y
Commits · 90 Days