savks/negotiator

2.16.0 2024-09-16 15:16 UTC

README

该包用作 Laravel JSON 资源的替代品。该包的优点在于严格的映射类型化以及内置的 TypeScript 类型生成工具。

安装

composer require savks/negotiator

映射器描述

要编写自定义映射器,需要创建一个继承自 \Savks\Negotiator\Support\Mapping\Mapper 的类。映射器示例

<?php

namespace App\Http\Mapping;

use App\Models\User;

use Savks\Negotiator\Support\Mapping\{
    Casts\Cast,
    Mapper,
    Schema
};

final class UserMapper extends Mapper
{
    public function __construct(public readonly User $user)
    {
    }

    public static function schema(): Cast
    {
        return Schema::object([
            'id' => Schema::string('id'),
            'firstName' => Schema::string('first_name'),
            'lastName' => Schema::string('last_name')->nullable(),
        ], 'user');
    }
}

映射器描述不应包含命令式代码,因为无法生成类型。这是因为类型生成过程中会模拟创建映射器以获取类型信息,如果描述中存在命令式代码,则将阻止它们的工作。

NULL 和非必需字段

如果字段可能具有 null 的值,则需要将其标记为 ->nullable(),因为严格类型化会导致抛出错误。如果字段不是必需的,则可以将其标记为 ->optional()(建议这样做以减少数据输出量),在这种情况下,如果值为 null,则在 objectkeyedArray 的类型转换中忽略该字段。

此外,为了减少数据输出量,不仅可以使 null 成为非必需的。为此,在 boolean 中有辅助方法 ->optionalIfFalse(),或在 stringarray 中有 ->optionalIfEmpty()。为了更灵活的配置,需要使用 ->optional() 方法的参数。

在极端情况下,可能需要仅将 optional 指定给类型,同时保留映射时的类型检查。在这种情况下,需要使用 ->maybeOptional()->maybeNullable() 方法。

内置类型

简单类型

  • stringbooleannumber — 原始类型。
  • constStringconstBooleanconstNumber — 静态类型。区别在于值是明确设置的。此外,它们也可以作为(静态值的)字面量。
  • anyObject — 允许描述对象,而不描述其字段(在 TypeScript 中是 Record<string, any>)。
  • enum — 枚举值。
  • null — 将值定义为 NULL。
  • any — 任何值(类似于 TypeScript 中的值)。

复杂类型

  • array — 常规数组类型 — 列表。基于任何可迭代值。例如
<?php

use Savks\Negotiator\Support\Mapping\{
    Casts\Cast,
    Schema
};

Schema::object([
    'items' => Schema::array(
        Schema::anyObject(),
        'items'
    ),
]);
  • object — 带有静态字段的对象。例如
<?php

use Savks\Negotiator\Support\Mapping\{
    Casts\Cast,
    Schema
};

Schema::object([
    'field' => Schema::string('field'),
]);
  • keyedArray — 关联数组/映射,与对象不同之处在于它基于可迭代值。例如
<?php

use Savks\Negotiator\Support\Mapping\{
    Casts\Cast,
    Schema
};

Schema::object([
    'items' => Schema::keyedArray(
        Schema::anyObject(),
        'items'
    ),
]);

实用类型

  • mapper — 允许指定其他映射器作为值。例如
<?php

use App\Models\User;

use Savks\Negotiator\Support\Mapping\{
    Casts\Cast,
    Schema
};

Schema::object([
    'user' => Schema::mapper(
        fn (User $user): UserMapper => new UserMapper($user),
        'user'
    ),
]);

Schema::object([
    'user' => Schema::mapper(UserMapper::class, 'user'),
]);

为了正确生成 TypeScript 中的类型,在映射器的解析函数中,重要的是指定映射器本身作为返回类型,否则值将具有 any 的值。

  • union — 允许指定多个可能的类型。指定为选项集(条件不影响类型生成)。例如
<?php

use App\Models\User;

use Savks\Negotiator\Support\Mapping\{
    Casts\Cast,
    Schema
};

