将JSON映射成特定的类

在 PHP 中,json_decode 函数默认会将 JSON 字符串解码为关联数组或 stdClass 对象。然而,当需要将 JSON 数据映射到特定的类(尤其是包含数组和多级嵌套结构的复杂对象)时,需要采取额外的步骤来实现这一点。以下是实现这一目标的详细指南,包括示例代码和解释。

一、基本概念

1. json_decode 的基本用法

$json = '{"name": "John", "age": 30}';
$data = json_decode($json);
// $data 是一个 stdClass 对象

2. 将 JSON 解码为关联数组

$data = json_decode($json, true);
// $data 是一个关联数组

然而,json_decode 并不直接支持将 JSON 数据解码为特定的自定义类实例。为了实现这一点,需要手动映射数据或使用辅助函数。

二、自定义类映射

假设有以下 JSON 数据,其中包含一个用户及其多个地址:

{
  "name": "John Doe",
  "age": 30,
  "addresses": [
    {
      "street": "123 Main St",
      "city": "New York",
      "country": "USA"
    },
    {
      "street": "456 High St",
      "city": "London",
      "country": "UK"
    }
  ]
}

1. 定义 PHP 类

首先,定义与 JSON 结构相对应的 PHP 类。

<?php

class Address {
    public string $street;
    public string $city;
    public string $country;

    public function __construct(string $street, string $city, string $country) {
        $this->street = $street;
        $this->city = $city;
        $this->country = $country;
    }
}

class User {
    public string $name;
    public int $age;
    /** @var Address[] */
    public array $addresses;

    public function __construct(string $name, int $age, array $addresses) {
        $this->name = $name;
        $this->age = $age;
        $this->addresses = $addresses;
    }
}

2. 创建映射函数

编写一个递归函数,将 stdClass 对象转换为特定的类实例。

<?php

function mapJsonToClass(object $data, string $className) {
    if (!class_exists($className)) {
        throw new Exception("Class $className does not exist.");
    }

    $reflectionClass = new ReflectionClass($className);
    $constructor = $reflectionClass->getConstructor();
    $parameters = $constructor->getParameters();
    $args = [];

    foreach ($parameters as $param) {
        $paramName = $param->getName();
        $paramType = $param->getType();

        if ($paramType === null) {
            throw new Exception("Parameter $paramName in class $className has no type hint.");
        }

        $typeName = $paramType->getName();

        if (class_exists($typeName)) {
            if (is_array($data->$paramName)) {
                // 假设数组元素类名为单数形式,例如 addresses 对应 Address
                $itemClass = rtrim($typeName, 's'); // 简单的复数转单数
                $items = [];
                foreach ($data->$paramName as $item) {
                    $items[] = mapJsonToClass($item, $itemClass);
                }
                $args[] = $items;
            } else {
                $args[] = mapJsonToClass($data->$paramName, $typeName);
            }
        } else {
            // 基本类型
            $args[] = $data->$paramName;
        }
    }

    return $reflectionClass->newInstanceArgs($args);
}

注意: 上述函数假设类的构造函数参数顺序与 JSON 属性顺序一致,并且复数形式的属性名对应单数形式的类名(例如 addresses 对应 Address)。在实际应用中,可能需要更复杂的逻辑来处理不同的命名约定和类型映射。

3. 使用映射函数

<?php

$json = '{
  "name": "John Doe",
  "age": 30,
  "addresses": [
    {
      "street": "123 Main St",
      "city": "New York",
      "country": "USA"
    },
    {
      "street": "456 High St",
      "city": "London",
      "country": "UK"
    }
  ]
}';

$data = json_decode($json);

$user = mapJsonToClass($data, 'User');

print_r($user);

输出结果:

User Object
(
    [name] => John Doe
    [age] => 30
    [addresses] => Array
        (
            [0] => Address Object
                (
                    [street] => 123 Main St
                    [city] => New York
                    [country] => USA
                )

            [1] => Address Object
                (
                    [street] => 456 High St
                    [city] => London
                    [country] => UK
                )

        )

)

三、处理多级嵌套

假设 JSON 数据更复杂,具有多级嵌套,例如用户包含多个订单,每个订单包含多个商品:

{
  "name": "John Doe",
  "age": 30,
  "addresses": [
    {
      "street": "123 Main St",
      "city": "New York",
      "country": "USA"
    }
  ],
  "orders": [
    {
      "orderId": 1001,
      "products": [
        {"productId": "A1", "name": "Product A"},
        {"productId": "B2", "name": "Product B"}
      ]
    },
    {
      "orderId": 1002,
      "products": [
        {"productId": "C3", "name": "Product C"}
      ]
    }
  ]
}

