- Notifications
You must be signed in to change notification settings - Fork2.4k
Laravel best practices
alexeymezenin/laravel-best-practices
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
You might also want to check out thereal-world Laravel example application andEloquent SQL reference
Translations:
Nederlands (byProtoqol)
Indonesia (byP0rguy,Doni Ahmad)
한국어 (bycherrypick)
বাংলা (byAnowar Hossain)
Українська (byTenevyk)
Français (byMikayil S.)
Deutsch (bySujal Patel)
Italiana (bySujal Patel)
العربية (byahmedsaoud31)
اردو (byRizwanAshraf1)
Single responsibility principle
Methods should do just one thing
Fat models, skinny controllers
Business logic should be in service class
Prefer to use Eloquent over using Query Builder and raw SQL queries. Prefer collections over arrays
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
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
A class should have only one responsibility.
Bad:
publicfunctionupdate(Request$request):string{$validated =$request->validate(['title' =>'required|max:255','events' =>'required|array:date,type' ]);foreach ($request->eventsas$event) {$date =$this->carbon->parse($event['date'])->toString();$this->logger->log('Update event' .$date .' ::' .$); }$this->event->updateGeneralEvent($request->validated());returnback();}
Good:
publicfunctionupdate(UpdateRequest$request):string{$this->logService->logEvents($request->events);$this->event->updateGeneralEvent($request->validated());returnback();}
A function should do just one thing and do it well.
Bad:
publicfunctiongetFullNameAttribute():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:
publicfunctiongetFullNameAttribute():string{return$this->isVerifiedClient() ?$this->getFullNameLong() :$this->getFullNameShort();}publicfunctionisVerifiedClient():bool{returnauth()->user() &&auth()->user()->hasRole('client') &&auth()->user()->isVerified();}publicfunctiongetFullNameLong():string{return'Mr.' .$this->first_name .'' .$this->middle_name .'' .$this->last_name;}publicfunctiongetFullNameShort():string{return$this->first_name[0] .'.' .$this->last_name;}
Put all DB related logic into Eloquent models.
Bad:
publicfunctionindex(){$clients = Client::verified() ->with(['orders' =>function ($q) {$q->where('created_at','>', Carbon::today()->subWeek()); }]) ->get();returnview('index', ['clients' =>$clients]);}
Good:
publicfunctionindex(){returnview('index', ['clients' =>$this->client->getWithNewOrders()]);}class Clientextends Model{publicfunctiongetWithNewOrders():Collection {return$this->verified() ->with(['orders' =>function ($q) {$q->where('created_at','>', Carbon::today()->subWeek()); }]) ->get(); }}
Move validation from controllers to Request classes.
Bad:
publicfunctionstore(Request$request){$request->validate(['title' =>'required|unique:posts|max:255','body' =>'required','publish_at' =>'nullable|date', ]);...}
Good:
publicfunctionstore(PostRequest$request){...}class PostRequestextends Request{publicfunctionrules():array {return ['title' =>'required|unique:posts|max:255','body' =>'required','publish_at' =>'nullable|date', ]; }}
A controller must have only one responsibility, so move business logic from controllers to service classes.
Bad:
publicfunctionstore(Request$request){if ($request->hasFile('image')) {$request->file('image')->move(public_path('images') .'temp'); }...}
Good:
publicfunctionstore(Request$request){$this->articleService->handleUploadedImage($request->file('image'));...}class ArticleService{publicfunctionhandleUploadedImage($image):void {if (!is_null($image)) {$image->move(public_path('images') .'temp'); } }}
Reuse code when you can. SRP is helping you to avoid duplication. Also, reuse Blade templates, use Eloquent scopes etc.
Bad:
publicfunctiongetActive(){return$this->where('verified',1)->whereNotNull('deleted_at')->get();}publicfunctiongetArticles(){return$this->whereHas('user',function ($q) {$q->where('verified',1)->whereNotNull('deleted_at'); })->get();}
Good:
publicfunctionscopeActive($q){return$q->where('verified',true)->whereNotNull('deleted_at');}publicfunctiongetActive():Collection{return$this->active()->get();}publicfunctiongetArticles():Collection{return$this->whereHas('user',function ($q) {$q->active(); })->get();}
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 outEloquent 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` ISNULL)AND`verified`='1'AND`active`='1'ORDER BY`created_at`DESC
Good:
Article::has('user.profile')->verified()->latest()->get();
Bad:
$article =newArticle;$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());
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 ($usersas$user) {{$user->profile->name }}@endforeach
Bad:
$users =$this->get();foreach ($usersas$user) {...}
Good:
$this->chunk(500,function ($users) {foreach ($usersas$user) {... }});
Bad:
// Determine if there are any joinsif (count((array)$builder->getQuery()->joins) >0)
Good:
if ($this->hasJoins())
Bad:
letarticle=`{{ json_encode($article) }}`;
Better:
<input type="hidden" value='@json($article)'>Or<button data-article='@json($article)'>{{ $article->name }}<button>
In a Javascript file:
letarticle=$('#article').val();
The best way is to use specialized PHP to JS package to transfer the data.
Bad:
publicfunctionisNormal():bool{return$article->type ==='normal';}returnback()->with('message','Your article has been added!');
Good:
publicfunctionisNormal(){return$article->type === Article::TYPE_NORMAL;}returnback()->with('message',__('app.article_added'));
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 |
FollowPSR standards.
Also, follow naming conventions accepted by Laravel community:
What | How | Good | Bad |
---|---|---|---|
Controller | singular | ArticleController | |
Route | plural | articles/1 | |
Route name | snake_case with dot notation | users.show_active | |
Model | singular | User | |
hasOne or belongsTo relationship | singular | articleComment | |
All other relationships | plural | articleComments | |
Table | plural | article_comments | |
Pivot table | singular model names in alphabetical order | article_user | |
Table column | snake_case without model name | meta_title | |
Model property | snake_case | $model->created_at | |
Foreign key | singular model name with _id suffix | article_id | |
Primary key | - | id | |
Migration | - | 2017_01_01_000000_create_articles_table | |
Method | camelCase | getAll | |
Method in resource controller | table | store | |
Method in test class | camelCase | testGuestCannotSeeArticle | |
Variable | camelCase | $articlesWithAuthor | |
Collection | descriptive, plural | $activeUsers = User::active()->get() | |
Object | descriptive, singular | $activeUser = User::active()->first() | |
Config and language files index | snake_case | articles_enabled | |
View | kebab-case | show-filtered.blade.php | |
Config | snake_case | google_calendar.php | |
Contract (interface) | adjective or noun | AuthenticationInterface | |
Trait | adjective | Notifiable | |
Trait(PSR) | adjective | NotifiableTrait | |
Enum | singular | UserType | |
FormRequest | singular | UpdateUserRequest | |
Seeder | singular | UserSeeder |
As long as you follow certain conventions, you do not need to add additional configuration.
Bad:
// Table name 'Customer'// Primary key 'customer_id'class Customerextends Model{constCREATED_AT ='created_at';constUPDATED_AT ='updated_at';protected$table ='Customer';protected$primaryKey ='customer_id';publicfunctionroles():BelongsToMany {return$this->belongsToMany(Role::class,'role_customer','customer_id','role_id'); }}
Good:
// Table name 'customers'// Primary key 'id'class Customerextends Model{publicfunctionroles():BelongsToMany {return$this->belongsToMany(Role::class); }}
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') |
new Class syntax creates tight coupling between classes and complicates testing. Use IoC container or facades instead.
Bad:
$user =newUser;$user->create($request->validated());
Good:
publicfunction__construct(protectedUser$user) {}...$this->user->create($request->validated());
Pass the data to config files instead and then use theconfig()
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');
A date as a string is less reliable than an object instance, e.g. a Carbon-instance. It's recommended to pass Carbon objects between classes instead of date strings. Rendering should be done in the display layer (templates):
Bad:
{{ Carbon::createFromFormat('Y-d-m H-i',$object->ordered_at)->toDateString() }}{{ Carbon::createFromFormat('Y-d-m H-i',$object->ordered_at)->format('m-d') }}
Good:
// Modelprotected$casts = ['ordered_at' =>'datetime',];// Blade view{{$object->ordered_at->toDateString() }}{{$object->ordered_at->format('m-d') }}
DocBlocks reduce readability. Use a descriptive method name and modern PHP features like return type hints instead.
Bad:
/** * The function checks if given string is a valid ASCII string * * @param string $string String we get from frontend which might contain * illegal characters. Returns True is the string * is valid. * * @return bool * @author John Smith * * @license GPL */publicfunctioncheckString($string){}
Good:
publicfunctionisValidAsciiString(string$string):bool{}
Avoid using patterns and tools that are alien to Laravel and similar frameworks (i.e. RoR, Django). If you like Symfony (or Spring) approach for building apps, it's a good idea to use these frameworks instead.
Never put any logic in routes files.
Minimize usage of vanilla PHP in Blade templates.
Use in-memory DB for testing.
Do not override standard framework features to avoid problems related to updating the framework version and many other issues.
Use modern PHP syntax where possible, but don't forget about readability.
Avoid using View Composers and similar tools unless you really know what you're doing. In most cases, there is a better way to solve the problem.
About
Laravel best practices
Topics
Resources
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Uh oh!
There was an error while loading.Please reload this page.