paysera/lib-normalization-bundle

在不紧密耦合到您的规范化格式的情况下对业务对象进行反/规范化

安装数: 17,562

依赖项: 2

建议者: 0

安全性: 0

星标: 0

关注者: 4

分支: 5

开放问题: 2

类型:symfony-bundle

1.3.0 2023-11-24 13:48 UTC

README

Latest Version on Packagist Software License Build Status Coverage Status Quality Score Total Downloads

此包允许在不紧密耦合到您的规范化格式的情况下对您的业务实体(纯PHP对象)进行反/规范化。通常,您会在将规范化结构转换为JSON之前或之后执行此操作。

为什么?

Symfony的Serializer组件包含正常化器作为其一部分。该组件是出于类似的原因创建的,但采用不同的方法。

Symfony组件默认公开您的业务实体,但允许复杂的但具有挑战性的配置选项。它还允许编写自定义规范化逻辑,但这些逻辑通常位于您的规范化类中(这些类可能是纯PHP对象)。

Paysera规范化库通过始终编写少量代码来拥抱简单性,以获得对情况的完全控制 - 规范化逻辑位于相关的类中,这些类通常从DIC注册。这允许使用其他服务,从数据库获取数据,在需要时调用远程服务,或在熟悉的PHP源代码中执行任何其他操作。您可以轻松重命名任何字段,使用任何自定义命名,为了向后兼容而复制某些数据,或者,好吧,只需编写任何其他代码。对于边缘情况,不需要复杂的配置,因为您可以对情况有完全的控制。

此包的主要功能

  • 在反规范化时通过集成 lib-object-wrapper 支持显式的类型安全;
  • 可以通过传递的数据猜测规范化类型;
  • 可以轻松重用其他反/规范化器,而不需要直接依赖;
  • 支持不同的规范化组,并提供默认组的回退;
  • 支持显式或隐式包含的字段,允许在规范化过程中调整性能。

安装

composer require paysera/lib-normalization-bundle

配置

paysera_normalization:
  register_normalizers:
    date_time:      # registers de/normalizers for DateTime, DateTimeImmutable and DateTimeInterface
      format: "U"   # Unix timestamp. Use any from https://php.ac.cn/manual/en/function.date.php

基本用法

为您的业务实体编写反/规范化器

<?php

// ...

class ContactDetailsNormalizer implements NormalizerInterface, ObjectDenormalizerInterface, TypeAwareInterface
{
    public function getType(): string
    {
        return ContactDetails::class;
    }

    /**
     * @param ContactDetails $data
     * @param NormalizationContext $normalizationContext
     *
     * @return array
     */
    public function normalize($data, NormalizationContext $normalizationContext)
    {
        return [
            'email' => $data->getEmail(),
            // will automatically follow-up with normalization by guessed types:
            'residence_address' => $data->getResidenceAddress(),
            'shipping_addesses' => $data->getShippingAddresses(),
        ];
    }

    public function denormalize(ObjectWrapper $data, DenormalizationContext $context)
    {
        return (new ContactDetails())
            ->setEmail($data->getRequiredString('email'))
            ->setResidenceAddress(
                $context->denormalize($data->getRequiredObject('residence_address'), Address::class)
            )
            ->setShippingAddresses(
                $context->denormalizeArray($data->getArrayOfObject('shipping_addesses'), Address::class)
            )
        ;
    }
}
<?php

// ...

class AddressNormalizer implements NormalizerInterface, ObjectDenormalizerInterface, TypeAwareInterface
{
    private $countryRepository;
    private $addressBuilder;

    // ...

    public function getType(): string
    {
        return Address::class;
    }

    /**
     * @param Address $data
     * @param NormalizationContext $normalizationContext
     *
     * @return array
     */
    public function normalize($data, NormalizationContext $normalizationContext)
    {
        return [
            'country_code' => $data->getCountry()->getCode(),
            'city' => $data->getCity(),
            'full_address' => $this->addressBuilder->buildAsText($data->getStreetData()),
        ];
    }

