155 lines
4.5 KiB
PHP
155 lines
4.5 KiB
PHP
<?php
|
||
namespace app\common\traits;
|
||
|
||
use support\Request;
|
||
|
||
trait FormatKvTrait
|
||
{
|
||
/**
|
||
* 主入口:一行把前端扁平化参数变成完整的查询配置
|
||
*
|
||
* @param Request $request
|
||
* @param string $modelClass Eloquent 模型类名
|
||
* @return array{
|
||
* model: string,
|
||
* where: array,
|
||
* order: array<string,string>,
|
||
* fields: string[],
|
||
* include: string[]
|
||
* }
|
||
*/
|
||
protected function buildQueryConfig(Request $request, string $modelClass): array
|
||
{
|
||
$all = $request->all();
|
||
|
||
return [
|
||
'model' => $modelClass,
|
||
'where' => $this->parseFilters($all),
|
||
'order' => $this->parseSort($all['sort'] ?? ''),
|
||
'fields' => $this->parseCsv($all['fields'] ?? ''),
|
||
'include' => $this->parseCsv($all['include'] ?? ''),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 把除 sort/fields/include/page/limit 以外的所有参数
|
||
* 智能转换为 where 条件
|
||
*
|
||
* 支持:
|
||
* - 等值:foo=bar
|
||
* - NOT:foo=!bar 或 foo=!bar1,bar2
|
||
* - 范围:foo=2025-07-01,2025-07-15 → between
|
||
* - 列表:foo=a,b,c → in
|
||
* - NULL:foo=null
|
||
* - NOT NULL:foo=!null
|
||
* - 通配符:foo=xia* → like 'xia%'
|
||
*
|
||
* 关联字段直接写 relation.field=xxx
|
||
*/
|
||
protected function parseFilters(array $params): array
|
||
{
|
||
$where = [];
|
||
$reserved = ['sort','fields','include','page','limit'];
|
||
|
||
foreach ($params as $key => $val) {
|
||
if (in_array($key, $reserved, true)) {
|
||
continue;
|
||
}
|
||
// NULL / NOT NULL
|
||
if ($val === 'null') {
|
||
$where[$key] = ['is null'];
|
||
continue;
|
||
}
|
||
if ($val === '!null') {
|
||
$where[$key] = ['is not null'];
|
||
continue;
|
||
}
|
||
// NOT equal or NOT IN
|
||
if (str_starts_with($val, '!')) {
|
||
$v = substr($val, 1);
|
||
if (str_contains($v, ',')) {
|
||
$list = array_map('trim', explode(',', $v));
|
||
$where[$key] = ['not in', $list];
|
||
} else {
|
||
$where[$key] = ['<>', $this->cast($v)];
|
||
}
|
||
continue;
|
||
}
|
||
// BETWEEN or IN
|
||
if (str_contains($val, ',')) {
|
||
$parts = array_map('trim', explode(',', $val));
|
||
if (count($parts) === 2 && $this->isRangeValue($parts[0], $parts[1])) {
|
||
$where[$key] = ['between', $parts];
|
||
} else {
|
||
$where[$key] = ['in', $this->castArray($parts)];
|
||
}
|
||
continue;
|
||
}
|
||
// LIKE
|
||
if (str_contains($val, '*') || str_contains($val, '%')) {
|
||
$pattern = str_replace('*', '%', $val);
|
||
$where[$key] = ['like', $pattern];
|
||
continue;
|
||
}
|
||
// 默认 EQ
|
||
$where[$key] = $this->cast($val);
|
||
}
|
||
|
||
return $where;
|
||
}
|
||
|
||
/**
|
||
* 解析 sort=id,-created_at → ['id'=>'asc','created_at'=>'desc']
|
||
*/
|
||
protected function parseSort(string $sort): array
|
||
{
|
||
$order = [];
|
||
foreach (explode(',', $sort) as $piece) {
|
||
$f = trim($piece);
|
||
if ($f === '') {
|
||
continue;
|
||
}
|
||
if ($f[0] === '-') {
|
||
$order[substr($f, 1)] = 'desc';
|
||
} else {
|
||
$order[$f] = 'asc';
|
||
}
|
||
}
|
||
return $order;
|
||
}
|
||
|
||
/**
|
||
* 逗号分割成数组
|
||
*/
|
||
protected function parseCsv(string $csv): array
|
||
{
|
||
if ($csv === '') {
|
||
return [];
|
||
}
|
||
return array_filter(array_map('trim', explode(',', $csv)));
|
||
}
|
||
|
||
/** 简单类型转换:全数字→int/float,其余保留原字符串 */
|
||
protected function cast(string $v): mixed
|
||
{
|
||
if (is_numeric($v)) {
|
||
return strpos($v, '.') !== false ? (float)$v : (int)$v;
|
||
}
|
||
return $v;
|
||
}
|
||
|
||
/** 批量类型转换 */
|
||
protected function castArray(array $arr): array
|
||
{
|
||
return array_map(fn($v) => $this->cast($v), $arr);
|
||
}
|
||
|
||
/** 判断两端是否可做 BETWEEN(数字或 YYYY-MM-DD 日期) */
|
||
protected function isRangeValue(string $a, string $b): bool
|
||
{
|
||
return (is_numeric($a) && is_numeric($b))
|
||
|| (preg_match('/^\d{4}-\d{2}-\d{2}$/', $a)
|
||
&& preg_match('/^\d{4}-\d{2}-\d{2}$/', $b));
|
||
}
|
||
}
|