| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422 |
- <?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, $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;
- }
- }
|