257 lines
8.4 KiB
PHP
257 lines
8.4 KiB
PHP
<?php
|
|
|
|
namespace app\mzl\common\traits;
|
|
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Collection;
|
|
|
|
trait DataTransformer
|
|
{
|
|
/**
|
|
* 核心方法:对任意数据进行转换并返回数组
|
|
*
|
|
* @param mixed $rawData 原始数据,可为 Paginator / Model / Collection / array 等
|
|
* @param callable|null $postProcessor 可选的后置处理回调,接受处理完的数组再返回新的数组
|
|
* @param callable|null $fieldFormatter 可选的字段命名转换回调,如 snake_case
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function transformData(
|
|
mixed $rawData,
|
|
?callable $postProcessor = null,
|
|
?callable $fieldFormatter = null
|
|
): array {
|
|
try {
|
|
$fieldFormatter = $fieldFormatter ?: fn($field) => $field;
|
|
|
|
if ($rawData instanceof LengthAwarePaginator) {
|
|
$formatted = $this->formatPaginated($rawData, $postProcessor, $fieldFormatter);
|
|
} elseif ($rawData instanceof Model) {
|
|
$formatted = $this->formatModel($rawData, $fieldFormatter);
|
|
} elseif ($rawData instanceof Collection) {
|
|
$formatted = $this->formatCollection($rawData, $fieldFormatter);
|
|
} elseif (is_array($rawData)) {
|
|
$formatted = $this->formatArray($rawData, $fieldFormatter);
|
|
} else {
|
|
$formatted = (array) $rawData;
|
|
}
|
|
|
|
if (is_callable($postProcessor)) {
|
|
$formatted = $postProcessor($formatted);
|
|
}
|
|
|
|
return $formatted;
|
|
} catch (\Throwable $e) {
|
|
return [
|
|
'error' => true,
|
|
'message' => $e->getMessage(),
|
|
'code' => $e->getCode(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 格式化分页数据:将每条记录也进行 transformData 递归处理
|
|
*
|
|
* @param LengthAwarePaginator $paginator
|
|
* @param callable|null $postProcessor
|
|
* @param callable $fieldFormatter
|
|
* @return array
|
|
*/
|
|
protected function formatPaginated(
|
|
LengthAwarePaginator $paginator,
|
|
?callable $postProcessor,
|
|
callable $fieldFormatter
|
|
): array {
|
|
$list = $paginator->getCollection()->map(function ($item) use ($postProcessor, $fieldFormatter) {
|
|
return $this->transformData($item, $postProcessor, $fieldFormatter);
|
|
})->toArray();
|
|
|
|
$meta = [
|
|
'current_page' => $paginator->currentPage(),
|
|
'per_page' => $paginator->perPage(),
|
|
'total' => $paginator->total(),
|
|
'last_page' => $paginator->lastPage(),
|
|
'first_page_url' => $paginator->url(1),
|
|
'last_page_url' => $paginator->url($paginator->lastPage()),
|
|
'next_page_url' => $paginator->nextPageUrl(),
|
|
'prev_page_url' => $paginator->previousPageUrl(),
|
|
];
|
|
|
|
return [
|
|
'list' => $list,
|
|
'meta' => $meta,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 格式化 Eloquent 模型:递归嵌套关联
|
|
*
|
|
* @param Model $model
|
|
* @param callable $fieldFormatter
|
|
* @return array
|
|
*/
|
|
protected function formatModel(Model $model, callable $fieldFormatter): array
|
|
{
|
|
$formatted = [];
|
|
|
|
// 主表字段
|
|
foreach ($model->getAttributes() as $key => $value) {
|
|
$formatted[$fieldFormatter($key)] = $value;
|
|
}
|
|
|
|
// 关联数据,递归嵌套
|
|
foreach ($model->getRelations() as $relation => $relationData) {
|
|
if ($relationData instanceof Model) {
|
|
// 单个关联模型
|
|
$formatted[$relation] = $this->formatModel($relationData, $fieldFormatter);
|
|
} elseif ($relationData instanceof Collection) {
|
|
// 关联集合
|
|
$formatted[$relation] = $relationData
|
|
->map(fn($item) => $this->transformData($item, null, $fieldFormatter))
|
|
->toArray();
|
|
}
|
|
}
|
|
|
|
return $formatted;
|
|
}
|
|
|
|
/**
|
|
* 格式化集合
|
|
*
|
|
* @param Collection $collection
|
|
* @param callable $fieldFormatter
|
|
* @return array
|
|
*/
|
|
protected function formatCollection(Collection $collection, callable $fieldFormatter): array
|
|
{
|
|
return $collection->map(fn($item) => $this->transformData($item, null, $fieldFormatter))
|
|
->toArray();
|
|
}
|
|
|
|
/**
|
|
* 格式化普通数组
|
|
*
|
|
* @param array $array
|
|
* @param callable $fieldFormatter
|
|
* @return array
|
|
*/
|
|
protected function formatArray(array $array, callable $fieldFormatter): array
|
|
{
|
|
return array_map(fn($item) => $this->transformData($item, null, $fieldFormatter), $array);
|
|
}
|
|
|
|
/**
|
|
* 递归裁剪数组,只保留指定字段
|
|
*
|
|
* @param array $data 当前要裁剪的数据(可能是单条关联,也可能是 list)
|
|
* @param array $fieldsMap 白名单映射,key=>[allowed fields]
|
|
* @param string $path 当前路径('' / 'items' / 'items.sub' …)
|
|
* @return array
|
|
*/
|
|
protected function pruneFields(array $data, array $fieldsMap, string $path = ''): array
|
|
{
|
|
// —— 新增:如果这是一个纯列表(list),逐项递归 prune 并返回
|
|
if ($this->isListArray($data)) {
|
|
return array_map(
|
|
fn($item) => is_array($item)
|
|
? $this->pruneFields($item, $fieldsMap, $path)
|
|
: $item,
|
|
$data
|
|
);
|
|
}
|
|
|
|
// 如果当前层没有配置白名单,原样返回(或也可以 return [] 视业务)
|
|
if (! isset($fieldsMap[$path])) {
|
|
return $data;
|
|
}
|
|
|
|
$allowed = $fieldsMap[$path];
|
|
$out = [];
|
|
|
|
foreach ($allowed as $key) {
|
|
if (! array_key_exists($key, $data)) {
|
|
continue;
|
|
}
|
|
$val = $data[$key];
|
|
|
|
// 如果下一层还是个数组,继续递归
|
|
if (is_array($val)) {
|
|
$nextPath = ltrim($path . '.' . $key, '.');
|
|
$out[$key] = $this->pruneFields($val, $fieldsMap, $nextPath);
|
|
} else {
|
|
$out[$key] = $val;
|
|
}
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* 把 Paginator/Model/Collection/array 统一格式化,并按 fieldsMap 裁剪
|
|
*
|
|
* @param mixed $rawData
|
|
* @param array|null $fieldsMap 顶层 '' + 各关联路径的白名单
|
|
* @param callable|null $fieldFormatter 字段格式化回调
|
|
* @param callable|null $postProcessor 单条记录后置处理回调
|
|
* @return array
|
|
*/
|
|
protected function transformAndPrune(
|
|
mixed $rawData,
|
|
?array $fieldsMap = null,
|
|
?callable $fieldFormatter = null,
|
|
?callable $postProcessor = null
|
|
): array {
|
|
// 1) 统一转换成数组
|
|
$data = $this->transformData($rawData, null, $fieldFormatter);
|
|
|
|
// 2) 如果没有白名单配置,直接返回原始数据
|
|
if (empty($fieldsMap) || !isset($fieldsMap[''])) {
|
|
return $data;
|
|
}
|
|
|
|
// 3) 分页结构:['list'=>[...],'meta'=>[...]]
|
|
if (isset($data['list'], $data['meta'])) {
|
|
$data['list'] = array_map(
|
|
fn($item) => $this->processItem($item, $fieldsMap, $postProcessor),
|
|
$data['list']
|
|
);
|
|
return $data;
|
|
}
|
|
|
|
// 4) 数字索引数组 => 当作“非分页列表”处理
|
|
if ($this->isListArray($data)) {
|
|
return array_map(
|
|
fn($item) => $this->processItem($item, $fieldsMap, $postProcessor),
|
|
$data
|
|
);
|
|
}
|
|
|
|
// 5) 单条记录
|
|
return $this->processItem($data, $fieldsMap, $postProcessor);
|
|
}
|
|
|
|
/**
|
|
* 裁剪并做后置处理
|
|
*/
|
|
private function processItem(
|
|
array $item,
|
|
array $fieldsMap,
|
|
?callable $postProcessor
|
|
): array {
|
|
$pruned = $this->pruneFields($item, $fieldsMap);
|
|
return $postProcessor ? $postProcessor($pruned) : $pruned;
|
|
}
|
|
|
|
/**
|
|
* 判断一个数组是不是“索引从 0 开始的列表”
|
|
*/
|
|
private function isListArray(array $arr): bool
|
|
{
|
|
if (empty($arr)) {
|
|
return true;
|
|
}
|
|
return array_keys($arr) === range(0, count($arr) - 1);
|
|
}
|
|
} |