Schema::object([
    'field' => Schema::union()
        ->variant(
            fn (User $user) => $user->role === 'admin',
            Schema::object([
                'field' => Schema::string('field'),
            ])
        )
        ->variant(
            ['role', 'guest'],
            Schema::object([
                'field' => Schema::string('field'),
            ])
        )
        ->default(
            Schema::object([
                'field' => Schema::string('field'),
            ])
        ),
]);
  • spread — 允许将一个对象展开到另一个对象中。例如
<?php

use Savks\Negotiator\Support\Mapping\Schema;

use Savks\Negotiator\Support\Mapping\Casts\{
    ObjectUtils\Spread,
    Cast
};

Schema::object([
    'field' => Schema::string('field'),
    
    new Spread([
        'otherField' => Schema::string('other_field')
    ], 'accessor'),
]);
  • typedField — 允许指定具有类型化键的字段。例如
<?php

use Savks\Negotiator\Support\Mapping\Schema;

use Savks\Negotiator\Support\Mapping\Casts\{
    ObjectUtils\TypedField,
    Cast
};

Schema::object([
    'field' => Schema::string('field'),
    
    new TypedField(SomeEnum::CASE, [
        'otherField' => Schema::string('other_field')
    ]),
]);
  • intersection — 用于指定交叉类型,通常用于需要扩展其他映射的情况。例如
<?php

use Savks\Negotiator\Support\Mapping\{
    Casts\Cast,
    Schema
};

Schema::object([
    'field' => Schema::intersection(
        Schema::mapper(UserMapper::class, 'user'),

        Schema::object([
            'otherField' => Schema::string('other_field')
        ], 'user'),
    ),
]);
  • oneOfConst — 允许指定值可以具有一个或多个类型常量。例如
<?php

use Savks\Negotiator\Support\Mapping\{
    Casts\Cast,
    Schema
};

Schema::object([
    'field' => Schema::oneOfConst([
        Schema::constNumber(1),
        Schema::constNumber(2),
        Schema::constNumber(3),
    ]),
]);
  • scope — 允许计算将被传递到映射中的值。例如
<?php

use Savks\Negotiator\Support\Mapping\{
    Casts\Cast,
    Schema
};

Schema::object([
    'field' => Schema::scope(
       Schema::oneOfConst([
           Schema::constNumber(1),
           Schema::constNumber(2),
           Schema::constNumber(3),
       ]),
       fn (array $data) => $data['value']
    ),
]);

类型生成

为了生成包类型,包含了一个生成器类 Savks\Negotiator\Support\TypeGeneration\Generator。要使用它,只需要指定要为哪些映射器和哪些命名空间生成代码。示例用法

<?php

use App\Http\Mapping\UserMapper;
use Savks\Negotiator\Enums\RefTypes;
use Illuminate\Support\Str;

use Savks\Negotiator\Support\TypeGeneration\TypeScript\{
    Generator,
    Target
};

$generator = new Generator(
    /* Функція для визначення референсів.  */
    fn (RefTypes $type, string $target) => match ($type) {
        RefTypes::ENUM => sprintf(
            'import(\'@enums\').%s',
            class_basename($target::class)
        ),
        RefTypes::MAPPER => sprintf(
            'import(\'@dto\').%s',
            class_basename($target::class)
        ),
    }
);

$generator->addTarget(
    new Target(UserMapper::class, '@dto')
);

$generator->saveTo('./path_to_file.ts');

有时,生成器无法为获取类型创建映射器,因为映射器在构造函数中接收特定的输入数据。在这种情况下,需要实现接口 Savks\Negotiator\Support\Mapping\WithCustomMock,并使用该方法创建带有自定义数据的映射器。

极端情况

  1. 无法声明性描述映射器的数据。

    解决这个问题的一种方法是使用类型转换来处理最终值。类型转换具有访问器,这是一种指定从哪里获取数据的方法,它可以是返回最终值的匿名函数,在这种情况下,类型转换中只剩下类型描述。