Files
usedcar/app/mzl/common/utils/SimpleQueryBuilder.php
T
2025-05-14 13:19:23 +08:00

584 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\mzl\common\utils;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
/**
* 通用查询构建器 SimpleQueryBuilder
*/
class SimpleQueryBuilder
{
protected ?Model $model = null;
protected ?Builder $query = null;
protected array $filters = [];
protected array $sorts = [];
protected array $fields = [];
protected array $relations = [];
protected array $hasRelations = [];
protected array $pagination = [
'page' => 1,
'per_page' => 10,
];
/** @var string[] 要执行 withCount 的关联 */
protected array $counts = [];
public array $allowedFields = [];
protected ?\Closure $customQueryCallback = null;
public function __construct(?Model $model = null)
{
if ($model) {
$this->setModel($model);
}
}
/**
* 设置模型
*/
public function setModel(Model $model): self
{
$this->model = $model;
$this->query = $model->newQuery();
return $this;
}
/**
* 批量应用查询配置
*
* @param array $config
* @return $this
*/
public function applyQueryConfig(array $config): self
{
foreach ($config['filters'] ?? [] as $filter) {
$this->addFilter($filter['field'], $filter['operator'], $filter['value']);
}
foreach ($config['sorts'] ?? [] as $sort) {
$this->addSort($sort['field'], $sort['direction'] ?? 'asc');
}
if (!empty($config['fields'])) {
$this->selectFields($config['fields']);
}
foreach ($config['relations'] ?? [] as $relation => $fields) {
$this->withRelation($relation, $fields);
}
foreach ($config['hasRelations'] ?? [] as $relation => $callback) {
if ($callback instanceof \Closure) {
$this->addHasRelation($relation, $callback);
}
}
// 新增:处理 counts
foreach ($config['counts'] ?? [] as $relation) {
$this->addCount($relation);
}
if (!empty($config['pagination'])) {
$this->setPagination(
$config['pagination']['page'] ?? 1,
$config['pagination']['per_page'] ?? 15
);
}
if (!empty($config['custom']) && $config['custom'] instanceof \Closure) {
$this->withCustomQuery($config['custom']);
}
return $this;
}
/**
* 添加单个过滤条件
*/
public function addFilter(string $field, string $operator, mixed $value): self
{
$this->filters[] = compact('field', 'operator', 'value');
return $this;
}
/**
* 添加排序字段
*/
public function addSort(string $field, string $direction = 'asc'): self
{
$this->sorts[$field] = strtolower($direction) === 'desc' ? 'desc' : 'asc';
return $this;
}
/**
* 设置查询字段(select),可配合 allowedFields 校验
*/
public function selectFields(array $fields): self
{
$this->fields = array_unique($fields);
return $this;
}
/**
* 加载关联(支持指定字段)
*/
public function withRelation2(string $relation, array $fields = []): self
{
$this->relations[$relation] = !empty($fields) ? $fields : '*';
return $this;
}
/**
* 加载关联(支持指定字段 或 自定义闭包)
*
* @param string $relation 关联名,支持点式多级
* @param array|\Closure $fields 字段数组 或 查询闭包
*/
public function withRelation(string $relation, $fields = []): self
{
if ($fields instanceof \Closure) {
// 自定义关联查询闭包
$this->relations[$relation] = $fields;
} else {
// 字段选择,空数组表示全部
$this->relations[$relation] = !empty($fields) ? $fields : '*';
}
return $this;
}
/**
* 添加 whereHas 条件
*/
public function addHasRelation(string $relation, \Closure $callback): self
{
$this->hasRelations[] = compact('relation', 'callback');
return $this;
}
/**
* 设置分页
*/
public function setPagination(int $page, int $perPage): self
{
$this->pagination = [
'page' => max(1, $page),
'per_page' => min(max(1, $perPage), 100),
];
return $this;
}
/**
* 注入复杂查询闭包(适合业务层扩展)
*/
public function withCustomQuery(\Closure $callback): self
{
$this->customQueryCallback = $callback;
return $this;
}
/**
* 添加关联数量统计 (withCount)
*
* @param string $relation 关联名,支持点式多层,如 "orders.items"
* @return $this
*/
public function addCount(string $relation): self
{
$this->counts[] = $relation;
return $this;
}
/**
* 构建完整查询(不执行)
*/
public function build(): Builder
{
if (!$this->query && $this->model) {
$this->query = $this->model->newQuery();
}
$this->applyFilters()
->applySorts()
->applyFields()
->applyRelations()
->applyWhereHas();
// ========== 新增:关联计数 ==========
if (!empty($this->counts)) {
// Laravel 支持 withCount(['rel1','rel2'])
$this->query->withCount($this->counts);
}
if ($this->customQueryCallback) {
call_user_func($this->customQueryCallback, $this->query);
}
return $this->query;
}
/**
* 获取集合(不分页)
*/
public function get(): Collection
{
return $this->build()->get();
}
/**
* 获取分页结果
*/
public function paginate(): LengthAwarePaginator
{
return $this->build()->paginate(
$this->pagination['per_page'],
['*'],
'page',
$this->pagination['page']
);
}
/**
* 应用过滤
*/
protected function applyFilters(): self
{
foreach ($this->filters as $filter) {
$field = $filter['field'];
$operator = $filter['operator'];
$value = $filter['value'];
if (str_contains($field, '.')) {
$segments = explode('.', $field);
$relationPath = implode('.', array_slice($segments, 0, -1));
$lastField = end($segments);
$this->query->whereHas($relationPath, function (Builder $q) use ($lastField, $operator, $value) {
$this->applyCondition($q, $lastField, $operator, $value);
});
} else {
$this->applyCondition($this->query, $field, $operator, $value);
}
}
return $this;
}
/**
* 应用排序
*/
protected function applySorts(): self
{
foreach ($this->sorts as $field => $direction) {
$this->query->orderBy($field, $direction);
}
return $this;
}
/**
* 应用字段选择
*/
// protected function applyFields2(): self
// {
// if (!empty($this->fields)) {
// // 排除关联字段(如 class.teacher.realname)
// $fields = array_filter($this->fields, fn($field) => !str_contains($field, '.'));
// // 白名单过滤
// $fields = !empty($this->allowedFields)
// ? array_intersect($fields, $this->allowedFields)
// : $fields;
// if (empty($fields)) {
// throw new \InvalidArgumentException('字段选择无效');
// }
// $this->query->select(array_unique($fields));
// }
// return $this;
// }
/**
* 应用字段选择,支持普通列和带 alias 的 Raw 表达式
*/
protected function applyFields(): self
{
if (empty($this->fields)) {
return $this;
}
// 只保留主表字段(排除带点的关联字段)
$cols = array_filter($this->fields, fn($f) => ! str_contains($f, '.'));
// 如果设置了 allowedFields,做一次白名单交集
if (! empty($this->allowedFields)) {
$cols = array_intersect($cols, $this->allowedFields);
}
if (empty($cols)) {
throw new \InvalidArgumentException('字段选择无效');
}
$plain = [];
$raws = [];
foreach ($cols as $col) {
// 简单判断带 “ as ” 的就归到 Raw
if (preg_match('/\s+as\s+/i', $col)) {
$raws[] = $col;
} else {
$plain[] = $col;
}
}
// 先 select 普通列
if ($plain) {
$this->query->select($plain);
}
// 再 selectRaw alias
if ($raws) {
// 注意:selectRaw 接受一整个字符串
$this->query->selectRaw(implode(', ', $raws));
}
return $this;
}
/**
* 应用关联加载,支持 dot-notation 多级关联
*/
// protected function applyRelations2(): self
// {
// if (empty($this->relations)) {
// return $this;
// }
// $with = [];
// foreach ($this->relations as $path => $fields) {
// $with[$path] = function ($q) use ($fields) {
// if ($fields === '*' || empty($fields)) {
// return;
// }
// // 保证 id 在内
// $model = $q->getModel();
// $key = $model->getKeyName();
// $q->select(array_merge([$key], $fields));
// };
// }
// $this->query->with($with);
// return $this;
// }
/**
* 应用多级关联加载,支持给关联字段加 alias
*/
protected function applyRelations2(): self
{
if (empty($this->relations)) {
return $this;
}
$with = [];
foreach ($this->relations as $path => $fields) {
$with[$path] = function ($q) use ($fields) {
if ($fields === '*' || empty($fields)) {
return;
}
// 模型主键一定要带上
$model = $q->getModel();
$key = $model->getKeyName();
$plain = [];
$raws = [];
foreach ($fields as $col) {
if (preg_match('/\s+as\s+/i', $col)) {
$raws[] = $col;
} else {
$plain[] = $col;
}
}
// 确保主键至少出现一次
if (! in_array($key, $plain, true)) {
array_unshift($plain, $key);
}
// select 普通列
if ($plain) {
$q->select($plain);
}
// selectRaw 带 alias 的列
if ($raws) {
$q->selectRaw(implode(', ', $raws));
}
};
}
$this->query->with($with);
return $this;
}
/**
* 应用多级关联加载,支持字段数组或直接传入闭包
*/
protected function applyRelations(): self
{
if (empty($this->relations)) {
return $this;
}
$with = [];
foreach ($this->relations as $path => $handler) {
if ($handler instanceof \Closure) {
// 直接使用用户传入的闭包
$with[$path] = $handler;
} else {
// 按字段数组构造查询
$fields = $handler;
$with[$path] = function ($q) use ($fields) {
if ($fields === '*' || empty($fields)) {
return;
}
// 确保主键被选中
$model = $q->getModel();
$key = $model->getKeyName();
$plain = [];
$raws = [];
foreach ($fields as $col) {
if (preg_match('/\s+as\s+/i', $col)) {
$raws[] = $col;
} else {
$plain[] = $col;
}
}
if (! in_array($key, $plain, true)) {
array_unshift($plain, $key);
}
if ($plain) {
$q->select($plain);
}
if ($raws) {
$q->selectRaw(implode(', ', $raws));
}
};
}
}
$this->query->with($with);
return $this;
}
/**
* 应用 whereHas 关联过滤
*/
protected function applyWhereHas(): self
{
foreach ($this->hasRelations as $relationData) {
$this->query->whereHas($relationData['relation'], $relationData['callback']);
}
return $this;
}
/**
* 应用单个过滤条件
*/
protected function applyCondition(Builder $query, string $field, string $operator, mixed $value): void
{
switch (strtolower($operator)) {
case 'between':
$query->whereBetween($field, (array)$value);
break;
case 'like':
$query->where($field, 'LIKE', "%{$value}%");
break;
case 'in':
$query->whereIn($field, (array)$value);
break;
case 'not_in':
$query->whereNotIn($field, (array)$value);
break;
case 'null':
$query->whereNull($field);
break;
case 'not_null':
$query->whereNotNull($field);
break;
default:
$query->where($field, $operator, $value);
}
}
/**
* 根据 queryConfig 构造 fieldsMap
* - 兼容 "col as alias"
* - 自动把 counts 生成的 xxx_count 加入根字段
*
* @param array $config 完整的 applyQueryConfig 输入
* @return array [''=>[主表字段…], 'relation'=>[字段…], …]
*/
public static function buildFieldsMap(array $config): array
{
$relations = $config['relations'] ?? [];
$counts = $config['counts'] ?? [];
$rawFields = $config['fields'] ?? [];
// 1) 根层字段:拆 alias + 自动加 count + 加顶级关联名
$root = [];
foreach ($rawFields as $f) {
if (preg_match('/\s+as\s+/i', $f)) {
[, $alias] = preg_split('/\s+as\s+/i', $f);
$root[] = $alias;
} else {
$root[] = $f;
}
}
// 把 withCount 生成的字段也加进来
foreach ($counts as $rel) {
$root[] = "{$rel}_count";
}
// 顶级关联名
foreach (array_keys($relations) as $path) {
$root[] = explode('.', $path, 2)[0];
}
$fieldsMap = ['' => array_values(array_unique($root))];
// 2) 每个关联:拆 alias + 加直接子关联
foreach ($relations as $path => $rawCols) {
$arr = [];
$segments = explode('.', $path);
$depth = count($segments);
// 拆 alias
foreach ($rawCols as $col) {
if (preg_match('/\s+as\s+/i', $col)) {
[, $alias] = preg_split('/\s+as\s+/i', $col);
$arr[] = $alias;
} else {
$arr[] = $col;
}
}
// 子关联名
if ($depth > 1) {
for ($i = 1; $i < $depth; $i++) {
$parent = implode('.', array_slice($segments, 0, $i));
$child = $segments[$i];
$fieldsMap[$parent][] = $child;
}
}
$fieldsMap[$path] = array_values(array_unique($arr));
}
return $fieldsMap;
}
}