illuminatech/data-provider

允许通过API请求轻松构建数据库查询

1.0.6 2024-03-25 11:01 UTC

This package is auto-updated.

Last update: 2024-08-25 11:51:13 UTC


README

Laravel 数据提供者


此扩展允许基于 Laravel 请求构建复杂的搜索查询。特别是,它对 REST API 组合很有用。

有关许可证信息,请检查LICENSE文件。

Latest Stable Version Total Downloads Build Status

安装

安装此扩展的首选方式是通过 composer

运行

php composer.phar require --prefer-dist illuminatech/data-provider

或添加

"illuminatech/data-provider": "*"

到您的 composer.json 文件中的 "require" 部分。

用法

此扩展允许根据请求数据构建复杂的搜索查询。它处理过滤、排序、分页、根据需要包含额外字段或关系。同时支持 Eloquent Active Record 和普通数据库查询。

此扩展提供了 Illuminatech\DataProvider\DataProvider 类,该类封装了给定的数据源对象(如数据库查询构建器),并提供了定义控制器级别的交互以通过此数据源进行搜索的方法。

用法示例

<?php

use App\Models\Item;
use Illuminate\Http\Request;
use Illuminatech\DataProvider\DataProvider;

class ItemController extends Controller
{
    public function index(Request $request)
    {
        $items = DataProvider::new(Item::class)
            ->filters([
                'id',
                'status' => 'status_id',
                'search' => ['name', 'description'],
            ])
            ->sort(['id', 'name', 'status', 'created_at'])
            ->sortDefault('-id')
            ->paginate($request);
            
        // ...
    }
}

此示例将响应以下请求

GET http://example.com/items?filter[status]=active&filter[search]=foo&sort=-id&page=2&per-page=20

与普通数据库查询使用的相同示例

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminatech\DataProvider\DataProvider;

class ItemController extends Controller
{
    public function index(Request $request)
    {
        $items = DataProvider::new(DB::table('items'))
            ->filters([
                'id',
                'status',
                'search' => ['name', 'description'],
            ])
            ->sort(['id', 'name', 'status', 'created_at'])
            ->sortDefault('-id')
            ->paginate($request);
            
        // ...
    }
}

数据提供者仅定义了少量数据查询方法

  • get() - 返回与请求匹配的所有记录
  • paginate() - 返回带有分页器的与请求匹配的所有记录
  • simplePaginate() - 返回带有简单分页器的与请求匹配的所有记录
  • cursorPaginate() - 返回带有光标分页器的与请求匹配的所有记录

但是,您可以使用 prepare() 方法获取调整到给定请求的数据源对象,以调用您需要的任何方法。例如

<?php

use App\Models\Item;
use Illuminatech\DataProvider\DataProvider;

$items = DataProvider::new(Item::class)
    ->filters(/* ... */)
    ->prepare($request) // applies all requested filters, returns `\Illuminate\Database\Eloquent\Builder` instance
    ->chunk(100, function ($items) {
        // ...
    });

方法 prepare() 是不可变的,保留原始数据源对象不变。因此,您可以使用相同的数据提供者处理多个不同的搜索请求。例如

<?php

use App\Models\Article;
use Illuminatech\DataProvider\DataProvider;

$query = Article::query() 
    ->with('category');

$dataProvider = DataProvider::new($query)
    ->filters([
        'status'
    ]);

$publishedArticles = $dataProvider->get(['filter' => ['status' => 'published']]); // has no side affect on `$query` instance
$draftArticles = $dataProvider->get(['filter' => ['status' => 'draft']]); // can process multiple requests in isolation

指定数据源

有几种方式可以为 DataProvider 实例指定数据源

  • Illuminate\Database\Eloquent\Builder 的实例
  • Illuminate\Database\Query\Builder 的实例
  • 类似 Illuminate\Database\Eloquent\Relations\HasMany 的 Eloquent 关系实例
  • Eloquent 模型类的名称

例如

<?php

use App\Models\Item;
use Illuminate\Support\Facades\DB;
use Illuminatech\DataProvider\DataProvider;

