# 数据格式化转换 **Repository Path**: laravel-admin/formatter ## Basic Information - **Project Name**: 数据格式化转换 - **Description**: php数组数据转换成对应模板结构数据,用于各种三方接口数据转换成统一的一套数据结构格式 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-08 - **Last Updated**: 2026-09-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # laraveladmin/formatter(中文说明) 模板驱动的**数据结构转换**包,适用于 PHP 8.2+。 用 **JSON / 数组模板**把各厂商、渠道、遗留接口的数据统一成内部结构,而不是写一大堆 `$out['a'] = $in['b'] ?? …` 赋值胶水代码。 可用于 **Hyperf**、**Laravel**、**ThinkPHP** 以及纯 PHP。无状态、适合单例:服务实例上不保存请求级数据,可在 Hyperf 常驻 worker 中安全复用。 > English: [README.md](./README.md) ## 为什么用这个包? | 手写映射的痛点 | 使用 `laraveladmin/formatter` | |----------------|-------------------------------| | 每个数据厂商再写一套 PHP Mapper | 每个厂商 / 渠道一份**模板配置**即可 | | 改名、默认值、类型转换、枚举散落在代码里 | 叶节点 DSL:路径 / `$def` / `:number` / `:string` / `:div`,再用 `map()` | | 嵌套列表、「固定一个投保人」等结构难写 | `$index` 循环、列表原型、`@arr` 拍平 | | 改一个字段就要改 PHP 再发版 | 改模板(库表 / 配置 / 文件)后重新 `render` | | 赋值代码难做 Code Review | 模板是数据,易 diff、版本管理、灰度 | **常见场景** - **多数据厂商 / 多渠道报文统一化** — 保险、支付、招投标、CRM、开放平台:把 A/B/C 厂商字段映射成同一内部 DTO - **网关 / BFF 出参整形** — 上游字段漂移时,对外契约仍由模板稳定输出 - **导入导出 / 轻量 ETL** — 表格或合作方 XML→数组 → 领域模型,少写专用 Mapper 类 - **枚举 / 状态码桥接** — `map` / `flipMaps` 做入站与出站码表互转 - **配置驱动的业务组装** — 订单 / 保单 / 表单等由配置中心存模板,而不是硬编码 PHP 赋值 ## 安装 ```bash composer require laraveladmin/formatter ``` 仅需 PHP ≥ 8.2,无框架依赖。 仓库地址:[https://gitee.com/laravel-admin/formatter](https://gitee.com/laravel-admin/formatter) · Packagist:`laraveladmin/formatter` ## 主流框架用法 核心是普通 PHP 类:有 DI 就注入;没有就 `new Formatter()`,或用静态代理。 ### Hyperf `ConfigProvider` 已注册到容器(默认进程单例): ```php use LaravelAdmin\Formatter\Formatter; class OrderAssembleService { public function __construct(private Formatter $formatter) {} public function build(array $template, array $signData, array $global = []): array { return $this->formatter->render($template, $signData, $global); } } ``` 或显式解析: ```php $formatter = $container->get(Formatter::class); // 优先 get() / 构造器注入;Hyperf make() 会新建实例,不是单例 ``` 静态入口(有 `ApplicationContext` 时仍走 DI 单例): ```php use LaravelAdmin\Formatter\Facades\Formatter; $data = Formatter::render($template, $source, $global); ``` ### Laravel 包内未附带 Laravel ServiceProvider,需要单例时可自行绑定: ```php // AppServiceProvider::register() $this->app->singleton(\LaravelAdmin\Formatter\Formatter::class); // Controller / Action public function __construct(private \LaravelAdmin\Formatter\Formatter $formatter) {} $result = $this->formatter->render($template, $payload); ``` 不用容器也可以: ```php use LaravelAdmin\Formatter\Formatter; $formatter = new Formatter(); $result = $formatter->render($template, $source); ``` ### ThinkPHP ```php use LaravelAdmin\Formatter\Formatter; // 服务类 / 控制器中 $formatter = app()->make(Formatter::class); // 或 new Formatter() $result = $formatter->render($template, $source, $global); ``` 可选:在服务提供者 / `provider.php` 里注册为单例,全请求复用同一实例。 ### 纯 PHP / 其他框架 ```php use LaravelAdmin\Formatter\Formatter; $formatter = new Formatter(); $result = $formatter->render($template, $source, $global); $result = $formatter->map($data, $maps); ``` ## API ```php use LaravelAdmin\Formatter\Formatter; /** @var Formatter $formatter */ // 推荐构造器注入(进程单例) $result = $formatter->render($template, $source, $global = []); $result = $formatter->map($data, $maps); $maps = $formatter->flipMaps($maps); ``` 可选静态入口(容器可用时仍走 DI 单例): ```php use LaravelAdmin\Formatter\Facades\Formatter; $data = Formatter::render($template, $source, $global); ``` 相对旧系统 `FormatterService`: - 不再用引用写回源数组(入参只读) - 旧 `encode` / `decode` 合并为 `map`;反向映射用显式 `flipMaps` ## 使用案例(输入 → 输出) 以下均假设:`$formatter = new Formatter();`(或注入同一单例)。 ### 1. 字段改名与点路径 **输入** ```php $template = ['name' => 'user_name', 'id' => 'user.id']; $source = ['user_name' => '张三', 'user' => ['id' => 42]]; ``` **输出** ```php $formatter->render($template, $source); // ['name' => '张三', 'id' => 42] ``` ### 2. 默认值 `$def` 与类型转换 **输入** ```php $template = [ 'age' => 'age$def:0', 'flag' => '$def:1', 'score' => 'score:number$def:0', 'tags' => 'tags:string', 'amount' => 'amount:div:2', ]; $source = ['tags' => ['a', 'b'], 'amount' => 10000]; ``` **输出** ```php [ 'age' => '0', // 缺省为字符串 '0'(未加 :number) 'flag' => '1', 'score' => 0, // :number 后为整型 0 'tags' => 'a,b', 'amount' => '100.00', // 10000 ÷ 10^2 ] ``` ### 3. 嵌套对象 **输入** ```php $template = ['order' => ['buyer' => ['name' => 'order.buyer.name']]]; $source = ['order' => ['buyer' => ['name' => '李四']]]; ``` **输出** ```php ['order' => ['buyer' => ['name' => '李四']]] ``` ### 4. 无 `$index` 的列表原型(单元素投保人等) 列表里写「原型对象」,对**当前上下文**求值(不循环源集合): **输入** ```php $template = [ 'applicants' => [[ 'name' => 'bidderName:string$def:', 'cf_type' => '$def:10', ]], ]; $source = ['bidderName' => '某某公司']; ``` **输出** ```php ['applicants' => [['name' => '某某公司', 'cf_type' => '10']]] ``` ### 5. `$index` 循环与 `$i` / `$n` / `$count` / `$global` **输入** ```php $template = [[ 'name' => 'items.$index.name', 'i' => '$i', 'n' => '$n', 'count' => '$count', 'currency' => 'currency', // 无 $index → 从 $global 取 ]]; $source = ['items' => [['name' => 'a'], ['name' => 'b']]]; $global = ['currency' => 'CNY']; ``` **输出** ```php [ ['name' => 'a', 'i' => 0, 'n' => 1, 'count' => 2, 'currency' => 'CNY'], ['name' => 'b', 'i' => 1, 'n' => 2, 'count' => 2, 'currency' => 'CNY'], ] ``` ### 6. `@arr` 拍平到父级 **输入** ```php $template = [ 'Risks' => [ 'keep' => '$def:header', 'rows' => [ '@arr' => true, 'code' => 'products.$index.code', ], ], ]; $source = ['products' => [['code' => 'P1'], ['code' => 'P2']]]; ``` **输出** ```php [ 'Risks' => [ 'keep' => 'header', 0 => ['code' => 'P1'], 1 => ['code' => 'P2'], ], ] ``` ### 7. 值映射 `map` / `flipMaps` **输入 / 输出** ```php $maps = ['status' => [1 => 'OK', 2 => 'NO', '@default' => 'UNK']]; $formatter->map(['status' => 1], $maps); // ['status' => 'OK'] $formatter->map(['status' => 9], $maps); // ['status' => 'UNK'] $formatter->map(['status' => null], $maps); // ['status' => null] // null 不参与映射 $formatter->map(['status' => 'OK'], $formatter->flipMaps($maps)); // ['status' => 1] ``` 嵌套路径与列表内字段同样按点路径对齐 maps: ```php $formatter->map( ['items' => [['type' => 1], ['type' => 2]]], ['items' => ['type' => [1 => 'A', 2 => 'B', '@default' => 'X']]], ); // ['items' => [['type' => 'A'], ['type' => 'B']]] ``` ### 8. 接近业务的订单组装(精简) **输入** ```php $template = [ 'pay_method' => ':number$def:0', 'applicants' => [[ 'name' => 'bidderName:string$def:', 'cf_type' => '$def:10', ]], 'mark' => [ 'type' => '$def:3', 'name' => 'sectionName$def:', 'options' => ['houseCertificate' => 'sectionCode'], ], 'order_product' => [[ 'amount_insured' => 'tenderBond', 'product_id' => 'zlts_data.product.id', ]], ]; $signData = [ 'bidderName' => '某某公司', 'sectionName' => '标段一', 'sectionCode' => 'SC-001', 'tenderBond' => '100000', 'zlts_data' => ['product' => ['id' => 16]], ]; ``` **输出** ```php $formatter->render($template, $signData, []); // [ // 'pay_method' => 0, // 'applicants' => [['name' => '某某公司', 'cf_type' => '10']], // 'mark' => [ // 'type' => '3', // 'name' => '标段一', // 'options' => ['houseCertificate' => 'SC-001'], // ], // 'order_product' => [ // ['amount_insured' => '100000', 'product_id' => 16], // ], // ] ``` ## 叶节点 DSL | 写法 | 含义 | |------|------| | `a.b` | 从源数据取点路径 | | `a.b$def:x` | 缺省为 `x`(`$def:` 后为空则默认 `''`) | | `$def:1` | 无路径,常量默认 | | `a.b:number` / `:string` / `:div:2` | 类型转换;可组合:`a.b:number$def:0` | | `$i` / `$n` / `$count` | 循环上下文 | | `$global.x` | 第三参全局数据 | ## 结构规则 - **关联数组** → 嵌套对象 - **列表且无 `$index`** → 原型模式(对当前上下文求值) - **列表且含 `$index.…`** → 按 `$index` 前集合路径循环 - **子节点带 `@arr`** → 先渲染成列表,再拍平进父级数值下标 ## 设计说明 - **不修改**调用方传入的 `$source` / `$global` - Hyperf 下优先构造器注入或 `container->get(Formatter::class)`;`make()` 会新建实例,不是单例 - 无 Laravel / ThinkPHP 框架依赖,仅需 PHP ≥ 8.2 ## 测试 ```bash vendor/bin/pest vendor/laraveladmin/formatter/tests # 本 monorepo 开发时: docker compose exec -T server vendor/bin/pest ../packages/formatter/tests ```