foorg / moose
Moose - l[oose]对象[m]apper。它允许您将任意数据映射到对象,尝试将值强制转换为指定的类型,并在发生验证错误时优雅地失败。这对于消费或编写API非常有用,其中传入的数据可能格式或类型无效。
Requires
Requires (Dev)
- php: ^7
- phpunit/phpunit: ^5
This package is not auto-updated.
Last update: 2024-09-14 19:47:36 UTC
README
L[oose]对象[M]apper - 它在对象上映射数据,仅在优雅地失败。当某些数据项不正确时,它不会抛出异常,而是只返回收集到的错误列表和一个部分对象。
这对于消费第三方API或构建自己的API非常有用,您需要报告所有无效的数据项。
安装
您可以使用以下命令将此库添加到项目中: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())
(嵌套级别没有限制)
顺便说一下,只需要一个参数的类型(即ArrayField
、DateField
、MapField(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"}
}
缓存元数据
有两个独立的元数据提供者:AnnotationMetadataProvider
和CacheMetadataProvider
。为了缓存元数据,您需要使用ProdCacheMetadataProvider
或InvalidatingAnnotationMetadataProvider
将它们连接起来。它们的行为不同,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; ... }