You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Yii2/原生PHP中DTO嵌套数组的设计模式处理方案

问题场景

基于Yii2框架与PHP 7.1开发,首次尝试使用DTO,定义了嵌套结构的DTO(OrderDto包含OrderedProductDto[]类型的orderedProducts属性)。在ExternalOrderHydrator中尝试用load()方法直接加载含嵌套子数组的数组到顶层DTO时,无法自动将子数组转换为对应的子DTO对象,手动构建效率低下,希望找到能自动完成嵌套数组到DTO对象转换的方案。

现有DTO定义

use yii\base\Model;
class OrderDto extends Model
{
    /** @var OrderedProductDto[] $orderedProducts */
    protected $orderedProducts;

    /** @var integer $orderTaxAmount */
    protected $orderTaxAmount;

    /** @var integer $orderTotalAmount */
    protected $orderTotalAmount;

    public function rules()
    {
        // 验证规则
    }
}

class OrderedProductDto extends Model
{
    /** @var integer $quantity */
    protected $quantity;

    /** @var integer $orderedProductName */
    protected $orderedProductName ;

    public function rules()
    {
        // 验证规则
    }
}

现有Hydrator逻辑

class ExternalOrderHydrator
{
   public static function getOrderDtoFromOrder(Order $order)
   {
     // 生成嵌套数组的逻辑
     $array = [
         'orderTaxAmount' => 5, 
         'orderTotalAmount' => 100, 
         'orderedProducts' => [
             ['quantity' => 5, 'orderedProductName' => 'Product 1'],
         ],
     ];
     
      $orderDto = new OrderDto();
      if (!($orderDto->load($array) and $orderDto->validate())) {
        throw new Exception('Something is wrong with provided data.');
      }
      return $orderDto;
   }
}

解决方案

针对Yii2与PHP7.1环境,提供三种可行方案:

方案1:重写顶层DTO的自定义加载方法

在OrderDto中新增自定义加载方法,手动处理嵌套DTO的转换,同时保留Yii2的验证逻辑:

use yii\base\Model;
use Exception;

class OrderDto extends Model
{
    // 属性与rules不变

    public function rules()
    {
        return [
            [['orderTaxAmount', 'orderTotalAmount'], 'integer'],
            ['orderedProducts', 'each', 'rule' => ['validateNestedProduct']],
        ];
    }

    // 验证嵌套DTO的有效性
    public function validateNestedProduct($attribute, $params)
    {
        foreach ($this->$attribute as $product) {
            if (!$product instanceof OrderedProductDto || !$product->validate()) {
                $this->addError($attribute, '无效的商品数据');
                break;
            }
        }
    }

    // 自定义加载方法,处理嵌套转换
    public function loadWithNested($data, $formName = null)
    {
        // 先加载顶层属性
        parent::load($data, $formName);
        
        // 转换嵌套商品数组为DTO对象
        if (!empty($data['orderedProducts'])) {
            $this->orderedProducts = [];
            foreach ($data['orderedProducts'] as $productData) {
                $productDto = new OrderedProductDto();
                if (!$productDto->load($productData) || !$productDto->validate()) {
                    throw new Exception('商品数据格式错误');
                }
                $this->orderedProducts[] = $productDto;
            }
        }
        
        return $this->validate();
    }
}

修改Hydrator调用:

class ExternalOrderHydrator
{
   public static function getOrderDtoFromOrder(Order $order)
   {
     $array = [/* 生成的嵌套数组 */];
     
     $orderDto = new OrderDto();
     if (!$orderDto->loadWithNested($array)) {
       throw new Exception('订单数据格式错误');
     }
     return $orderDto;
   }
}

方案2:给DTO添加静态创建方法

为每个DTO添加静态方法,封装数组到对象的转换逻辑,代码更模块化:

// 子DTO添加静态创建方法
class OrderedProductDto extends Model
{
    // 属性与rules不变
    
    public static function createFromArray(array $data): self
    {
        $dto = new self();
        if (!$dto->load($data) || !$dto->validate()) {
            throw new Exception('商品数据无效');
        }
        return $dto;
    }
}

// 顶层DTO添加静态创建方法
class OrderDto extends Model
{
    // 属性与rules不变
    
    public static function createFromArray(array $data): self
    {
        $dto = new self();
        $dto->load($data);
        
        // 转换嵌套商品
        if (!empty($data['orderedProducts'])) {
            $dto->orderedProducts = array_map([OrderedProductDto::class, 'createFromArray'], $data['orderedProducts']);
        }
        
        if (!$dto->validate()) {
            throw new Exception('订单数据无效');
        }
        return $dto;
    }
}

修改Hydrator调用:

class ExternalOrderHydrator
{
   public static function getOrderDtoFromOrder(Order $order)
   {
     $array = [/* 生成的嵌套数组 */];
     
     try {
         return OrderDto::createFromArray($array);
     } catch (Exception $e) {
         throw new Exception('数据错误: ' . $e->getMessage());
     }
   }
}

方案3:通用反射Hydrator工具类

如果有大量嵌套DTO,可封装通用工具类,利用PHP反射识别属性类型,自动完成转换:

use yii\base\Model;
use ReflectionClass;
use ReflectionProperty;
use Exception;

class DtoHydrator
{
    public static function hydrate(array $data, string $dtoClass): Model
    {
        $reflection = new ReflectionClass($dtoClass);
        $dto = $reflection->newInstance();
        
        foreach ($data as $key => $value) {
            if (!$reflection->hasProperty($key)) {
                continue;
            }
            
            $property = $reflection->getProperty($key);
            $property->setAccessible(true);
            $docComment = $property->getDocComment();
            
            // 识别DTO数组类型
            if (preg_match('/@var\s+([a-zA-Z0-9]+Dto)\[\s*\]/', $docComment, $matches)) {
                $itemClass = $matches[1];
                if (is_array($value)) {
                    $property->setValue($dto, array_map(function($itemData) use ($itemClass) {
                        return self::hydrate($itemData, $itemClass);
                    }, $value));
                }
            } 
            // 识别单个DTO类型
            elseif (preg_match('/@var\s+([a-zA-Z0-9]+Dto)/', $docComment, $matches)) {
                $itemClass = $matches[1];
                if (class_exists($itemClass) && is_array($value)) {
                    $property->setValue($dto, self::hydrate($value, $itemClass));
                }
            } 
            // 普通属性直接赋值
            else {
                $property->setValue($dto, $value);
            }
        }
        
        if (!$dto->validate()) {
            throw new Exception("{$dtoClass}数据验证失败");
        }
        
        return $dto;
    }
}

修改Hydrator调用:

class ExternalOrderHydrator
{
   public static function getOrderDtoFromOrder(Order $order)
   {
     $array = [/* 生成的嵌套数组 */];
     
     try {
         return DtoHydrator::hydrate($array, OrderDto::class);
     } catch (Exception $e) {
         throw new Exception('数据错误: ' . $e->getMessage());
     }
   }
}

内容的提问来源于stack exchange,提问作者rosecabbagedragon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 10:25:55