Files
usedcar/app/mzl/common/traits/DataTransformer.php
T
2025-05-14 13:19:23 +08:00

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