1. 定义额外的 PHP 类

<?php

class Product {
    public string $productId;
    public string $name;

    public function __construct(string $productId, string $name) {
        $this->productId = $productId;
        $this->name = $name;
    }
}

class Order {
    public int $orderId;
    /** @var Product[] */
    public array $products;

    public function __construct(int $orderId, array $products) {
        $this->orderId = $orderId;
        $this->products = $products;
    }
}

class User {
    public string $name;
    public int $age;
    /** @var Address[] */
    public array $addresses;
    /** @var Order[] */
    public array $orders;

    public function __construct(string $name, int $age, array $addresses, array $orders) {
        $this->name = $name;
        $this->age = $age;
        $this->addresses = $addresses;
        $this->orders = $orders;
    }
}

2. 更新映射函数以处理多级嵌套

现有的 mapJsonToClass 函数已经是递归的,可以处理多级嵌套。然而,为了更健壮地处理不同的命名约定,可以对其进行改进。例如,可以使用注释或属性来指定数组元素的类名。

以下是一个改进版的映射函数,使用 PHP 8 的属性来指定类的类型(需要 PHP 8.0+):

1. 使用属性指定类型

<?php

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
class JsonType {
    public string $type;

    public function __construct(string $type) {
        $this->type = $type;
    }
}

2. 更新类定义以使用属性

<?php

class Address {
    public string $street;
    public string $city;
    public string $country;

    public function __construct(string $street, string $city, string $country) {
        $this->street = $street;
        $this->city = $city;
        $this->country = $country;
    }
}

class Product {
    public string $productId;
    public string $name;

    public function __construct(string $productId, string $name) {
        $this->productId = $productId;
        $this->name = $name;
    }
}

class Order {
    public int $orderId;

    #[JsonType('Product')]
    public array $products;

    public function __construct(int $orderId, array $products) {
        $this->orderId = $orderId;
        $this->products = $products;
    }
}

class User {
    public string $name;
    public int $age;

    #[JsonType('Address')]
    public array $addresses;

    #[JsonType('Order')]
    public array $orders;

    public function __construct(string $name, int $age, array $addresses, array $orders) {
        $this->name = $name;
        $this->age = $age;
        $this->addresses = $addresses;
        $this->orders = $orders;
    }
}

3. 更新映射函数以使用属性信息

<?php

function mapJsonToClassWithAttributes(object $data, string $className) {
    if (!class_exists($className)) {
        throw new Exception("Class $className does not exist.");
    }

    $reflectionClass = new ReflectionClass($className);
    $constructor = $reflectionClass->getConstructor();
    $parameters = $constructor->getParameters();
    $args = [];

    foreach ($parameters as $param) {
        $paramName = $param->getName();
        $paramType = $param->getType();

        if ($paramType === null) {
            throw new Exception("Parameter $paramName in class $className has no type hint.");
        }

        $typeName = $paramType->getName();

        // 获取属性反射
        $property = $reflectionClass->getProperty($paramName);
        $attributes = $property->getAttributes(JsonType::class);
        $itemClass = null;

        if (!empty($attributes)) {
            // 假设只有一个 JsonType 属性
            $jsonType = $attributes[0]->newInstance();
            $itemClass = $jsonType->type;
        }

        if (class_exists($typeName)) {
            if (is_array($data->$paramName)) {
                if ($itemClass) {
                    $items = [];
                    foreach ($data->$paramName as $item) {
                        $items[] = mapJsonToClassWithAttributes($item, $itemClass);
                    }
                    $args[] = $items;
                } else {
                    $args[] = $data->$paramName;
                }
            } else {
                $args[] = mapJsonToClassWithAttributes($data->$paramName, $typeName);
            }
        } else {
            // 基本类型
            $args[] = $data->$paramName;
        }
    }

    return $reflectionClass->newInstanceArgs($args);
}

4. 使用改进后的映射函数

<?php

$json = '{
  "name": "John Doe",
  "age": 30,
  "addresses": [
    {
      "street": "123 Main St",
      "city": "New York",
      "country": "USA"
    }
  ],
  "orders": [
    {
      "orderId": 1001,
      "products": [
        {"productId": "A1", "name": "Product A"},
        {"productId": "B2", "name": "Product B"}
      ]
    },
    {
      "orderId": 1002,
      "products": [
        {"productId": "C3", "name": "Product C"}
      ]
    }
  ]
}';

$data = json_decode($json);

$user = mapJsonToClassWithAttributes($data, 'User');