    public function denormalize(ObjectWrapper $data, DenormalizationContext $context)
    {
        $code = $data->getRequiredString('country_code');
        $country = $this->countryRepository->findOneByCode($code);
        if ($country === null) {
            throw new InvalidDataException(sprintf('Unknown country %s', $code));
        }   

        return (new Address())
            ->setCountry($country)
            ->setCity($data->getRequiredString('city'))
            ->setStreetData(
                $this->addressBuilder->parseFromText($data->getRequiredString('full_address'))
            )
        ;
    }
}

如果您不使用自动配置(还请注意,它仅在您实现 TypeAwareInterface 时才有效),则标记您的服务

<services>
    <service id="ContactDetailsNormalizer">
        <tag name="paysera_normalization.normalizer"/>
        <tag name="paysera_normalization.object_denormalizer"/>
    </service>

    <service id="AddressNormalizer">
        <!-- you can also use just this tag when you implement TypeAwareInterface -->
        <tag name="paysera_normalization.autoconfigured_normalizer"/>
    </service>
</services>

用于反/规范化

// inject $coreDenormalizer as paysera_normalization.core_denormalizer
// FQCN also works as service ID, so autowiring should work if you use it

// must be stdClass, not array
$data = json_decode('{
    "email":"a@example.com",
    "residence_address":{"country_code":"LT","city":"Vilnius","full_address":"Park street 182b-12"},
    "shipping_addresses":[]
}');
$contactDetails = $coreDenormalizer->denormalize($data, ContactDetails::class);


// inject $coreNormalizer as paysera_normalization.core_normalizer
// FQCN also works as service ID, so autowiring should work if you use it

$normalized = $coreNormalizer->normalize($contactDetails);

var_dump($normalized);
// object(stdClass)#1 (3) { ...

高级用法

可用的标签

如果服务不实现 TypeAwareInterface 接口,则需要 type 属性。您可以在任何情况下提供它以覆盖从 getType 方法返回的任何值。

group 属性指示将反/规范化器注册到特定规范化组,而不是默认组。有关更多详细信息,请参见下文规范化组的用法。

您可以在服务上使用多个(甚至相同的)标签。例如

<services>
    <service id="ContactDetailsNormalizer">
        <!-- Register as normalizer, use type returned from getType() -->
        <tag name="paysera_normalization.normalizer"/>
        <!-- Register with additional type -->
        <tag name="paysera_normalization.normalizer" type="contact_details"/>
        <!-- Register as object denormalizer, use type returned from getType() -->
        <tag name="paysera_normalization.object_denormalizer"/>
        <!-- Also register for normalization group "v2" -->
        <tag name="paysera_normalization.object_denormalizer" group="v2"/>
    </service>
</services>

规范化数据

规范化是将您的业务对象转换为“规范化”(纯)结构的过程。

当返回它们作为REST请求的响应、在发送到某些MQ系统之前、在存储到任何关系型或NoSQL数据库之前或在您需要平面、可管理的表示的任何其他情况下,都可以进行此操作。

规范化是通过使用 CoreNormalizerpaysera_normalization.core_normalizer 服务)的 normalize 方法来启动的,该方法的接口如下

public function normalize($data, string $type = null, NormalizationContext $context = null)

如果没有传递 $type,代码将按照以下顺序尝试找到已注册的规范化器

  • 对于标量值,返回相同的值;
  • 对于数组,它将其值映射到通过递归猜测它们的规范化器类型;
  • 对于对象,将按以下顺序查找具有以下类型的规范化器
    • 对象的完全限定类名;
    • 对象的全部父类;
    • 对象的全部实现接口;
    • 如果对象实现了Traversable接口,它会被当作数组对待;
  • 如果类型未解析,则会抛出NormalizerNotFoundException异常。

使用NormalizationContext,你可以自定义归一化组和包含的字段。请注意,当构建NormalizationContext时,它需要相同的CoreNormalizer实例——它会被传递给需要递归归一化内部结构的具体归一化实例。

<?php

