pricecurrent/laravel-eloquent-filters

高级 Laravel 模型过滤功能

0.1.4 2024-04-02 18:57 UTC

This package is auto-updated.

Last update: 2024-09-02 19:49:39 UTC


README

Latest Version on Packagist run-tests GitHub Code Style Action Status Total Downloads

安装

您可以通过 composer 安装此包

composer require pricecurrent/laravel-eloquent-filters

用法

此包允许您细粒度控制 Eloquent 模型过滤的方式。

当您需要处理复杂的用例,实现多个参数的过滤和复杂逻辑时,此包尤其适用。

让我们从一个简单的例子开始

假设您有一个产品,需要按 name 过滤产品

use App\Filters\NameFilter;
use Pricecurrent\LaravelEloquentFilters\EloquentFilters;

class ProductsController
{
    public function index(Request $request)
    {
        $filters = EloquentFilters::make([new NameFilter($request->name)]);

        $products = Product::filter($filters)->get();
    }
}

生成 eloquent-filter

php artisan make:eloquent-filter NameFilter

这将默认将您的过滤器放在 app/Filters 目录中。您可以使用路径作为前缀,例如 Models/Product/NameFilter

php artisan make:eloquent-filter Models/Product/NameFilter

您可以使用 --field=name 参数使用字段名生成您的过滤器

php artisan make:eloquent-filter Models/Product/NameFilter --field=name

以下是一个可能的 NameFilter 示例

use Pricecurrent\LaravelEloquentFilters\AbstractEloquentFilter;
use Illuminate\Database\Eloquent\Builder;

class NameFilter extends AbstractEloquentFilter
{
    protected $name;

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

    public function apply(Builder $query): Builder
    {
        return $query->where('name', 'like', "{$this->name}%");
    }
}

注意我们的过滤器并不知道它与一个特定的 Eloquent 模型相关联?这意味着,我们可以简单地将其重用于任何其他模型,其中我们需要引入相同的名称过滤功能

use App\Filters\NameFilter;
use App\Models\User;
use Pricecurrent\LaravelEloquentFilters\EloquentFilters;

class UsersController
{
    public function index(Request $request)
    {
        $filters = EloquentFilters::make([new NameFilter($request->user_name)]);

        $products = User::filter($filters)->get();
    }
}

您可以像使用 Eloquent Builder 方法一样链式调用过滤器中的方法

use App\Filters\NameFilter;
use App\Models\User;
use Pricecurrent\LaravelEloquentFilters\EloquentFilters;

class UsersController
{
    public function index(Request $request)
    {
        $filters = EloquentFilters::make([new NameFilter($request->user_name)]);

        $products = User::query()
            ->filter($filters)
            ->limit(10)
            ->latest()
            ->get();
    }
}

要使 Eloquent 模型具有过滤能力,只需导入 Filterable 特性

use Pricecurrent\LaravelEloquentFilters\Filterable;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use Filterable;
}

更复杂的用例

当您处理涉及从数据库查询数据且远超简单按名称字段比较的真实大型应用程序时,这种方法扩展性非常好。

考虑一个应用程序,其中我们有一个包含位置坐标的商店,我们还有库存中的产品,我们需要查询所有在 10 英里半径内的商店中的产品

我们可以在控制器中添加一些伪代码来放置所有逻辑

class ProductsController
{
    public function index(Request $request)
    {
        $products Product::query()
            ->when($request->in_stock, function ($query) {
                $query->join('product_stock', fn ($q) => $q->on('product_stock.product_id', '=', 'products.id')->where('product_stock.quantity', '>', 0));
            })
            ->when($request->within_radius, function ($query) {
                $coordinates = auth()->user()->getCoordinates();
                $query->join('stores', 'stores.id', '=', 'product_stock.store_id');
                $query->whereRaw('
                    ST_Distance_Sphere(
                        Point(stores.longitude, stores.latitude),
                        Point(?, ?)
                    ) <= ?',
                    [$coordinates->longitude, $coordinates->latitude, $query->within_radius]
                );

            })
            ->get();

        return response()->json(['data' => $products]);
    }
}

这打破了开放-封闭原则,使得代码更难测试和维护。添加新功能变得是一场灾难。

class ProductsController
{
    public function index(Request $request)
    {
        $filters = EloquentFilters::make([
            new ProductInStockFilter($request->in_stock),
            new StoreWithinDistanceFilter($request->within_radius, auth()->user()->getCoordinates())
        ]);

        $products = Product::filter($filters)->get();

        return response()->json(['data' => $products]);
    }
}

更好!

现在,我们可以将过滤逻辑分散到专用类中

class StoreWithinDistanceFilter extends AbstractEloquentFilter
{
    public function __construct($distance, Coordinates $fromCoordinates)
    {
        $this->distance = $distance;
        $this->fromCoordinates = $fromCoordinates;
    }

    public function apply(Builder $query): Builder
    {
        return $builder->join('stores', 'stores.id', '=', 'product_stock.store_id')
            ->whereRaw('
                ST_Distance_Sphere(
                    Point(stores.longitude, stores.latitude),
                    Point(?, ?)
                ) <= ?',
                [$this->coordinates->longitude, $this->coordinates->latitude, $this->distance]
            );
    }
}

现在我们没有任何问题来测试我们的功能

class StoreWithinDistanceFilterTest extends TestCase
{
    /**
     * @test
     */
    public function it_filters_products_by_store_distance()
    {
        $user = User::factory()->create(['latitude' => '...', 'longitude' => '...']);
        $store = Store::factory()->create(['latitude' => '...', 'longitude' => '...']);
        $products = Product::factory()->create();
        $store->stock()->attach($product, ['quantity' => 3]);

        $result = Product::filter(new EloquentFilters([new StoreWithinDistanceFilter(10, $user->getCoordinates())]));

        $this->assertCount(1, $result);
    }
}

控制器可以通过模拟或存根进行测试,只需确保我们调用了必要的过滤器。

检查过滤是否适用

每个过滤器都提供了 isApplicable() 方法,您可以实现它并返回布尔值。如果返回 false,则不会调用 apply 方法。

当您不控制传入过滤器类的参数时,这很有用。在上面的示例中,我们可以这样做

class StoreWithinDistanceFilter extends AbstractEloquentFilter
{
    public function __construct($distance, Coordinates $fromCoordinates)
    {
        $this->distance = $distance;
        $this->fromCoordinates = $fromCoordinates;
    }

    public function isApplicable(): bool
    {
        return $this->distance && is_numeric($this->distance);
    }

    public function apply(Builder $query): Builder
    {
        // your code
    }
}

当然,您也可以采取另一种方法,即您控制传递给过滤器参数的内容,而不仅仅是盲目地传递请求负载。您可以使用 DTO 和类型提示来实现这一点,并拥有 Filters 集合工厂来正确构建过滤器集合。例如

class ProductsController
{
    public function index(IndexProductsRequest $request)
    {
        $products = Product::filter(FiltersFactory::fromIndexProductsRequest($request))->get();

        return response()->json(['data' => $products]);
    }
}

测试

composer test

变更日志

有关最近更改的更多信息,请参阅 CHANGELOG

贡献

有关详细信息,请参阅 CONTRIBUTING

安全漏洞

请查看我们的安全策略,了解如何报告安全漏洞。

致谢

许可证

MIT许可证(MIT)。请参阅许可证文件获取更多信息。