首頁  >  文章  >  php框架  >  手把手教你實作一個 Laravel 查詢過濾器

手把手教你實作一個 Laravel 查詢過濾器

青灯夜游
青灯夜游轉載
2023-01-18 21:10:491364瀏覽

手把手教你實作一個 Laravel 查詢過濾器

查詢過濾器... 開發系統時常見的問題。但是當開始編寫程式碼時,每個開發人員都會出現許多熟悉的問題:「我應該把這個查詢邏輯放在哪裡?我應該如何管理它以方便使用?」。老實說,對於我開發的每個項目,我都會根據以前創建的項目的經驗以不同的風格寫作。而每次我開始一個新項目,這次我都會問自己同樣的問題,我如何安排查詢過濾器!本文可以認為是一個查詢過濾系統的逐步開發,有相應的問題。

上下文

在撰寫本文時,我在 PHP 8.1 和 MySQL 8 上使用 Laravel 9。我相信技術堆疊不是一個大問題,這裡我們主要專注於建立一個查詢過濾器系統。在本文中,我將示範為 users 表建立過濾器。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * 运行迁移
     *
     * @return void
     */
    public function up()
    {
        Schema::create(&#39;users&#39;, function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->string('gender', 10)->nullable()->index();
            $table->boolean('is_active')->default(true)->index();
            $table->boolean('is_admin')->default(false)->index();
            $table->timestamp('birthday')->nullable();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * 回退迁移
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}

此外,我還使用 Laravel Telescope 輕鬆監控查詢。

初始點

在學習使用 Laravel 的第一天,我經常直接在控制器上呼叫過濾器。簡單,沒有魔法,容易理解,但是這種方式有問題:

  • 控制器中放置的大量邏輯導致控制器膨脹
  • 不能重複使用

許多相同的工作重複

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;

class UserController extends Controller
{
    public function __invoke(Request $request)
    {
        // /users?name=ryder&email=hartman&gender=male&is_active=1&is_admin=0&birthday=2014-11-30
        $query = User::query();

        if ($request->has('name')) {
            $query->where('name', 'like', "%{$request->input('name')}%");
        }

        if ($request->has('email')) {
            $query->where('email', 'like', "%{$request->input('email')}%");
        }

        if ($request->has('gender')) {
            $query->where('gender', $request->input('gender'));
        }

        if ($request->has('is_active')) {
            $query->where('is_active', $request->input('is_active') ? 1 : 0);
        }

        if ($request->has('is_admin')) {
            $query->where('is_admin', $request->input('is_admin') ? 1 : 0);
        }

        if ($request->has('birthday')) {
            $query->whereDate('birthday', $request->input('birthday'));
        }

        return $query->paginate();

        // select * from `users` where `name` like '%ryder%' and `email` like '%hartman%' and `gender` = 'male' and `is_active` = 1 and `is_admin` = 0 and date(`birthday`) = '2014-11-30' limit 15 offset 0
    }
}
使用Local Scope

為了能夠在過濾期間隱藏邏輯,讓我們嘗試使用Laravel 的Local Scope。將查詢轉換為 User 模型中的函數範圍:

// User.php
public function scopeName(Builder $query): Builder
{
    if (request()->has('name')) {
        $query->where('name', 'like', "%" . request()->input('name') . "%");
    }
    return $query;
}

public function scopeEmail(Builder $query): Builder
{
    if (request()->has('email')) {
        $query->where('email', 'like', "%" . request()->input('email') . "%");
    }
    return $query;
}

public function scopeGender(Builder $query): Builder
{
    if (request()->has('gender')) {
        $query->where('gender', request()->input('gender'));
    }
    return $query;
}

public function scopeIsActive(Builder $query): Builder
{
    if (request()->has('is_active')) {
        $query->where('is_active', request()->input('is_active') ? 1 : 0);
    }
    return $query;
}

public function scopeIsAdmin(Builder $query): Builder
{
    if (request()->has('is_admin')) {
        $query->where('is_admin', request()->input('is_admin') ? 1 : 0);
    }
    return $query;
}

public function scopeBirthday(Builder $query): Builder
{
    if (request()->has('birthday')) {
        $query->where('birthday', request()->input('birthday'));
    }
    return $query;
}

// UserController.php
public function __invoke(Request $request)
{
    // /users?name=john&email=desmond&gender=female&is_active=1&is_admin=0&birthday=2015-04-11

     $query = User::query()
             ->name()
            ->email()
            ->gender()
            ->isActive()
            ->isAdmin()
            ->birthday();

    return $query->paginate();

    // select * from `users` where `name` like '%john%' and `email` like '%desmond%' and `gender` = 'female' and `is_active` = 1 and `is_admin` = 0 and `birthday` = '2015-04-11' limit 15 offset 0
}
透過這種設置,我們將大部分資料庫操作移到了模型類別中,但是程式碼重複非常多。範例 2 的名稱和電子郵件範圍篩選器相同,性別生日和 is_active/is_admin 群組相同。我們將對類似的查詢功能進行分組。

// User.php
public function scopeRelativeFilter(Builder $query, $inputName): Builder
{
    if (request()->has($inputName)) {
        $query->where($inputName, 'like', "%" . request()->input($inputName) . "%");
    }
    return $query;
}

public function scopeExactFilter(Builder $query, $inputName): Builder
{
    if (request()->has($inputName)) {
        $query->where($inputName, request()->input($inputName));
    }
    return $query;
}

public function scopeBooleanFilter(Builder $query, $inputName): Builder
{
    if (request()->has($inputName)) {
        $query->where($inputName, request()->input($inputName) ? 1 : 0);
    }
    return $query;
}

// UserController.php
public function __invoke(Request $request)
{
    // /users?name=john&email=desmond&gender=female&is_active=1&is_admin=0&birthday=2015-04-11

    $query = User::query()
        ->relativeFilter('name')
        ->relativeFilter('email')
        ->exactFilter('gender')
        ->booleanFilter('is_active')
        ->booleanFilter('is_admin')
        ->exactFilter('birthday');

    return $query->paginate();

    // select * from `users` where `name` like '%john%' and `email` like '%desmond%' and `gender` = 'female' and `is_active` = 1 and `is_admin` = 0 and `birthday` = '2015-04-11' limit 15 offset 0
}
至此,我們已經對大部分重複項進行了分組。但是,刪除 if 語句或將這些過濾器擴展到另一個模型有點困難。我們正在尋找一種方法來徹底解決這個問題。

使用管道設計模式

管道設計模式

是一種設計模式,它提供了逐步建立和執行一系列操作的能力。 Laravel 有內建的 Pipeline 讓我們可以輕鬆地在實際中應用這種設計模式,但由於某種原因,它沒有在官方文件中列出。 Laravel 本身也將 Pipeline 應用於請求和回應之間的中間件。最基本的,要在Laravel 中使用Pipeline,我們可以這樣使用

app(\Illuminate\Pipeline\Pipeline::class)
    ->send($intialData)
    ->through($pipes)
    ->thenReturn(); // data with pipes applied
對於我們的問題,可以將初始查詢User:query() 傳遞給pipeline,通過過濾器步驟,並返回應用過濾器的查詢建構器。

// UserController
public function __invoke(Request $request)
{
    // /users?name=john&email=desmond&gender=female&is_active=1&is_admin=0&birthday=2015-04-11

    $query = app(Pipeline::class)
        ->send(User::query())
        ->through([
            // filters
        ])
        ->thenReturn();

    return $query->paginate();
現在我們需要建立管道過濾器:

// File: app/Models/Pipes/RelativeFilter.php

<?php

namespace App\Models\Pipes;

use Illuminate\Database\Eloquent\Builder;

class RelativeFilter
{
    public function __construct(protected string $inputName)
    {
    }

    public function handle(Builder $query, \Closure $next)
    {
        if (request()->has($this->inputName)) {
            $query->where($this->inputName, 'like', "%" . request()->input($this->inputName) . "%");
        }
        return $next($query);
    }
}

// File: app/Models/Pipes/ExactFilter.php

<?php

namespace App\Models\Pipes;

use Illuminate\Database\Eloquent\Builder;

class ExactFilter
{
    public function __construct(protected string $inputName)
    {
    }

    public function handle(Builder $query, \Closure $next)
    {
        if (request()->has($this->inputName)) {
            $query->where($this->inputName, request()->input($this->inputName));
        }
        return $next($query);
    }
}

//File: app/Models/Pipes/BooleanFilter.php
<?php

namespace App\Models\Pipes;

use Illuminate\Database\Eloquent\Builder;

class BooleanFilter
{
    public function __construct(protected string $inputName)
    {
    }

    public function handle(Builder $query, \Closure $next)
    {
        if (request()->has($this->inputName)) {
            $query->where($this->inputName, request()->input($this->inputName) ? 1 : 0);
        }
        return $next($query);
    }
}

// UserController
public function __invoke(Request $request)
{
    // /users?name=john&email=desmond&gender=female&is_active=1&is_admin=0&birthday=2015-04-11

    $query = app(Pipeline::class)
        ->send(User::query())
        ->through([
            new \App\Models\Pipes\RelativeFilter('name'),
            new \App\Models\Pipes\RelativeFilter('email'),
            new \App\Models\Pipes\ExactFilter('gender'),
            new \App\Models\Pipes\BooleanFilter('is_active'),
            new \App\Models\Pipes\BooleanFilter('is_admin'),
            new \App\Models\Pipes\ExactFilter('birthday'),
        ])
        ->thenReturn();

    return $query->paginate();

    // select * from `users` where `name` like '%john%' and `email` like '%desmond%' and `gender` = 'female' and `is_active` = 1 and `is_admin` = 0 and `birthday` = '2015-04-11' limit 15 offset 0
}
透過將每個查詢邏輯移動到單獨的類,我們解鎖了使用OOP 的自訂可能性,包括多態、繼承、封裝、抽象化。例如你在pipeline 的handle 函數中看到,只有if 語句中的邏輯不同,我會透過建立抽象類別BaseFilter 的方式將其分離抽像出來
//File: app/Models/Pipes/BaseFilter.php

<?php

namespace App\Models\Pipes;

use Illuminate\Database\Eloquent\Builder;

abstract class BaseFilter
{
    public function __construct(protected string $inputName)
    {
    }

    public function handle(Builder $query, \Closure $next)
    {
        if (request()->has($this->inputName)) {
            $query = $this->apply($query);
        }
        return $next($query);
    }

    abstract protected function apply(Builder $query): Builder;
}

// BooleanFilter
class BooleanFilter extends BaseFilter
{
    protected function apply(Builder $query): Builder
    {
        return $query->where($this->inputName, request()->input($this->inputName) ? 1 : 0);
    }
}

// ExactFilter
class ExactFilter extends BaseFilter
{
    protected function apply(Builder $query): Builder
    {
        return $query->where($this->inputName, request()->input($this->inputName));
    }
}

// RelativeFilter
class RelativeFilter extends BaseFilter
{
    protected function apply(Builder $query): Builder
    {
        return $query->where($this->inputName, 'like', "%" . request()->input($this->inputName) . "%");
    }
}

現在我們的過濾器直觀且高度可重複使用,易於實現甚至擴展,只需建立一個管道,擴展BaseFilter 並聲明函數apply 即可應用到Pipeline.中。

將Local Scope 與Pipeline 結合

此時,我們將嘗試在控制器上隱藏Pipeline,並透過在Model 中建立一個呼叫Pipeline 的作用域來使我們的程式碼更簡潔。

// User.php
public function scopeFilter(Builder $query)
{
    $criteria = $this->filterCriteria();
    return app(\Illuminate\Pipeline\Pipeline::class)
        ->send($query)
        ->through($criteria)
        ->thenReturn();
}

public function filterCriteria(): array
{
    return [
        new \App\Models\Pipes\RelativeFilter('name'),
        new \App\Models\Pipes\RelativeFilter('email'),
        new \App\Models\Pipes\ExactFilter('gender'),
        new \App\Models\Pipes\BooleanFilter('is_active'),
        new \App\Models\Pipes\BooleanFilter('is_admin'),
        new \App\Models\Pipes\ExactFilter('birthday'),
    ];
}

// UserController.php
public function __invoke(Request $request)
{
    // /users?name=john&email=desmond&gender=female&is_active=1&is_admin=0&birthday=2015-04-11

    return User::query()
        ->filter()
        ->paginate()
        ->appends($request->query()); // 将所有当前查询附加到分页链接中

    // select * from `users` where `name` like '%john%' and `email` like '%desmond%' and `gender` = 'female' and `is_active` = 1 and `is_admin` = 0 and `birthday` = '2015-04-11' limit 15 offset 0
}
用戶現在可以從任何地方呼叫過濾器。但其他模型也想實現過濾,我們將建立一個包含範圍的 Trait,並在模型內部聲明參與過濾過程的 Pipeline。

// User.php

use App\Models\Concerns\Filterable;

class User extends Authenticatable {
        use Filterable;

        protected function getFilters()
        {
            return [
                new \App\Models\Pipes\RelativeFilter('name'),
                new \App\Models\Pipes\RelativeFilter('email'),
                new \App\Models\Pipes\ExactFilter('gender'),
                new \App\Models\Pipes\BooleanFilter('is_active'),
                new \App\Models\Pipes\BooleanFilter('is_admin'),
                new \App\Models\Pipes\ExactFilter('birthday'),
            ];
        }

        // 其余代码

// File: app/Models/Concerns/Filterable.php

namespace App\Models\Concerns;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pipeline\Pipeline;

trait Filterable
{
    public function scopeFilter(Builder $query)
    {
        $criteria = $this->filterCriteria();
        return app(Pipeline::class)
            ->send($query)
            ->through($criteria)
            ->thenReturn();
    }

    public function filterCriteria(): array
    {
        if (method_exists($this, 'getFilters')) {
            return $this->getFilters();
        }

        return [];
    }
}
我們已經解決了分而治之的問題,每個文件,每個類,每個函數現在都有明確的職責。程式碼也乾淨、直覺且更容易重複使用,不是嗎!我把這個帖子 Demo 整個流程的程式碼都放在這裡了。 ###結語#########以上是我建立進階查詢過濾器系統的一部分,同時向你介紹了一些 Laravel 程式設計方法,例如 Local Scope 尤其是 Pipeline 設計模式。要快速輕鬆地將此設定套用到新項目,你可以使用套件 Pipeline Query Collection,其中包括一組預先建置的管道,使其易於安裝和使用。希望大家多多支持! #########原文網址:https://baro.rezonia.com/blog/building-a-sexy-query-filter######翻譯網址:https://learnku.com/ laravel/t/68762#########更多程式相關知識,請造訪:###程式設計影片###! ! ###

以上是手把手教你實作一個 Laravel 查詢過濾器的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:learnku.com。如有侵權,請聯絡admin@php.cn刪除