v5.0 2024-08-10 09:28 UTC

README

这个库主要用于Symfony,但也可以在原生PHP中使用。这个库与其它库有什么不同?该库可以根据您指定的规则自动从您指定的数据中收集对象,基本上这是来自Request的数据

安装

$ composer require codememory/dto

本文档将涵盖哪些内容?

  • 如何使用DTO?
  • 如何使用symfony/validator验证DTO?
  • 如何使用装饰器?
  • 装饰器中的上下文是什么意思?
  • 如何创建自己的装饰器?
  • 收集器是什么?如何创建自己的收集器?
  • 如何创建上下文工厂?
  • 如何创建自己的键命名策略?
  • 如何创建自己的DTO属性提供者?

[ ! ] 注意:在DataTransfer中,我们处理的所有属性都必须有访问修饰符 "public"

使用示例

<?php

use Codememory\Dto\Collectors\BaseCollector;
use Codememory\Dto\Decorators as DD;
use Codememory\Dto\Factory\ConfigurationFactory;
use Codememory\Dto\DecoratorHandlerRegistrar;
use Codememory\Reflection\ReflectorManager;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Codememory\Dto\DataTransferObjectManager;
use Codememory\Dto\Factory\ExecutionContextFactory;

enum StatusEnum
{
    case ACTIVATED;
    case NOT_ACTIVATED;
}

#[DD\ToType]
final class UserDto extends DataTransferObjectManager
{
    public ?string $name = null;
    public ?string $surname = null;
    public ?int $age = null;
    
    #[DD\ToEnum]
    public ?StatusEnum $status = null;
}

$userDto = new UserDto(
    new BaseCollector(), 
    new ConfigurationFactory(),
    new ExecutionContextFactory(),
    new DecoratorHandlerRegistrar(),
    new ReflectorManager(new FilesystemAdapter('dto', '/var/cache/codememory'))
);

// We start the assembly of DTO based on the transferred data
$userDto->collect([
    'name' => 'My Name',
    'surname' => 'My Surname',
    'age' => 80,
    'status' => 'ACTIVATED'
]);

// Result dump UserDto
/** 
  name    -> My Name
  surname -> My Surname
  age     -> 80
  status  -> StatusEnum::ACTIVATED (object)
*/

使用symfony/validator验证DTO

use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Constraints as Assert;
use Codememory\Reflection\ReflectorManager;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Codememory\Dto\DataTransferObjectManager;
use Codememory\Dto\Factory\ConfigurationFactory;
use Codememory\Dto\DecoratorHandlerRegistrar;
use Codememory\Dto\Factory\ExecutionContextFactory;

final ProductDto extends DataTransferObjectManager
{
    #[DD\Validation([
        new Assert\NotBlank(message: 'Name is required'),
        new Assert\Length(max: 5, maxMessage: 'Name must not exceed 5 characters')
    ])]
    public ?string $name = null;
}


$productDto = new ProductDto(
    new BaseCollector(),
    new ConfigurationFactory()
    new ExecutionContextFactory(),
    new DecoratorHandlerRegistrar(),
    new ReflectorManager(new FilesystemAdapter('dto', '/var/cache/codememory'))
);

$productDto->collect(['name' => 'Super name']);

// Validate
$validator = Validation::createValidatorBuilder()
    ->enableAnnotationMapping()
    ->getValidator();

$errors = $validator->validate($productDto);

foreach ($errors as $error) {
    echo $error->getMessage(); // Name must not exceed 5 characters
}

使用装饰器

装饰器有两种类型,一种指向DTO类,另一种指向DTO属性

与类上的装饰器不同的是,给定的装饰器将针对所有DTO属性执行,并且给定的装饰器将首先执行

use Codememory\Dto\Decorators as DD

// Decorator for class
#[DD\ToType] // This decorator will cast all DTO properties to the type specified by the property
final class OneDto extends AbstractDataTransferObject 
{
    public ?int $number = null;
    public array $list = [];
}