$items = DataProvider::new(Item::query()) // instance of `\Illuminate\Database\Eloquent\Builder`
    ->filters(/* ... */)
    ->get();
    
$items = DataProvider::new( // all default conditions and eager loading should be applied to data source before passing it to data provider
        Item::query()
            ->with('category')
            ->where('status', 'published')
    )
    ->filters(/* ... */)
    ->get();

$items = DataProvider::new(Item::class) // invokes `Item::query()` automatically
    ->filters(/* ... */)
    ->get();

$items = DataProvider::new(DB::table('items')) // instance of `\Illuminate\Database\Query\Builder`
    ->filters(/* ... */)
    ->get();

$item = Item::query()->first();
$purchases = DataProvider::new($item->purchases()) // instance of `\Illuminate\Database\Eloquent\Relations\HasMany`
    ->filters(/* ... */)
    ->get();

注意:此扩展不对数据源对象类型施加显式限制 - 它只期望匹配数据库查询构建器符号。因此,您可能创建一个自定义查询构建器类,它与特殊的数据存储(如 MongoDB 或 Redis)一起工作,并将其实例作为数据源传递。如果其方法签名匹配 \Illuminate\Database\Query\Builder - 它应该工作。尽管不能保证。

配置

您可以使用以下控制台命令发布预定义的配置文件

php artisan vendor:publish --provider="Illuminatech\DataProvider\DataProviderServiceProvider" --tag=config

这将为所有 DataProvider 实例创建一个应用程序范围的配置。您可以在 config/data_provider.php 中查看其示例。您可以使用其构造函数的第二个参数调整每个 DataProvider 实例的配置。例如

<?php

use App\Models\Item;
use Illuminatech\DataProvider\DataProvider;

$items = DataProvider::new(Item::class, [
    'pagination' => [
        'per_page' => [
            'max' => 80,
            'default' => 20,
        ],
    ],
])
    ->filters([
        // ...
    ])
    ->paginate($request); // creates paginator with page size 20

附加配置将递归地与您在 "config/data_provider.php" 文件中指定的配置合并,因此您只需指定您想要更改的键。

过滤

过滤设置示例

<?php

use App\Models\Item;
use Illuminatech\DataProvider\DataProvider;
use Illuminatech\DataProvider\Filters\FilterExact;

$items = DataProvider::new(Item::class)
    ->filters([
        'id', // short syntax, equals to `'id' => new FilterExact('id')`,
        'status' => 'status_id', // short syntax, equals to `'status' => new FilterExact('status_id')`,
        'search' => ['name', 'description'], // short syntax, equals to `'search' => new FilterSearch(['name', 'description'])`
        'exact_name' => new FilterExact('name'), // pass filter instance directly
        'callback' => function ($query, $name, $value) { // short syntax, equals to `'callback' => new FilterCallback(function ($query, $name, $value) {})`
            $query->whereNotIn('status', $value);
        },
    ])
    ->paginate($request);

此示例将响应以下请求

GET http://example.com/items?filter[id]=12&filter[status]=2&filter[search]=any&filter[exact_name]=foo&filter[callback][0]=1&filter[callback][1]=2

提示:您可以通过将 data_provider.filter.keyword 配置值设置为 null 来禁用请求中的过滤分组。

在指定过滤属性时,您可以使用点('.')符号来使过滤挑战针对关系而不是主源。在这种情况下,Illuminate\Database\Eloquent\Concerns::QueriesRelationships::whereHas()将在底层执行。然而,此行为仅适用于Eloquent查询和关系。例如

<?php

use App\Models\Item;
use Illuminate\Support\Facades\DB;
use Illuminatech\DataProvider\DataProvider;
use Illuminatech\DataProvider\Filters\FilterExact;

// Eloquent processes dot attributes via relations:
$items = DataProvider::new(Item::class)
    ->filters([
        'category_name' => new FilterExact('category.name'),
    ])
    ->get(['category_name' => 'programming']); // applies $itemQuery->whereHas('category', function() {...});

