個人在 laravel 開發中使用到的一些技巧(持續更新)


1、更高效率地查詢:使用批量查詢代替 foreach 查詢(多次 io 操作轉換為一次 io操作)

如果想要查看更詳盡的介紹,可以看看這篇文章 什么是 N+1 問題,以及如何解決 Laravel 的 N+1 問題?

在維護的項目中, 我發現了有不少需要查詢關聯數據的時候是這樣做的:先查詢出列表,然后 foreach 列表去查詢列表每一條記錄的關聯數據,好比如這樣:

$products = \DB::table('product')->where('category_id', 1)->paginate();
foreach ($products as $key => $product) {
    $product['some_other_info'] = \DB::table('some_other_info')
        ->where('product_id', $product['id'])
        ->get();

    $products[$key] = $product;
}

  解決方法:使用一次查詢代替多次查詢

    a. 定義模型關聯,使用 Model 的 with 進行查詢。如:

AdminUser::with('admin_user_info')->where('id', '>', 1)->get();

    b. 獲取查詢結果集的 id 列(根據實際情況而定),使用 whereIn 一次查詢關聯數據。對於那些不能定義關聯的才用這種方法,因為這種寫法有點啰嗦

$products = \DB::table('product')->where('category_id', 1)->paginate();
$product_ids = $products->pluck('id')->toArray(); // 獲取所有 id , 下面第二點有說到這個用法
$some_other_infos = \DB::table('some_other_info') // 根據 id 數組一次查詢所有關聯數據 ->whereIn('product_id', $product_ids) ->get();
$some_other_infos = array_column($some_other_infos, null, 'product_id'); // 使用 id 把結果集轉換為關聯數組,這樣下面可以更高效地操作,否則我們只能兩次 foreach 了 foreach ($products as $key => $product) { $product['some_other_info'] = array_get($some_other_infos, $product['id']); $products[$key] = $product; }

 

  先說說 關聯查詢:我們在 Model 類里定義的關聯,我們不一定在第一次查詢就全部查出來,我們可以在需要的時候再去查詢 ,使用 load 方法,可以實現這個目標,但是這個 load 只是針對單個 model 對象的,如果我們 Model::xxx()->xx() 鏈式操作最后是 get(),我們得到的結果將會是一個 Collection 實例,最后調用的方法是 first() 或其他類似的 firstOrFail() 的時候,返回的才是一個 Model 實例。也就是說 load 方法只針對 Model 實例。如下:

我們假設有如下模型定義:

AdminUser

<?php

namespace App\Model;

class AdminUser extends BaseModel
{

    public function admin_user_info()
    {
        return $this->hasOne(AdminUserInfo::class);
    }
}

  

AdminUserInfo

<?php

namespace App\Model;

class AdminUserInfo{
    
}

 

$admin_users = \App\Model\AdminUser::where('id', '>', 1)->get();
$admin_users->load('admin_user_info'); // 錯誤, 因為 $admin_users 是一個 Collection 實例

$admin_user->load('admin_user_info'); // 可以 load 的,返回的是 EloquentCollection
$admin_user = \App\Model\AdminUser::first();
$admin_user->load('admin_user_info'); // 正確, $admin_user 是一個 Model 實例

  

  好像有點跑題了,而我們使用 with 的話,效果相當於一次性給結果集 load 完了所有的關聯。如果我們有使用 laravel-debugbar 來,就會發現一對多關聯使用 with 時候執行的 sql 都是使用了 whereIn 進行查詢的。

  

 

 2、更優雅地處理你的 paginate() 結果(利用 Collection 里的方法)

  這里應該有一小點,我們想要實現某些功能的時候可以先看看框架有沒有提供相關的支持,如命名在駝峰式、下划線式轉換;字符串是否以某些子串開始、結束;數組排序、過濾等等,其實很多常用的功能 laravel 都有現成的輪子,最常用的一些功能可以查看 ArrStrCollection 的源碼,利用好的話可以把代碼寫得簡潔一些。

  這個其實最近才發現的,最近腦洞開了一下,因為想從 paginate() 返回的對象中獲取結果集的 id 列,所以就想能不能  $paginator->pluck('id')->toArray() 這樣操作,最后發現真的可以,然后去看了源碼發現 AbstractPaginator 這個抽象類里有一個 __call 的方法,如下:

