laraveladmin / formatter
Template-driven data structure transformation for PHP (Hyperf / Laravel / ThinkPHP). Unify multi-vendor payloads via config instead of hand-written mappers.
Requires
- php: >=8.2
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-09-09 12:47:02 UTC
README
Template-driven data structure transformation for PHP 8.2+.
Turn vendor / partner / legacy payloads into a unified shape with JSON/array templates — no piles of $out['a'] = $in['b'] ?? … glue code.
Works with Hyperf, Laravel, ThinkPHP, and plain PHP. Stateless and singleton-safe: the service never stores request data on the instance, so it is safe in long-running Hyperf workers.
中文说明:README.zh-CN.md
Why this package?
| Pain (hand-written mapping) | With laraveladmin/formatter |
|---|---|
| Every data vendor needs another PHP mapper class | One template config per vendor / channel |
| Field rename, defaults, casts, enum maps scattered in code | Leaf DSL: path / $def / :number / :string / :div + map() |
| Nested lists & “always one applicant” shapes are messy | $index loops, prototype lists, @arr flatten |
| Changing a field means redeploying PHP | Adjust template JSON (DB / config / file) and re-render |
| Hard to review diffs of assignment soup | Templates are data — easy to diff, version, and A/B |
Typical scenarios
- Multi-vendor / multi-channel payload unification — insurance, payment, bidding, CRM, open platforms: map A/B/C vendor schemas into one internal DTO
- API gateway / BFF reshape — stabilize outbound contracts while upstream fields drift
- Import / export / ETL-lite — spreadsheet or partner XML→array → domain model without custom mapper classes
- Enum / status / type code bridging —
map/flipMapsfor inbound vs outbound code tables - Config-driven product assembly — order / policy / form payloads built from templates stored in options, not hard-coded PHP
Install
composer require laraveladmin/formatter
Requires PHP ≥ 8.2. No framework dependency.
Repository: https://gitee.com/laravel-admin/formatter · Packagist: laraveladmin/formatter
Framework usage
The core is a plain PHP class. Use DI when the framework has it; otherwise new Formatter() or the static proxy.
Hyperf
ConfigProvider registers the class for the container (process singleton by default):
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);
}
}
Or resolve explicitly:
$formatter = $container->get(Formatter::class);
// Prefer get() / injection. Hyperf make() creates a *new* instance.
Static proxy (uses DI singleton when ApplicationContext is available):
use LaravelAdmin\Formatter\Facades\Formatter;
$data = Formatter::render($template, $source, $global);
Laravel
No Laravel binding is shipped; bind once if you want a singleton:
// 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);
Or without container:
use LaravelAdmin\Formatter\Formatter;
$formatter = new Formatter();
$result = $formatter->render($template, $source);
ThinkPHP
use LaravelAdmin\Formatter\Formatter;
// In a service / controller
$formatter = app()->make(Formatter::class); // or new Formatter()
$result = $formatter->render($template, $source, $global);
Optional: register as singleton in a service provider / provider.php so every request reuses one instance.
Plain PHP / other frameworks
use LaravelAdmin\Formatter\Formatter;
$formatter = new Formatter();
$result = $formatter->render($template, $source, $global);
$result = $formatter->map($data, $maps);
API
use LaravelAdmin\Formatter\Formatter;
/** @var Formatter $formatter */ // prefer constructor injection (process singleton)
$result = $formatter->render($template, $source, $global = []);
$result = $formatter->map($data, $maps);
$maps = $formatter->flipMaps($maps);
Optional static proxy (still uses the DI singleton when the Hyperf container is available):
use LaravelAdmin\Formatter\Facades\Formatter;
$data = Formatter::render($template, $source, $global);
Examples (input → output)
1. Rename & dot path
$formatter->render(
['name' => 'user_name', 'id' => 'user.id'],
['user_name' => 'Alice', 'user' => ['id' => 42]],
);
// => ['name' => 'Alice', 'id' => 42]
2. Default ($def) & type cast
$formatter->render(
[
'age' => 'age$def:0',
'flag' => '$def:1',
'score' => 'score:number$def:0',
'tags' => 'tags:string',
'amount' => 'amount:div:2',
],
['tags' => ['a', 'b'], 'amount' => 10000],
);
// => [
// 'age' => '0',
// 'flag' => '1',
// 'score' => 0,
// 'tags' => 'a,b',
// 'amount' => '100.00',
// ]
3. Nested object
$formatter->render(
['order' => ['buyer' => ['name' => 'order.buyer.name']]],
['order' => ['buyer' => ['name' => 'Bob']]],
);
// => ['order' => ['buyer' => ['name' => 'Bob']]]
4. Prototype list (no $index)
Fill a list-shaped slot from the current context (common for “always one applicant” payloads):
$formatter->render(
['applicants' => [['name' => 'bidderName:string$def:', 'cf_type' => '$def:10']]],
['bidderName' => 'Acme Ltd'],
);
// => ['applicants' => [['name' => 'Acme Ltd', 'cf_type' => '10']]]
5. $index loop + $i / $n / $count + $global
$formatter->render(
[[
'name' => 'items.$index.name',
'i' => '$i',
'n' => '$n',
'count' => '$count',
'currency' => 'currency',
]],
['items' => [['name' => 'a'], ['name' => 'b']]],
['currency' => 'CNY'],
);
// => [
// ['name' => 'a', 'i' => 0, 'n' => 1, 'count' => 2, 'currency' => 'CNY'],
// ['name' => 'b', 'i' => 1, 'n' => 2, 'count' => 2, 'currency' => 'CNY'],
// ]
Leaves without $index are rewritten to $global.… after the collection prefix is stripped.
6. @arr flatten into parent
$formatter->render(
[
'Risks' => [
'keep' => '$def:header',
'rows' => [
'@arr' => true,
'code' => 'products.$index.code',
],
],
],
['products' => [['code' => 'P1'], ['code' => 'P2']]],
);
// => [
// 'Risks' => [
// 'keep' => 'header',
// 0 => ['code' => 'P1'],
// 1 => ['code' => 'P2'],
// ],
// ]
7. Value map / flipMaps
$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 is never mapped
$formatter->map(['status' => 'OK'], $formatter->flipMaps($maps));
// => ['status' => 1]
8. Order-like assemble (mini business sample)
$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]],
];
$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]],
// ]
Leaf DSL
| Expression | Meaning |
|---|---|
a.b | Dot path from source |
a.b$def:x | Default x when missing ($def: alone → '') |
$def:1 | Constant default (no path) |
a.b:number / :string / :div:2 | Cast; combinable: a.b:number$def:0 |
$i / $n / $count | Loop context |
$global.x | Third argument ($global) |
Structure rules
- Associative array → nested object
- List without
$index→ prototype mode (each element resolved against current context) - List with
$index.…→ iterate the collection before$index @arron a child → render as list then flatten into parent numeric keys
Design notes
- Inputs are not mutated
- Prefer constructor injection /
container->get(Formatter::class)for the process singleton; Hyperfmake()creates a new instance - No Laravel / ThinkPHP framework dependency; PHP ≥ 8.2 only
Tests
vendor/bin/pest vendor/laraveladmin/formatter/tests
# or from a Hyperf app that path-requires the package:
docker compose exec -T server vendor/bin/pest ../packages/formatter/tests