articus/data-transfer

库,仅当目标数据在此之后仍然有效时才合并源数据到目标数据

0.6.2 2024-02-18 13:37 UTC

This package is auto-updated.

Last update: 2024-09-12 15:30:59 UTC


README

GitHub Actions: Run tests Coveralls Codacy

这个库提供了一个“验证式注入器”,一种仅在目标数据在此之后仍然有效时才使用源数据修复目标数据的服务的服务。源和目标可以是任何东西 - 标量、数组、对象等。所以,无论是你想用解析自HTTP请求的JSON来部分更新ORM实体,还是从实体生成一个简单的DTO以发送到AMQP消息,这个库都可以帮助你以一种整洁、方便的方式做到这一点。

它是如何工作的?

让我们先定义几个概念

  • 类型化数据 - 一些复杂的应用特定、严格结构化的数据,如对象或对象数组。例如,DTO或ORM实体。
  • 未类型化数据 - 与 类型化数据 相反 - 一些简单、通用、无形状的数据,如标量或标量数组或stdClass实例。例如,json_decodeyaml_parse的结果。
  • 提取 - 一种将 类型化数据 转换为 未类型化数据 的算法
  • 合并 - 一种将一块 未类型化数据 与另一块 未类型化数据 修补的算法
  • 验证 - 一种检查 未类型化数据 是否根据某些规则正确的算法
  • 注入 - 一种将 类型化数据未类型化数据 修补的算法

所以,如果我们有两块 类型化数据 - AB - 这个库会执行一个相当简单的操作来 传输 AB:它会合并从 AB提取未类型化数据 块,验证 结果,如果验证成功,则使用从 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);
<?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 实现。

享受!

我真心希望这个库除了我之外对其他人也有用。它用于生产目的,但缺少了很多细化,特别是在测试和文档方面。

如果您有任何建议、建议、问题或修复,请随时提交问题或拉取请求。