/**
* Make dynamic calls into the collection.
*
* @param  string  $method
* @param  array  $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
   return $this->getCollection()->$method(...$parameters);
}

  我們會發現,這個魔術方法調用的是 Collection 里面的方法,也就是說,我們可以把 paginate() 返回的結果當作一個 Collection 實例來操作。所有的 Collection 方法我們都可以使用,什么 filtermapsort 等等等等。而我之前獲取 id 列都是使用 array_column($paginator->items(), 'id'),兩種寫法差不多,但是對於另外一些如根據某個字段排序等操作,利用 Collectionsort 方法明顯就簡潔多了。

  

3、利用 Macroable Trait 更優雅地對框架某些功能進行擴展

  我們想想,如果有一天,我們需要自定義一些 Query Builder 的方法的時候,我們會怎么做?當然我們可以利用 Global Scopes 這個特性來實現某些類似的功能,但是如果我們想要 DB 類也用上這個擴展的方法的話,這樣就行不通了。也不是完全沒有辦法,laravel 的開發者很早開始就考慮到了擴展性的問題,我們去查看 Query Builder 的源碼就會發現,里面有以下的 trait 調用:

use BuildsQueries, Macroable {
      __call as macroCall;
}

  而這個 Macroable 是 Illuminate\Support\Traits\Macroable,我們可以從這個 trait 中發現,其實這個 trait 提供了一個很強大的擴展功能,對於所有 use Macroable 的類,我們都可以通過 XXX::macro('xx', function (){}) 的方式來對這個類進行擴展。

  我們全局搜索這個 Macroable,會發現框架不少地方都有使用:如 Cache、Console、Eloquent、Schema 等等,如果我們想要對框架中某些類進行擴展的時候,不妨先看看這個類有沒有 use Macroable, 如果有,我們就可以省去一大部分功夫。

  當然,我們利用 macro 方法來擴展的功能,IDE 不會有任何提示,除非,我們像 ide-helper 那樣處理。具體可參照另外一篇文章:laravel query builder/ Eloquent builder 添加自定義方法,或者

 

4、利用 array_getdata_get  方法代替多個 isset 判斷

  最常用到的地方是,我們利用 with 關聯查詢出多層關聯,但是我們不確定所有層級關聯是否存在,所以我們可能就要一層層地 isset 來判斷。

  假設有一個這樣的關聯 User -> HasOne -> Article -> HasOne -> Comment,這個假設不太妥當,但可以說明問題

  我們查詢的時候按如下方式查詢:

// 獲取一個用戶下文章的一條評論
$user = User::with(['article.comment'])->first();
$content = isset($user->article) ? (isset($user->article->comment) ? $user->article->comment : '') : ''; // 傳統寫法
$content = data_get($user, 'article.comment.content'); // 評論內容

  上面的例子中,我們可以發現 data_get 寫出的代碼更簡潔、可讀性也更強,array_get 功能類似,但是 array_get 不能針對對象操作利用 array_get 可以更方便完成地進行對多維數組的操作,因為 laravel 把多維數組抽象成了點號分隔的一維數組,不得不說,說 laravel 優雅不是沒有道理的。

 

5、多對多關聯更方便的操作方法 attachdetachsync

// 附加多對多關系
$user->roles()->attach($roleId);
$user->roles()->attach($roleId, ['expires' => $expires]);
// 從用戶上移除單一身份...
$user->roles()->detach($roleId);
// 從用戶上移除所有身份...
$user->roles()->detach();
$user->roles()->detach([1, 2, 3]);
$user->roles()->attach([1 => ['expires' => $expires], 2, 3]);

// 任何不在給定數組中的 IDs 將會從中介表中被刪除。
$user->roles()->sync([1, 2, 3]);
// 你也可以傳遞中介表上該 IDs 額外的值:
$user->roles()->sync([1 => ['expires' => true], 2, 3]);

  使用 sync 方法的時候需要注意,如果第二個參數不設置 false,會把不在給定數組中的 ID 的關聯數據全部刪除。

  上面的關聯是 User -> belongsToMany -> Role,通過多對多表的關聯,具體可查看另一篇文章:laravel5.1 使用中間表的多對多關聯,我們通過下面的例子來說明一下 attach 的用法,其他幾個方法也是類似的功能,

   我們查看以下數據庫中間表記錄:

  我們可以發現,user 關聯了 id 為 1 和 2 的兩個 role,同時 attach 第二個參數可以讓我們傳遞一些額外數據保存到中間表。

  sync 的功能和 attach 類似,但是 sync 還有個功能是,可以把不在指定數組的關聯數據刪除(如果我們不需要刪除可以傳遞第二個參數 false,或者直接使用 attach)。

 

6、使用 debugbar 盡早發現性能問題

  這個其實使用 laravel 的人基本上都知道,但是可能我們沒有用到其中一些功能,其實 laravel-debugbar 可以通過配置獲取更加詳情的信息(如 sql 的堆棧信息、是否 explain 等等非常多實用的功能), 具體功能還是得查看其官方文檔,有很多實用的功能,可以讓我們對我們的代碼了解更多。

 

7、使用 chunk 處理表全部數據

  有時候,我們給表新增了一個字段,這個字段是由其他字段算出來的,這時候我們就需要跑一遍該表,進行該字段的更新。一種做法是一次性全表查詢出來,foreach 循環處理,這種做法在數據量小的時候問題不大,但是數據量達到一定程度的時候會產生比較嚴重的問題:內存耗盡,更嚴重的是把服務器也弄掛。另外一種方法是使用 Builder 的 chunk 方法進行批量處理,可以解決內存占用過大的問題。如下:

DB::table('users')->chunk(100, function($users)
{
    foreach ($users as $user)
    {
        //
    }
});

  

 

8、關聯數據分頁、統計或者其他類似操作

  我們先來看看 Relation 的定義(5.6),我們可以發現里面有個 __call 方法,

/**
* Handle dynamic method calls to the relationship.
*
* @param  string  $method
* @param  array   $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
    if (static::hasMacro($method)) {
        return $this->macroCall($method, $parameters);
    }
    $result = $this->query->{$method}(...$parameters);
    if ($result === $this->query) {
        return $this;
    }
    return $result;
}

  我們可以發現它會先看 有沒有定義相關的 macro,如果沒有則去調用 \Illuminate\Database\Eloquent\Builder 里面的方法,而我們繼續看 \Illuminate\Database\Eloquent\Builder 里面的源碼(5.6)發現,里面也有一個 __call 方法,

/**
* Dynamically handle calls into the query instance.
*
* @param  string  $method
* @param  array  $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
    if ($method === 'macro') {
        $this->localMacros[$parameters[0]] = $parameters[1];
        return;
    }
    if (isset($this->localMacros[$method])) {
        array_unshift($parameters, $this);
        return $this->localMacros[$method](...$parameters);
    }
    if (isset(static::$macros[$method])) {
        if (static::$macros[$method] instanceof Closure) {
            return call_user_func_array(static::$macros[$method]->bindTo($this, static::class), $parameters);
        }
        return call_user_func_array(static::$macros[$method], $parameters);
    }
    if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
        return $this->callScope([$this->model, $scope], $parameters);
    }
    if (in_array($method, $this->passthru)) {
        return $this->toBase()->{$method}(...$parameters);
    }
    $this->query->{$method}(...$parameters);
    return $this;
}

  上面是 5.6 版本的 __call,5.1 版本的 __call 相對來說,功能少一些,但是都有的功能就是,先去看 macro 有沒有定義,如果前面條件都不滿足就去調用 Query Builder 中對應的方法,當然,如果 Query Builder 里面也沒有這個方法就會拋出異常。而實現這些功能都得益於 php 的 __call 魔術方法。

  關於 Eloquent BuilderQuery Builder 的區別:

    a、我們使用 Eloquent Model 類或 Eloquent Model 子類進行操作的時候,如果使用的都是 Eloquent Builder 里面的方法,那么返回的也是一個 Eloquent Builder 或者是最終結果(視操作而定,有可能是 Collection 實例、null、某個 Model 子類的實例等)。雖然 __call 中可能會調用到 Query Builder 的方法,而 Query Builder 的方法里面可能會返回 $this,這時候我們可能會有一種錯覺,我們通過 __call 間接調用了 Query Builder 里面方法的時候,返回的是不是一個 Query Builder 的實例。看起來好像的確是這樣,但我們實際操作一下就發現,返回的還是 Eloquent Builder 實例。這是因為上面 __call 最后兩行,如果寫成

return $this->query->{$method}(...$parameters);

    那么返回的就是 Query Builder 了,但實際上,調用 Query Builder 里面的方法和 return 語句是分開的,當然,這里的 $this 肯定是 Eloquent Builder 的實例了。    

    b、Eloquent Builder 可以使用 Query Builder 的方法,但是 Query Builder 不能使用 Eloquent Builder 的方法。所以開發中使用 Model 進行 curd 操作的話,相對來說操控性更強,再加上有模型關聯這些強大的功能,可以使用 Model 就沒必要用 DB 類了。

 

  好像又說得有點遠了,這一點應該說的是,Relation 的相關操作,因為 Relation 的 __call 里面也用到了 Query Builder,所以我們可以直接在關聯上調用 Query Builder 里面的方法。但是這個時候關聯使用需要加上括號。如:

$user->roles; // 用戶所有角色
$user->roles()->count(); // roles() 調用返回 BelongsToMany 實例,
                         // 可以在此后調用 Query Builder 里面的方法

   分頁也類似 $user->roles()->paginate(); 這在我們需要查看某條記錄的關聯數據時候非常有用。

 

9、表名使用單數命名時候 Model 類不用定義 protected $table

  laravel 中表名默認是復數形式的,不知道大家有沒有用復數做表名,我是沒有這種習慣。如果我們想用單數命名,又不想每個 Model 類里面寫一個 protected $table;可以重寫 Model 類的 getTable 方法

/**
* Get the table associated with the model.
*
* @return string
*/
public function getTable()
{
    if (isset($this->table)) {
        return $this->table;
    }

    return str_replace('\\', '', Str::snake(class_basename($this)));
}

  把轉復數的調用去掉就好了。

 

