eventsourced/valueobjects

该软件包已废弃,不再维护。作者建议使用dql/valueobjects软件包。

用于创建值对象的辅助库

4.7.1 2017-11-10 12:05 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 对象中获取。就像对象反序列化器一样,如果参数无法反序列化,它将抛出异常。