423 lines
14 KiB
PHP
423 lines
14 KiB
PHP
<?php
|
||
/**
|
||
* JSON字段提取器
|
||
* Extract json data by fields with type conversion.
|
||
*
|
||
* 语法规则说明:
|
||
* - 简单字段:field1,field2
|
||
* - 嵌套字段:parent:{child1,child2}
|
||
* - 数组索引:users[0]:{name,email}, users[*]:{name}
|
||
* - 字段别名:field|alias, parent|parentAlias:{child|childAlias}
|
||
* - 类型转换:field(类型)|alias,支持类型:array/int/float/bool/string
|
||
* 示例:user(array)(map转数组)、age(int)|用户年龄(字符串转整数)
|
||
*/
|
||
class jsonextractor
|
||
{
|
||
/**
|
||
* 错误信息存储
|
||
* @var string[]
|
||
*/
|
||
private $errors = array();
|
||
|
||
/**
|
||
* 从JSON中提取字段并应用类型转换
|
||
* @param string $json JSON字符串
|
||
* @param string $query 查询字符串(支持类型转换语法)
|
||
* @return string|false 提取后的JSON字符串,失败返回false
|
||
*/
|
||
public function extract($json, $query)
|
||
{
|
||
$this->errors = array();
|
||
|
||
/* 解析原始JSON */
|
||
$data = json_decode($json, true);
|
||
if(json_last_error() !== JSON_ERROR_NONE)
|
||
{
|
||
$this->errors[] = "JSON解析错误: " . json_last_error_msg();
|
||
return false;
|
||
}
|
||
|
||
/* 解析查询语句结构 */
|
||
$queryStructure = $this->parseQuery($query);
|
||
if($queryStructure === false) return false;
|
||
|
||
/* 提取字段并应用类型转换 */
|
||
$result = $this->extractFields($data, $queryStructure);
|
||
|
||
/* 返回格式化JSON */
|
||
return json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||
}
|
||
|
||
/**
|
||
* 解析查询字符串,生成查询结构数组
|
||
* @param string $query 查询字符串
|
||
* @return array|bool 查询结构数组,失败返回false
|
||
*/
|
||
private function parseQuery($query)
|
||
{
|
||
$query = trim($query);
|
||
|
||
if (empty($query)) {
|
||
$this->errors[] = "empty";
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
return $this->parseExpression($query, 0)[0];
|
||
}
|
||
catch(Exception $e)
|
||
{
|
||
$this->errors[] = "error: " . $e->getMessage();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 递归解析表达式(处理嵌套结构)
|
||
* @param string $query 查询字符串
|
||
* @param int $start 起始解析位置
|
||
* @return array [查询结构数组, 下一个解析位置]
|
||
* @throws Exception 语法错误时抛出异常
|
||
*/
|
||
private function parseExpression($query, $start)
|
||
{
|
||
$result = [];
|
||
$i = $start;
|
||
$len = strlen($query);
|
||
|
||
while($i < $len)
|
||
{
|
||
/* 跳过空白字符 */
|
||
while($i < $len && in_array($query[$i], [' ', "\t", "\n", "\r"])) $i++;
|
||
|
||
if($i >= $len) break;
|
||
|
||
/* 遇到闭合符,退出当前嵌套层级 */
|
||
if($query[$i] === '}') break;
|
||
|
||
/* 解析字段名、类型、别名、数组索引 */
|
||
$fieldInfo = $this->parseFieldName($query, $i);
|
||
$fieldName = $fieldInfo['name'];
|
||
$fieldAlias = $fieldInfo['alias'];
|
||
$arrayIndex = $fieldInfo['index'];
|
||
$targetType = $fieldInfo['target_type'];
|
||
$i = $fieldInfo['pos'];
|
||
|
||
/* 跳过空白字符 */
|
||
while($i < $len && in_array($query[$i], [' ', "\t", "\n", "\r"])) $i++;
|
||
|
||
/* 处理嵌套结构(如 parent:{child}) */
|
||
if($i < $len && $query[$i] === ':')
|
||
{
|
||
$i++; // 跳过冒号
|
||
|
||
/* 跳过空白字符 */
|
||
while($i < $len && in_array($query[$i], [' ', "\t", "\n", "\r"])) $i++;
|
||
|
||
if($i < $len && $query[$i] === '{')
|
||
{
|
||
$i++; // 跳过左大括号
|
||
|
||
/* 递归解析嵌套字段 */
|
||
list($nestedResult, $newPos) = $this->parseExpression($query, $i);
|
||
$i = $newPos;
|
||
|
||
/* 检查是否有闭合符 */
|
||
if($i < $len && $query[$i] === '}')
|
||
{
|
||
$i++; // 跳过右大括号
|
||
}
|
||
else
|
||
{
|
||
throw new Exception("缺少闭合符 '}'");
|
||
}
|
||
|
||
/* 存储嵌套字段结构(包含类型转换信息)*/
|
||
$result[$fieldName] = array(
|
||
'type' => 'nested',
|
||
'alias' => $fieldAlias,
|
||
'index' => $arrayIndex,
|
||
'target_type' => $targetType,
|
||
'fields' => $nestedResult
|
||
);
|
||
}
|
||
else
|
||
{
|
||
throw new Exception("'{' afer ':'");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
/* 存储简单字段结构(包含类型转换信息)*/
|
||
$result[$fieldName] = array(
|
||
'type' => 'simple',
|
||
'alias' => $fieldAlias,
|
||
'index' => $arrayIndex,
|
||
'target_type' => $targetType
|
||
);
|
||
}
|
||
|
||
/* 跳过空白字符 */
|
||
while($i < $len && in_array($query[$i], [' ', "\t", "\n", "\r"])) $i++;
|
||
|
||
/* 处理字段分隔符 */
|
||
if($i < $len && $query[$i] === ',') $i++; // 跳过逗号
|
||
}
|
||
|
||
return [$result, $i];
|
||
}
|
||
|
||
/**
|
||
* 解析字段名、类型转换、别名、数组索引
|
||
* @param string $query 查询字符串
|
||
* @param int $start 起始解析位置
|
||
* @return array 包含字段信息的数组
|
||
* @throws Exception 语法错误时抛出异常
|
||
*/
|
||
private function parseFieldName($query, $start)
|
||
{
|
||
$i = $start;
|
||
$len = strlen($query);
|
||
$name = '';
|
||
$alias = null;
|
||
$index = null;
|
||
$targetType = null;
|
||
|
||
/* 1. 读取字段名(可能包含类型转换,如 user(array))*/
|
||
while($i < $len && !in_array($query[$i], [':', ',', '}', '[', '|', ' ', "\t", "\n", "\r"]))
|
||
{
|
||
$name .= $query[$i];
|
||
$i++;
|
||
}
|
||
|
||
/* 2. 解析类型转换(格式:字段名(类型))*/
|
||
$typePattern = '/^(.+)\((\w+)\)$/';
|
||
if(preg_match($typePattern, $name, $matches))
|
||
{
|
||
$name = trim($matches[1]);
|
||
$targetType = trim($matches[2]);
|
||
|
||
/* 验证支持的类型 */
|
||
$allowedTypes = array('array', 'int', 'float', 'bool', 'string');
|
||
if(!in_array($targetType, $allowedTypes))
|
||
{
|
||
throw new Exception("Not support '{$targetType}', only " . implode(',', $allowedTypes));
|
||
}
|
||
}
|
||
|
||
/* 3. 解析别名(格式:字段名|别名)*/
|
||
if($i < $len && $query[$i] === '|')
|
||
{
|
||
$i++; // 跳过竖线
|
||
|
||
$alias = '';
|
||
while($i < $len && !in_array($query[$i], [':', ',', '}', '[', ' ', "\t", "\n", "\r"]))
|
||
{
|
||
$alias .= $query[$i];
|
||
$i++;
|
||
}
|
||
$alias = trim($alias);
|
||
|
||
/* 别名不能为空 */
|
||
if(empty($alias)) throw new Exception("alias is empty");
|
||
}
|
||
|
||
/* 4. 解析数组索引(格式:字段名[索引])*/
|
||
if($i < $len && $query[$i] === '[')
|
||
{
|
||
$i++; // 跳过左中括号
|
||
|
||
$indexStr = '';
|
||
while($i < $len && $query[$i] !== ']')
|
||
{
|
||
$indexStr .= $query[$i];
|
||
$i++;
|
||
}
|
||
|
||
/* 检查是否有闭合符 */
|
||
if($i < $len && $query[$i] === ']')
|
||
{
|
||
$i++; // 跳过右中括号
|
||
|
||
/* 处理通配符索引和数字索引 */
|
||
if($indexStr === '*')
|
||
{
|
||
$index = '*';
|
||
}
|
||
elseif(is_numeric($indexStr))
|
||
{
|
||
$index = intval($indexStr);
|
||
}
|
||
else
|
||
{
|
||
throw new Exception("index error '{$indexStr}', only support number or '*'");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
throw new Exception("need ']'");
|
||
}
|
||
}
|
||
|
||
/* 字段名不能为空 */
|
||
if(empty($name)) throw new Exception("field is empty");
|
||
|
||
return array(
|
||
'name' => $name,
|
||
'alias' => $alias,
|
||
'index' => $index,
|
||
'target_type' => $targetType,
|
||
'pos' => $i
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 提取字段并应用类型转换
|
||
* @param array|object $data 原始数据(数组或对象)
|
||
* @param array $queryStructure 查询结构数组
|
||
* @return array 提取并转换后的结果数组
|
||
*/
|
||
private function extractFields($data, $queryStructure)
|
||
{
|
||
/* 非数组结构直接返回(避免后续报错)*/
|
||
if(!is_array($queryStructure)) return $this->convertValueType($data, null);
|
||
if(!is_array($data) && !is_object($data)) return null;
|
||
|
||
$result = array();
|
||
$dataArray = (array)$data;
|
||
|
||
foreach($queryStructure as $field => $config)
|
||
{
|
||
$resultKey = $config['alias'] ?? $field; // 优先使用别名
|
||
$targetType = $config['target_type'] ?? null; // 目标转换类型
|
||
|
||
/* 1. 处理通配符字段(如 *:{name})*/
|
||
if($field === '*')
|
||
{
|
||
foreach($dataArray as $key => $value)
|
||
{
|
||
if($config['type'] === 'simple')
|
||
{
|
||
/* 简单通配符字段:直接转换值 */
|
||
$result[$key] = $this->convertValueType($value, $targetType);
|
||
}
|
||
elseif($config['type'] === 'nested')
|
||
{
|
||
/* 嵌套通配符字段:先提取嵌套字段,再转换 */
|
||
$nestedValue = $this->extractFields($value, $config['fields']);
|
||
$result[$key] = $this->convertValueType($nestedValue, $targetType);
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
|
||
/* 字段不存在时跳过 */
|
||
if(!array_key_exists($field, $dataArray)) continue;
|
||
$fieldData = $dataArray[$field];
|
||
|
||
/* 2. 处理数组索引(如 users[0]、users[*])*/
|
||
if($config['index'] !== null)
|
||
{
|
||
/* 非数组类型跳过 */
|
||
if(!is_array($fieldData)) continue;
|
||
|
||
/* 2.1 通配符索引(提取所有数组元素)*/
|
||
if($config['index'] === '*')
|
||
{
|
||
$extractedArray = array();
|
||
foreach($fieldData as $item)
|
||
{
|
||
/* 提取元素值(简单/嵌套)*/
|
||
$itemValue = $config['type'] === 'simple'
|
||
? $item
|
||
: $this->extractFields($item, $config['fields']);
|
||
/* 应用类型转换 */
|
||
$extractedArray[] = $this->convertValueType($itemValue, $targetType);
|
||
}
|
||
$result[$resultKey] = $extractedArray;
|
||
}
|
||
/* 2.2 特定数字索引(提取指定位置元素)*/
|
||
elseif(isset($fieldData[$config['index']]))
|
||
{
|
||
$item = $fieldData[$config['index']];
|
||
/* 提取元素值(简单/嵌套)*/
|
||
$itemValue = $config['type'] === 'simple'
|
||
? $item
|
||
: $this->extractFields($item, $config['fields']);
|
||
/* 应用类型转换并保留索引*/
|
||
$result[$resultKey] = array(
|
||
$config['index'] => $this->convertValueType($itemValue, $targetType)
|
||
);
|
||
}
|
||
}
|
||
/* 3. 无数组索引(普通字段)*/
|
||
else
|
||
{
|
||
/* 提取字段值(简单/嵌套)*/
|
||
$fieldValue = $config['type'] === 'simple'
|
||
? $fieldData
|
||
: $this->extractFields($fieldData, $config['fields']);
|
||
/* 应用类型转换 */
|
||
$result[$resultKey] = $this->convertValueType($fieldValue, $targetType);
|
||
}
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 按目标类型转换值
|
||
* @param mixed $value 原始值
|
||
* @param string|null $targetType 目标类型(array/int/float/bool/string)
|
||
* @return mixed 转换后的值
|
||
*/
|
||
private function convertValueType($value, ?string $targetType)
|
||
{
|
||
/* 无目标类型时返回原始值 */
|
||
if($targetType === null) return $value;
|
||
|
||
switch($targetType)
|
||
{
|
||
/* 转换为数组:关联数组→索引数组,非数组→包装为数组 */
|
||
case 'array':
|
||
if(is_array($value)) return array_values($value);
|
||
return [$value];
|
||
|
||
/* 转换为整数:仅对数值/数值字符串有效 */
|
||
case 'int':
|
||
return is_numeric($value) ? intval($value) : $value;
|
||
|
||
/* 转换为浮点数:仅对数值/数值字符串有效 */
|
||
case 'float':
|
||
return is_numeric($value) ? floatval($value) : $value;
|
||
|
||
/* 转换为布尔值:处理字符串"true"/"false"和数字1/0 */
|
||
case 'bool':
|
||
if(is_string($value))
|
||
{
|
||
$lowerValue = strtolower($value);
|
||
return $lowerValue === 'true' || $lowerValue === '1';
|
||
}
|
||
return boolval($value);
|
||
|
||
/* 转换为字符串:直接强制转换 */
|
||
case 'string':
|
||
return strval($value);
|
||
|
||
/* 未知类型返回原始值 */
|
||
default:
|
||
return $value;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取错误信息列表
|
||
* @return string[] 错误信息数组
|
||
*/
|
||
public function getErrors()
|
||
{
|
||
return $this->errors;
|
||
}
|
||
}
|