dql/valueobjects

制作值对象的辅助库

4.7.1 2017-11-10 12:05 UTC

README

值对象(VOs)是任何领域驱动设计(DDD)应用的核心,它们确保值是有效的,并且将被您的领域接受。

根据我们的经验,大多数值对象库提供了一系列值对象,但它们已经限制了它们,因此很难扩展它们并构建新的对象。

这就是我们构建这个值对象工具包的原因,它使构建新的值对象变得快速、简单且无痛。

对于使用洋葱架构的用户,请将此库视为核心部分。

值对象和验证器

单值

这些是给定单个值并必须验证的值对象。对于这些值对象,您只需通过扩展父对象来指定其验证器即可。

创建新的单值VO

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。

集合

有时您可能希望有一个值对象集合。现在,您不应该使用标准数组,因为您需要强类型(反序列化器还必须知道集合中包含哪种类型的值对象,更多内容将在后面介绍)。这就是我们创建了一个简单的辅助类来创建强类型值对象集合的原因。

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值对象必须实现“标识符”合约,这样做的原因是为了使意图明确,这样你就不会错误地将错误的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的ValueObjects,它返回基本值;对于AbstractComposite和更复杂的ValueObjects,它返回一个键=>值数组作为树结构。以下是它是如何工作的。

use EventSourced\ValueObject\Serializer\Serializer;

$float = new Float(0.121);
$serializer = new Serializer();
$serialized = $serializer->serialize($float);

反序列化

一旦你序列化了一个ValueObject,你希望在将来某个时间点对其进行反序列化。要这样做,将序列化的结果传递给反序列化函数,并指定你想要重新创建的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);

错误信息

你可能已经注意到,我们没有提到关于报告用户错误原因的错误信息。好吧,有原因。ValueObjects不是错误报告者,它们不是旨在返回可读性高的错误。

原因有很多,但主要原因是错误信息通常是特定于应用程序的,几乎不可能编写通用的错误信息,使其在所有上下文中都可用。所以我们没有尝试解决这个问题,相反,我们专注于使ValueObjects成为不良输入的防护,不发送不良数据是应用程序的责任,以上下文敏感的方式报告错误。

这并不意味着它不会报告出了什么问题。无效的VO会自动返回一个包含ValueObjects类和导致崩溃的值的异常。这使得重复错误并确定确切出错原因变得容易。

访问验证错误

try {
    new ValueObject\Coordinate(90.00001);
} catch (Assert\IsException $ex) {
    $exception->value();
    $exception->valueobject_class();
}

扩展

有可能添加来自第三方库的自定义值对象。
为了做到这一点,你应该提供实现以下两个接口的YourClassSerializer

  • 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();
    }
}

然后你应该按照以下方式在extensions.php中注册它

return [
    \Money\Currency::class =>
        \EventSourced\ValueObject\Extensions\Serializers\Currency::class
];

反序列化方法参数

我们添加的一个便利功能是将序列化的VO反序列化到可调用的方法中。

考虑一个包含接受值对象作为参数的方法的类。

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 对象中获取。和对象反序列化器一样,如果参数无法反序列化,它将抛出异常。