[扩展推荐] 精干的查询构建器支持自然语句和数组式语法

Laravel框架
384
0
0
2022-09-03

介绍

illusionist/searcher 是一个精干的查询构建器,支持 自然语句数组式两种语法,本库提炼自 lorisleiva/laravel-search-string

使用场景

  • 中后台系统
  • 复杂多变的前端查询条件
<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostsController extends Controller
{
    public function index(Request $request)
    {
        return Post::search($request->all())->get();
    }
}

✨ 特性

📦 安装

通过 Composer 安装

composer require illusionist/searcher

🔨 使用

添加 Searchable trait 到你的模型

Laravel/Lumen

<?php

namnespace  App\Models;

use Illuminate\Database\Eloquent\Model;
use Illusionist\Searcher\Contracts\Searchable  as SearchableContract;
use Illusionist\Searcher\Eloquent\Searchable;

class  Post  extends Model implements SearchableContract
{
    use  Searchable;
}

ThinkPHP

ThinkPHP 版本必须 >= 5.x

<?php

namnespace  app\model;

use think\Model;
use Illusionist\Searcher\Contracts\Searchable  as SearchableContract;
use Illusionist\Searcher\Eloquent\Searchable;

class  Post  extends Model implements SearchableContract
{
    use  Searchable;
}

现在,可以使用 搜索语法 创建数据库查询了

Post::search('title:"Hello world" sort:-created_at,published')->get();

💡 语法

⚠️ 注意:操作符之间的空格对于字符串语法并不重要

精确匹配

字符串语法

'rating: 0'
'rating = 0'
'title: Hello'                          // Strings without spaces do not need quotes
'title: "Hello World"'              // Strings with spaces require quotes
"title: 'Hello World'"              // Single quotes can be used too
'rating = 99.99'
'created_at: "2018-07-06 00:00:00"'

数组语法

['rating'  =>  0]
['title'  =>  'Hello World']
['rating'  =>  99.99]
['created_at'  =>  '2018-07-06 00:00:00']

比较

字符串语法

'title < B'
'rating > 3'
'created_at >= "2018-07-06 00:00:00"'

数组语法

['title'  => ['<', 'B']]
['rating'  => ['>', 3]]
['created_at'  => ['>=', '2018-07-06 00:00:00']]

布尔值

字符串语法

'published'  // published = true
'not published'  // published = false

数组语法

['published'] // published = true
['not'  =>  'published'] // published = false

日期值

字符串语法

'created_at'                                  // created_at is not null
'not created_at'                          // created_at is null

// Year precision
'created_at >= 2020'                                             // 2020-01-01 00:00:00 <= created_at
'created_at > 2020'                                              // 2020-12-31 23:59:59 < created_at
'created_at = 2020'                                              // 2020-01-01 00:00:00 <= created_at <= 2020-12-31 23:59:59
'not created_at = 2020'                                      // created_at < 2020-01-01 00:00:00 and created_at > 2020-12-31 23:59:59

// Month precision
'created_at = 01/2020'                                      // 2020-01-01 00:00:00 <= created_at <= 2020-01-31 23:59:59
'created_at <= "Jan 2020"'                                  // created_at <= 2020-01-31 23:59:59
'created_at < 2020-1'                                          // created_at < 2020-01-01 00:00:00

// Day precision
'created_at = 2020-12-31'                                  // 2020-12-31 00:00:00 <= created_at <= 2020-12-31 23:59:59
'created_at >= 12/31/2020"'                          // 2020-12-31 23:59:59 <= created_at
'created_at > "Dec 31 2020"'                          // 2020-12-31 23:59:59 < created_at

// Hour and minute precisions
'created_at = "2020-12-31 16"'                              // 2020-12-31 16:00:00 <= created_at <= 2020-12-31 16:59:59
'created_at = "2020-12-31 16:30"'                          // 2020-12-31 16:30:00 <= created_at <= 2020-12-31 16:30:59
'created_at = "Dec 31 2020 5pm"'                          // 2020-12-31 17:00:00 <= created_at <= 2020-12-31 17:59:59
'created_at = "Dec 31 2020 5:15pm"'                  // 2020-12-31 17:15:00 <= created_at <= 2020-12-31 17:15:59