// Decorators for properties
final class TestDto extends AbstractDataTransferObject 
{
    #[DD\NestedDTO(OneDto::class)]
    public ?OneDto $one = null;
    
   
    // Multiple decorators
    // Priority works here, first ToEnum will be executed, and then IgnoreSetterCallForHarvestableObject
    #[DD\ToEnum]
    #[DD\IgnoreSetterCallForHarvestableObject]
    public ?StatusEnum $status = null;
}

装饰器列表

  • IgnoreSetterCallForHarvestableObject - 忽略对可收集对象的setter调用

  • PrefixSetterMethodForHarvestableObject - 改变设置可收集对象值的方法的前缀

    • $prefix - 前缀名称,例如 "set"
  • SetterMethodForHarvestableObject - 通过更改方法的全名来设置可收集对象的值

    • $name - 方法名称,例如 "setName"
  • NestedDTO - 嵌套数据传输,嵌套在数据传输属性中

    • $dto - 数据传输命名空间
    • $object (默认: null) - 要收集的对象的命名空间。如果没有传递值,则此装饰器附加的属性将忽略对收集对象的setter调用
    • $thenCallback (默认: null) - 回调方法名称,该方法应返回一个bool值,表示是否值得检查
  • ToEnum - 将收集数据中的值转换为枚举对象

    • $byValue (默认: false) - 在枚举中按值搜索,默认按名称搜索
  • ToEnumList - 与ToEnum装饰器类似,但此装饰器期望一个数组,并将尝试将值的每个元素转换为枚举

    • $unique (默认: true) - 这是一个新的参数,它将过滤输入数组以实现唯一性
  • ToType - 将收集数据中的值转换为特定类型

    • $type (默认: auto) - PHP类型或Interface DateTime的名称。默认情况下,它在属性的类型上工作
    • $onyData (默认: false) - 强制类型转换,仅对收集数据级别的值进行转换
  • Validation - 将symfony assert约束添加到验证队列

    • $assert - 如果此属性将被处理,则包含验证规则的数组
  • XSS - 保护输入字符串或数组中的字符串免受XSS攻击

  • ExpectArray - 期望一个常规数组

    • $expectKeys - 待处理键的数组,其余将被删除
  • ExpectMultiArray - 期望一个常规数组

    • $expectKeys - 待处理键的数组,其余将被删除
    • $itemKeyAsNumber (默认: true) - 将所有项目键转换为数字顺序
  • ExpectOneDimensionalArray - 期望一个一维数组

    • $types (默认: any) - 跳过的值类型数组

数据键命名策略列表

  • Codememory\Dto\DataKeyNamingStrategy\DataKeyNamingStrategyCamelCase - 将属性名称转换为 camelCase 以在数据中进行查找
  • Codememory\Dto\DataKeyNamingStrategy\DataKeyNamingStrategySnakeCase - 将属性名称转换为 snake_case 以在数据中进行查找
  • Codememory\Dto\DataKeyNamingStrategy\DataKeyNamingStrategyUpperCase - 将属性名称转换为 UPPER_CASE 以在数据中进行查找

属性提供者列表

  • Codememory\Dto\Provider\DataTransferObjectPrivatePropertyProvider - 只允许私有属性,忽略 AbstractDataTransferObject 属性
  • Codememory\Dto\Provider\DataTransferObjectProtectedPropertyProvider - 只允许受保护的属性,忽略 AbstractDataTransferObject 属性
  • Codememory\Dto\Provider\DataTransferObjectPublicPropertyProvider - 只允许公共属性,忽略 AbstractDataTransferObject 属性

解析上下文

这是一个 API 类,它位于装饰器内部,用于管理 dto 的状态或值、收集的对象以及收集数据中的值

方法

  • getDataTransferObject - 返回包含正在处理的属性的当前数据传输对象
  • getProperty - 返回当前处理的属性
  • getData - 返回用于收集 dto 和对象的输入数据
  • getDataValue - 从数据(在数据传输构建期间传递)中返回一个值
  • getDataTransferObjectValue - 返回设置到数据传输属性中的值
  • getValueForHarvestableObject - 返回设置到正在收集的对象中的值
  • getDataKey - 返回可用于从数据中获取值的键
  • getNameSetterMethodForHarvestableObject - 返回正在收集的对象的设置方法名称
  • isIgnoredSetterCallForHarvestableObject - 是否忽略收集对象的设置方法调用
  • isSkippedThisProperty - 是否跳过当前属性的处理

