此栏/值对象

PHP 8.0 值对象辅助库。


README

Build Status Version

要求

需要 PHP 7.1

安装

通过 Composer 显然

composer require funeralzone/valueobjects

扩展

此库仅处理基本值(标量)。我们还发布了一个 扩展库,它为更复杂的值提供起点。

我们的方法

我们在《在 PHP 中编写值对象的更好方法》文章中阐述了我们对 PHP 值对象的理念。[链接](https://medium.com/funeralzone)

单个值对象

如果你的 VO 封装了一个值,那么它很可能是标量。我们在 src/Scalars 下提供了一些特质来处理标量。

src/Scalars

假设你有一个域名值叫做 '用户邮箱'。你需要创建一个实现 ValueObject 接口类的类。

final class UserEmail implements ValueObject {
...

现在你需要实现接口。但是因为电子邮件本质上可以被认为是特殊类型的字符串(在这个简单的情况下),所以 StringTrait 辅助特质可以为你实现大部分接口。

final class UserEmail implements ValueObject {

    use StringTrait;
...

在我们的例子中,用户的电子邮件有其他域名逻辑,我们可以将其封装在我们的 VO 中。用户电子邮件必须是有效的电子邮件地址。

...
    public function __construct(string $string)
    {
        Assert::that($string)->email();
        $this->string = $string;
    }
...

你可以在示例目录中看到如何实现单个值对象的示例。

枚举

可以通过使用 EnumTrait 容易地定义枚举。然后,枚举值简单地作为类上的常量列出。

final class Fastening implements ValueObject
{
    use EnumTrait;
    
    public const BUTTON = 0;
    public const CLIP = 1;
    public const PIN = 2;
    public const ZIP = 3;
}

在处理值对象序列化时,使用常量名称。它们是区分大小写的。所以

$fastening = Fastening::fromNative('BUTTON');
$fastening->toNative(); // Equals to string: 'BUTTON'

在代码中,特质使用魔法方法根据常量名称创建对象,如下所示

$fastening = Fastening::ZIP();
$fastening->toNative(); // Equals 'ZIP'

如果你的 IDE 支持代码补全,并且你想使用命名方法创建枚举,你可以在你的枚举类中添加以下 PHPDoc 块

/**
 * @method static Fastening BUTTON()
 * @method static Fastening CLIP()
 * @method static Fastening PIN()
 * @method static Fastening ZIP()
 */
final class Fastening implements ValueObject

复合值对象

复合值对象是由其他值组成的更复杂的值。

final class Location implements ValueObject
{
    use CompositeTrait;
    
    private $latitude;
    private $longitude;
    
    public function __construct(Latitude $latitude, Longitude $longitude)
    {
        $this->latitude = $latitude;
        $this->longitude = $longitude;
    }
    
    public function getLatitude(): Latitude
    {
        return $this->latitude;
    }
    
    public function getLongitude(): Longitude
    {
        return $this->longitude;
    }
...

一个 Location 由两个 VO(纬度,经度)组成。我们提供了一个 CompositeTrait,可以轻松自动实现大部分 ValueObject 接口。它通过反射返回所有类属性数组来处理 toNative 序列化。

CompositeTrait 不实现 fromNative。我们将对象构建留给您。

...
    public static function fromNative($native)
    {
        return new static(
            Latitude::fromNative($native['latitude']),
            Longitude::fromNative($native['longitude'])
        );
    }
...

你可以在示例目录中看到如何实现复合对象的示例。

空值、非空值和可选值

此软件包允许您处理可选值对象。

首先创建一个值对象类型。

interface PhoneNumber extends ValueObject
{
}

实现值对象的非空版本。

final class NonNullPhoneNumber implements PhoneNumber
{
    use StringTrait;
}

实现值对象的空版本。

final class NullPhoneNumber implements PhoneNumber
{
    use NullTrait;
}

实现值对象的可选版本。

final class NullablePhoneNumber extends Nullable implements PhoneNumber
{
    protected static function nonNullImplementation(): string
    {
        return NonNullPhoneNumber::class;
    }
    
    protected static function nullImplementation(): string
    {
        return NullPhoneNumber::class;
    }
}

此 '可选' 可以根据原生输入自动创建接口的非空或空版本。例如

$phoneNumber = NullablePhoneNumber::fromNative(null);

上面的 $phoneNumber 将自动使用上面指定的 NullPhoneNumber 实现版本。

$phoneNumber = NullablePhoneNumber::fromNative('+44 73715525763');

上面的 $phoneNumber 将自动使用上面指定的 NonNullPhoneNumber 实现版本。

值对象集合

值对象集合应实现 Set 接口。它只是 ValueObject 接口的一些简单扩展。

interface Set extends ValueObject, \IteratorAggregate, \ArrayAccess, \Countable
{
    public function add($set);
    public function remove($set);
    public function contains(ValueObject $value): bool;
    public function toArray(): array;
}
  • add 将来自另一个集合的值添加到当前集合中。
  • remove 从当前集合中移除另一个集合中包含的所有值。
  • contains 如果值存在于当前集合中,则返回 true
  • toArray 返回包含所有值对象的简单PHP数组。

Set 接口扩展的其他接口(\IteratorAggregate\ArrayAccess\Countable)用于将集合对象作为数组访问。

非空集合

库提供了接口的默认实现。

final class SetOfLocations extends NonNullSet implements Set
{
    protected function typeToEnforce(): string
    {
        return Location::class;
    }

    public static function valuesShouldBeUnique(): bool
    {
        return true;
    }
}

需要实现两个抽象方法。

  • typeToEnforce 应返回一个字符串,表示您想要将其制作成集合的值对象的类名。
  • valuesShouldBeUnique 应返回一个布尔值,表示您是否想要强制集合唯一。

如果将集合设置为唯一,则在添加重复值到集合(在实例化或通过 add 方法)时,将过滤掉重复项。

空和非空集合

与标准值对象类似,有一些结构可以帮助创建可空和空集合。有关更多信息,请参阅“空值、非空值和可空值”部分。

  • NullableSetNullable 的集合等价物。
  • NullSetTraitNullTrait 的集合等价物。

集合的使用

迭代、访问和计数

// Iteration
$set = new SetOfLocations([$one, $two]);
foreach($set as $value) {
    // TODO: Do something with each value object
}

// Access
$one = $set[0];
$two = $set[1];

//Counting
$count = count($set); // Returns 2

添加

合并另一个集合。

$set = new SetOfLocations([$one, $two]);
$anotherSet = new SetOfLocations([$three]);
$mergedSet = $set->add($anotherSet);
count($mergedSet) // Equals: 3

删除

使用另一个集合作为参考值从集合中删除值。

$set = new SetOfLocations([$one, $two, $three]);
$anotherSet = new SetOfLocations([$one]);
$remove = $set->remove($anotherSet);
count($remove) // Equals: 2

包含

检查集合是否包含特定的值对象。

$set = new SetOfLocations([$one, $two, $three]);
$one = new Location(0);
$check = $set->contains($one);