如何在Laravel中优雅地实现搜索功能

Elegant Search Implementation in Laravel

Posted by Brian on Friday, December 6, 2024

引言

在日常开发中,搜索功能是一个非常常见的需求。然而,很多开发者特别是初学者往往会采用一些不够优雅的实现方式。本文将带你一步步优化搜索功能的实现,从最基础的写法开始,逐步演进到一个优雅且易维护的解决方案。

常见的实现方式

作为初学者,你可能会写出这样的代码:

$conditions = [];

if (request('status')) {
    $conditions[] = ['status', '=', request('status')];
}

if (request('email')) {
    $conditions[] = ['email', '=', request('email')];
}

// 更多条件...

$users = User::where($conditions)->get();

这种实现方式存在以下问题:

  • 代码冗长,充斥着大量的 IF 语句
  • 可读性差,特别是在条件较多时
  • 维护困难,每增加一个搜索条件都需要添加新的 IF 语句
  • 代码结构混乱,不符合单一职责原则

优化思路一:使用查询构造器

Laravel 提供了丰富的查询构造器方法:

  • where/orWhere
  • whereIn/whereNotIn
  • whereNull/whereNotNull
  • whereDate/whereMonth/whereDay/whereYear 等等

你可能会这样改写:

User::where('status', request('status'))
    ->where('email', request('email'))
    ->get();

优化思路二:使用 scope 方法

进一步,你可能会想到在模型中定义 scope 方法:

User::search()->get();

优化思路三:使用 when 方法

使用 Laravel 的 when 方法可以让代码更加优雅:

User::query()
    ->when(request('status'), function($query, $status) {
        return $query->where('status', $status);
    })
    ->when(request('email'), function($query, $email) {
        return $query->where('email', $email);
    })
    ->get();

优化思路四:使用过滤器类

为了更好地组织代码,我们可以创建专门的过滤器类:

class UserFilters
{
    protected $query;

    public function __construct($query)
    {
        $this->query = $query;
    }

    public function apply($filters)
    {
        foreach ($filters as $filter => $value) {
            if (method_exists($this, $filter)) {
                $this->{$filter}($value);
            }
        }
        return $this->query;
    }

    protected function status($value)
    {
        $this->query->where('status', $value);
    }

    protected function email($value)
    {
        $this->query->where('email', $value);
    }
}

最终方案:使用 Pipeline 模式

通过使用 Laravel 的 Pipeline 特性,我们可以实现一个更加优雅的解决方案。

首先创建一个 Filterable trait:

<?php declare(strict_types=1);

namespace App\Traits;

use Illuminate\Pipeline\Pipeline;

trait Filterable
{
    public function scopeFilter($query, array $through)
    {
        return app(Pipeline::class)
            ->send($query)
            ->through($through)
            ->thenReturn();
    }
}

然后为每个搜索条件创建独立的过滤器类:

<?php declare(strict_types=1);

namespace App\QueryFilters\User;

use Illuminate\Database\Eloquent\Builder;

class Email
{
    public function handle(Builder $query, $next)
    {
        $query->when(request('email'), function($query, $email) {
            $query->where('email', $email);
        });

        return $next($query);
    }
}

最后在控制器中优雅地使用:

$users = User::filter([
    Email::class,
    Status::class,
    // 更多过滤器...
])->get();

总结

通过使用 Pipeline 模式,我们实现了以下目标:

  1. 移除了所有的 IF 语句
  2. 提高了代码的可读性和可维护性
  3. 实现了单一职责原则
  4. 使代码结构更加清晰

虽然这种实现方式会创建较多的文件,但每个类的职责都很明确,便于维护和扩展。对于文件数量的问题,我们可以通过编写脚本来自动生成过滤器类,从而提高开发效率。

最重要的是,这种实现方式让我们可以专注于业务逻辑,而不是纠结于代码结构。当你需要修改某个搜索条件的逻辑时,只需要找到对应的过滤器类进行修改即可,不会影响到其他代码。这就是一个优雅的解决方案应该具备的特质。

当然,目前这些方法是作者目前能想到的一些比较好的方案。之前看其它框架中封装有有类似的功能。但大多都需要遵循特定的规则,前端UI也是需要按特定的规则去实现。这在实际开发中如果遇到特殊的要求的话,前端跟后端改起来都很痛苦。

如果您有更好的解决方案,也欢迎与分联系分享。