foorg/moose

Moose - l[oose]对象[m]apper。它允许您将任意数据映射到对象,尝试将值强制转换为指定的类型,并在发生验证错误时优雅地失败。这对于消费或编写API非常有用,其中传入的数据可能格式或类型无效。

0.0.1 2016-12-17 18:52 UTC

This package is not auto-updated.

Last update: 2024-09-14 19:47:36 UTC


README

L[oose]对象[M]apper - 它在对象上映射数据,仅在优雅地失败。当某些数据项不正确时,它不会抛出异常,而是只返回收集到的错误列表和一个部分对象。

这对于消费第三方API或构建自己的API非常有用,您需要报告所有无效的数据项。

  1. 安装
  2. 使用
  3. 扩展

安装

您可以使用以下命令将此库添加到项目中:composer require foorg/moose

如何使用它

使用它的最方便方法是使用AnnotationMetadataProvider。假设我们有这样的API格式来发送电子邮件消息

{
    "recipient": {"fullname": "Leonard Cohen", "email": "leo@example.com"},
    "subject": "Subject text",
    "body": "message text"
}

如果我们使用适当的注解定义它们,我们可以将这些映射到对象上

class Message
{
    /**
     * @ObjectField("Recipient")
     **/
    private $recipient;

    /**
     * @StringField()
     **/
    private $subject;

    /**
     * @StringField()
     **/
    private $body;
}

class Recipient
{
    /**
     * @StringField()
     **/
    private $fullname;

    /**
     * @StringField()
     **/
    private $email;
}

以下是我们将json获取对象的示例

use moose\Mapper;
use moose\metadata\AnnotationMetadataProvider;
use Doctrine\Common\Annotations\AnnotationReader;
use function moose\default_coercers;

$reader = new AnnotationReader();
$mapper = new Mapper(new AnnotationMetadataProvider($reader), default_coercers());

$result = $mapper->map(Message::class, $json);

此外,还有为MetadataProvider提供的装饰器,允许正确缓存元数据,但这又是另一个故事

此库包含一些预定义的类型,以下是它们的列表

  • ArrayField([T: TypeRef])(如果您不需要同构数组,则可以省略T参数)
  • DateField(format)
  • MapField([K: TypeRef, V: TypeRef]|[V: TypeRef])
  • ObjectField(classname: string)
  • BoolField()
  • FloatField()
  • IntField()
  • StringField()
  • TaggedUnionField(tag: string, map: { tag: string => typeOrClassname: string|TypeRef })

TypeRef代表另一个(嵌套)的Field类型的注解,例如ArrayField(T=IntField())(嵌套级别没有限制)

顺便说一下,只需要一个参数的类型(即ArrayFieldDateFieldMapField(V)ObjectField)可以像这样实例化:ArrayField(IntField())(第一个参数不需要名称)

TaggedUnion允许您拥有可以包含列表中任何类型的字段。以下是使其工作的方式

class Event
{
    /**
     * @StringField()
     **/
    public $name;

    /**
     * @TaggedUnionField("type", "map" = {
     *   "payment" = "PaymentObject",
     *   "withdrawal" = "WithdrawalObject",
     *   "unknown" = MapField()
     * })
     **/
    public $payload;
}

此配置将能够映射以下形式的数据

{
  "name": "any name",
  "payload": {"type": "payment", "payment": "object", "fields": "etc"}
}

or

{
  "name": "any name",
  "payload": {"type": "unknown", "anything": "can", "go": "here"}
}

缓存元数据

有两个独立的元数据提供者:AnnotationMetadataProviderCacheMetadataProvider。为了缓存元数据,您需要使用ProdCacheMetadataProviderInvalidatingAnnotationMetadataProvider将它们连接起来。它们的行为不同,ProdCacheMetadataProvider将始终使用缓存中存在的元数据(如果存在),否则将创建它并将其存储在缓存中;而InvalidatingAnnotationMetadataProvider将始终检查是否需要预热缓存。在生产环境中自然使用前者,在开发模式中使用后者,尽管您可以使用无效提供者来处理两者,因为stat()系统调用并不昂贵。

添加新的数据类型

如果您想添加自己的数据类型,可能要替换和扩展现有的某些类型,或者添加一个新的类型,您可以非常容易地做到这一点,以下是方法。

让我们考虑一个假设的情况,我们有一个json API,其中有一个端点,在传入的数据中有一个名为ids的字段,它是一个用逗号分隔的ID列表,例如:ids=1,2,3,4。正如你可能猜到的,我们现有的ArrayField类型期望它是一个数组,但实际上它是一个字符串。

然而,我们可以创建自己的类型来优雅地处理这种情况。注意,在这种情况下扩展ArrayField可能更好,但这不会展示创建新类型的所有步骤。

我们将从创建自己的注解类开始。

use moose\annotation\Field;
use moose\annotation\exception\InvalidTypeException;
use function moose\type;

/**
 * @Annotation
 * @Target({"PROPERTY", "ANNOTATION"})
 */
class CommaArrayField extends Field
{
    public $T;

    public $separator;

    public function __construct(array $options)
    {
        if (isset($options["value"])) {
            $options["T"] = $options["value"]; // "value" is always the unnamed first argument of an annotation, if any
        }
        if ( ! isset($options["T"]) || ! $options["T"] instanceof Field) {
            throw new InvalidTypeException(self::class, "T", Field::class, type($options["T"]));
        }
        if ( ! isset($options["separator"])) {
            throw new InvalidTypeException(self::class, "separator", "string", "null");
        }

        parent::__construct($options);
    }

    public function getArgs()
    {
        return [$this->T, $this->separator]; // this will be placed in $metadata->args
    }

    public function getTypeName(): string
    {
        return "comma_array";
    }
}

现在我们需要创建映射器本身,但我们将它称为coercer,因为Moose不仅映射数据,还尝试将类型强制转换为正确的类型。

use moose\coercer\TypeCoercer;
use moose\Context;
use moose\ConversionResult;
use moose\error\TypeError;
use moose\metadata\TypeMetadata;
use function moose\type;

class CommaArrayCoercer implements TypeCoercer
{
    public function coerce($value, TypeMetadata $metadata, Context $ctx): ConversionResult
    {
        if ( ! \is_string($value)) {
            return ConversionResult::error(new TypeError("string", type($value)));
        }
        if (\strlen($value) === 0) {
            return ConversionResult::value([]);
        }

        $value = explode($metadata->args[1], $value);

        $errors = [];
        $type = $metadata->args[0]; /** @var TypeMetadata $type */
        $mapped = [];

        foreach ($value as $idx => $v) {
            $result = $ctx->coerce($v, $type);
            if ($result->getErrors()) {
                $errors[] = $result->errorsAtIdx($idx);

                // if some of our values couldn't be coerced completely, we can't say that this array
                // is correct so we bail out and return only errors
                if ($result->getValue() === null) {
                    return ConversionResult::errors(array_merge(...$errors));
                }
            }

            $mapped[] = $result->getValue();
        }

        $errors = $errors ? array_merge(...$errors) : [];

        return ConversionResult::errors($errors, $mapped);
    }
}

现在我们需要将此类型添加到传递给moose\Mapper的coercer中。

$coercers = default_coercers() + [
    // "comma_array" = CommaArrayField::getTypeName()
    "comma_array" => new CommaArrayField()
];
$mapper = new Mapper(new AnnotationMetadataProvider($reader), $coercers);

下面是如何在类中使用这个新注解的示例。

class IncrementImpressions
{
    /**
     * @CommaArrayField(@IntField(), separator=",")
     **/
    private $ids;

    ...
}