许多这些方法都有设置器。

数据传输对象方法

  • getCollector - 返回用于收集 dto 的收集器
  • getConfiguration - 返回 dto 配置
  • getExecutionContextFactory - 返回用于创建装饰器上下文的工厂
  • getReflectorManager - 返回反射管理器,更多详细信息请参阅 codememory/reflection
  • getClassReflector - 返回当前 dto 的反射
  • getHarvestableObject - 返回可收集的对象
  • setHarvestableObject - 设置构建对象
  • addDataTransferObjectPropertyConstraintsCollection - 添加 DTO 属性验证集合
  • getListDataTransferObjectPropertyConstrainsCollection - 获取所有 dto 属性验证集合的列表
  • getDataTransferObjectPropertyConstrainsCollection - 获取特定 dto 的集合验证属性
  • collect - 开始整个构建过程
  • recollectHarvestableObject - 将可收集对象重新构建为新传递的对象

创建自己的装饰器

use Attribute;
use Codememory\Dto\Interfaces\DecoratorInterface;
use Codememory\Dto\Interfaces\DecoratorHandlerInterface;
use Codememory\Dto\Interfaces\ExecutionContextInterface;
use Codememory\Reflection\ReflectorManager;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Codememory\Dto\DataTransferObjectManager;
use Codememory\Dto\Factory\ExecutionContextFactory;
use Codememory\Dto\Factory\ConfigurationFactory;
use Codememory\Dto\DecoratorHandlerRegistrar;

// Let's create a decorator that will combine the value of all properties and separate it with a certain character
#[Attribute(Attribute::TARGET_PROPERTY)] // Will only apply to properties
final class PropertyConcatenation implements DecoratorInterface 
{
    public function __construct(
        public readonly string $propertyNames,
        public readonly string $separator
    ) {}
    
    public function getHandler() : string
    {
        return PropertyConcatenationHandler::class;
    }
}

// Create decorator Handler
final class PropertyConcatenationHandler implements DecoratorHandlerInterface 
{
    /**
     * @param PropertyConcatenation $decorator
     */
    public function handle(ConstraintInterface $decorator, ExecutionContextInterface $context) : void
    {
        // Get the values of all passed properties
        $assignedValues = array_map(static function (string $property) use ($context) {
            return $context->getDataTransferObject()->$property;
        }, $decorator->propertyNames);
        
        // Update the current value by concatenating multiple values separating them with $separator
        $context->setDataTransferObjectValue(implode($decorator->separator, $assignedValues));
        $context->setValueForHarvestableObject($context->getDataTransferObject());
    }
}

// Let's test our decorator
final class TestDto extends DataTransferObjectManager
{
    public ?string $name = null;
    public ?string $surname = null;
    
    #[PropertyConcatenation(['name', 'surname'], '+')]
    public ?string $fullName = null;
}

$testDto = new TestDto(
    new BaseCollector(),
    new ConfigurationFactory(),
    new ExecutionContextFactory(),
    new DecoratorHandlerRegistrar(),
    new ReflectorManager(new FilesystemAdapter('dto', '/var/cache/codememory'))
);

// To register this decorator when you create a new instance by passing the configuration as the second argument to it, you must first register the decorator through this configuration
$testDto->getDecoratorHandlerRegistrar()->register(new PropertyConcatenationHandler());

$testDto->collect([
    'name' => 'Code',
    'surname' => 'Memory',
    'full_name' => 'test_full_name' // Our decorator will override this value
]);

echo $testDto->fullName // Code+Memory

创建自己的收集器

收集器 - 是一个 DTO 收集器,它在处理每个 DTO 属性中起着重要作用

