Hướng dẫn laravel coding convention

  • January 25, 2021
  • 1265

Khi làm việc nhóm thì coding convention là cực kì quan trọng, nó là rule để mọi người làm việc cùng nhau, coding convesion giúp mọi người trong team hiểu được code của bạn dễ dàng, bài viết này sẽ hướng dẫn về laravel coding convention. Laravel coding conventions được nhiều người sử dụng nhất là Laravel best practices chúng ta cùng tìm hiểu về nó xem sao nhé.

Nguyên tắc đơn nhiệm - Single responsibility principle

Một lớp hoặc một phương thức chỉ nên có 1 nhiệm vụ duy nhất. Đây là nguyên tắc đầu tiên trong các nguyên tắc SOLID của hướng đối tượng.


// Bad
public function getFullNameAttribute()
{
	if ($this->gender) {
		$genderText = 'Mr. ';
	} else {
		$genderText = 'Mrs. ';
	}
	
	if (auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified()) {
		return $genderText . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
	} else {
		return $this->first_name . ' ' . $this->last_name;
	}
}

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

public function isVerifiedClient()
{
	// Trả về true or false
    return auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified();
}

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

public function getFullNameShort()
{
    return $this->first_name . ' ' . $this->last_name;
}

public function getGenderTextAttribute()
{
	// Nếu là nam
	if ($this->gender) {
		return 'Mr. ';
	}
	return 'Mrs. ';
}

Việc tách code ra mỗi hàm chỉ có một nhiệm vụ duy nhất giúp code rõ ràng hơn, dễ dàng nâng cấp bảo trì, không bị lặp code.

Fat models, skinny controllers

Các logic lấy dữ liệu phải viết trong model, or repository, controller chỉ việc gọi một dòng duy nhất


// Bad code
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()
    {
        return $this->verified()
            ->with(['orders' => function ($q) {
                $q->where('created_at', '>', Carbon::today()->subWeek());
            }])
            ->get();
    }
}

Tại sao phải viết 1 hàm lấy dữ liệu ở model riêng vì nó giúp mình tái sử dụng code và logic lấy dữ liệu có thay đổi thì mình chỉ việc sửa ở 1 chỗ là xong.

Validate - Kiểm tra dữ liệu đầu vào

Chuyển dữ liệu validate từ controller sang Request class


// 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()
    {
        return [
            'title' => 'required|unique:posts|max:255',
            'body' => 'required',
            'publish_at' => 'nullable|date',
        ];
    }
}

Business logic viết ở service class

Mỗi 1 method trong controller chỉ có 1 nhiệm vụ duy nhất, vì vậy các logic nghiệp vụ nên viết vào lớp service


// 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)
    {
        if (!is_null($image)) {
            $image->move(public_path('images') . 'temp');
        }
    }
}

Don't repeat yourself

Không được phép viết lặp code, code càng tái sử dụng được càng tốt


// 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', 1)->whereNotNull('deleted_at');
}

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

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

Ưu tiên dùng Eloquent hơn Query Builder and raw SQL queries

Việc sử dụng Eloquent giúp việc đọc code vào maintain code dễ dàng


// 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();

Mass assignment

Tính năng gán dữ liệu hàng loạt, thay vì gán từng giá trị gán hàng loạt sẽ giúp code gọn gàng hơn


// 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->all());

Tránh lỗi N + 1 query


// Bad
// 100 user có 101 query được thực thi
@foreach (User::all() as $user)
    {{ $user->profile->name }}
@endforeach

// Good
// n user cũng chỉ có 2 query được thực thi mà thôi
$users = User::with('profile')->get();

...

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

Đặt tên hàm tên biến có ý nghĩa thay cho comment code


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

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

// Good
if ($this->hasJoins())

Không viết css, js trong view blade, không viết html trong class php


code convensions

Sử dụng file config, lang, hằng số thay cho text


// Bad
public function isNormal()
{
    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'));

các message phải được định nghĩa trong file lang, giúp tái sử dụng và dễ maintain, các hằng số phải viết trong file constant or khay báo trong model, khi chỉ sửa message or hằng số chỉ cần sử 1 chỗ là xong.

Ưu tiên dùng các package có sẵn của laravel thay vì bên thứ 3

Development Environment Homestead Docker
Task Standard tools 3rd party tools
Authorization Policies Entrust, Sentinel and other packages
Compiling assets Laravel Mix Grunt, Gulp, 3rd party packages
Deployment Laravel Forge Deployer and other solutions
Unit testing PHPUnit, Mockery Phpspec
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

Qui tắc đặt tên trong laravel

What How Good Bad
Controller số ít ArticleController ArticlesController
Route số nhiều articles/1 article/1
Named route snake_case với dấu chấm users.show_active users.show-active, show-active-users
Model số ít User Users
hasOne hoặc belongsTo relationship số ít articleComment articleComments, article_comment
Tất cả các relationships khác số nhiều articleComments articleComment, article_comments
Tên bảng số nhiều article_comments article_comment, articleComments
Bảng Pivot tên 2 bảng số ít, xếp tăng dần article_user user_article, articles_users
Tên cột trong bảng snake_case meta_title MetaTitle; article_meta_title
Thuộc tính của Model snake_case $model->created_at $model->createdAt
Foreign key - khóa ngoại tên model, kèm _id đằng sau article_id ArticleId, id_article, articles_id
Primary key - id custom_id
Migration - xxx_create_articles_table xxx_articles
Method camelCase getAll get_all
Method trong resource controller xem tài liệu store saveArticle
Method in test class camelCase testArticle test_article
Variable camelCase $articlesWithAuthor $articles_with_author
Collection có nghĩa mô tả, số nhiều $activeUsers $active, $data
Object có nghĩa mô tả, số ít $activeUser $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) tính từ hoặc danh từ Authenticatable AuthenticationInterface, IAuthentication
Trait tính từ Notifiable NotificationTrait

Ưu tiên dùng shorter syntax


// Bad
$request->session()->get('cart');
$request->input('name');
// Good
session('cart');
$request->name;
Sử dụng short code thay cho cú pháp thông thường
Common syntax Shorter 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
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')

Sử dụng IoC container or facades thay việc khới tạo class


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

// Good
public function __construct(User $user)
{
    $this->user = $user;
}

....

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

Không lấy dữ liệu trực tiếp từ file .env


// Bad
$apiKey = env('API_KEY');

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

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

Format datetime


// 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 $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 }}

Như vậy là chúng ta đã tìm hiểu xong các quy tắc code với project laravel, nhớ sử dụng các quy tắc này nhé, code của chúng ta sẽ ngắn gọn và dễ đọc hơn nhiều đó :D