Laravel 5.7 最佳實踐和開發技巧分享

file

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.phprender() 方法中捕獲了這個異常:

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_namelast_nameage 這幾個字段,我們可以增加一個叫做 name 的自定義字段來把 first_namelast_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_namelast_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 層使用索引來加快查詢。

善用擴展包、不要重複造輪子

如下是一些我在用的擴展包:

以下是我(原文作者)編寫的一些擴展包:

  • Befriended(類似社交媒體的點贊、收聽、屏蔽操作)
  • Schedule(創建日程表並檢查某個時間點是否可用)
  • Rating(爲模型增加評分功能)
  • Guardian(易於使用的權限系統)

太難理解?聯繫我吧!

如果你有更多關於 Laravel 的問題,如果你需要運維方面的幫助,或者只是想說聲 謝謝,你可以在 Twitter @rennokki 上找到我!

轉自 PHP / Laravel 開發者社區 https://laravel-china.org/top...
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章