Laravel best practices

Prinsip single responsibility

Kelas dan metode seharusnya hanya memiliki satu tanggung jawab.

Contoh buruk:

1
2
3
4
5
6
7
8
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;
    }
}

Contoh terbaik:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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;
}

Model tebal, controller tipis

Masukkan semua logika terkait DB ke model eloquent atau ke dalam kelas repositori jika anda menggunakan Query Builder atau kueri SQL mentah.

Contoh buruk:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public function index()
{
    $clients = Client::verified()
        ->with(['orders' => function ($q) {
            $q->where('created_at', '>', Carbon::today()->subWeek());
        }])
        ->get();

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

Contoh terbaik:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public function index()
{
    return view('index', ['clients' => $this->client->getWithNewOrders()]);
}

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

Validasi

Pindahkan validasi dari controller ke kelas request.

Contoh buruk:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public function store(Request $request)
{
    $request->validate([
        'title' => 'required|unique:posts|max:255',
        'body' => 'required',
        'publish_at' => 'nullable|date',
    ]);

    ...
}

Contoh terbaik:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public function store(PostRequest $request)
{
    ...
}

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

Business logic harus di dalam kelas services

Controller harus hanya memiliki satu tanggung jawab, jadi pindahkan business logic dari controller ke kelas service.

Contoh buruk:

1
2
3
4
5
6
7
8
public function store(Request $request)
{
    if ($request->hasFile('image')) {
        $request->file('image')->move(public_path('images') . 'temp');
    }
    
    ...
}

Contoh terbaik:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public function store(Request $request)
{
    $this->articleService->handleUploadedImage($request->file('image'));

    ...
}

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

Don’t repeat yourself (DRY)

Gunakan kembali kode ketika anda bisa. PSR membantu anda menghindari duplikasi. Juga, gunakan kembali template blade, scope eloquent, dll.

Contoh buruk:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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();
}

Contoh terbaik:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public function scopeActive($q)
{
    return $q->where('verified', 1)->whereNotNull('deleted_at');
}

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

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

Lebih memilih menggunakan Eloquent daripada menggunakan Query Builder dan query SQL mentah. Lebih memilih collections daripada array

Eloquent memungkinkan anda menulis kode yang dapat dibaca dan maintainable. Dan, Eloquent memiliki built-in tools yang bagus seperti soft deletes, events, scopes, dll.

Contoh buruk:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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

Contoh terbaik:

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

Mass assignment

Contoh buruk:

1
2
3
4
5
6
7
8
$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();

Contoh terbaik:

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

Jangan mengeksekusi kueri dalam template blade dan gunakan eager loading (masalah N + 1)

Contoh buruk (untuk 100 user, 101 kueri DB akan dieksekusi):

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

Contoh terbaik (untuk 100 user, 2 kueri DB akan dieksekusi):

1
2
3
4
5
$users = User::with('profile')->get();

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

Komentari kode anda, tetapi lebih baik method dan nama variabel yang deskriptif daripada komentar

Contoh buruk:

1
if (count((array) $builder->getQuery()->joins) > 0)

Contoh lebih baik:

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

Contoh terbaik:

1
if ($this->hasJoins())

Jangan letakkan JS dan CSS di template blade dan jangan letakkan HTML apa pun di kelas PHP

Contoh buruk:

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

Contoh lebih baik:

1
2
3
4
5
<input id="article" type="hidden" value='@json($article)'>

Atau

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

Dalam file javascript:

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

Cara terbaik adalah dengan menggunakan package PHP to JS khusus untuk mentransfer data.

Gunakan file config, language, dan konstanta daripada teks dalam kode

Contoh buruk:

1
2
3
4
5
6
public function isNormal()
{
    return $article->type === 'normal';
}

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

Contoh terbaik:

1
2
3
4
5
6
public function isNormal()
{
    return $article->type === Article::TYPE_NORMAL;
}

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

Gunakan tools standar Laravel yang diterima oleh komunitas

Selalu gunakan fungsi built-in bawaan laravel dan packages komunitas daripada menggunakan packages dan tools pihak ke-3. Developer manapun yang akan bekerja dengan aplikasi anda di masa mendatang perlu mempelajari tools baru. Dan juga, peluang untuk mendapatkan bantuan dari komunitas Laravel jauh lebih rendah saat anda menggunakan packages atau tools pihak ke-3. Jangan membuat klien Anda membayar untuk itu.

