simexis/laravel-filterable

为 Laravel 模型添加查询过滤功能的特性

dev-master 2022-04-12 09:37 UTC

This package is auto-updated.

Last update: 2024-09-12 14:33:09 UTC


README

为 Laravel 模型提供可组合查询的特质。

预期用法是提供一种简单的方式来声明模型字段的可过滤性,并允许过滤查询以易于表示的形式(例如 JSON 对象)来表示。

Simexis\Filterable\FilterableTrait 为模型添加了一个 filter() 方法,该方法与 Laravel 查询构建器一起工作。filter() 方法接受一个数组作为单个参数,该数组包含 [字段 => 值] 对,定义了正在进行的搜索查询。

安装

composer require simexis/laravel-filterable

用法

基本用法是将特质添加到模型类中并配置可过滤字段。

use Simexis\Filterable\Filterable;

class User extends Model
{
  use \Simexis\Filterable\FilterableTrait;
  
  protected $filterable = [
    'name' => Filterable::String,
    'email' => Filterable::String
  ];
}

$filter = [
  'name' => 'John'
];
User::filter($filter)->toSql();
// select * from users where name = ?

$filter = [
  'name_LIKE' => 'Jo%n',
  'email' => 'john@example.com'
];
User::filter($filter)->toSql();
// select * from users where name like ? and email = ?

$filterable 属性定义了可用于过滤查询的字段。其值是一个包含过滤器可能使用的可能变体的 过滤规则 的数组。

内置规则包括

  • EQ - 等于
  • LIKE - SQL LIKE
  • MATCH - 通配符模式匹配
  • MIN - 大于等于
  • MAX - 小于等于
  • GT - 大于
  • LT - 小于
  • RE - 正则表达式
  • FT - 全文搜索(见以下说明)
  • IN - 包含在列表中
  • NULL - NULL 比较

提供一组标准规则

  • 字符串 = [EQ, LIKE, MATCH]
  • 数字 = [EQ, MIN, MAX, LT, GT]
  • 枚举 = [EQ, IN]
  • 日期 = [EQ, MIN, MAX, LT, GT]
  • 布尔值 = [EQ]

模型的可过滤定义设置了每个字段可用的规则。列表中的第一个规则是该字段的默认规则。其他规则必须在查询字段名称中添加规则名称作为后缀。

在以下定义中,'name' 字段可以使用任何 String 规则:EQ、LIKE、MATCH。

User::$filterable = [
  'name' => Filterable::String
]

因此,查询可以使用

$filter = [
  'name_MATCH' => 'John'
];
User::filter($filter);

运行的 SQL 将匹配任何名称为不区分大小写的匹配包含 'John' 的用户,例如 'Little john'、'Johnathon'、'JOHN'。

模型的可过滤定义还可以通过在类上重载 getFilterable() 方法动态返回。

class User extends Model
{
  use \Simexis\Filterable\FilterableTrait;
  
  public function getFilterable () {
    return [
      'name' => Filterable::String,
      'email' => Filterable::String
    ];
  }
}

自定义规则

类可以为字段提供自定义规则。

class User extends Model
{
  use \Simexis\Filterable\FilterableTrait;
  protected $filterable = [
    'keyword' => 'Keyword'
  ];
  public function scopeFilterKeyword($query, $field, $arg) {
    $query->where(function ($q) use ($arg) {
      $q->where('name', $arg);
      $q->orWhere('email', $arg);
      $q->orWhere('phone', $arg);
    });
    return $query;
  }
}

自定义规则可以与内置规则一起列出在 $filterable 定义中,并按相同的方式工作。$filterable 中的第一个规则是该字段的默认规则。如果不是第一个规则,它必须在查询字段名称中添加规则名称作为后缀。

规则名称被转换为 'ucfirst' 并附加到 'scopeFilter'。

否定规则

可以使用 'NOT' 修饰符否定默认规则,或者对于其他规则,可以在规则修饰符前缀 'NOT_'。

// anyone but John
$filter = [
  'name_NOT' => 'John'
];
User::filter($filter)->toSql();
// select * from users where name != ?

// any status except 'active' or 'expired'
$filter = [
  'status_NOT_IN' => ['active', 'expired']
];
User::filter($filter)->toSql();
// select * from users where status not in (?, ?)

注意:比较规则(MIN、MAX、LT、GT)没有否定形式。

自定义规则可以通过定义相应的范围函数来实现否定版本,该函数具有类似的名称,但在规则名称之前添加了单词 'Not'。

class User extends Model
{
   ...
   public function scopeFilterNotKeyword($query, $field, $arg) {
     return $query
       ->where('name', '!=', $arg)
       ->where('email', '!=', $arg)
       ->where('phone', '!=', $arg);
   }
}

使用 AND、OR、NOT、和 NOR 进行逻辑组合

通过将过滤结构为嵌套查询,可以创建更复杂的查询。

$filter = [
  'OR' => [
    [
      'name' => 'John'
    ],
    [
      'name' => 'Alice'
    ]
  ]
];
User::filter($filter)->toSql();
// select * from users where ((name = ?) or (name = ?))

'AND'、'OR'、'NOT' 和 'NOR' 嵌套操作符分别接受要应用的一组嵌套过滤。

关系

可以使用关系来过滤相关模型。