// Regular DB query consider dot attribute as 'table.column' specification:
$items = DataProvider::new(
        DB::table('items')
            ->join('categories', 'categories.id', '=', 'items.category_id')
    )
    ->filters([
        'category_name' => new FilterExact('category.name'),
    ])
    ->get(['category_name' => 'programming']); // applies $itemQuery->where('category.name', '=', 'programming');

支持的过滤列表

请参阅特定过滤类的更多详细信息和示例。

您可以通过实现Illuminatech\DataProvider\FilterContract接口来创建您自己的自定义过滤。

排序

排序设置示例

<?php

use App\Models\User;
use Illuminatech\DataProvider\DataProvider;

$dataProvider = DataProvider::new(User::class)
    ->sort([
        'id', // short syntax, equals to `['id' => ['asc' => ['id' => 'asc'], 'desc' => ['id' => 'desc']]]`
        'name' => [
            'asc' => ['first_name' => 'asc', 'last_name' => 'asc'],
            'desc' => ['first_name' => 'desc', 'last_name' => 'desc'],
        ],
    ])
    ->sortDefault('-id');

$users = $dataProvider->get(['sort' => 'id']); // applies 'ORDER BY id ASC'
$users = $dataProvider->get(['sort' => '-id']); // applies 'ORDER BY id DESC'
$users = $dataProvider->get(['sort' => 'name']); // applies 'ORDER BY first_name ASC, last_name ASC'
$users = $dataProvider->get([]); // applies default sort: 'ORDER BY id DESC'

您可以通过将数据提供程序设置data_provider.sort.enable_multisort的配置值设置为true来启用多排序支持。例如

<?php

use App\Models\User;
use Illuminatech\DataProvider\DataProvider;

$dataProvider = DataProvider::new(User::class, [
        'sort' => [
            'enable_multisort' => true,
        ],
    ])
    ->sort([
        'id',
        'first_name',
    ]);

$users = $dataProvider->get(['sort' => 'first_name,-id']); // applies 'ORDER BY first_name ASC, id DESC'

注意:多排序的排序参数可以作为逗号分隔的字符串或数组传递。

分页

数据提供程序定义以下分页方法,封装了查询构建器提供的方法

  • paginate() - 返回带有分页器的与请求匹配的所有记录
  • simplePaginate() - 返回带有简单分页器的与请求匹配的所有记录
  • cursorPaginate() - 返回带有光标分页器的与请求匹配的所有记录

除了Laravel标准分页行为之外,这些方法还允许通过请求参数控制页面大小。例如

<?php

use App\Models\Item;
use Illuminatech\DataProvider\DataProvider;

$dataProvider = DataProvider::new(Item::class);

var_dump(count($dataProvider->paginate([])->items())); // outputs: 15
var_dump(count($dataProvider->paginate(['per-page' => 5])->items())); // outputs: 5
var_dump(count($dataProvider->paginate(['per-page' => 999999999])->items())); // throws a 'bad request' HTTP exception

您可以通过构造函数配置参数来控制每个数据提供程序的分页大小边界。例如

<?php

use App\Models\Item;
use Illuminatech\DataProvider\DataProvider;

$dataProvider = DataProvider::new(Item::class, [
    'pagination' => [
        'per_page' => [
            'min' => 1,
            'max' => 200,
            'default' => 20,
        ],
    ],
]);

如果启用data_provider.pagination.appends,则所有分页方法将自动将传递的请求数据追加到创建的分页器实例中,因此您不需要手动调用Illuminate\Contracts\Pagination\Paginator::appends()

包含关系

在创建API时,您可能允许其客户端“展开”特定实体,包括它们与HTTP响应的关系。

包含关系设置示例

<?php

use App\Models\Item;
use Illuminatech\DataProvider\DataProvider;
use Illuminatech\DataProvider\Includes\IncludeRelation;

$dataProvider = DataProvider::new(Item::class)
    ->includes([
        'category', // short syntax, equals to `'category' => new IncludeRelation('category')`
        'alias' => 'relation', // short syntax, equals to `'alias' => new IncludeRelation('relation')`,
        'published_comments' => new IncludeRelation('comments', function ($commentsQuery) {
            $commentsQuery->where('status', '=', 'published');
        }),
        'category.group', // nested relation include
    ]);

