Laravel 5.7 最佳實踐和開發技巧分享
Laravel 因可編寫出乾淨,可用可除錯的程式碼而為廣大的 PHP 開發者所熟知。它同樣也支援許許多多的功能,有時卻未能在文件中體現,或者由於某種原因它們出現過又被移除了。
我已經在生產環境中使用 Laravel 2 年了,從中我學到如何把程式碼變得更好,從我首次使用它以來我都充分發掘它的優勢。接下來我將向你展示一些可能對你在用 Laravel 寫程式碼時很有幫助的奧義之招。
查詢資料時使用本地範圍
Laravel 有一種非常棒的方式來使用 查詢構造器 編寫查詢。就像這樣:
$orders = Order::where('status', 'delivered' )->where('paid', true)->get();
複製程式碼
很不錯。這讓我專注於編寫更友好的程式碼而不是 SQL 語句。但如果用 本地範圍 ,我們可以讓這行程式碼變得更好些。
當查詢資料時, 本地範圍 允許我們建立自己的 查詢構造器 鏈式方法。舉個例子,取代 ->where()
,我們可以用更簡潔的 ->delivered()
和 ->paid()
。
首先在 Order
模型,我們加入一些方法:
class Order extends Model
{
...
public function scopeDelivered($query) {
return $query->where('status', 'delivered');
}
public function scopePaid($query) {
return $query->where('paid', true);
}
}
複製程式碼
當宣告本地範圍時,你應該使用 scope[Something]
來命名。這樣 Laravel 便會知道這是一個本地範圍並且可以在查詢構造器中使用。請確保你在方法中傳入了第一個引數 $query
,也就是由 Laravel 自動注入的查詢構造器例項。
$orders = Order::delivered()->paid()->get();
複製程式碼
對於可接受額外引數的查詢,你可以使用動態範圍。每個範圍都允許你傳入額外的引數。
class Order extends Model
{
...
public function scopeStatus($query, string $status) {
return $query->where('status', $status);
}
}
$orders = Order::status('delivered')->paid()->get();
複製程式碼
在本文的後面,你會知道為什麼資料庫欄位應該使用 蛇形命名
,但這裡有第一個原因:Laravel 預設用 where[Something]
來替換 scope[Something]
。所以作為 scopeStatus
範圍的代替,你可以這樣做:
Order::whereStatus('delivered')->paid()->get();
複製程式碼
對於 where[Something]
,Laravel 會搜尋 蛇形命名
版本的資料庫欄位。如果你的資料庫中有個 status
欄位,你可以用上面那個例子。如果有個 shipping_status
欄位,你可以用:
Order::whereShippingStatus('delivered')->paid()->get();
複製程式碼
由你決定!
必要的時候使用請求類
Laravel 提供了一種優秀的方式來驗證表單提交的資料。如果你需要它,不管是 POST 還是 GET 請求,它都可以驗證。
在控制器中,你可以這樣做:
public function store(Request $request)
{
$validatedData = $request->validate([
'title' => 'required|unique:posts|max:255',
'body' => 'required',
]);
// 如果這篇部落格的內容無效……
}
複製程式碼
但是當控制器中已經有很多程式碼時,再把驗證表單資料的程式碼加進去就會顯得很凌亂。你想盡可能地減少控制器的程式碼 —— 至少這是我在控制器中寫很多邏輯時想到的第一件事。
Laravel 提供了一種很萌的方式來驗證表單請求,那就是建立並使用專門的 請求類 而不是用原始的 Request
。你只需要建立你的請求類:
php artisan make:request StoreBlogPost
複製程式碼
在 app/Http/Requests/
目錄中可以找到你剛建立的請求類:
class StoreBlogPostRequest extends FormRequest
{
public function authorize()
{
return $this->user()->can('create.posts');
}
public function rules()
{
return [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
];
}
}
複製程式碼
現在,你應該用新建立的 App\Http\Requests\StoreBlogPostRequest
來代替原先的 Illuminate\Http\Request
類:
use App\Http\Requests\StoreBlogPostRequest;
public function store(StoreBlogPostRequest $request)
{
// 如果這篇部落格的內容無效……
}
複製程式碼
請求類中的 authorize()
方法應返回一個布林值。如果返回了 false
,它會丟擲一個 403
異常,請確保你在 app/Exceptions/Handler.php
的 render()
方法中捕獲了這個異常:
public function render($request, Exception $exception)
{
if ($exception instanceof \Illuminate\Auth\Access\AuthorizationException) {
//
}
return parent::render($request, $exception);
}
複製程式碼
請求類中還有一個 messages()
方法,當驗證失敗時,它會返回一個包含了錯誤資訊的陣列:
class StoreBlogPostRequest extends FormRequest
{
public function authorize()
{
return $this->user()->can('create.posts');
}
public function rules()
{
return [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
];
}
public function messages()
{
return [
'title.required' => 'The title is required.',
'title.unique' => 'The post title already exists.',
...
];
}
}
複製程式碼
@if ($errors->any())
@foreach ($errors->all() as $error)
{{ $error }}
@endforeach
@endif
複製程式碼
如果你想得到某個欄位的驗證資訊,你可以這樣做(當這個欄位驗證通過時 $errors->has()
會返回一個 false
):
<input type="text" name="title" />
@if ($errors->has('title'))
<label class="error">{{ $errors->first('title') }}</label>
@endif
複製程式碼
魔術範圍
構建查詢時,可以使用已有的魔術範圍:
- 根據
created_at
倒序查詢:
User::latest()->get();
複製程式碼
- 根據任意欄位倒序查詢:
User::latest('last_login_at')->get();
複製程式碼
- 隨機查詢(即 SQL 語句中的
ORDER BY RAND()
)
User::inRandomOrder()->get();
複製程式碼
使用關聯關係代替冗長的查詢(或者寫得不好的查詢)
你是否曾經為了獲取更多的資訊而在查詢語句中使用大量的 join 操作?即使在使用查詢構造器的情況下,編寫這樣的 SQL 語句也是困難的,但是資料模型已經使用 關聯關係 來實現同樣的功能。由於文件提供了太多的資訊,因此剛開始時你可能對關聯關係並不熟悉,但是這些內容可以幫助你更好的理解事物的執行原理,同時讓你的程式執行得更加順暢。
通過 這裡 查詢關聯關係的文件。
為耗時的任務使用任務系統
Laravel 的任務 是後臺執行程式必用的功能強大的工具。
- 你要傳送電子郵件? 任務系統。
- 你要廣播一個訊息? 任務系統。
- 你要處理一張圖片? 任務系統。
任務系統能夠幫助你實現,在執行上述這些任務時,減少你的使用者的應用載入時間。這些任務可以被放進命名的佇列,它們能夠被安排優先順序,Laravel 幾乎在所有可能的地方都實現了佇列:無論在後臺執行一些 PHP 任務,或者傳送訊息,或者廣播事件,佇列都在這些場景中出現。
你可以在 這裡 查詢佇列的文件。
在使用佇列時,我喜歡使用 Laravel Horizon ,因為它很容易安裝,它能夠通過 Supervisor 工具或者配置檔案實現後臺執行,同時我能夠告訴 Horizon 我希望每個佇列產生多少個程序。
遵守資料庫標準 & 訪問器
Laravel 從一開始就教給你變數和方法應使用像 $camelCase
camelCase()
這樣的小駝峰命名而資料庫欄位應使用像 snake_case
這樣的蛇形命名。為什麼呢?因為這有助於我們構造更好的 訪問器。
訪問器是可以直接在模型中構造的自定義欄位。如果我們的資料庫包含了 first_name
、last_name
、age
這幾個欄位,我們可以增加一個叫做 name
的自定義欄位來把 first_name
和 last_name
拼接起來。別擔心,這個 name
不會被寫入到資料庫。它只是某個模型的自定義屬性。所有的訪問器,和 範圍 一樣,都有自定義命名語法:getSomethingAttribute
:
class User extends Model
{
...
public function getNameAttribute(): string
{
return $this->first_name.' '.$this->last_name;
}
}
複製程式碼
當使用 $user->name
,訪問器會返回拼接好的字串。
預設情況下,用 dd($user)
是看不到 name
屬性的,但是通過 $appends
變數我們可以使它一直可用:
class User extends Model
{
protected $appends = [
'name',
];
...
public function getNameAttribute(): string
{
return $this->first_name.' '.$this->last_name;
}
}
複製程式碼
現在每次 dd($user)
,我們都可以看到 name
了。(不過仍然,這個屬性不是從資料庫取得的,而是每次使用時將 first_name
和 last_name
拼接得到的)。
要注意下,如果你資料庫裡已經有 name
這個欄位了,那情況就會有點不一樣:$appends
數組裡的 name
元素就不需要了,然後訪問器需要傳入一個引數,這個引數就是資料庫中的 name
(也就是說我們用不著再使用 $this
了)。
舉個例子,我們也許想用 ucfirst()
來使名字的首字母轉為大寫:
class User extends Model
{
protected $appends = [
//
];
...
public function getFirstNameAttribute($firstName): string
{
return ucfirst($firstName);
}
public function getLastNameAttribute($lastName): string
{
return ucfirst($lastName);
}
}
複製程式碼
現在當我們用 $user->first_name
,它會返回一個首字母大寫的字串。
由於這個特性,資料庫欄位最好是用 snake_case
這種蛇形命名。
不要在配置檔案中儲存模型相關的靜態資料
我喜歡把與模型相關的靜態資料存放在模型檔案中。讓我們一起來看一下。
不要像下面這樣:
BettingOdds.php
class BettingOdds extends Model
{
...
}
複製程式碼
config/bettingOdds.php
return [
'sports' => [
'soccer' => 'sport:1',
'tennis' => 'sport:2',
'basketball' => 'sport:3',
...
],
];
複製程式碼
使用下面的方式訪問:
config('bettingOdds.sports.soccer');
複製程式碼
我更喜歡這樣做:
BettingOdds.php
class BettingOdds extends Model
{
protected static $sports = [
'soccer' => 'sport:1',
'tennis' => 'sport:2',
'basketball' => 'sport:3',
...
];
}
複製程式碼
然後訪問它們:
BettingOdds::$sports['soccer'];
複製程式碼
為什麼這樣?因為這樣有益於後續操作:
class BettingOdds extends Model
{
protected static $sports = [
'soccer' => 'sport:1',
'tennis' => 'sport:2',
'basketball' => 'sport:3',
...
];
public function scopeSport($query, string $sport)
{
if (! isset(self::$sports[$sport])) {
return $query;
}
return $query->where('sport_id', self::$sports[$sport]);
}
}
複製程式碼
現在我們可以使用範圍查詢:
BettingOdds::sport('soccer')->get();
複製程式碼
使用集合替代原始的陣列處理
在過去,我們通常以一種原始的方式使用陣列:
$fruits = ['apple', 'pear', 'banana', 'strawberry'];
foreach ($fruits as $fruit) {
echo 'I have '. $fruit;
}
複製程式碼
現在,我們可以使用一種高階的方法(譯者注:集合的方式)處理陣列中的資料。我們可以過濾、轉換、遍歷和修改陣列中資料:
$fruits = collect($fruits);
$fruits = $fruits->reject(function ($fruit) {
return $fruit === 'apple';
})->toArray();
['pear', 'banana', 'strawberry']
複製程式碼
想要了解細節, 請檢視 集合的文件.
當使用 查詢構造器時,->get()
方法返回一個 Collection
例項。但要注意別搞混了 Collection
和 Query
builder:
- 從 Query Builder 中,我們無法獲取任何資料.。但我們有大量的查詢相關的方法可以使用:
orderBy()
,where()
,等等。 - 最終呼叫
->get()
方法之後,資料被獲取到,記憶體空間被消耗。它返回一個Collection
例項。某些查詢構造器不可用或者說可用但是方法名不同,關於這些請查閱 所有集合的方法。
如果你能在 Query Builder
層次過濾資料,就去做吧!不要依賴於等到結果 Collection
例項返回時再過濾---你將會消耗更多的記憶體空間。 使用 Limit 限制結果條數,在 DB 層使用索引來加快查詢。
善用擴充套件包、不要重複造輪子
如下是一些我在用的擴充套件包:
- Laravel Blade Directives
- Laravel CORS(跨域請求時,將你的路由限制為指定域名訪問)
- Laravel Tag Helper(在 Blade 內更方便地使用 HTML 標籤)
- Laravel Sluggable(在 Eloquent 模型內生成 Slug 時十分實用)
- Laravel Responder(更容易地構建 JSON API)
- Image Intervention(處理圖片)
- Horizon(使用少量配置即可管理佇列)
- Socialite(使用少量配置即可整合第三方社交媒體登入)
- Passport(整合 OAuth 路由)
- Spatie's ActivityLog(追蹤模型的修改活動)
- Spatie's Backup(備份檔案和資料庫)
- Spatie's Blade-X(定義你自己的 HTML 標籤;可與 Laravel Tag Helper 結合)
- Spatie's Media Library(快速將模型與檔案關聯)
- Spatie's Response Cache(快取控制器的完整響應內容)
- Spatie's Collection Macros(給集合新增更多巨集)
以下是我(原文作者)編寫的一些擴充套件包:
- Befriended(類似社交媒體的點贊、收聽、遮蔽操作)
- Schedule(建立日程表並檢查某個時間點是否可用)
- Rating(為模型增加評分功能)
- Guardian(易於使用的許可權系統)
太難理解?聯絡我吧!
如果你有更多關於 Laravel 的問題,如果你需要運維方面的幫助,或者只是想說聲 謝謝
,你可以在 Twitter @rennokki 上找到我!
轉自 PHP / Laravel 開發者社群 laravel-china.org/topics/2216…