Laravel
Laravel Coding Style Guide
General Principles
- Follow the PSR-12 coding standard for Laravel.
- Maintain consistent and readable code.
Folder and File Structure
- Organize backend code into appropriate folders (
Models,Controllers,Services,Jobs, etc.).
Naming Conventions
| 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 |
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();
}Methods
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;
}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();
}
}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',
];
}
}Services (Business logic)
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');
}
}
}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();
}Prefer Eloquent
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` DESCGood:
Article::has('user.profile')->verified()->latest()->get();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());Do not execute queries in Blade templates
Bad (for 100 users, 101 DB queries will be executed):
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeachGood (for 100 users, 2 DB queries will be executed):
$users = User::with('profile')->get();
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeachChunk data for data-heavy tasks
Bad:
$users = $this->get();
foreach ($users as $user) {
...
}Good:
$this->chunk(500, function ($users) {
foreach ($users as $user) {
...
}
});Prefer descriptive method
Bad:
// Determine if there are any joins
if (count((array) $builder->getQuery()->joins) > 0)Good:
if ($this->hasJoins())
```### **Prefer descriptive method and variable names over comments**
Bad:
```php
// Determine if there are any joins
if (count((array) $builder->getQuery()->joins) > 0)Good:
if ($this->hasJoins())Do not put JS and CSS in Blade templates
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.
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'));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);
}
}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') |
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());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');Store dates in the standard format
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:
// Model
protected $casts = [
'ordered_at' => 'datetime',
];
// Blade view
{{ $object->ordered_at->toDateString() }}
{{ $object->ordered_at->format('m-d') }}Code Formatting
- Use 4 spaces for indentation.
- Place one blank line between logical blocks of code.
- Limit line length to 120 characters where possible.
Auto format using Laravel Pint learn more
./vendor/bin/pint --repairTesting
- Write unit tests using Jest for React components and Laravel's built-in PHPUnit for backend logic.
- Use factories for creating test data in Laravel.
- Test Inertia responses to ensure proper integration.
Standard Laravel tools
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 |
Recommend Tools & Configuration
- Awesomecode theme and code editor setup for VS Code
- Laravel Herd PHP development environment.
- Laravel Boost for AI-assisted coding.
- Laravel Pint for PHP code formatting.
- Dbngin for managing databases.
- VS Code extension Official Laravel, Laravel Pack, ES7+ React/Redux/React-Native snippets