use Codememory\Dto\Interfaces\CollectorInterface;
use Codememory\Dto\Interfaces\ExecutionContextInterface;
use Codememory\Dto\Interfaces\DecoratorInterface;

final class MyCollector implements CollectorInterface
{
    public function collect(ExecutionContextInterface $context) : void
    {
        // Here all processing begins on each property, this method is called every iteration of the properties
        
        // Example get property attributes
        foreach ($context->getProperty()->getAttributes() as $attribute) {
            $attributeInstance = $attribute->newInstance();
        
            if ($attributeInstance instanceof DecoratorInterface) {
                $decoratorHandler = $context->getDataTransferObject()->getDecoratorHandlerRegistrar()->getHandler($attributeInstance->getHandler());
                
                // ....
            }
        }
        
        // ....
    }
}

如何创建上下文工厂?

use Codememory\Dto\Interfaces\ExecutionContextFactoryInterface;
use Codememory\Dto\Interfaces\ExecutionContextInterface;
use Codememory\Dto\Interfaces\DataTransferObjectInterface;
use Codememory\Reflection\Reflectors\PropertyReflector;
use Codememory\Dto\Interfaces\ExecutionContextInterface;
use Codememory\Dto\Factory\ConfigurationFactory;

// Create a context
final class MyContext implements ExecutionContextInterface
{
    // Implementing Interface Methods...
}

// Creating a context factory
final class MyContextFactory implements ExecutionContextFactoryInterface
{
    public function createExecutionContext(DataTransferObjectInterface $dataTransferObject, PropertyReflector $property, array $data) : ExecutionContextInterface
    {
        $context = new MyContext();
        // ...
        
        return $context;
    }
}

// When creating a DTO instance, we pass this context factory
// Example:
new MyDto(new BaseCollector(), new ConfigurationFactory(), new MyContextFactory(), ...);

如何创建自己的键命名策略?

此策略将在传递给收集的“_{dto 属性名称}”中的数据中查找值

use Codememory\Dto\Interfaces\DataKeyNamingStrategyInterface;
use Codememory\Dto\Factory\ConfigurationFactory;

final class MyStrategyName implements DataKeyNamingStrategyInterface
{
    private ?\Closure $extension = null;

    public function convert(string $propertyName) : string
    {
        $name =  "_$propertyName";
        
        if (null !== $this->extension) {
            return call_user_func($this->extension, $name);
        }
        
        return $name;
    }
    
    // With this method, you need to give the opportunity to extend the convert method
    public function setExtension(callable $callback) : DataTransferObjectPropertyProviderInterface
    {
        $this->extension = $callback;
        
        return $this;
    }
}

$myDto = new MyDTO(new BaseCollector(), new ConfigurationFactory(), ...);

// To use this strategy, you need to change the configuration
$myDto->getConfiguration()->setDataKeyNamingStrategy(new MyStrategyName());

如何创建自己的DTO属性提供者?

提供者必须返回允许由收集器处理的 dto 属性!不要忘记忽略 AbstractDataTransferObject 属性,否则这些属性也将被处理

use Codememory\Dto\Interfaces\DataTransferObjectPropertyProviderInterface;
use Codememory\Reflection\Reflectors\ClassReflector;
use Codememory\Dto\Factory\ConfigurationFactory;

// The provider will say that only private properties need to be processed
final class MyPropertyProvider implements DataTransferObjectPropertyProviderInterface
{
    private ?\Closure $extension = null;

    public function getProperties(ClassReflector $classReflector) : array
    {
        $properties = $classReflector->getPrivateProperties();
        
        if (null !== $this->extension) {
            return call_user_func($this->extension, $properties);
        }
        
        return $properties;
    }
    
    // With this method, you need to give the opportunity to extend the getProperties method
    public function setExtension(callable $callback) : DataTransferObjectPropertyProviderInterface
    {
        $this->extension = $callback;
        
        return $this;
    }
}

$myDto = new MyDTO(new BaseCollector(), new ConfigurationFactory(), ...);

// Change the provider in the configuration
$myDto->getConfiguration()->setDataTransferObjectPropertyProvider(new MyPropertyProvider());