print_r($user);

输出结果:

User Object
(
    [name] => John Doe
    [age] => 30
    [addresses] => Array
        (
            [0] => Address Object
                (
                    [street] => 123 Main St
                    [city] => New York
                    [country] => USA
                )

        )

    [orders] => Array
        (
            [0] => Order Object
                (
                    [orderId] => 1001
                    [products] => Array
                        (
                            [0] => Product Object
                                (
                                    [productId] => A1
                                    [name] => Product A
                                )

                            [1] => Product Object
                                (
                                    [productId] => B2
                                    [name] => Product B
                                )

                        )

                )

            [1] => Order Object
                (
                    [orderId] => 1002
                    [products] => Array
                        (
                            [0] => Product Object
                                (
                                    [productId] => C3
                                    [name] => Product C
                                )

                        )

                )

        )

)

四、使用现有库简化映射

手动编写映射函数在处理复杂或多变的 JSON 结构时可能会变得繁琐。可以使用现有的库来简化这一过程。以下是几个常用的 PHP 库:

1. JMS Serializer

JMS Serializer 是一个功能强大的库,支持复杂的序列化和反序列化,包括 JSON 到 PHP 对象的映射。

安装:

composer require jms/serializer

使用示例:

<?php

require 'vendor/autoload.php';

use JMS\Serializer\SerializerBuilder;

class Address {
    public string $street;
    public string $city;
    public string $country;
}

class Product {
    public string $productId;
    public string $name;
}

class Order {
    public int $orderId;
    /** @var Product[] */
    public array $products;
}

class User {
    public string $name;
    public int $age;
    /** @var Address[] */
    public array $addresses;
    /** @var Order[] */
    public array $orders;
}

$json = '{
  "name": "John Doe",
  "age": 30,
  "addresses": [
    {
      "street": "123 Main St",
      "city": "New York",
      "country": "USA"
    }
  ],
  "orders": [
    {
      "orderId": 1001,
      "products": [
        {"productId": "A1", "name": "Product A"},
        {"productId": "B2", "name": "Product B"}
      ]
    }
  ]
}';

$serializer = SerializerBuilder::create()->build();
$user = $serializer->deserialize($json, User::class, 'json');

print_r($user);

优点:

  • 支持复杂的映射和注释。

  • 支持循环引用和多态类型。

  • 丰富的配置选项。

缺点:

  • 需要学习和理解其配置和注解方式。

  • 增加项目的依赖。

2. Spatie Data Transfer Object (DTO)

Spatie DTO 是一个轻量级的库,用于将数组或 JSON 数据映射到 PHP 对象。

安装:

composer require spatie/data-transfer-object

使用示例:

<?php

require 'vendor/autoload.php';

use Spatie\DataTransferObject\DataTransferObject;

class AddressDTO extends DataTransferObject {
    public string $street;
    public string $city;
    public string $country;
}

class ProductDTO extends DataTransferObject {
    public string $productId;
    public string $name;
}

class OrderDTO extends DataTransferObject {
    public int $orderId;
    /** @var ProductDTO[] */
    public array $products;
}

class UserDTO extends DataTransferObject {
    public string $name;
    public int $age;
    /** @var AddressDTO[] */
    public array $addresses;
    /** @var OrderDTO[] */
    public array $orders;
}

$json = '{
  "name": "John Doe",
  "age": 30,
  "addresses": [
    {
      "street": "123 Main St",
      "city": "New York",
      "country": "USA"
    }
  ],
  "orders": [
    {
      "orderId": 1001,
      "products": [
        {"productId": "A1", "name": "Product A"},
        {"productId": "B2", "name": "Product B"}
      ]
    }
  ]
}';

$data = json_decode($json, true);
$user = new UserDTO($data);

print_r($user);

优点:

  • 简单易用,适合基本的映射需求。

  • 自动类型转换和验证。

缺点:

  • 功能不如 JMS Serializer 丰富。

  • 需要定义 DTO 类。

五、总结

将 JSON 数据映射到特定的 PHP 类涉及以下步骤:

  1. 定义 PHP 类:根据 JSON 结构定义相应的 PHP 类,确保属性和类型与 JSON 数据匹配。

  2. 编写映射逻辑:使用递归函数或现有库,将 stdClass 对象或关联数组转换为特定的类实例。

  3. 处理嵌套结构:确保映射函数能够递归处理多级嵌套的数组和对象。

  4. 使用库简化过程:对于复杂的映射需求,考虑使用现有的序列化/反序列化库,如 JMS Serializer 或 Spatie DTO。