TaskTools standarTools pihak ke-3
AuthorizationPoliciesEntrust, Sentinel and other packages
Compiling assetsLaravel Mix, ViteGrunt, Gulp, 3rd party packages
Development EnvironmentLaravel Sail, HomesteadDocker
DeploymentLaravel ForgeDeployer and other solutions
Unit testingPHPUnit, MockeryPhpspec, Pest
Browser testingLaravel DuskCodeception
DBEloquentSQL, Doctrine
TemplatesBladeTwig
Working with dataLaravel collectionsArrays
Form validationRequest classes3rd party packages, validation in controller
AuthenticationBuilt-in3rd party packages, your own solution
API authenticationLaravel Passport, Laravel Sanctum3rd party JWT and OAuth packages
Creating APIBuilt-inDingo API and similar packages
Working with DB structureMigrationsWorking with DB structure directly
LocalizationBuilt-in3rd party packages
Realtime user interfacesLaravel Echo, Pusher3rd party packages and working with WebSockets directly
Generating testing dataSeeder classes, Model Factories, FakerCreating testing data manually
Task schedulingLaravel Task SchedulerScripts and 3rd party packages
DBMySQL, PostgreSQL, SQLite, SQL ServerMongoDB

Ikuti konvensi penamaan Laravel

Ikuti PSR standards.

Dan juga, ikuti konvensi penamaan yang diterima oleh komunitas Laravel:

WhatHowGoodBad
ControllersingularArticleControllerArticlesController
Routepluralarticles/1article/1
Route namesnake_case with dot notationusers.show_activeusers.show-active, show-active-users
ModelsingularUserUsers
hasOne or belongsTo relationshipsingulararticleCommentarticleComments, article_comment
All other relationshipspluralarticleCommentsarticleComment, article_comments
Tablepluralarticle_commentsarticle_comment, articleComments
Pivot tablesingular model names in alphabetical orderarticle_useruser_article, articles_users
Table columnsnake_case without model namemeta_titleMetaTitle; article_meta_title
Model propertysnake_case$model->created_at$model->createdAt
Foreign keysingular model name with _id suffixarticle_idArticleId, id_article, articles_id
Primary key-idcustom_id
Migration-2017_01_01_000000_create_articles_table2017_01_01_000000_articles
MethodcamelCasegetAllget_all
Method in resource controllertablestoresaveArticle
Method in test classcamelCasetestGuestCannotSeeArticletest_guest_cannot_see_article
VariablecamelCase$articlesWithAuthor$articles_with_author
Collectiondescriptive, plural$activeUsers = User::active()->get()$active, $data
Objectdescriptive, singular$activeUser = User::active()->first()$users, $obj
Config and language files indexsnake_casearticles_enabledArticlesEnabled; articles-enabled
Viewkebab-caseshow-filtered.blade.phpshowFiltered.blade.php, show_filtered.blade.php
Configsnake_casegoogle_calendar.phpgoogleCalendar.php, google-calendar.php
Contract (interface)adjective or nounAuthenticationInterfaceAuthenticatable, IAuthentication
TraitadjectiveNotifiableNotificationTrait
Trait (PSR)adjectiveNotifiableTraitNotification
EnumsingularUserTypeUserTypes, UserTypeEnum
FormRequestsingularUpdateUserRequestUpdateUserFormRequest, UserFormRequest, UserRequest
SeedersingularUserSeederUsersSeeder

Gunakan sintaks yang lebih pendek dan lebih mudah dibaca jika memungkinkan

Contoh buruk:

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

Contoh terbaik:

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

Contoh:

Sintaks umumSintaks pendek dan mudah dibaca
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->idoptional($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')

Gunakan IoC Container atau facades daripada kelas baru

Sintaks Kelas baru membuat penggabungan yang sempit antar kelas dan memperumit proses pengujian. Gunakan facades atau IoC container sebagai gantinya.

Contoh buruk:

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

Contoh terbaik:

1
2
3
4
5
6
7
8
public function __construct(User $user)
{
    $this->user = $user;
}

...

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

Jangan mendapatkan data dari file .env secara langsung

Alihkan data ke file konfigurasi sebagai gantinya dan kemudian gunakan fungsi config () untuk menggunakan data dalam aplikasi.

Contoh buruk:

1
$apiKey = env('API_KEY');

Contoh terbaik:

1
2
3
4
5
// config/api.php
'key' => env('API_KEY'),

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

Simpan tanggal dalam format standar. Gunakan accessors dan mutators untuk mengubah format tanggal

Contoh buruk:

1
2
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->toDateString() }}
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->format('m-d') }}

Contoh terbaik:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Model
protected $dates = ['ordered_at', 'created_at', 'updated_at'];
public function getSomeDateAttribute($date)
{
    return $date->format('m-d');
}

// View
{{ $object->ordered_at->toDateString() }}
{{ $object->ordered_at->some_date }}

Praktik bagus lainnya

Jangan pernah menaruh logika apa pun di file route.

Minimalkan penggunaan vanilla PHP di template blade.