721 lines
23 KiB
PHP
721 lines
23 KiB
PHP
<?php
|
||
|
||
namespace plugin\admin\app\common;
|
||
|
||
use InvalidArgumentException;
|
||
use plugin\admin\app\model\OpmMwPermissionRule;
|
||
use support\Db;
|
||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||
|
||
/**
|
||
* 数据权限规则服务类
|
||
*
|
||
* 作用:
|
||
* 将数据库里配置的数据权限规则,动态应用到 Eloquent 查询构造器上。
|
||
*
|
||
* 规则配置示例:
|
||
* - table: opm_mw_info_data // 规则对应的主表
|
||
* - field: organ_id // 需要限制的字段
|
||
* - field_table: opm_mw_info_data // 字段属于哪个表,可选
|
||
* - admin_attr: hospitals // 当前管理员属性名
|
||
* - admin_attr_map: opm_mw_department.id...opm_mw_department.name
|
||
* - action: in
|
||
* - status: 1
|
||
* - sort: 1
|
||
*
|
||
* 说明:
|
||
* 1. 默认规则字段会拼到主表别名上;
|
||
* 2. 如果 field_table 配置了,就优先使用对应表别名;
|
||
* 3. admin_attr_map 当前按“同表映射”使用,即:
|
||
* 从 sourceField 查出 targetField 的值。
|
||
*/
|
||
class DataPermissionService
|
||
{
|
||
/**
|
||
* 当前登录管理员信息
|
||
*
|
||
* 例如:
|
||
* [
|
||
* 'id' => 1,
|
||
* 'name' => 'admin',
|
||
* 'hospitals' => '1,2,3'
|
||
* ]
|
||
*/
|
||
protected array $admin;
|
||
|
||
/**
|
||
* 构造函数
|
||
*
|
||
* @param array $admin 当前管理员信息
|
||
*/
|
||
public function __construct(array $admin)
|
||
{
|
||
$this->admin = $admin;
|
||
}
|
||
|
||
/**
|
||
* 将权限规则应用到查询构造器
|
||
*
|
||
* 逻辑:
|
||
* 1. 从查询中解析主表和别名;
|
||
* 2. 读取该表对应的权限规则;
|
||
* 3. 收集查询中所有表别名;
|
||
* 4. 逐条应用权限规则;
|
||
*
|
||
* @param QueryBuilder|EloquentBuilder $query 查询对象
|
||
* @return QueryBuilder|EloquentBuilder
|
||
*/
|
||
public function apply(QueryBuilder|EloquentBuilder $query): QueryBuilder|EloquentBuilder
|
||
{
|
||
// 统一拿 baseQuery(核心修复点)
|
||
if ($query instanceof EloquentBuilder) {
|
||
$baseQuery = $query->getQuery();
|
||
$tableRaw = $query->getModel()->getTable();
|
||
} else {
|
||
$baseQuery = $query;
|
||
$tableRaw = $query->from;
|
||
}
|
||
|
||
// 解析主表 & 别名
|
||
[$fromTable, $fromAlias] = $this->parseTableAlias((string)($tableRaw ?? ''));
|
||
|
||
if ($fromTable === '') {
|
||
return $query;
|
||
}
|
||
|
||
// 加载规则
|
||
$rules = $this->loadRules($fromTable);
|
||
|
||
if (empty($rules)) {
|
||
return $query;
|
||
}
|
||
|
||
// 获取所有表别名
|
||
$tableAliases = $this->getAllTableAliases($baseQuery);
|
||
|
||
// 应用规则
|
||
foreach ($rules as $rule) {
|
||
$query = $this->applySingleRule($query, $rule, $fromAlias, $tableAliases);
|
||
}
|
||
|
||
return $query;
|
||
}
|
||
// public function apply(EloquentBuilder $query): Builder
|
||
// {
|
||
// // 兼容传入 Builder 的情况;这里统一取底层 Query Builder
|
||
// $baseQuery = $query->getQuery();
|
||
//
|
||
// // 解析主表名和主表别名,例如:table as t => [table, t]
|
||
// [$fromTable, $fromAlias] = $this->parseTableAlias((string)($baseQuery->from ?? ''));
|
||
//
|
||
// // 如果无法识别主表,直接返回,不处理
|
||
// if ($fromTable === '') {
|
||
// return $query;
|
||
// }
|
||
//
|
||
// // 加载该主表对应的启用规则
|
||
// $rules = $this->loadRules($fromTable);
|
||
//
|
||
// // 没有规则则不做任何限制
|
||
// if (empty($rules)) {
|
||
// return $query;
|
||
// }
|
||
//
|
||
// // 获取查询里所有表的别名映射,后续用于拼字段
|
||
// $tableAliases = $this->getAllTableAliases($baseQuery);
|
||
//
|
||
// // 逐条应用规则
|
||
// foreach ($rules as $rule) {
|
||
// $query = $this->applySingleRule($query, $rule, $fromAlias, $tableAliases);
|
||
// }
|
||
//
|
||
// return $query;
|
||
// }
|
||
|
||
/**
|
||
* 加载指定表的启用权限规则
|
||
*
|
||
* 只读取:
|
||
* - table = $tableName
|
||
* - status = 1
|
||
* 并按照 sort 升序排列
|
||
*
|
||
* @param string $tableName 表名
|
||
* @return array
|
||
*/
|
||
protected function loadRules(string $tableName): array
|
||
{
|
||
$this->validateIdentifierPath($tableName, '表名');
|
||
|
||
return OpmMwPermissionRule::query()
|
||
->where('table', $tableName)
|
||
->where('status', 1)
|
||
->orderBy('sort', 'asc')
|
||
->get()
|
||
->toArray();
|
||
}
|
||
|
||
/**
|
||
* 应用单条权限规则
|
||
*
|
||
* 核心流程:
|
||
* 1. 读取 rule 配置;
|
||
* 2. 取出管理员对应属性;
|
||
* 3. 如为 * 则表示拥有全部权限,直接跳过;
|
||
* 4. 根据 admin_attr_map 解析映射值;
|
||
* 5. 得到完整字段名;
|
||
* 6. 根据 action 生成 where 条件。
|
||
*
|
||
* @param QueryBuilder|EloquentBuilder $query 查询对象
|
||
* @param array $rule 单条规则
|
||
* @param string $mainAlias 主表别名
|
||
* @param array $tableAliases 查询内所有表别名映射
|
||
* @return QueryBuilder|EloquentBuilder
|
||
*/
|
||
protected function applySingleRule(
|
||
QueryBuilder|EloquentBuilder $query,
|
||
array $rule,
|
||
string $mainAlias,
|
||
array $tableAliases
|
||
): QueryBuilder|EloquentBuilder
|
||
{
|
||
$field = trim((string)($rule['field'] ?? ''));
|
||
$adminAttr = trim((string)($rule['admin_attr'] ?? ''));
|
||
$adminAttrMap = trim((string)($rule['admin_attr_map'] ?? ''));
|
||
$action = strtolower(trim((string)($rule['action'] ?? 'in')));
|
||
$fieldTable = trim((string)($rule['field_table'] ?? ''));
|
||
|
||
// 规则字段或管理员属性为空,视为无效规则,直接跳过
|
||
if ($field === '' || $adminAttr === '') {
|
||
return $query;
|
||
}
|
||
|
||
// 校验字段写法是否合法
|
||
$this->validateFieldPath($field, '规则字段');
|
||
|
||
// 如果配置了字段所属表,也要校验
|
||
if ($fieldTable !== '') {
|
||
$this->validateIdentifierPath($fieldTable, '字段所属表');
|
||
}
|
||
|
||
// 取管理员对应属性值
|
||
$rawValue = $this->admin[$adminAttr] ?? '';
|
||
|
||
// * 表示不受限制
|
||
if ($rawValue === '*') {
|
||
return $query;
|
||
}
|
||
|
||
// 解析管理员属性值,必要时通过映射转换成目标值
|
||
$resolvedValues = $this->resolveAdminAttrValue($rawValue, $adminAttrMap);
|
||
|
||
// 如果最终解析不到任何可用值,则直接返回空结果
|
||
if (empty($resolvedValues) && !in_array('0', $resolvedValues, true)) {
|
||
return $query->whereRaw('1 = 0');
|
||
}
|
||
|
||
// 获取规则字段的完整字段名,如 dept.id / t.organ_id
|
||
$fullField = $this->getFullFieldName($field, $mainAlias, $tableAliases, $fieldTable);
|
||
|
||
// in / not in 这类操作符需要数组值
|
||
if ($this->isArrayOperator($action)) {
|
||
return $this->buildCondition($query, $fullField, $action, $resolvedValues);
|
||
}
|
||
|
||
// 单值操作符只能对应一个结果,否则说明配置有问题
|
||
if (count($resolvedValues) !== 1) {
|
||
throw new InvalidArgumentException(
|
||
"规则配置错误:操作符 {$action} 只能对应单个值,但当前解析出 " . count($resolvedValues) . " 个值"
|
||
);
|
||
}
|
||
|
||
return $this->buildCondition($query, $fullField, $action, $resolvedValues[0]);
|
||
}
|
||
|
||
// protected function applySingleRule(
|
||
// Builder $query,
|
||
// array $rule,
|
||
// string $mainAlias,
|
||
// array $tableAliases
|
||
// ): Builder
|
||
// {
|
||
// $field = trim((string)($rule['field'] ?? ''));
|
||
// $adminAttr = trim((string)($rule['admin_attr'] ?? ''));
|
||
// $adminAttrMap = trim((string)($rule['admin_attr_map'] ?? ''));
|
||
// $action = strtolower(trim((string)($rule['action'] ?? 'in')));
|
||
// $fieldTable = trim((string)($rule['field_table'] ?? ''));
|
||
//
|
||
// // 规则字段或管理员属性为空,视为无效规则,直接跳过
|
||
// if ($field === '' || $adminAttr === '') {
|
||
// return $query;
|
||
// }
|
||
//
|
||
// // 校验字段写法是否合法
|
||
// $this->validateFieldPath($field, '规则字段');
|
||
//
|
||
// // 如果配置了字段所属表,也要校验
|
||
// if ($fieldTable !== '') {
|
||
// $this->validateIdentifierPath($fieldTable, '字段所属表');
|
||
// }
|
||
//
|
||
// // 取管理员对应属性值
|
||
// $rawValue = $this->admin[$adminAttr] ?? '';
|
||
//
|
||
// // * 表示不受限制
|
||
// if ($rawValue === '*') {
|
||
// return $query;
|
||
// }
|
||
//
|
||
// // 解析管理员属性值,必要时通过映射转换成目标值
|
||
// $resolvedValues = $this->resolveAdminAttrValue($rawValue, $adminAttrMap);
|
||
//
|
||
// // 如果最终解析不到任何可用值,则直接返回空结果
|
||
// if (empty($resolvedValues) && !in_array('0', $resolvedValues, true)) {
|
||
// return $query->whereRaw('1 = 0');
|
||
// }
|
||
//
|
||
// // 获取规则字段的完整字段名,如 dept.id / t.organ_id
|
||
// $fullField = $this->getFullFieldName($field, $mainAlias, $tableAliases, $fieldTable);
|
||
//
|
||
// // in / not in 这类操作符需要数组值
|
||
// if ($this->isArrayOperator($action)) {
|
||
// return $this->buildCondition($query, $fullField, $action, $resolvedValues);
|
||
// }
|
||
//
|
||
// // 单值操作符只能对应一个结果,否则说明配置有问题
|
||
// if (count($resolvedValues) !== 1) {
|
||
// throw new InvalidArgumentException(
|
||
// "规则配置错误:操作符 {$action} 只能对应单个值,但当前解析出 " . count($resolvedValues) . " 个值"
|
||
// );
|
||
// }
|
||
//
|
||
// return $this->buildCondition($query, $fullField, $action, $resolvedValues[0]);
|
||
// }
|
||
|
||
/**
|
||
* 解析管理员属性值
|
||
*
|
||
* 支持:
|
||
* - 字符串:1,2,3 / 1,2,3 / 1 2 3
|
||
* - 数组:['1', '2']
|
||
*
|
||
* admin_attr_map 格式:
|
||
* - source_table.source_field...target_table.target_field
|
||
*
|
||
* 示例:
|
||
* - opm_mw_department.id...opm_mw_department.name
|
||
*
|
||
* 说明:
|
||
* - 当前实现按照“同表映射”使用 sourceField -> targetField
|
||
* - sourceTable 主要用于配置语义和校验
|
||
*
|
||
* @param mixed $rawValue 管理员原始属性值
|
||
* @param string $mapStr 映射配置字符串
|
||
* @return array
|
||
*/
|
||
protected function resolveAdminAttrValue($rawValue, string $mapStr): array
|
||
{
|
||
// 先把原始值拆成列表
|
||
$values = $this->parseListValues($rawValue);
|
||
|
||
// 没有任何值,直接返回空数组
|
||
if (empty($values)) {
|
||
return [];
|
||
}
|
||
|
||
// 没有映射配置,则直接返回原始值列表
|
||
if ($mapStr === '') {
|
||
return $values;
|
||
}
|
||
|
||
// 映射格式必须包含 ...
|
||
if (!str_contains($mapStr, '...')) {
|
||
throw new InvalidArgumentException("无效的 admin_attr_map 格式:{$mapStr}");
|
||
}
|
||
|
||
[$sourcePart, $targetPart] = explode('...', $mapStr, 2);
|
||
|
||
$sourcePart = trim($sourcePart);
|
||
$targetPart = trim($targetPart);
|
||
|
||
if ($sourcePart === '' || $targetPart === '') {
|
||
throw new InvalidArgumentException("无效的 admin_attr_map 格式:{$mapStr}");
|
||
}
|
||
|
||
if (!str_contains($sourcePart, '.') || !str_contains($targetPart, '.')) {
|
||
throw new InvalidArgumentException("admin_attr_map 必须包含表名与字段名:{$mapStr}");
|
||
}
|
||
|
||
[$sourceTable, $sourceField] = explode('.', $sourcePart, 2);
|
||
[$targetTable, $targetField] = explode('.', $targetPart, 2);
|
||
|
||
$sourceTable = trim($sourceTable);
|
||
$sourceField = trim($sourceField);
|
||
$targetTable = trim($targetTable);
|
||
$targetField = trim($targetField);
|
||
|
||
// 校验表名和字段名合法性,防止非法 SQL 标识符
|
||
$this->validateIdentifierPath($sourceTable, '映射源表');
|
||
$this->validateIdentifierPath($sourceField, '映射源字段');
|
||
$this->validateIdentifierPath($targetTable, '映射目标表');
|
||
$this->validateIdentifierPath($targetField, '映射目标字段');
|
||
|
||
if ($sourceTable !== $targetTable) {
|
||
// 当前版本先按同表映射处理,避免复杂 join 映射带来歧义
|
||
// 如果后面要扩展跨表映射,可以在这里补 join / 子查询 方案
|
||
// admin_attr_map 本身是将 admin.sourceField 进行了一个跨表
|
||
// 如果 admin_attr_map 内部再次跨表逻辑会更加混乱
|
||
}
|
||
|
||
// 通过 sourceField 找到对应 targetField 的值
|
||
$results = Db::table($targetTable)
|
||
->whereIn($sourceField, $values)
|
||
->pluck($targetField)
|
||
->toArray();
|
||
|
||
// 统一转成字符串,去除空值
|
||
$results = array_map(static fn($v) => trim((string)$v), $results);
|
||
$results = array_filter($results, static fn($v) => $v !== '');
|
||
|
||
// 去重并重新排序
|
||
return array_values(array_unique($results));
|
||
}
|
||
|
||
/**
|
||
* 构建 WHERE 条件
|
||
*
|
||
* 支持两类:
|
||
* 1. 数组型条件:in / not in
|
||
* 2. 单值条件:= / > / < / like / is null 等
|
||
*
|
||
* @param QueryBuilder|EloquentBuilder $query 查询对象
|
||
* @param string $field 完整字段名
|
||
* @param string $action 操作符
|
||
* @param mixed $value 条件值
|
||
* @return QueryBuilder|EloquentBuilder
|
||
*/
|
||
protected function buildCondition(QueryBuilder|EloquentBuilder $query, string $field, string $action, $value): QueryBuilder|EloquentBuilder
|
||
{
|
||
// 数组条件
|
||
if (is_array($value)) {
|
||
return match ($action) {
|
||
'in' => $query->whereIn($field, $value),
|
||
'not in', '<>in' => $query->whereNotIn($field, $value),
|
||
default => throw new InvalidArgumentException("不支持的数组操作符:{$action}"),
|
||
};
|
||
}
|
||
|
||
// 单值条件
|
||
return match ($action) {
|
||
'=', 'eq' => $query->where($field, '=', $value),
|
||
'>', 'gt' => $query->where($field, '>', $value),
|
||
'<', 'lt' => $query->where($field, '<', $value),
|
||
'>=', 'gte' => $query->where($field, '>=', $value),
|
||
'<=', 'lte' => $query->where($field, '<=', $value),
|
||
'<>', '!=', 'ne' => $query->where($field, '<>', $value),
|
||
'like' => $query->where($field, 'like', $this->normalizeLikeValue((string)$value)),
|
||
'not like' => $query->where($field, 'not like', $this->normalizeLikeValue((string)$value)),
|
||
'is null' => $query->whereNull($field),
|
||
'is not null' => $query->whereNotNull($field),
|
||
default => throw new InvalidArgumentException("不支持的操作符:{$action}"),
|
||
};
|
||
}
|
||
|
||
// protected function buildCondition(Builder $query, string $field, string $action, $value): Builder
|
||
// {
|
||
// // 数组条件
|
||
// if (is_array($value)) {
|
||
// return match ($action) {
|
||
// 'in' => $query->whereIn($field, $value),
|
||
// 'not in', '<>in' => $query->whereNotIn($field, $value),
|
||
// default => throw new InvalidArgumentException("不支持的数组操作符:{$action}"),
|
||
// };
|
||
// }
|
||
//
|
||
// // 单值条件
|
||
// return match ($action) {
|
||
// '=', 'eq' => $query->where($field, '=', $value),
|
||
// '>', 'gt' => $query->where($field, '>', $value),
|
||
// '<', 'lt' => $query->where($field, '<', $value),
|
||
// '>=', 'gte' => $query->where($field, '>=', $value),
|
||
// '<=', 'lte' => $query->where($field, '<=', $value),
|
||
// '<>', '!=', 'ne' => $query->where($field, '<>', $value),
|
||
// 'like' => $query->where($field, 'like', $this->normalizeLikeValue((string)$value)),
|
||
// 'not like' => $query->where($field, 'not like', $this->normalizeLikeValue((string)$value)),
|
||
// 'is null' => $query->whereNull($field),
|
||
// 'is not null' => $query->whereNotNull($field),
|
||
// default => throw new InvalidArgumentException("不支持的操作符:{$action}"),
|
||
// };
|
||
// }
|
||
|
||
/**
|
||
* 获取完整字段名
|
||
*
|
||
* 优先级:
|
||
* 1. 字段已带表名:直接返回
|
||
* 2. 配置了 field_table:使用对应表别名
|
||
* 3. 默认使用主表别名
|
||
*
|
||
* @param string $field 字段名
|
||
* @param string $mainAlias 主表别名
|
||
* @param array $tableAliases 查询中的表别名映射
|
||
* @param string $fieldTable 字段所属表
|
||
* @return string
|
||
*/
|
||
protected function getFullFieldName(string $field, string $mainAlias, array $tableAliases, string $fieldTable = ''): string
|
||
{
|
||
// 如果 field 已经是 table.field 格式,直接返回
|
||
if (str_contains($field, '.')) {
|
||
$this->validateFieldPath($field, '规则字段');
|
||
return $field;
|
||
}
|
||
|
||
// 默认使用主表别名
|
||
$alias = $mainAlias;
|
||
|
||
// 如果规则里指定了字段所属表,则按该表找别名
|
||
if ($fieldTable !== '') {
|
||
$alias = $this->resolveTableAlias($fieldTable, $tableAliases, $mainAlias);
|
||
}
|
||
|
||
if ($alias === '') {
|
||
$alias = $mainAlias;
|
||
}
|
||
|
||
// 校验别名是否合法
|
||
$this->validateIdentifierPath($alias, '表别名');
|
||
|
||
return "{$alias}.{$field}";
|
||
}
|
||
|
||
/**
|
||
* 获取查询中所有表的别名映射
|
||
*
|
||
* 返回格式:
|
||
* [
|
||
* 'opm_mw_info_data' => 'data',
|
||
* 'data' => 'data',
|
||
* 'opm_mw_department' => 'dept',
|
||
* 'dept' => 'dept',
|
||
* ]
|
||
*
|
||
* @param mixed $baseQuery 基础查询对象
|
||
* @return array
|
||
*/
|
||
protected function getAllTableAliases($baseQuery): array
|
||
{
|
||
$aliases = [];
|
||
|
||
// 主表别名
|
||
[$fromTable, $fromAlias] = $this->parseTableAlias((string)($baseQuery->from ?? ''));
|
||
if ($fromTable !== '') {
|
||
$aliases[$fromTable] = $fromAlias;
|
||
$aliases[$fromAlias] = $fromAlias;
|
||
}
|
||
|
||
// 关联表别名
|
||
foreach (($baseQuery->joins ?? []) as $join) {
|
||
[$joinTable, $joinAlias] = $this->parseTableAlias((string)($join->table ?? ''));
|
||
if ($joinTable !== '') {
|
||
$aliases[$joinTable] = $joinAlias;
|
||
$aliases[$joinAlias] = $joinAlias;
|
||
}
|
||
}
|
||
|
||
return $aliases;
|
||
}
|
||
|
||
/**
|
||
* 解析表名和别名
|
||
*
|
||
* 支持:
|
||
* - table
|
||
* - table as t
|
||
* - table t
|
||
*
|
||
* @param string $table 表表达式
|
||
* @return array [tableName, alias]
|
||
*/
|
||
protected function parseTableAlias(string $table): array
|
||
{
|
||
// 去掉反引号并去首尾空格
|
||
$table = trim(str_replace('`', '', $table));
|
||
|
||
if ($table === '') {
|
||
return ['', ''];
|
||
}
|
||
|
||
// 子查询或复杂表达式不处理
|
||
if (str_starts_with($table, '(')) {
|
||
return ['', ''];
|
||
}
|
||
|
||
// 处理 as 写法
|
||
if (stripos($table, ' as ') !== false) {
|
||
[$name, $alias] = preg_split('/\s+as\s+/i', $table, 2);
|
||
$name = trim((string)$name);
|
||
$alias = trim((string)$alias);
|
||
|
||
return [$name, $alias !== '' ? $alias : $name];
|
||
}
|
||
|
||
// 处理 table alias 写法
|
||
$parts = preg_split('/\s+/', $table) ?: [];
|
||
if (count($parts) >= 2) {
|
||
return [trim($parts[0]), trim($parts[1])];
|
||
}
|
||
|
||
// 没有别名时,默认表名即别名
|
||
return [$table, $table];
|
||
}
|
||
|
||
/**
|
||
* 解析列表值
|
||
*
|
||
* 支持:
|
||
* - 数组
|
||
* - 字符串:1,2,3 / 1,2,3 / 1 2 3
|
||
*
|
||
* 返回值统一为字符串数组,避免把非数字内容强制转 int 后出错
|
||
*
|
||
* @param mixed $raw 原始值
|
||
* @return array
|
||
*/
|
||
protected function parseListValues($raw): array
|
||
{
|
||
if (is_array($raw)) {
|
||
$values = [];
|
||
array_walk_recursive($raw, static function ($item) use (&$values) {
|
||
$values[] = trim((string)$item);
|
||
});
|
||
} else {
|
||
$raw = trim((string)$raw);
|
||
|
||
// 空值或 * 视为无值
|
||
if ($raw === '' || $raw === '*') {
|
||
return [];
|
||
}
|
||
|
||
// 支持英文逗号、中文逗号、空格分隔
|
||
$values = preg_split('/[,\s,]+/u', $raw) ?: [];
|
||
$values = array_map(static fn($v) => trim((string)$v), $values);
|
||
}
|
||
|
||
// 清理空项
|
||
$values = array_filter($values, static fn($v) => $v !== '');
|
||
|
||
// 去重并重建索引
|
||
return array_values(array_unique($values));
|
||
}
|
||
|
||
/**
|
||
* 判断是否为数组型操作符
|
||
*
|
||
* @param string $action 操作符
|
||
* @return bool
|
||
*/
|
||
protected function isArrayOperator(string $action): bool
|
||
{
|
||
return in_array($action, ['in', 'not in', '<>in'], true);
|
||
}
|
||
|
||
/**
|
||
* like 值自动补 %
|
||
*
|
||
* 规则:
|
||
* - 如果用户已经写了 %,则保持原样
|
||
* - 如果没有写 %,则默认转为 %xxx%
|
||
*
|
||
* @param string $value 原始值
|
||
* @return string
|
||
*/
|
||
protected function normalizeLikeValue(string $value): string
|
||
{
|
||
if ($value === '') {
|
||
return $value;
|
||
}
|
||
|
||
if (!str_contains($value, '%')) {
|
||
return '%' . $value . '%';
|
||
}
|
||
|
||
return $value;
|
||
}
|
||
|
||
/**
|
||
* 解析表名或别名到实际别名
|
||
*
|
||
* @param string $tableOrAlias 表名或别名
|
||
* @param array $tableAliases 别名映射
|
||
* @param string $mainAlias 主表别名
|
||
* @return string
|
||
*/
|
||
protected function resolveTableAlias(string $tableOrAlias, array $tableAliases, string $mainAlias): string
|
||
{
|
||
$tableOrAlias = trim($tableOrAlias);
|
||
|
||
if ($tableOrAlias === '') {
|
||
return $mainAlias;
|
||
}
|
||
|
||
if (isset($tableAliases[$tableOrAlias])) {
|
||
return $tableAliases[$tableOrAlias];
|
||
}
|
||
|
||
return $tableOrAlias;
|
||
}
|
||
|
||
/**
|
||
* 校验字段路径
|
||
*
|
||
* 支持:
|
||
* - field
|
||
* - table.field
|
||
*
|
||
* @param string $field 字段路径
|
||
* @param string $label 错误提示标签
|
||
* @return void
|
||
*/
|
||
protected function validateFieldPath(string $field, string $label = '字段'): void
|
||
{
|
||
$field = trim($field);
|
||
|
||
if ($field === '') {
|
||
throw new InvalidArgumentException("{$label}不能为空");
|
||
}
|
||
|
||
$parts = explode('.', $field);
|
||
foreach ($parts as $part) {
|
||
$this->validateIdentifierPath($part, $label);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 校验单个标识符
|
||
*
|
||
* 仅允许:
|
||
* - 字母
|
||
* - 数字
|
||
* - 下划线
|
||
*
|
||
* 且不能以数字开头。
|
||
*
|
||
* @param string $value 标识符
|
||
* @param string $label 错误提示标签
|
||
* @return void
|
||
*/
|
||
protected function validateIdentifierPath(string $value, string $label = '标识符'): void
|
||
{
|
||
$value = trim($value);
|
||
|
||
if ($value === '') {
|
||
throw new InvalidArgumentException("{$label}不能为空");
|
||
}
|
||
|
||
// 仅允许字母、数字、下划线,且首字符不能为数字
|
||
if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $value)) {
|
||
throw new InvalidArgumentException("{$label}不合法:{$value}");
|
||
}
|
||
}
|
||
} |