• 技术文章 >php框架 >Laravel

    手把手教你实现一个 Laravel 查询过滤器

    青灯夜游青灯夜游2023-01-18 21:10:49转载68

    查询过滤器... 开发系统时常见的问题。但是当开始编写代码时,每个开发人员都会出现许多熟悉的问题:「我应该把这个查询逻辑放在哪里?我应该如何管理它以方便使用?」。老实说,对于我开发的每个项目,我都会根据以前创建的项目的经验以不同的风格写作。而每次我开始一个新项目,这一次我都会问自己同样的问题,我如何安排查询过滤器!本文可以认为是一个查询过滤系统的逐步开发,有相应的问题。

    上下文

    在撰写本文时,我在 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('users', 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,如有侵犯,请联系admin@php.cn删除
    专题推荐:php Laravel
    上一篇:聊聊Laravel中怎么向About命令添加有用的信息 下一篇:自己动手写 PHP MVC 框架(40节精讲/巨细/新人进阶必看)

    相关文章推荐

    • 分析Laravel中HasOne和BelongsTo有什么不同之处?• 浅析Laravel中isDirty()和wasChanged()的区别• 教你用laravel-websockets搞个“低配”广播系统• Laravel10要来了,带大家先睹为快!• 聊聊Laravel Excel 的五个鲜为人知的功能• 解决laravel8.x用户认证vite指令不生效问题
    1/1

    PHP中文网