articus / data-transfer
库,仅当目标数据在此之后仍然有效时才合并源数据到目标数据
Requires
- php: ^7.4|^8.0
- articus/plugin-manager: ^0.1
Requires (Dev)
- ext-uopz: *
- doctrine/annotations: ^1.0|^2.0
- friends-of-phpspec/phpspec-code-coverage: *
- kahlan/kahlan: ^5.0
- laminas/laminas-servicemanager: ^3.17
- laminas/laminas-stdlib: ^3.2
- laminas/laminas-validator: ^2.12
- mockery/mockery: ^1.3
- phpspec/phpspec: ^5.1|^6.1|^7.0
- phpunit/php-code-coverage: ^9.2
- psr/simple-cache: ^1.0|^2.0|^3.0
Suggests
- doctrine/annotations: It is required for Articus\DataTransfer\MetadataProvider\Annotation (default metadata provider)
- laminas/laminas-stdlib: It is required for included metadata providers: Articus\DataTransfer\MetadataProvider\Annotation and Articus\DataTransfer\MetadataProvider\PhpAttribute
- laminas/laminas-validator: It is required for Articus\DataTransfer\Validator\Laminas
- psr/simple-cache: It is required for included metadata providers: Articus\DataTransfer\MetadataProvider\Annotation and Articus\DataTransfer\MetadataProvider\PhpAttribute
README
这个库提供了一个“验证式注入器”,一种仅在目标数据在此之后仍然有效时才使用源数据修复目标数据的服务的服务。源和目标可以是任何东西 - 标量、数组、对象等。所以,无论是你想用解析自HTTP请求的JSON来部分更新ORM实体,还是从实体生成一个简单的DTO以发送到AMQP消息,这个库都可以帮助你以一种整洁、方便的方式做到这一点。
它是如何工作的?
让我们先定义几个概念
- 类型化数据 - 一些复杂的应用特定、严格结构化的数据,如对象或对象数组。例如,DTO或ORM实体。
- 未类型化数据 - 与 类型化数据 相反 - 一些简单、通用、无形状的数据,如标量或标量数组或stdClass实例。例如,
json_decode
或yaml_parse
的结果。 - 提取 - 一种将 类型化数据 转换为 未类型化数据 的算法
- 合并 - 一种将一块 未类型化数据 与另一块 未类型化数据 修补的算法
- 验证 - 一种检查 未类型化数据 是否根据某些规则正确的算法
- 注入 - 一种将 类型化数据 与 未类型化数据 修补的算法
所以,如果我们有两块 类型化数据 - A 和 B - 这个库会执行一个相当简单的操作来 传输 A 到 B:它会合并从 A 和 B 中 提取 的 未类型化数据 块,验证 结果,如果验证成功,则使用从 A 中 提取 的 未类型化数据 修补 B。
为什么要这样做?
我个人只需要一些东西来轻松地从不受信任的来源(如请求解析后的主体、请求头、请求查询参数等)更新DTO和Doctrine实体。就像FOSRestBundle的请求体转换器和JMS Serializer一样,但更灵活。最初的原型在构建API时非常有用,在几个生产项目中使用后,我最终决定将其作为一个独立的库发布。希望它对其他人也有用。
如何安装?
只需将 "articus/data-transfer"
添加到您的 composer.json,并查看库建议的包,以获取您可能想要使用的可选组件的额外依赖项。
如何使用?
该库提供了一个名为 Articus\DataTransfer\Service
的单一服务,允许以多种方式传输数据。因此,首先您需要将其注册到您的 PSR-11 容器中。您可以使用您喜欢的任何 PSR-11 实现,但与 Laminas Service Manager 的集成具有更多功能(更准确地说 - 利用 插件管理器 和对 Laminas 验证器 的支持)。以下是两个示例配置
// Full example configuration in YAML just for readability $configContent = <<<'CONFIG' # Required container services dependencies: factories: # Service to inject wherever you need data transfer Articus\DataTransfer\Service: Articus\DataTransfer\Factory # ..and its dependencies Articus\DataTransfer\MetadataProvider\Annotation: Articus\DataTransfer\MetadataProvider\Factory\Annotation Articus\DataTransfer\Strategy\PluginManager: Articus\DataTransfer\Strategy\Factory\LaminasPluginManager Articus\DataTransfer\Validator\PluginManager: Articus\DataTransfer\Validator\Factory\LaminasPluginManager # Optional - only if you want to use validators from laminas/laminas-validator Laminas\Validator\ValidatorPluginManager: Laminas\Validator\ValidatorPluginManagerFactory # Default metadata provider service allows to get metadata both for classes and for class fields so two aliases for single service aliases: Articus\DataTransfer\ClassMetadataProviderInterface: Articus\DataTransfer\MetadataProvider\Annotation Articus\DataTransfer\FieldMetadataProviderInterface: Articus\DataTransfer\MetadataProvider\Annotation # Configure metadata provider Articus\DataTransfer\MetadataProvider\Annotation: # Configure directory to store cached class metadata cache: directory: ./data # ... or use existing service implementing Psr\SimpleCache\CacheInterface (PSR-16) #cache: MyMetadataCache # Configure strategy plugin manager using options supported by Laminas\ServiceManager\AbstractPluginManager Articus\DataTransfer\Strategy\PluginManager: invokables: MySampleStrategy: My\SampleStrategy # Configure validator plugin manager using options supported by Laminas\ServiceManager\AbstractPluginManager Articus\DataTransfer\Validator\PluginManager: invokables: MySampleValidator: My\SampleValidator CONFIG; $config = yaml_parse($configContent); $container = new Laminas\ServiceManager\ServiceManager($config['dependencies']); $container->setService('config', $config); /** @var Articus\DataTransfer\Service $service */ $service = $container->get(Articus\DataTransfer\Service::class);
- 针对 Symfony 依赖注入
<?php require_once __DIR__ . '/vendor/autoload.php'; // Full example configuration in YAML just for readability $configContent = <<<'CONFIG' # Required container services dependencies: factories: # Service to inject wherever you need data transfer Articus\DataTransfer\Service: Articus\DataTransfer\Factory # ..and its dependencies Articus\DataTransfer\MetadataProvider\Annotation: Articus\DataTransfer\MetadataProvider\Factory\Annotation Articus\DataTransfer\Strategy\PluginManager: Articus\DataTransfer\Strategy\Factory\SimplePluginManager Articus\DataTransfer\Validator\PluginManager: Articus\DataTransfer\Validator\Factory\SimplePluginManager # Default metadata provider service allows to get metadata both for classes and for class fields so two aliases for single service aliases: Articus\DataTransfer\ClassMetadataProviderInterface: Articus\DataTransfer\MetadataProvider\Annotation Articus\DataTransfer\FieldMetadataProviderInterface: Articus\DataTransfer\MetadataProvider\Annotation # Configure metadata provider Articus\DataTransfer\MetadataProvider\Annotation: # Configure directory to store cached class metadata cache: directory: ./data # ... or use existing service implementing Psr\SimpleCache\CacheInterface (PSR-16) #cache: MyMetadataCache # Configure strategy plugin manager, check Articus\PluginManager\Options\Simple for supported options Articus\DataTransfer\Strategy\PluginManager: invokables: MySampleStrategy: My\SampleStrategy # Configure validator plugin manager, check Articus\PluginManager\Options\Simple for supported options Articus\DataTransfer\Validator\PluginManager: invokables: MySampleValidator: My\SampleValidator CONFIG; $config = yaml_parse($configContent); $container = new Symfony\Component\DependencyInjection\ContainerBuilder(); $containerRef = new Symfony\Component\DependencyInjection\Reference('service_container'); foreach ($config['dependencies']['factories'] as $serviceName => $factoryClass) { $container->register($factoryClass); $container->register($serviceName) ->setFactory(new Symfony\Component\DependencyInjection\Reference($factoryClass)) ->setArguments([$containerRef, $serviceName]) ->setPublic(true) ; } foreach ($config['dependencies']['aliases'] as $alias => $serviceName) { $container->setAlias($alias, $serviceName)->setPublic(true); } // Just to reduce sample code size - there should be a dedicated factory class for normal usage $configFactory = new class ($config) { protected ArrayAccess $config; public function __construct(array $config) { $this->config = new ArrayObject($config); } public function getConfig(): ArrayAccess { return $this->config; } }; $container->register('config', ArrayAccess::class)->setFactory([$configFactory, 'getConfig'])->setPublic(true); $container->compile(); /** @var Articus\DataTransfer\Service $service */ $service = $container->get(Articus\DataTransfer\Service::class);
这是使用提供最明确和精细粒度控制的 Articus\DataTransfer\Service::transfer
方法所需的唯一要求。
如果您为要使用数据传输服务的类提供一些额外的元数据,则将提供更多方便的方法
Articus\DataTransfer\Service::transferTypedData
Articus\DataTransfer\Service::transferToTypedData
Articus\DataTransfer\Service::transferFromTypedData
Articus\DataTransfer\Service::extractFromTypedData
目前,本说明书中代码示例中显示的默认声明元数据的方式是通过 Doctrine 注解。如果您的项目使用 PHP 8+,您可以通过 属性 来声明元数据(只需将 Articus\DataTransfer\MetadataProvider\Annotation
切换到 Articus\DataTransfer\MetadataProvider\PhpAttribute
)。并且如果您想从其他来源获取元数据,您可以创建自己的 Articus\DataTransfer\ClassMetadataProviderInterface
实现。
元数据由两部分组成
- 策略 - 知道如何 提取、合并 和 填充 类对象的
Articus\DataTransfer\Strategy\StrategyInterface
实现 - 验证器 - 知道如何验证来自类对象的 未类型化数据 的一个或多个
Articus\DataTransfer\Validator\ValidatorInterface
实现
一个类可能有几个由名称区分的元数据子集,默认子集名称为空字符串
<?php use Articus\DataTransfer\Annotation as DTA; /** * Default metadata subset. * @DTA\Strategy(name="MySampleStrategy") * @DTA\Validator(name="MySampleValidator") * * Metadata subset with several validators. * They will be checked in the same order they declared or according priority. * If validator is "blocker" then all following validators will be skipped when it finds violations. * @DTA\Strategy(name="MySampleStrategy", subset="several-validators") * @DTA\Validator(name="MySampleValidator2", subset="several-validators", blocker=true) * @DTA\Validator(name="MySampleValidator3", subset="several-validators") * @DTA\Validator(name="MySampleValidator1", priority=2, subset="several-validators") * * Strategies and validators are constructed via plugin managers from articus/plugin-manager, * so you may pass options to their factories. * Check Articus\DataTransfer\Strategy\Factory\SimplePluginManager and Articus\DataTransfer\Validator\Factory\SimplePluginManager for details. * @DTA\Strategy(name="MySampleStrategy", options={"test":123}, subset="with-options") * @DTA\Validator(name="MySampleValidator", options={"test":123}, subset="with-options") */ class Sample { }
内置策略和验证器
通常对象的数据传输仅意味着其属性的数据传输。库提供了一个方便的方式来处理这种情况。如果您为类属性添加一些特殊元数据,则将使用 Articus\DataTransfer\Strategy\FieldData
作为类策略,并将 Articus\DataTransfer\Validator\FieldData
添加到类验证器列表的最高优先级
<?php use Articus\DataTransfer\Annotation as DTA; class Sample { /** * Usual public property will be accessed directly * @DTA\Data() */ public $property; /** * Property name and untyped data field for extraction/hydration may differ * @DTA\Data(field="fancy-property") */ public $renamedProperty; /** * Protected or private property will be accessed by conventional getter and setter if they exist * @DTA\Data() */ protected $propertyWithAccessors; public function getPropertyWithAccessors() { return $this->propertyWithAccessors; } public function setPropertyWithAccessors($propertyWithAccessors) { $this->propertyWithAccessors = $propertyWithAccessors; } /** * And that is how you can set custom getter and setter names for protected or private property * @DTA\Data(getter="customGetAccessor", setter="customSetAccessor") */ protected $propertyWithCustomAccessors; public function customGetAccessor() { return $this->propertyWithCustomAccessors; } public function customSetAccessor($propertyWithCustomAccessors) { $this->propertyWithCustomAccessors = $propertyWithCustomAccessors; } /** * If you property does not have setter (or getter) just set empty string. * Property without setter will not be hydrated, property without getter will not be extracted. * @DTA\Data(setter="") */ protected $propertyWithoutSetter; public function getPropertyWithoutSetter() { return $this->propertyWithoutSetter; } /** * You can also use your own strategy and/or your own validators for property like for whole class * @DTA\Data() * @DTA\Strategy(name="MyStrategy") * @DTA\Validator(name="MyValidator") * @var mixed */ public $customValue; /** * Library provides simple strategy and simple validator for embedded objects. * Check Articus\DataTransfer\Strategy\Factory\NoArgObject and Articus\DataTransfer\Validator\Factory\TypeCompliant for details. * @DTA\Data() * @DTA\Strategy(name="Object", options={"type":MyClass::class}) * @DTA\Validator(name="TypeCompliant", options={"type":MyClass::class}) * @var MyClass */ public $objectValue; /** * ... and simple strategy for lists of embedded objects and simple validator for lists * Check Articus\DataTransfer\Strategy\Factory\NoArgObjectList and Articus\DataTransfer\Validator\Factory\Collection for details. * @DTA\Data() * @DTA\Strategy(name="ObjectArray", options={"type":MyClass::class}) * @DTA\Validator(name="Collection",options={"validators":{ * {"name": "TypeCompliant", "options": {"type":MyClass::class}}, * }}) * @var MyClass[] */ public $objectArray; /** * Even if there is no validators value will be tested not to be null. * Mark property "nullable" if you do not want that. * And if you set any validators for nullable property they will be executed only for not null value. * @DTA\Data(nullable=true) * @DTA\Validator(name="MyValidatorForNotNullValue") * @var string */ public $nullableString; /** * Library provides simple abstract factory to use validators from laminas/laminas-validator seamlessly. * If you enable this integration in your container configuration (check configuration sample for details) * you may use any validator registered in Laminas\Validator\ValidatorPluginManager. * @DTA\Data() * @DTA\Validator(name="StringLength",options={"min": 1, "max": 5}) * @DTA\Validator(name="Hex") */ public $laminasValidated; }
与类元数据类似,属性元数据可能有多个子集,并且 Doctrine 注解 是声明属性元数据的默认方式。如果您的项目使用 PHP 8+,您可以通过 属性 来声明属性元数据(只需将 Articus\DataTransfer\MetadataProvider\Annotation
切换到 Articus\DataTransfer\MetadataProvider\PhpAttribute
)。并且如果您想使用其他元数据源,您可以创建自己的 Articus\DataTransfer\FieldMetadataProviderInterface
实现。
享受!
我真心希望这个库除了我之外对其他人也有用。它用于生产目的,但缺少了很多细化,特别是在测试和文档方面。
如果您有任何建议、建议、问题或修复,请随时提交问题或拉取请求。