eventsourced / valueobjects
Requires
- php: >=5.5.9
- moneyphp/money: ^3.0
- nesbot/carbon: ~1.20
- ramsey/uuid: ~2.8
- respect/validation: 1.0.3
Requires (Dev)
- phpunit/phpunit: 5.2.1
This package is not auto-updated.
Last update: 2022-02-01 13:00:00 UTC
README
值对象(VOs)是任何领域驱动设计(DDD)应用的核心,它们确保值是有效的,并且会被您的领域接受。
根据我们的经验,大多数值对象库提供一系列值对象,但它们将它们锁定,因此很难扩展它们并创建新的对象。
因此,我们构建了这个值对象工具包,它使创建新的值对象快速、简单且无痛苦。
对于那些使用洋葱架构的人,请将这个库视为核心的一部分。
值对象和验证器
单个值
这些是给定单个值并必须进行验证的值对象。对于这些值对象,您只需要通过扩展父对象来指定它们的验证器。
创建新的单个值对象
use EventSourced\ValueObject\ValueObject; class Integer extends ValueObject\AbstractSingleValue { protected function validator() { return parent::validator()->intVal(); } }
访问值
如果您想访问单个值对象中持有的值,则可以这样做。
$integer = new Integer(1); echo $integer->value();
简单易行。
验证器
值对象使用验证器来完成其工作。我们决定使用出色的Respect Validation库,而不是编写自己的库。它拥有所有您可能需要的验证器,其语法简洁且优雅。
辅助方法"validator"返回Respect验证器的新实例,它已添加到所有抽象类中。
验证器链式调用
Respect验证器是可链式的,因此为您的值对象构建复杂的验证器轻而易举。
use EventSourced\ValueObject\ValueObject\Type\AbstractSingleValue; class Coordinate extends AbstractSingleValue { protected function validator() { return parent::validator()->floatVal()->between(-90, 90); } }
复合值对象
这些是由两个或更多值对象组成的值对象。它们是表示值对象配对的复合对象。一个例子是位置GPS坐标,实际上它是两个坐标的复合,即纬度和经度。
创建复合值对象
use EventSourced\ValueObject\ValueObject\Type\AbstractComposite; class GPSCoordinates extends AbstractComposite { protected $latitude; protected $longitude; public function __construct(Coordinate $latitude, Coordinate $longitude) { $this->latitude = $latitude; $this->longitude = $longitude; } public function latitude() { return $this->latitude; } public function longitude() { return $this->longitude; } }
因此,它只是一个持有多个值对象的容器。如果您想在值对象之间运行任何验证,应在构造函数中执行。基类负责处理"equals"方法,因此您不必担心。
可空复合对象
创建可空单个值对象很容易,创建可空复合对象则更难,并且应该作为最后的手段使用。尽管如此,它仍然很有用。
要创建可空复合对象,请将复合的所有默认值设置为null。就是这样。
<?php namespace EventSourced\ValueObject\ValueObject; class NullableGPSCoordinates extends GPSCoordinates { public function __construct(Coordinate $latitude=null, Coordinate $longitude=null) { $this->latitude = $latitude; $this->longitude = $longitude; } }
每个复合对象都提供is_null()
方法,因此您可以轻松检查VO是否实际上为null。
注意:当您将上述实例序列化,并且所有值都为null时,您将得到一个null响应,而不是带有键和值的数组,只是null。
集合
有时您可能希望有一个ValueObjects的集合。现在,您不应该使用标准数组,因为您需要强类型(序列化器也需要知道集合中ValueObject的类型,稍后会更详细地说明)。这就是我们为什么创建了一个简单的辅助类来创建强类型ValueObjects集合的原因。
use EventSourced\ValueObject\ValueObject\Type\AbstractCollection; class IntegerCollection extends AbstractCollection { public function collection_of() { return Integer::class; } }
您只需要定义"collection_of",然后返回集合的类类型。基类将确保列表中添加的所有项目都是正确的类型。集合允许您对集合执行各种操作,如下所示。集合是不可变的,因此对它的任何操作都将返回一个新的集合,而保留原始集合不变。
$collection = new IntegerCollection([new Integer(1)]); $collection = $collection->add(new Integer(2)); $collection->count(); //2 $collection->exists(new Integer(2)); //true $collection->get(0)->value(); //1 $collection = $collection->remove(new Integer(2)); $collection->exists(new Integer(2)); //false
实体
实体是一个组合值对象,其中第一个值是实体的ID,其余值只是值。关于标识符的关键点是,如果ID匹配,则它“等于”另一个实体,其余的值对于比较不重要。
ID值对象必须实现"Identifier"合约,这样做的原因是为了使意图明确,这样您就不会错误地将错误的ValueObject传递给父构造函数。
use EventSourced\ValueObject\ValueObject\Type\AbstractEntity; class SampleEntity extends AbstractEntity { public $date; //Uuid and Date are base types that comes with the library public function __construct(Uuid $id, Date $date) { $this->date = $date; parent::__construct($id); } } $entity = new SampleEntity(new Uuid("153111a5-2d77-48b7-a88d-ee1d626c1d5d"), new Date('2013-10-12'); //Accessing the id property, part of the base class echo $entity->id()->value();
这就是实体。您会注意到值"$date"是公共的。这是因为它是一个实体,其值可以更改。您可以随意将其设置为受保护的,这样会更好,上面的只是为了简洁。
索引
索引是一组实体,其中实体的ID用作集合的键。通过ID访问和删除实体。创建索引与创建集合一样简单。
use EventSourced\ValueObject\ValueObject\Type\AbstractIndex; class SampleEntityIndex extends Type\AbstractIndex { public function collection_of() { return SampleEntity::class; } }
索引具有与集合类似的功能,但重点是实体及其ID。以下是完整的功能集。
$index = new SampleEntityIndex([]); $id = new Uuid("153111a5-2d77-48b7-a88d-ee1d626c1d5d"); $index = $index->add(new SampleEntity($id, new Date('2013-10-12'))); $index->count(); //1 $index->exists($id); //true $index->get($id)->date()->value(); //'2013-10-12' $index = $index->replace(new SampleEntity($id, new Date('2014-10-12')); $index->get($id)->date()->value(); //'2014-10-12' $index = $index->remove($id); $index->exists($id); //false
比较
比较ValueObjects很容易。只需使用内置的equals函数。如果您扩展上述任何抽象类,您将获得这个功能。如果所有值都匹配,则它们相等(实体除外,只有"id"对于比较很重要)。
$float_a = new Float(0.121); $float_b = new Float(0.121); $float_a->equals($float_b); //true
序列化
如您上面所见,您可以访问任何值对象的值,并且可以导航复杂的值对象以提取其树结构。这意味着您可以序列化值对象并将其存储在数据库中,以供稍后反序列化和使用。
问题是,编写这些序列化器很痛苦。这就是我们为什么创建了一组通用的序列化器/反序列化器类,这些类将这些ValueObjects转换为其基本数据结构,并将其转换回来。此序列化器仅适用于我们的抽象类,因此如果您扩展了这些类,则可以序列化ValueObject。
对于基于AbstractSingleValue的ValueObject,它返回基本值,对于AbstractComposite和更复杂的ValueObject,它返回一个数组,其中键=>值表示树结构。以下是它的工作原理。
use EventSourced\ValueObject\Serializer\Serializer; $float = new Float(0.121); $serializer = new Serializer(); $serialized = $serializer->serialize($float);
反序列化
一旦您序列化了ValueObject,您将希望在未来某个时间对其进行反序列化。为此,将序列化结果传递给deserialize函数,并指定您想要重新创建的ValueObject类,您将获得完整的ValueObject。这对于简单和复杂的情况都有效,例如集合和索引。
use EventSourced\ValueObject\Serializer\Serializer; use EventSourced\ValueObject\Deserializer\Deserializer; $float = new Float(0.121); $serializer = new Serializer(); $serialized = $serializer->serialize($float); $deserializer = new Deserializer(); $float_again = $deserializer->deserialize(Float:class, $serialized);
错误信息
你可能已经注意到了,我们还没有提到关于错误信息的任何内容,这些错误信息会向用户报告发生了什么错误。好吧,这有一个原因。值对象不是错误报告者,它们不是为了返回可读错误而设计的。
原因有很多,但最主要的一个原因是错误信息通常是与应用程序相关的,几乎不可能编写通用的错误信息,使其在所有情况下都可用。所以我们没有尝试解决这个问题,相反,我们专注于使值对象充当对不良输入的保护,确保应用程序不发送不良数据,并且以上下文敏感的方式报告错误。
但这并不是说它不会报告发生了什么错误。无效的值对象会自动返回一个包含值对象类和导致崩溃的值的异常。这使得重复错误并找出确切错误原因变得容易。
访问验证错误
try { new ValueObject\Coordinate(90.00001); } catch (Assert\IsException $ex) { $exception->value(); $exception->valueobject_class(); }
扩展
有可能添加来自第三方库的自定义值对象。
为了做到这一点,您应该向 YourClassSerializer
类提供实现了以下2个接口的类
EventSourced\ValueObject\Contracts\Deserializer
EventSourced\ValueObject\Contracts\Serializer
.
下面是货币的一个示例
class Currency implements Serializer, Deserializer { public function deserialize($class, $parameters) { try { return new \Money\Currency($parameters); } catch (\Exception $e) { throw new Exception($e->getMessage()); } } public function serialize($serializable) { /** * @var \Money\Currency $serializable */ return $serializable->getCode(); } }
然后您应该在 extenstions.php
中按照以下方式注册它
return [ \Money\Currency::class => \EventSourced\ValueObject\Extensions\Serializers\Currency::class ];
反序列化方法参数
我们添加了一个方便的功能,可以将序列化的值对象反序列化为可调用的方法。
考虑一个包含接收值对象作为参数的方法的类。
class PhoneBook { public function addPhoneNumber(PhoneNumber $number, Name $name, Address $address) { //... } }
如果不需要将这些参数包装在单个对象中,直接调用这个方法,这不是很方便吗?实际上,您可以这样做!
$phone_book = new PhoneBook(); $payload = [ 'number' => "085343534545", 'name' => "Tim Beedle", 'address' => [ '83 Lambsgate', 'Herbert Road', 'Ballbridge', 'D4', 'Dublin' ] ]; $deserializer = new Deserializer(); $method = $deserializer->deserializeMethod($phone_book, "addPhoneNumber", $payload); $result = $method->run();
$method->run();
在该对象上调用该方法,所有参数都从 $payload
对象中获取。就像对象反序列化器一样,如果参数无法反序列化,它将抛出异常。