Search by

laraveladmin / formatter

laraveladmin

Template-driven data structure transformation for PHP (Hyperf / Laravel / ThinkPHP). Unify multi-vendor payloads via config instead of hand-written mappers.

Package info

gitee.com/laravel-admin/formatter

pkg:composer/laraveladmin/formatter

Statistics

Installs: 4

Dependents: 0

Suggesters: 0

v1.0.1 2026-09-09 01:45 UTC

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 classOne template config per vendor / channel
Field rename, defaults, casts, enum maps scattered in codeLeaf 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 PHPAdjust template JSON (DB / config / file) and re-render
Hard to review diffs of assignment soupTemplates 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 bridgingmap / flipMaps for 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

ExpressionMeaning
a.bDot path from source
a.b$def:xDefault x when missing ($def: alone → '')
$def:1Constant default (no path)
a.b:number / :string / :div:2Cast; combinable: a.b:number$def:0
$i / $n / $countLoop context
$global.xThird 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
  • @arr on 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; Hyperf make() 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