class Post extends Model
{
  use \Simexis\Filterable\FilterableTrait;
  
  public function getFilterable () {
    return [
      'comment' => $this->comments()
    ];
  }
  
  public function comments() {
    return $this->hasMany(Comment::class);
  }
}

class Comment extends Model
{
  use \Simexis\Filterable\FilterableTrait;
  
  public function getFilterable () {
    return [
      'created_at' => Filterable::Date
    ];
  }
  
  public function post() {
    return $this->belongsTo(Post::class);
  }
}

# Get all posts that have a comment in June 2017
$filter = [
  'comment' => [
    'created_at_MIN' => '2017-06-01',
    'cerated_at_MAX' => '2017-07-01'
  ]
];
Post::filter($filter)->toSql()
// select * from "posts"
// inner join (
//   select distinct "comments"."post_id" from "comments"
//   where "created_at" >= ? and "created_at" <= ?
// ) as "comments_1" on "posts"."id" = "comments_1"."post_id"

链式过滤

在链式过滤时,会进行深度合并。

class Post extends Model
{
  use \Simexis\Filterable\FilterableTrait;
  
  public function getFilterable () {
    return [
      'comment' => $this->comments()
    ];
  }
  
  public function comments() {
    return $this->hasMany(Comment::class);
  }
}

class User extends Model
{
  use \Simexis\Filterable\FilterableTrait;
  
  protected $filterable = [
    'id' => Filterable::Integer
  ];
}

class Comment extends Model
{
  use \Simexis\Filterable\FilterableTrait;
  
  public function getFilterable () {
    return [
      'created_at' => Filterable::Date,
      'author' => $this->author()
    ];
  }
  
  public function post() {
    return $this->belongsTo(Post::class);
  }
  
  public function author() {
    return $this->belongsTo(User::class);
  }
}
$filter1 = [
  'comment' => [
    'created_at_MIN' => '2017-06-01',
    'created_at_MAX' => '2017-07-01'
  ]
];
$filter2 = [
  'comment' => [
    'author' => [
      'id' => 1
    ]
  ]
];

Post::filter($filter1)->filter($filter2)->toSql()
// select * from "posts"
// inner join (
//   select distinct "comments"."post_id"
//   from "comments"
//   inner join (
//     select distinct "users"."id" as "author_id" from "users"
//     where "id" = ?
//   ) as "authors_1" on "comments"."author_id" = "authors_1"."author_id"
//   where "created_at" >= ? and "created_at" <= ?
// ) as "comments_1" on "posts"."id" = "comments_1"."post_id"

要避免深度合并,可以使用 filterApply() 方法。

Post::filter($filter1)->filterApply()->filter($filter2)->toSql()
// select * from "posts"
// inner join (
//   select distinct "comments"."post_id" from "comments"
//   where "created_at" >= ? and "created_at" <= ?
// ) as "comments_1" on "posts"."id" = "comments_1"."post_id"
// inner join (
//   select distinct "comments"."post_id" from "comments"
//   inner join (
//     select distinct "users"."id" as "author_id" from "users"
//     where "id" = ?
//   ) as "authors_1" on "comments"."author_id" = "authors_1"."author_id"
// ) as "comments_2" on "posts"."id" = "comments_2"."post_id"

全文搜索

Filterable::FT 规则提供了在 PostgreSQL 中使用 tsearch 进行全文搜索的基本形式。

为了使用全文规则,应用程序必须提供一个包含搜索数据的表。默认情况下,表的名称以模型名称为基础,后缀为 _filterable,并且与模型的表使用相同的唯一键进行一对一映射。tsearch 矢量数据存储在以字段名称为基础,后缀为 _vector 的列中。可以自定义表和矢量字段名称,通过为 filterableFtTable 和 filterableFtVector 属性提供值来实现。

-- Content model table
create table posts (id int primary key, body text);
-- Full text search data
create table posts_filterable (id int references posts (id), body_vector tsvector);

应用程序必须确保矢量字段得到适当的更新(通常通过定义触发器函数)。

class Post
{
  use \Simexis\Filterable\FilterableTrait;
  protected $filterable = [
    'body' => Filterable::FT
  ];
}
$filter = [
  'body' => 'fat cats'
];
Post::filter($filter)->orderBy('body_rank', 'desc')->toSql();
// select * from "posts" inner join (
//  select "id", ts_rank("body_vector", query) as "body_rank"
//  from "posts_filterable"
//  cross join plainto_tsquery(?) query
//  where "body_vector" @@ "query"
// ) as body_1 on "posts"."id" = "body_1"."id"
// order by "body_rank" desc

模型可以提供自定义的搜索表、外键和字段名称的值。

class Post
{
  use \Simexis\Filterable\FilterableTrait;
  protected $filterable = [
    'body' => Filterable::FT
  ];
  protected $filterableFtTable = 'search';
  protected $filterableFtKeyName = 'post_id';
  protected $filterableFtVector = 'data';
}

这些自定义值也可以通过提供“获取”函数动态评估。

class Post
{
  use \Simexis\Filterable\FilterableTrait;
  protected $filterable = [
    'body' => Filterable::FT
  ];
  public function getFilterableFtTable ($field) {
    return 'search';
  };
  public function getFilterableFtKeyName ($field) {
    return 'post_id';
  }
  public function getFilterableFtVector ($field) {
    return $field;
  }
}