// Exact precision
'created_at = "2020-12-31 16:30:00"'                              // created_at = 2020-12-31 16:30:00
'created_at = "Dec 31 2020 5:15:10pm"'                      // created_at = 2020-12-31 17:15:10

// Relative dates
'created_at = today'                                          // today between 00:00 and 23:59
'not created_at = today'                                  // any time before today 00:00 and after today 23:59
'created_at >= tomorrow'                              // from tomorrow at 00:00
'created_at <= tomorrow'                              // until tomorrow at 23:59
'created_at > tomorrow'                              // from the day after tomorrow at 00:00
'created_at < tomorrow'                              // until today at 23:59

数组语法

['created_at']                                                         // created_at is not null
['not'  =>  'created_at']                                         // created_at is null

// Year precision
['created_at'  => ['>=', '2020']]                             // 2020-01-01 00:00:00 <= created_at
['created_at'  => ['>', '2020']]                             // 2020-12-31 23:59:59 < created_at
['created_at'  =>  '2020']                                     // 2020-01-01 00:00:00 <= created_at <= 2020-12-31 23:59:59
['not'  => ['created_at'  =>  '2020']]                     // created_at < 2020-01-01 00:00:00 and created_at > 2020-12-31 23:59:59

// Month precision
['created_at'  =>  '01/2020']                             // 2020-01-01 00:00:00 <= created_at <= 2020-01-31 23:59:59
['created_at'  => ['<=', 'Jan 2020']                     // created_at <= 2020-01-31 23:59:59
['created_at'  => ['<', '2020-1']]                         // created_at < 2020-01-01 00:00:00

// Day precision
['created_at'  =>  '2020-12-31']                     // 2020-12-31 00:00:00 <= created_at <= 2020-12-31 23:59:59
['created_at'  => ['>=', '12/31/2020']                 // 2020-12-31 23:59:59 <= created_at
['created_at'  => ['>', 'Dec 31 2020']]                 // 2020-12-31 23:59:59 < created_at

// Hour and minute precisions
['created_at'  =>  '2020-12-31 16']                     // 2020-12-31 16:00:00 <= created_at <= 2020-12-31 16:59:59
['created_at'  =>  '2020-12-31 16:30']                     // 2020-12-31 16:30:00 <= created_at <= 2020-12-31 16:30:59
['created_at'  =>  'Dec 31 2020 5pm']                     // 2020-12-31 17:00:00 <= created_at <= 2020-12-31 17:59:59
['created_at'  =>  'Dec 31 2020 5:15pm']                 // 2020-12-31 17:15:00 <= created_at <= 2020-12-31 17:15:59

// Exact precision
['created_at'  =>  '2020-12-31 16:30:00']                 // created_at = 2020-12-31 16:30:00
['created_at'  =>  'Dec 31 2020 5:15:10pm']         // created_at = 2020-12-31 17:15:10

// Relative dates
['created_at'  =>  'today']                                 // today between 00:00 and 23:59
['not'  => ['created_at'  =>  'today']]                 // any time before today 00:00 and after today 23:59
['created_at'  => ['>=', 'tomorrow']]                 // from tomorrow at 00:00
['created_at'  => ['<=', 'tomorrow']]                 // until tomorrow at 23:59
['created_at'  => ['>', 'tomorrow']]                 // from the day after tomorrow at 00:00
['created_at'  => ['<', 'tomorrow']]                 // until today at 23:59

列表

字符串语法

'status:Finished,Archived'
'status in(Finished,Archived)'
'title in (Hello, Hi, "My super article")'

数组语法

['status'  => ['Finished', 'Archived']]
['status'  => ['in', 'Finished', 'Archived']]
['title'  => ['in', 'Hello', 'Hi', 'My super article']]

区间

字符串语法

'created_at:2021-1-1~2021-12-31'
'created_at between(2021-1-1, 2021-12-31)'

数组语法

['created_at'  => ['between', ['2021-1-1', '2021-12-31']]]
['created_at'  => ['between', '2021-1-1', '2021-12-31']]

否定

字符串语法

'not title:Hello'
'not title="My super article"'
'not rating:0'
'not rating>4'
'not status in (Finished,Archived)'
'not published'  // published = false
'not created_at'  // created_at is null

数组语法

['not'  => ['title'  =>  'Hello']]
['not'  => ['rating'  =>  0]]
['not'  => ['rating'  => ['>', 4]]]
['not'  => ['status'  => ['in', 'Finished', 'Archived']]]
['not'  => ['published']] // published = false
['not'  => ['created_at']] // created_at is null

空值

字符串语法

NULL 不区分大小写

'body:NULL'  // body is null
'not body:null'  // body is not null

数组语法

['body'  =>  null] // body is null
['not'  => ['body'  =>  null]] // body is not null

搜索查询

⚠️ 术语不能设置成布尔或日期型,否则将当作布尔或日期值处理

字符串语法

'Apple'  // %Apple% like at least one of the searchable columns
'"John Doe"'  // %John Doe% like at least one of the searchable columns
'not "John Doe"'  // %John Doe% not like any of the searchable columns

数组语法

['Apple'] // %Apple% like at least one of the searchable columns
['not'  =>  'John Doe'] // %John Doe% not like any of the searchable columns

与/或嵌套查询

字符串语法

'title:Hello body:World'                          // 隐式 and
'title:Hello and body:World'                  // 显示 and
'title:Hello or body:World'                  // 显示 or
'A B or C D'                                      // 等同于 '(A and B) or (C and D)'
'A or B and C or D'                              // 等同于 'A or (B and C) or D'
'(A or B) and (C or D)'                      // 显式嵌套优先级
'not (A and B)'                              // 等同于 'not A or not B'
'not (A or B)'                              // 等同于 'not A and not B'

数组语法

关键字使用驼峰格式, 示例: andOr 可以写成 and_orand-orand orAndOr;

['title'  =>  'Hello', 'body'  =>  'World']                     // 隐式 and
['and'  => ['title'  =>  'Hello', 'body'  =>  'World']]         // 显示 and
['or'  => ['title'  =>  'Hello', 'body'  =>  'World']]             // 显示 or
['or'  => [['A', 'B'], ['C', 'D']]]                                     // 等同于 '(A and B) or (C and D)'
['or'  => ['A', ['B', 'C'], 'D']]                                         // 等同于 'A or (B and C) or D'
['andOr'  => [['A', 'B'], ['C', 'D']]]                                 // 等同于 '(A or B) and (C or D)'
['not'  => ['A', 'B']]                                                     // 等同于 'not A or not B'
['notOr'  => ['A', 'B']]                                             // 等同于 'not A and not B'

关联

字符串语法

// 简单 has 检查
'comments'                                                              // Has comments
'not comments'                                                      // Doesn't have comments
'comments = 3'                                                      // Has 3 comments
'not comments = 3'                                              // Doesn't have 3 comments
'comments > 10'                                                  // Has more than 10 comments
'not comments <= 10'                                          // Same as before
'comments <= 5'                                                  // Has 5 or less comments
'not comments > 5'                                              // Same as before

// "WhereHas" 检查
'comments: (title: Superbe)'                                  // 具有 title 为 "Superbe" 的 comments
'comments: (not title: Superbe)'                          // 具有 title 不为 "Superbe" 的 comments
'not comments: (title: Superbe)'                          // 具有 title 不为 "Superbe" 的 comments
'comments: (quality)'                                          // Has comments whose searchable columns match "%quality%"
'not comments: (spam)'                                      // Doesn't have comments marked as spam
'comments: (spam) >= 3'                                      // Has at least 3 spam comments
'not comments: (spam) >= 3'                                  // Has at most 2 spam comments
'comments: (not spam) >= 3'                                  // Has at least 3 comments that are not spam
'comments: (likes < 5)'                                          // Has comments with less than 5 likes
'comments: (likes < 5) <= 10'                                  // Has at most 10 comments with less than 5 likes
'not comments: (likes < 5)'                                  // Doesn't have comments with less than 5 likes
'comments: (likes > 10 and not spam)'                  // Has non-spam comments with more than 10 likes

// "WhereHas" shortcuts
'comments.title: Superbe'                              // Same as 'comments: (title: Superbe)'
'not comments.title: Superbe'                          // Same as 'not comments: (title: Superbe)'
'comments.spam'                                          // Same as 'comments: (spam)'
'not comments.spam'                                      // Same as 'not comments: (spam)'
'comments.likes < 5'                                          // Same as 'comments: (likes < 5)'
'not comments.likes < 5'                                  // Same as 'not comments: (likes < 5)'

// Nested relationships
'comments: (author: (name: John))'                  // Has comments from the author named John
'comments.author: (name: John)'                      // Same as before
'comments.author.name: John'                          // Same as before

// Nested relationships are optimised
'comments.author.name: John and comments.author.age > 21'            // Same as: 'comments: (author: (name: John and age > 21))
'comments.likes > 10 or comments.author.age > 21'                      // Same as: 'comments: (likes > 10 or author: (age > 21))

数组语法

// Simple "has" check
['comments']                                                             // Has comments
['not'  => ['comments']]                                             // Doesn't have comments
['comments'  =>  3]                                                 // Has 3 comments
['not'  => ['comments'  =>  3]]                                 // Doesn't have 3 comments
['comments'  => ['>', 10]]                                             // Has more than 10 comments
['not'  => ['comments'  => ['<=', 10]]]                             // Same as before
['comments'  => ['<=', 5]]                                                 // Has 5 or less comments
['not'  => ['comments'  => ['>', 5]]]                                     // Same as before

// "WhereHas" check
['comments'  => ['title'  =>  'Superbe']]                                     // Has comments with the title "Superbe"
['comments'  => ['not'  => ['title'  =>  'Superbe']]]                     // Has comments whose titles are different than "Superbe"
['not'  => ['comments'  => ['title'  =>  'Superbe']]]                     // Doesn't have comments with the title "Superbe"
['comments'  =>  'quality']                                                     // Has comments whose searchable columns match "%quality%"
['not'  => ['comments'  =>  'spam']]                                         // Doesn't have comments marked as spam
['comments'  => ['spam', ['>=', 3]]]                                         // Has at least 3 spam comments
['not'  => ['comments'  => ['spam', ['>=', 3]]]]                         // Has at most 2 spam comments
['comments'  => ['not'  =>  'spam', ['>=', 3]]]                         // Has at least 3 comments that are not spam
['comments'  => ['likes'  => ['<', 5]]]                                     // Has comments with less than 5 likes
['comments'  => ['likes'  => ['<', 5], ['<=', 10]]]                     // Has at most 10 comments with less than 5 likes
['not'  => ['comments'  => ['likes'  => ['<', 5]]]]                     // Doesn't have comments with less than 5 likes
['comments'  => ['likes'  => ['<', 5], 'not'  =>  'spam']]             // Has non-spam comments with more than 10 likes

// Nested relationships
['comments'  => ['author'  => ['name'  =>  'John']]]             // Has comments from the author named John

⚔️ 进阶

短语搜索

如果一个搜索列不是布尔或日期列,就会调用 getQueryPhraseColumns 函数来获取列名,如果在返回值中没有指定操作符,默认为 like

返回多个列时将共享一个值,并以 的形式组装查询条件

示例:

<?php

namnespace  App\Models;

use Illuminate\Database\Eloquent\Model;
use Illusionist\Searcher\Contracts\Searchable  as SearchableContract;
use Illusionist\Searcher\Eloquent\Searchable;

class  Post  extends Model implements SearchableContract
{
    use  Searchable;

    /**
    * 获取查询短语的列
    *
    * @param  string $phrase
    * @return  array
    */ 
    public  function  getQueryPhraseColumns($phrase)
    {
        if (is_numeric($phrase)) {
            return ['stars'  =>  '>=', 'comments.stars'  =>  '>='];
        }
        return ['title'];
    }
}

'lonely'  // 等同于:
$query->where('title', '%lonely%');

'3000'  // 等同于:
$query->where(function ($query) {
    $query->where('stars', '>=', '3000', 'or')
        ->whereHas('comments', function ($query) {
            $query->where('stars', '>=', '3000')
    });
});

关联查询

如果定义了一个关联方法并且它是可搜索的列,就可以执行关联查询。例如:关联统计、关联加载、基于关联是否存在的查询等

<?php

namnespace  App\Models;

use Illuminate\Database\Eloquent\Model;
use Illusionist\Searcher\Contracts\Searchable  as SearchableContract;
use Illusionist\Searcher\Eloquent\Searchable;

class  Post  extends Model implements SearchableContract
{
    use  Searchable;

    public  function  comments()
    {
        return  $this->hasMany(Comment::class);
    }
}

// 查询关联是否存在 (会自动处理关系的本地键以及外键)
'comments'                          // $query->has('comments');

// 关联统计
'select:comments_count'      // $query->withCount('comments');

// 关联加载 (会自动处理关系的本地键以及外键)
'select:comments'                      // $query->select('id')->with('comments');
'select:comments.title'              // $query->select('id')->with('comments:id,title')

配置可搜索的列

多条件查询时可以设置 searchable 属性,因为针对多条件查询时设置了查询保护,当用户通过 HTTP 请求传入了非预期的参数,可以通过该属性过滤掉非预期参数以此来防止超权获取数据。

默认值是 模型表的真实列 以及 关联方法名

<?php

namnespace  App\Models;

use Illuminate\Database\Eloquent\Model;
use Illusionist\Searcher\Contracts\Searchable  as SearchableContract;
use Illusionist\Searcher\Eloquent\Searchable;

class  Post  extends Model implements SearchableContract
{
    use  Searchable;

    protected  $searchable  = ['author', 'created_at'];
}

'author:kayson title:hello'  // 等同于:
$query->where('author', '=', 'kayson');

配置布尔和日期列

Laravel/Lumen

使用 casts 属性指定布尔和日期列

<?php

namnespace  App\Models;

use Illuminate\Database\Eloquent\Model;
use Illusionist\Searcher\Contracts\Searchable  as SearchableContract;
use Illusionist\Searcher\Eloquent\Searchable;

class  Post  extends Model implements SearchableContract
{
    use  Searchable;

    protected  $casts  = [
        'published'  =>  'boolean',
        'created_at'  =>  'datetime',
    ];
}

ThinkPHP

使用 type 属性指定布尔和日期列

<?php

namnespace  app\model;

use think\Model;
use Illusionist\Searcher\Contracts\Searchable  as SearchableContract;
use Illusionist\Searcher\Eloquent\Searchable;
class  Post  extends Model implements SearchableContract
{
    use  Searchable;

    protected  $type  = [
        'published'  =>  'boolean',
        'created_at'  =>  'datetime',
    ];
}

配置关键字

通过重写 getRelaSearchName 函数来实现自定义关键字以及共体列配置。

⚠️ selec, order_by, offset 是保留关键字,请不要跟查询列冲突

<?php

namnespace  App\Models;

use Illuminate\Database\Eloquent\Model;
use Illusionist\Searcher\Contracts\Searchable  as SearchableContract;
use Illusionist\Searcher\Eloquent\Searchable;

class  Post  extends Model implements SearchableContract
{
    use  Searchable;

    /**
    * Get the real name of the given search column.
    *
    * @param  string $key
    * @return  string|array
    */ 
    public  function  getRelaSearchName($key)
    {
        switch ($key) {
            case  'field':
                return  'select';
            case  'sort':
                return  'order_by';
            case  'from':
                return  'offset';
            case  'stars':
                return ['stars', 'comments.stars'];
            default:
                return  $key;
        }
    }
}

'field:id,name'  // 等同于:
$query->select(['id', 'name']);

'stars:3000'  // 等同于:
$query->where(function ($query) {
    $query->where('stars', '>=', '3000', 'or')
        ->whereHas('comments', function ($query) {
                $query->where('stars', '>=', '3000')
    });
});