10、為復雜表單創建一個 validator

  這個其實也不算是什么技巧,可能一開始寫得舒服就一個個 if 判斷,但是這樣子到最后會發現我們的代碼越來越長,然后可讀性也會越來越差。我們可以嘗試使用一下 validator:

$validator = \Validator::make(request()->all(), [
    'buyer_id' => 'required',
    'amount' => 'required|numeric|gt:0'
], [
    'buyer_id.required' => ':attribute不能為空',
    'amount.required' => '請填寫:attribute',
    'amount.numeric' => ':attribute必須為數字',
    'amount.gt' => ':attribute必須大於0'
], [
    'buyer_id' => '客戶id',
    'amount' => '金額'
]);
if ($validator->fails()) {
    return $this->ajaxFail($validator->messages()->first());
}

  有些看起來是沒什么用的,如 buyer_id,一般情況下,我們會用一個隱藏域保存這個 id,但是我們還是得防止一些非法的請求,同時也是為了保持數據的完整性,因為這的確是必須的數據,如果有一天某個錯誤導致這個關聯的 id 沒有保存到,可能會導致一些神奇的 bug 出現。

 

11、返回頁面的同時返回一些額外的信息(如警告、錯誤)

return redirect()->back()->withErrors('some error');

  前端獲取:

@if(Session::has('error'))
      toastr.warning('{{Session::get('error')}}', '', options);