/* @var CoreNormalizer $coreNormalizer */

$context = new NormalizationContext($coreNormalizer, ['*', 'user.address'], 'custom_group');

$normalized = $coreNormalizer->normalize($order, 'my_custom_type', $context);

$serialized = json_encode($normalized);

包含的字段

你可以配置一个字段数组,这些字段将包含在归一化结果中。

*表示对象的全部(默认)字段。

你可以使用.来表示子元素,例如user.addressuser.*

包含的字段用于两个不同的地方

  • 支持附加字段(不是默认提供的)或想要提供某些优化的归一化器,应手动使用传递的NormalizationContext检查字段包含情况。以下是一个示例;
  • 在获得归一化结构后,它会过滤掉,只留下最初包含的字段。
<?php

// ...

class ContactDetailsNormalizer implements NormalizerInterface
{
    /**
     * @param ContactDetails $data
     * @param NormalizationContext $normalizationContext
     *
     * @return array
     */
    public function normalize($data, NormalizationContext $normalizationContext)
    {
        return [
            'email' => $data->getEmail(),

            // this is only a possible optimization, as field would be still filtered out afterwards:
            'residence_address' => $normalizationContext->isFieldIncluded('residence_address')
                ? $data->getResidenceAddress()
                : null,

            // this is a field that will not be returned except if explicitly asked for:
            'shipping_addesses' => $normalizationContext->isFieldExplicitlyIncluded('shipping_addesses')
                ? $data->getShippingAddresses()
                : null,
        ];
    }
}

使用此结构,以下表格显示了不同字段配置下返回的结构。

通常,优化仅在您进行一些远程调用以获取数据或至少进行任何额外的数据库调用时才有意义。在此示例中,如果Doctrine之前没有通过关系加载数据,则可能是这种情况。

反归一化数据

反归一化是将“归一化”(平面)结构转换为您的业务对象的过程。

这可以在通过某个端点接收JSON、从MQ、数据库或其他任何您想读取归一化结构的地方完成。

使用CoreDenormalizerpaysera_normalization.core_denormalizer服务)的denormalize方法启动归一化。该方法具有以下接口

public function denormalize($data, string $type, DenormalizationContext $context = null)

这里需要类型,因为无法从其中猜测它。

你可以配置与DenormalizationContext一起使用的归一化组。与归一化过程一样,确保您将相同的CoreDenormalizer传递给您的结构化的DenormalizationContext

<?php

$normalized = json_decode($serialized);

/* @var CoreDenormalizer $coreDenormalizer */

$context = new DenormalizationContext($coreDenormalizer, 'custom_group');

$order = $coreDenormalizer->denormalize($normalized, 'my_custom_type', $context);

注册的反归一化器有一个接口(但永远不会同时拥有两个接口)

  • 对象反归一化器。它用于仅从(JSON)对象中反归一化。它们将ObjectWrapper实例作为第一个参数传递;
  • 混合类型反归一化器。它可以用于从任何结构中反归一化——标量类型、数组或对象。在对象的情况下,传递的是 plain stdClass实例,它们不会被转换为ObjectWrapper实例。

归一化组

每个反/归一化器可以属于某个具体的归一化组。未注册group属性的反/归一化器属于默认组——这意味着它们始终作为后备使用。

当使用自定义的归一化组时,查找反/归一化的算法如下

  • 查找与提供的组相同的反/归一化器;
  • 如果没有找到,则查找具有默认组的反/归一化器。

这允许轻松覆盖具体归一化器的逻辑,但在大多数常见用例中也有默认行为。

语义版本化

此包遵循语义版本化

此包的公共API(换句话说,如果您想轻松更新到新版本,则应仅使用这些功能)

  • 包的配置;
  • 只有本README中记录的服务;
  • 支持的DIC标签,在本README中有记录。

有关API中可以更改和不能更改的基本信息的更多信息,请参阅Symfony BC规则

运行测试

composer update
composer test

贡献

请随意创建问题并提交pull请求。

您可以使用以下命令修复任何代码风格问题

composer fix-cs