Files
usedcar/app/mzl/common/utils/SimpleQueryBuilder.php
T

584 lines
16 KiB
PHP
Raw Normal View History

2025-05-14 13:19:23 +08:00
<?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;
}
}