@endif

  其實 laravel 在處理表單的時候也有一些類似的處理, 如 redirect('form')->withInput();

 

12、在控制器以外的地方返回響應

  這種做法可能會導致難以調試的 bug(因為維護的人可能不知道在哪里返回了),不過實在需要的時候,可以用一用。

response()->json(['message' => 'test send response directly'])->send();exit;

  

13. 使用 EloquentCollection 加載關聯

我們在使用 User::get() 的時候獲取到的是一個 Illuminate\Database\Eloquent\Collection 實例,這個實例繼承了 Illuminate\Support\Collection,可以直接使用 Illuminate\Support\Collection 的方法。除此之外,還有一些自身特有的方法,比如 load,我們可以使用 load 方法加載每一項的關聯數據,好比如,如果每一個 User 又一個 info 關聯,我們可以使用 $users->load('info') 關聯,這種方法的好處是,避免了 n+1 的問題

 

14. 函數、 類方法的依賴注入正確使用姿勢

  a.  函數

class TestService
{
    public $name = 'testService';
}

function test(TestService $testService)
{
    var_dump($testService->name);
}

// 通過依賴注入的方式調用這個 test 函數
// test 的參數會通過依賴注入的方式注入
app()->call('test');

  b. 類方法  

class TestService
{
    public function test(stdClass $stdClass)
    {
        var_dump($stdClass);
    }
}

// 類方法的依賴注入(非構造方法)
app()->call('TestService@test');

  

 

 

最后推薦一個好東西:Laravel 5.1 LTS 速查表


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM