brick/json-mapper

将 JSON 数据映射到强类型 PHP DTO

资助包维护!
BenMorel

0.1.1 2023-04-15 22:02 UTC

This package is auto-updated.

Last update: 2024-09-03 00:03:57 UTC


README

将 JSON 数据映射到强类型 PHP DTO

Build Status Coverage Status Latest Stable Version Total Downloads License

简介

此库提供了一个易于使用、安全且强大的方式来将 JSON 数据映射到强类型 PHP 对象。

它读取在类构造函数上定义的参数类型和注解,以将 JSON 数据映射到您的 DTO,并且可以零配置工作。

安装

此库可以通过 Composer 安装。

composer require brick/json-mapper

要求

此库需要 PHP 8.1 或更高版本。

项目状态和发布流程

尽管此库仍在开发中,但它经过充分测试,被认为足够稳定,可以在生产环境中使用。

当前版本为 0.x.y。当引入非破坏性更改(添加新方法、优化现有代码等)时,y 增量。

当引入破坏性更改时,始终启动一个新的 0.x 版本周期。

因此,可以将项目锁定到给定的版本周期,例如 0.1.*

如果您需要升级到较新的版本周期,请查看 发布历史 以获取每个后续 0.x.0 版本引入的更改列表。

用法

基本用法

JsonMapper 提供了一个名为 map() 的单方法,它接受一个 JSON 字符串和一个类名,并返回给定类的实例。

use Brick\JsonMapper\JsonMapper;

class User
{
    public function __construct(
        public int $id,
        public string $name,
    ) {
    }
}

$json = '{
  "id": 123,
  "name": "John Doe"
}';

$mapper = new JsonMapper();
$user = $mapper->map($json, User::class);

echo $user->name; // John Doe

嵌套对象

JsonMapper 将读取参数类型和注解以映射嵌套对象

class Album
{
    public function __construct(
        public int $id,
        public string $title,
        public Artist $artist,
    ) {
    }
}

class Artist
{
    public function __construct(
        public int $id,
        public string $name,
    ) {
    }
}

$json = '{
  "id": 456,
  "title": "The Wall",
  "artist": {
    "id": 789,
    "name": "Pink Floyd"
  }
}';

$mapper = new JsonMapper();
$album = $mapper->map($json, Album::class);

echo $album->artist->name; // Pink Floyd

数组

可以使用 @param 注解记录数组,这些注解将被解析并用于映射 JSON 数据

class Customer
{
    /**
     * @param Address[] $addresses
     */
    public function __construct(
        public int $id,
        public string $name,
        public array $addresses,
    ) {
    }
}

class Address
{
    public function __construct(
        public string $street,
        public string $city,
    ) {
    }
}

$json = '{
  "id": 123,
  "name": "John Doe",
  "addresses": [
    {
      "street": "123 Main Street",
      "city": "New York"
    },
    {
      "street": "456 Side Street",
      "city": "New York"
    }
  ]
}';

$mapper = new JsonMapper();
$customer = $mapper->map($json, Customer::class);

foreach ($customer->addresses as $address) {
    var_export($address instanceof Address); // true
}

联合类型

如果参数被声明为可能的类型联合,则 JsonMapper 将自动尝试将 JSON 数据映射到正确的类型

class Order
{
    public function __construct(
        public readonly int $id,
        public readonly string $amount,
        public readonly Person|Company $customer, // union type
    ) {
    }
}

class Person
{
    public function __construct(
        public readonly int $id,
        public readonly string $firstname,
        public readonly string $lastname,
    ) {
    }
}

class Company
{
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly string $companyNumber,
    ) {
    }
}

$json = '{
  "id": 1,
  "amount": "24.99",
  "customer": {
    "id": 2,
    "firstname": "John",
    "lastname": "Doe"
  }
}';

$mapper = new JsonMapper();
$order = $mapper->map($json, Order::class);

// JsonMapper automatically determined that the "id", "firstname",
// and "lastname" properties correspond to a Person and not a Company.
var_export($order->customer instanceof Person); // true

为此,JsonMapper 尝试将 JSON 对象映射到联合中可能的所有 PHP 类。如果没有类匹配,或者有多个类匹配,则抛出异常。

复杂联合

JsonMapper 可以解析、映射和验证任何可能嵌套的类型组合

/**
 * @param (Person|Company|(string|int)[])[]|null $customers
 */
public function __construct(
    public readonly ?array $customers,
) {
}

这目前有两个限制

  • 您必须使用 Type[] 语法,而不是 array 语法;

  • 联合中不能使用多个数组类型;例如,这是允许的

    /**
     * @param (Person|Company)[] $value
     */

    但不允许这种情况

    /**
     * @param Person[]|Company[] $value
     */

枚举

JsonMapper 可以将 JSON 字符串和整数映射到有后盾的枚举

class Order
{
    public function __construct(
        public readonly int $id,
        public readonly OrderStatus $status,
    ) {
    }
}

enum OrderStatus: string {
    case PENDING = 'pending';
    case SHIPPED = 'shipped';
    case DELIVERED = 'delivered';
}

$json = '{
  "id": 1,
  "status": "shipped"
}';

$mapper = new JsonMapper();
$order = $mapper->map($json, Order::class);

var_export($order->status === OrderStatus::SHIPPED); // true

非后盾枚举,即没有 stringint 值的枚举,出于故意不支持。

严格性

该库具有非常严格的默认值(其中一些可以通过配置覆盖),如果 JSON 数据与 DTO 的构造函数签名不精确匹配,或者 DTO 包含无效或不受支持的 @param 注解,则将抛出异常。

参数类型必须完全匹配,与 PHP 的 strict_types 具有相同的语义。

