lorisleiva / laravel-search-string
使用简单可定制的语法,根据一个唯一的字符串生成数据库查询。
Requires
- php: ^8.1
- hoa/compiler: ^3.17
- illuminate/support: ^9.0|^10.0
- sanmai/hoa-protocol: ^1.17
Requires (Dev)
- orchestra/testbench: ^7.0|^8.0
- phpunit/phpunit: ^9.0
README
使用简单可定制的语法,根据一个唯一的字符串生成数据库查询。
简介
Laravel Search String 提供了一种简单的方法,使用人类可读和可定制的语法来范围数据库查询。它将简单的字符串转换成强大的查询构建器。
例如,以下搜索字符串将检索最新未发布的博客文章或标题为 "我的博客文章" 的文章。
Article::usingSearchString('title:"My blog article" or not published sort:-created_at'); // Equivalent to: Article::where('title', 'My blog article') ->orWhere('published', false) ->orderBy('created_at', 'desc');
下一个示例将在 customer
和 description
列上搜索 "John",同时确保发票已支付或存档。
Invoice::usingSearchString('John and status in (Paid,Archived) limit:10 from:10'); // Equivalent to: Invoice::where(function ($query) { $query->where('customer', 'like', '%John%') ->orWhere('description', 'like', '%John%'); }) ->whereIn('status', ['Paid', 'Archived']) ->limit(10) ->offset(10);
您还可以查询相关记录的存在,例如,2020年发布的文章,这些文章有超过100条评论,这些评论不是垃圾邮件或由John编写的。
Article::usingSearchString('published = 2020 and comments: (not spam or author.name = John) > 100'); // Equivalent to: Article::where('published_at', '>=', '2020-01-01 00:00:00') ->where('published_at', '<=', '2020-12-31 23:59:59') ->whereHas('comments', function ($query) { $query->where('spam', false) ->orWhereHas('author' function ($query) { $query->where('name', 'John'); }); }, '>', 100);
如您所见,它不仅提供了一种方便的方式与您的 Laravel API 通信(而不是允许成百上千的查询字段),还可以将其展示给您的用户作为探索其数据的一种工具。
安装
# Install via composer composer require lorisleiva/laravel-search-string # (Optional) Publish the search-string.php configuration file php artisan vendor:publish --tag=search-string
基本用法
将 SearchString
特性添加到您的模型中,并配置搜索字符串中应使用的列。
use Lorisleiva\LaravelSearchString\Concerns\SearchString; class Article extends Model { use SearchString; protected $searchStringColumns = [ 'title', 'body', 'status', 'rating', 'published', 'created_at', ]; }
注意,您可以在代码的其他部分中定义这些内容,并自定义每列的行为。
这就完成了!现在您可以使用搜索字符串语法创建数据库查询。
Article::usingSearchString('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"'
比较
'title < B' 'rating > 3' 'created_at >= "2018-07-06 00:00:00"'
列表
'title in (Hello, Hi, "My super article")' 'status in(Finished,Archived)' 'status:Finished,Archived'
日期
该列必须被转换为日期或明确标记为日期在 列选项 中。
// 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
布尔值
该列必须被转换为布尔值或明确标记为布尔值在 列选项 中。
或者,如果列被标记为日期,它将自动使用 is null
和 is not null
标记为布尔值。
'published' // published = true 'created_at' // created_at is not null
否定
'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
空值
术语 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
与/或
'title:Hello body:World' // Implicit and 'title:Hello and body:World' // Explicit and 'title:Hello or body:World' // Explicit or 'A B or C D' // Equivalent to '(A and B) or (C and D)' 'A or B and C or D' // Equivalent to 'A or (B and C) or D' '(A or B) and (C or D)' // Explicit nested priority 'not (A and B)' // Equivalent to 'not A or not B' 'not (A or B)' // Equivalent to 'not A and not B'
关系
该列必须明确 定义为关系,并且与该关系关联的模型也必须使用 SearchString
特性。
在关系内部进行嵌套查询时,Laravel Search String 将使用相关模型的列定义。
在以下示例中,comments
是一个 HasMany
关系,而 author
是在 Comment
模型内部的一个嵌套的 BelongsTo
关系。
// 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 > 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))
请注意,所有这些表达式都委托给 has
查询方法。因此,它与以下关系类型无缝工作: HasOne
、HasMany
、HasOneThrough
、HasManyThrough
、BelongsTo
、BelongsToMany
、MorphOne
、MorphMany
和 MorphToMany
。
目前不支持的关系类型是 MorphTo
,因为 Laravel Search String 需要一个明确的关联模型来用于嵌套查询。
特殊关键字
请注意,这些关键字可以自定义。
'fields:title,body,created_at' // Select only title, body, created_at 'not fields:rating' // Select all columns but rating 'sort:rating,-created_at' // Order by rating asc, created_at desc 'limit:1' // Limit 1 'from:10' // Offset 10
配置列
列别名
如果您想用不同的名称查询列,您可以将其定义为键值对,其中键是数据库列名称,值是您希望使用的别名。
protected $searchStringColumns = [ 'title', 'body' => 'content', 'published_at' => 'published', 'created_at' => 'created', ];
您还可以提供一个正则表达式模式以进行更灵活的别名定义。
protected $searchStringColumns = [ 'published_at' => '/^(published|live)$/', // ... ];
列选项
您可以通过为列分配选项数组进一步配置列。
protected $searchStringColumns = [ 'created_at' => [ 'key' => 'created', // Default to column name: /^created_at$/ 'date' => true, // Default to true only if the column is cast as date. 'boolean' => true, // Default to true only if the column is cast as boolean or date. 'searchable' => false // Default to false. 'relationship' => false // Default to false. 'map' => ['x' => 'y'] // Maps data from the user input to the database values. Default to []. ], // ... ];
键
key
选项是到目前为止我们一直在配置的,即列的别名。它可以是正则表达式模式(因此允许多个匹配)或用于精确匹配的常规字符串。
日期
如果列被标记为date
,查询值将使用Carbon
进行解析,同时保持用户提供的精度级别。例如,如果created_at
列被标记为date
'created_at >= tomorrow' // Equivalent to: $query->where('created_at', '>=', 'YYYY-MM-DD 00:00:00'); // where `YYYY-MM-DD` matches the date of tomorrow. 'created_at = "July 6, 2018"' // Equivalent to: $query->where('created_at', '>=', '2018-07-06 00:00:00'); ->where('created_at', '<=', '2018-07-06 23:59:59');
默认情况下,任何被转换为日期的列(使用Laravel属性),对于LaravelSearchString都会被标记为日期。您可以通过将date
分配给false
来强制一个列不标记为日期。
布尔值
如果列被标记为boolean
,它可以不使用运算符或值进行使用。例如,如果paid
列被标记为boolean
'paid' // Equivalent to: $query->where('paid', true); 'not paid' // Equivalent to: $query->where('paid', false);
如果列被同时标记为boolean
和date
,当用作布尔值时,它将比较null
。例如,如果published_at
列被标记为boolean
和date
并使用published
别名
'published' // Equivalent to: $query->whereNotNull('published'); 'not published_at' // Equivalent to: $query->whereNull('published');
默认情况下,任何被转换为布尔值或日期的列(使用Laravel属性),都会被标记为布尔值。您可以通过将boolean
分配给false
来强制一个列不标记为布尔值。
可搜索
如果列被标记为searchable
,它将用于匹配搜索查询,即单独的术语,但不像是布尔值那样的Apple Banana
或"John Doe"
。
例如,如果列title
和description
都被标记为searchable
'Apple Banana' // Equivalent to: $query->where(function($query) { $query->where('title', 'like', '%Apple%') ->orWhere('description', 'like', '%Apple%'); }) ->where(function($query) { $query->where('title', 'like', '%Banana%') ->orWhere('description', 'like', '%Banana%'); }); '"John Doe"' // Equivalent to: $query->where(function($query) { $query->where('title', 'like', '%John Doe%') ->orWhere('description', 'like', '%John Doe%'); });
如果没有提供可搜索的列,则此类术语或字符串将被忽略。
关系
如果列被标记为relationship
,它将用于查询关系。
列名称必须匹配模型上的有效关系方法,但像往常一样,可以使用key
选项创建别名。
与该关系方法关联的模型也必须使用SearchString
特性以嵌套关系查询。
例如,假设您有一个Article模型,并且您想查询其相关的评论。那么,必须有一个有效的comments
关系方法,并且Comment
模型本身必须使用SearchString
特性。
use Lorisleiva\LaravelSearchString\Concerns\SearchString; class Article extends Model { use SearchString; protected $searchStringColumns = [ 'comments' => [ 'key' => '/^comments?$/', // aliases the column to `comments` or `comment`. 'relationship' => true, // There must be a `comments` method that defines a relationship. ], ]; public function comments() { return $this->hasMany(Comment::class); } } class Comment extends Model { use SearchString; protected $searchStringColumns = [ // ... ]; }
请注意,由于Laravel Search String只是委托给$builder->has(...)
方法,因此您可以提供任何您想要的复杂关系方法,并且约束将会保留。例如
protected $searchStringColumns = [ 'myComments' => [ 'key' => 'my_comments', 'relationship' => true, ], ]; public function myComments() { return $this->hasMany(Comment::class)->where('author_id', Auth::user()->id); }
配置特殊关键字
您可以通过在$searchStringKeywords
属性内定义键值对来自定义关键字的名称。
protected $searchStringKeywords = [ 'select' => 'fields', // Updates the selected query columns 'order_by' => 'sort', // Updates the order of the query results 'limit' => 'limit', // Limits the number of results 'offset' => 'from', // Starts the results at a further index ];
类似于列值,您可以为关键字定义一个自定义的key
数组。请注意,对于关键字,date
、boolean
、searchable
和relationship
选项不适用。
protected $searchStringKeywords = [ 'select' => [ 'key' => 'fields', ], // ... ];
其他配置位置
如我们所见,您可以通过在模型上的searchStringColumns
和searchStringKeywords
属性上配置您的列和特殊关键字。
您还可以在模型上重写getSearchStringOptions
方法,默认为
public function getSearchStringOptions() { return [ 'columns' => $this->searchStringColumns ?? [], 'keywords' => $this->searchStringKeywords ?? [], ]; }
如果您不想在模型本身上定义这些配置,可以直接在config/search-string.php
文件中定义,如下所示
// config/search-string.php return [ 'default' => [ 'keywords' => [ /* ... */ ], ], Article::class => [ 'columns' => [ /* ... */ ], 'keywords' => [ /* ... */ ], ], ];
当解决特定模型的选项时,LaravelSearchString将按照以下顺序合并这些配置
- 首先使用在模型上定义的配置
- 然后使用与模型类匹配键的配置文件
- 然后使用
default
键的配置文件 - 最后使用一些回退配置
配置不区分大小写的搜索
当使用PostgreSql等数据库时,您可以通过在选项中将case_insensitive设置为true来覆盖默认的大小写敏感搜索行为。例如,在config/search-string.php中
return [ 'default' => [ 'case_insensitive' => true, // <- Globally. // ... ], Article::class => [ 'case_insensitive' => true, // <- Only for the Article class. // ... ], ];
设置为true时,它会在使用like运算符比较之前将列和值都转换为小写。
$value = mb_strtolower($value, 'UTF8');
$query->whereRaw("LOWER($column) LIKE ?", ["%$value%"]);
错误处理
提供的搜索字符串可能因多种原因无效。
- 它不符合搜索字符串语法
- 它试图查询一个不存在的列或列别名
- 它为特殊关键词(如
limit
)提供了无效的值 - 等等。
任何这些错误都将抛出InvalidSearchStringException
。
但是,您可以选择是否希望这些异常冒泡到Laravel异常处理器,或者希望它们静默失败。为此,您需要在config/search-string.php
配置文件中选择一个失败策略
// config/search-string.php return [ 'fail' => 'all-results', // (Default) Silently fail with a query containing everything. 'fail' => 'no-results', // Silently fail with a query containing nothing. 'fail' => 'exceptions', // Throw exceptions. // ... ];