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
相关产品推荐
相关产品推荐