$item = $dataProvider->prepare(['include' => 'category'])->first();
var_dump($item->relationLoaded('category')); // outputs `true`

选择字段

在创建API时,您可能允许其客户端指定特定列表端点返回的字段列表。这可以减少HTTP流量,跳过响应中的大型文本字段。

选择字段设置示例

<?php

use App\Models\Item;
use Illuminatech\DataProvider\DataProvider;
use Illuminatech\DataProvider\Fields\Field;

$dataProvider = DataProvider::new(Item::class)
    ->fields([
        'id', // short syntax, equals to `'id' => new Field('id')`
        'name' => new Field('name'),
        'brief' => 'description', // short syntax, equals to `'brief' => new Field('description')`
        'price',
    ]);

$item = $dataProvider->prepare(['fields' => 'id,name'])->first();
var_dump(isset($item->id)); // outputs `true`
var_dump(isset($item->name)); // outputs `true`
var_dump(isset($item->description)); // outputs `false`

您还可以为相关模型指定可选择的字段。例如

<?php

use App\Models\Item;
use Illuminatech\DataProvider\DataProvider;

$dataProvider = DataProvider::new(Item::class)
    ->fields([
        'id',
        'name',
        'category' => [
            'id',
            'name',
        ],
    ]);

$item = $dataProvider->prepare([
    'fields' => [
        'id',
        'category' => [
            'id',
        ],
    ],
])->first();

var_dump(isset($item->id)); // outputs `true`
var_dump(isset($item->name)); // outputs `false`
var_dump(isset($item->category->id)); // outputs `true`
var_dump(isset($item->category->name)); // outputs `false`

请注意,传递特定关系的字段会导致其预加载。实际上,这是在编写“字段”时声明“包含”。这可能导致您的API不一致,因为它允许通过“字段”加载特定关系,但不允许通过“包含”加载它。这是您的责任,以一致的方式设置includes()fields()

JSON API规范支持

此扩展与JSON API规范兼容。然而,由于它提供了与本地Laravel分页的兼容性,因此分页的默认配置与其不匹配。但是,您可以通过适当的配置轻松修复此问题。例如

<?php
// file 'config/data_provider.php'

return [
    // ...
    'pagination' => [
        'keyword' => 'page',
        'page' => [
            'keyword' => 'number',
        ],
        'per_page' => [
            'keyword' => 'size',
            // ...
        ],
        // ...
    ],
];

专用数据提供程序

您可以创建一个针对特定用例的专用数据提供程序类。这种方法允许组织代码并保持控制器干净。

可以使用Illuminatech\DataProvider\DedicatedDataProvider作为基类轻松实现此目标。它预定义了一组名为define*的方法,例如defineConfig()defineFilters()等,您可以通过重写它们来创建结构化的自定义类。此外,请注意,与其他方法不同,__construct()在扩展时不受通常的签名兼容性规则的约束。因此,您可以在类中指定其签名,定义自己的依赖关系。例如

<?php

use App\Models\User;
use Illuminatech\DataProvider\DedicatedDataProvider;
use Illuminatech\DataProvider\Filters\FilterIn;

class UserPurchasesList extends DedicatedDataProvider
{
    public function __construct(User $user)
    {
        parent::__construct($user->purchases()->with('item'));
    }

    protected function defineConfig(): array
    {
        return [
            'pagination' => [
                'per_page' => [
                    'default' => 16,
                ],
            ],
        ];
    }

    protected function defineFilters(): array
    {
         return [
             'id',
             'status' => new FilterIn('status'),
             // ...
         ];
    }

    protected function defineSort(): array
    {
        return [
            'id',
            'created_at',
            // ...
        ];
    }
    
    // ...
}

// Controller code :

use Illuminate\Http\Request;

class PurchaseController extends Controller
{
    public function index(Request $request)
    {
        $items = UserPurchasesList::new($request->user())
            ->paginate($request);
            
        // ...
    }
}