本文件中列出的並不是 Laravel 版的 SOLID 原則、模式等。在本文件中,我們列出許多在實際 Laravel 專案中常常被忽略的一些最佳實踐。
優先使用 Eloquent 而不是 Query Builder 與原始 SQL 語句;優先使用 Collection 而不是陣列
不要在 Blade 樣板中執行查詢,並使用 Eager Loading (N + 1 問題)
在程式碼中加上註解,但比起註解應儘量使用描述性的方法與變數名稱
不要將 JS 與 CSS 放到 Blade 樣板內,也不要把 HTML 放到 PHP 內
使用 IoC Container 或 Facade 而不是直接 new Class
以標準格式來儲存日期時間,並以 Accesor 或 Mutator 來修改日期格式
一個類別與方法應只有一個職責。
例如:
public function getFullNameAttribute()
{
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()
{
return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort();
}
public function isVerifiedClient()
{
return auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified();
}
public function getFullNameLong()
{
return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
}
public function getFullNameShort()
{
return $this->first_name[0] . '. ' . $this->last_name;
}
如果使用 Query Builder 或原始 SQL 查詢,則請將所有 DB 關聯的邏輯放在 Eloquent Model 或 Repository 類別中。
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()
{
return $this->verified()
->with(['orders' => function ($q) {
$q->where('created_at', '>', Carbon::today()->subWeek());
}])
->get();
}
}
將資料類別從 Controller 中移到 Request 類別內。
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',
];
}
}
Controller 必須只能有單一職責,因此將商業邏輯移到 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');
}
}
}
盡可能重複使用程式碼。通過 SRP (單一職責原則) 有助於避免重複。另外,請重複使用 Blade 樣板,並使用 Eloquent Scope 等。
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();
}
使用 Eloquent 可以寫出有較高可讀性與可維護性的程式碼。另外,Eloquent 還內建了許多不錯的工具,如軟刪除、事件、Scope 等功能。
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();
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());
例子 (若有 100 個使用者,則會執行 101 次 DB 查詢):
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach
更優的寫法 (若有 100 個使用者,則會執行 2 次 DB 查詢):
$users = User::with('profile')->get();
...
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach
Bad:
if (count((array) $builder->getQuery()->joins) > 0)
加上註釋:
// 確定是否有任何 Join
if (count((array) $builder->getQuery()->joins) > 0)
Good:
if ($this->hasJoins())
Bad:
let article = `{{ json_encode($article) }}`;
Good:
<input id="article" type="hidden" value='@json($article)'>
或
<button class="js-fav-article" data-article='@json($article)'>{{ $article->name }}<button>
在 JavaScript 檔案中:
let article = $('#article').val();
最好的方法是用專門的 PHP 或 JS 套件來傳遞資料。
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'));
儘量使用內建的 Laravel 功能以及社群套件,而不是使用第三方套件與工具。若未來有哪位開發者接手你的專案,就必須要再學習新工具。另外,若使用第三方套件或工具,那麼從 Laravel 社群中取得協助的機會也會減少。請避免增加客戶的成本。
任務 | 標準工具 | 第三方工具 |
---|---|---|
權限控制 | Policies | Entrust, Sentinel 或其他套件 |
編譯資源 | Laravel Mix | Grunt, Gulp, 或其他第三方套件 |
開發環境 | Homestead | Docker |
部署 | Laravel Forge | Deployer 或其他解決方案 |
單元測試 | PHPUnit, Mockery | Phpspec |
瀏覽器測試 | Laravel Dusk | Codeception |
DB | Eloquent | SQL, Doctrine |
樣板 | Blade | Twig |
資料操作 | Laravel Collection | 陣列 |
表單驗證 | Request 類別 | 第三方套件、在 Controller 中驗證 |
登入驗證 | 內建 | 其他第三方套件或自製解決方案 |
API 登入驗證 | Laravel Passport, Laravel Sanctum | 第三方 JWT 或 OAuth 套件 |
建立 API | 內建 | Dingo API 或類似套件 |
處理 DB 結構 | Migrations | 直接操作 DB 結構 |
本地化 | 內建 | 第三方套件 |
即時使用者界面 | Laravel Echo, Pusher | 第三方套件或直接使用 WebSocket |
建立測試資料 | Seeder 類別, Model Factories, Faker | 手動建立測試資料 |
任務排程 | Laravel Task Scheduler | 腳本或第三方套件 |
資料庫 | MySQL, PostgreSQL, SQLite, SQL Server | MongoDB |
遵守 PSR 標準 (英語)。
另外,請遵守 Laravel 社群認可的命名規範:
東西 | 命名方式 | Good | Bad |
---|---|---|---|
Controller | 單數 | ArticleController | |
Route - 路由 | 複數 | articles/1 | |
Named Route - 路由命名 | 使用點標記的 snake_case | users.show_active | |
Model | 單數 | User | |
hasOne 或 belongsTo 關聯 | 單數 | articleComment | |
所有其他關聯 | 複數 | articleComments | |
資料表 | 複數 | article_comments | |
Pivat Table 透視表 | 以字母順序排列的單數 Model 名稱 | article_user | |
資料表欄位 | 使用 snake_case,並且不包含 Model 名稱 | meta_title | |
Model 屬性 | snake_case | $model->created_at | |
Foreign Key - 外鍵 | 以單數 Model 名稱後方加上 _id | article_id | |
Primary Key - 主鍵 | - | id | |
Migration | - | 2017_01_01_000000_create_articles_table | |
方法 | camelCase | getAll | |
Resource Controller 中的方法 | table | store | |
測試類別中的方法 | camelCase | testGuestCannotSeeArticle | |
變數 | camelCase | $articlesWithAuthor | |
Collection | 描述性名稱、複數 | $activeUsers = User::active()->get() | |
物件 | 秒屬性名稱、單數 | $activeUser = User::active()->first() | |
設定檔與語系檔的索引鍵 | snake_case | articles_enabled | |
View | kebab-case | show-filtered.blade.php | |
設定檔 | snake_case | google_calendar.php | |
Contract (界面) | 形容詞或名詞 | AuthenticationInterface | |
Trait | 形容詞 | Notifiable |
Bad:
$request->session()->get('cart');
$request->input('name');
Good:
session('cart');
$request->name;
更多範例:
一般語法 | Good |
---|---|
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') |
new Class 語法增加物件間的耦合度,且會讓測試更複雜。應使用 IoC Container 或 Facade 來代替。
Bad:
$user = new User;
$user->create($request->validated());
Good:
public function __construct(User $user)
{
$this->user = $user;
}
....
$this->user->create($request->validated());
請改而將資料傳至設定檔並使用 config()
helper 函式來在應用程式中使用資料。
Bad:
$apiKey = env('API_KEY');
Good:
// config/api.php
'key' => env('API_KEY'),
// Use the data
$apiKey = config('api.key');
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 }}
絕對不要在路由檔案中撰寫任何邏輯。
在 Blade 樣板中避免使用原始 PHP。