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; } }