JsonMapper 保证每个构造函数参数,即使是使用 @param 软类型声明的,都将传递与声明类型兼容的值。 结果是您可以100%信任的 DTO。

选项

JsonMapper 构造函数接受以下选项

  • $allowUntypedArrays

    默认情况下,如果参数被声明为 array 而没有对应的 @param 注解,或者仅仅被文档说明为 @param array,则 JsonMapper 将会抛出异常。

    将此选项设置为 true,则 JsonMapper 将允许此类参数,并接受将 JSON 数组原样传递,而不检查或映射其内容

    $mapper = new JsonMapper(
        allowUntypedArrays: true,
    );
  • $allowUntypedObjects

    默认情况下,如果参数被声明为 objectstdClass,则 JsonMapper 将会抛出异常。

    将此选项设置为 true,则 JsonMapper 将允许此类参数,并接受将 JSON 对象作为 stdClass 实例传递,而不检查或映射其内容

    $mapper = new JsonMapper(
        allowUntypedObjects: true,
    );
  • $allowMixed

    默认情况下,如果参数被声明为 mixed,则 JsonMapper 将会抛出异常。

    将此选项设置为 true,则 JsonMapper 将允许此类参数,并接受将 JSON 值原样传递,而不检查或映射其内容

    $mapper = new JsonMapper(
        allowMixed: true,
    );
  • $onExtraProperties

    此选项接受一个 OnExtraProperties 枚举值,并控制当 JSON 对象包含在相应 DTO 构造函数签名中未匹配的属性时,JsonMapper 的反应方式

    • OnExtraProperties::THROW_EXCEPTION

      JsonMapper 将抛出 JsonMapperException。这是默认值。

    • OnExtraProperties::IGNORE

      JsonMapper 将忽略任何额外属性

      use Brick\JsonMapper\JsonMapper;
      use Brick\JsonMapper\OnExtraProperties;
      
      class Order
      {
          public function __construct(
              public readonly int $id,
              public readonly string $amount,
          ) {
          }
      }
      
      $json = '{
        "id": 1,
        "amount": "100.00",
        "extraProperty": "foo",
        "otherExtraProperty": "bar"
      }';
      
      $mapper = new JsonMapper(
          onExtraProperties: OnExtraProperties::IGNORE,
      );
      
      // extra properties "extraProperty" and "otherExtraProperty" are ignored,
      // and do not throw an exception anymore.
      $order = $mapper->map($json, Order::class);
  • $onMissingProperties

    此选项接受一个 OnMissingProperties 枚举值,并控制当 JSON 对象缺少在相应 DTO 构造函数签名中声明的属性时,JsonMapper 的反应方式

    • OnMissingProperties::THROW_EXCEPTION

      JsonMapper 将抛出 JsonMapperException。这是默认值。

    • OnMissingProperties::SET_NULL

      如果 JSON 属性缺失且参数是可空的,则 JsonMapper 将参数设置为 null

      use Brick\JsonMapper\JsonMapper;
      use Brick\JsonMapper\OnMissingProperties;
      
      class Order
      {
          public function __construct(
              public readonly int $id,
              public readonly ?string $customerName,
          ) {
          }
      }
      
      $json = '{
        "id": 1
      }';
      
      $mapper = new JsonMapper(
          onMissingProperties: OnMissingProperties::SET_NULL,
      );
      
      $order = $mapper->map($json, Order::class);
      var_export($order->customerName); // NULL

      如果属性缺失且参数不可空,则无论此选项如何,都会抛出异常。

    • OnMissingProperties::SET_DEFAULT

      如果 JSON 属性缺失且参数有默认值,则 JsonMapper 将参数设置为它的默认值

      use Brick\JsonMapper\JsonMapper;
      use Brick\JsonMapper\OnMissingProperties;
      
      class Order
      {
          public function __construct(
              public readonly int $id,
              public readonly string $customerName = 'no name',
          ) {
          }
      }
      
      $json = '{
        "id": 1
      }';
      
      $mapper = new JsonMapper(
          onMissingProperties: OnMissingProperties::SET_DEFAULT,
      );
      
      $order = $mapper->map($json, Order::class);
      var_export($order->customerName); // 'no name'

      如果属性缺失且参数没有默认值,则无论此选项如何,都会抛出异常。

  • $jsonToPhpNameMapper & $phpToJsonNameMapper

    默认情况下,JsonMapper 假设 JSON 属性名称与 PHP 参数名称相同。

    通过提供 NameMapper 接口的实现,您可以自定义两者之间的映射。

    该库为常见用例提供了两个实现

    • SnakeCaseToCamelCaseMapper 将转换 snake_casecamelCase
    • CamelCaseToSnakeCaseMapper 将转换 camelCasesnake_case

    示例

    use Brick\JsonMapper\JsonMapper;
    use Brick\JsonMapper\NameMapper\CamelCaseToSnakeCaseMapper;
    use Brick\JsonMapper\NameMapper\SnakeCaseToCamelCaseMapper;
    
    class Order
    {
        public function __construct(
            public readonly int $id,
            public readonly int $amountInCents,
            public readonly string $customerName,
        ) {
        }
    }
    
    $json = '{
      "id": 1,
      "amount_in_cents": 2499,
      "customer_name": "John Doe"
    }';
    
    $mapper = new JsonMapper(
        jsonToPhpNameMapper: new SnakeCaseToCamelCaseMapper(),
        phpToJsonNameMapper: new CamelCaseToSnakeCaseMapper(),
    );
    
    $order = $mapper->map($json, Order::class);
    
    echo $order->amountInCents; // 2499
    echo $order->customerName; // 'John Doe'