dql/valueobjects 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

dql/valueobjects

Composer 安装命令:

composer require dql/valueobjects

包简介

A helper library for making valueobjects

README 文档

README

ValueObjects (VOs) are the core of any DDD (Domain Driven Design) application, they ensure that values are valid and will be accepted by your domain.

In our experience, most ValueObject libraries offer a collection of ValueObjects, but they've locked them down, so it's hard to extend them and build new ones.

That's why we've built this ValueObjects toolkit, it makes building new ValueObjects quick, easy and painless.

For those using an onion architecture, consider this libary as part of the core.

ValueObjects and Validators

Single Values

These are ValueObjects that are given a single value that they must validate. For these ValueObjects all you need to do is specify their validator by extending the parent.

Making a new Single Value VO

use EventSourced\ValueObject\ValueObject;

class Integer extends ValueObject\AbstractSingleValue 
{    
    protected function validator()
    {
        return parent::validator()->intVal();
    }
}

Accessing the value

If you want to access the value held within a single ValueObject, then do the following.

$integer = new Integer(1);
echo $integer->value();

Nice and easy.

Validators

ValueObjects use validators to do their job. Instead of writing our own library, we've decided to use the excellent Respect Validation library. It has all the validators you could ask for, and it's syntax is concise and elegant.

A helper method "validator" returns a new instance of the respect validator, it has been added to all abstract classes.

Chaining Validators

Respect Validators are chainable, so building complex validators for your value objects is a piece of cake.

use EventSourced\ValueObject\ValueObject\Type\AbstractSingleValue;

class Coordinate extends AbstractSingleValue 
{    
    protected function validator()
    {
        return parent::validator()->floatVal()->between(-90, 90);
    }
}

Composite ValueObjects

These are ValueObjects that are made from two or more ValueObjects. They are a composite that represents the pairing of the ValueObjects. An example is a locations GPS coordinate, it's actually a composite of two Coordinates, latitude and longitude.

Making a composite ValueObject

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;
    }
}

So it's simply just a holder for a bunch of valueobjects. If you want to run any validation across value objects, you should do it in the constructor. The base class takes care of the "equals" method, so you don't have to worry about that.

Nullable Composite

Making a nullable single value is easy, making a nullable composite is harder, and really should be used as a last resort. That said, it's useful to have.

To create a nullable composite, you set all the defaults values for the composite to null. That's it.

<?php namespace EventSourced\ValueObject\ValueObject;

class NullableGPSCoordinates extends GPSCoordinates
{
    public function __construct(Coordinate $latitude=null, Coordinate $longitude=null)
    {
        $this->latitude = $latitude;
        $this->longitude = $longitude;
    }
}

Each composite offers a is_null() method, so you can easily check if the VO is actually null.

NB: When you serialize an instance of the above, and all the values are null, you will get a null response, not an array with keys and values, just null.

Collections

Sometimes you'll want to have a collection of ValueObjects. Now, you shouldn't use a standard array, because you want strong typing (also the deserializer has to know what type of ValueObject is in the collection, more on that later). That's why we created a simple helper class for creating strongly typed collections of ValueObjects.

use EventSourced\ValueObject\ValueObject\Type\AbstractCollection;

class IntegerCollection extends AbstractCollection 
{    
    public function collection_of()
    {
        return Integer::class;
    }
}

You just need to define the "collection_of" and return the class type of the collection. The base class will ensure that all items added to the list are of the correct type. Collections allow you to perform various operations on the collection, such as the following. Collections are immutable, so any operations on it will return a new collection, leaving the original intact.

$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

Entities

An entity is a composite valueobject, where the first value is the ID of the entity, and the rest of the values are just values. The key thing about an identity is that it is "equal" to another entity if the IDs match, the rest of the values don't matter for comparisons.

The ID valueobject must implement the "Identifier" contract, the reason for this is to make intent clear, so you don't accidentily pass the wrong ValueObject to the parent constructor.

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

That's an entity. You'll notice that the value "$date" is public. That's because it's an entity and the values can change. Feel free to make this protected, it would be better, the above is just for brevity.

Index

An index is a collection of entities, where the id of the entity is used as the key for the collection. Entities are accessed and removed by their ID. Creating one is as simple as creating a collection.

use EventSourced\ValueObject\ValueObject\Type\AbstractIndex;

class SampleEntityIndex extends Type\AbstractIndex
{    
    public function collection_of()
    {
        return SampleEntity::class;
    }
}

Indexes have similar functionality to collections, except the focus is around entities and their ids. Here is the full feature set.

$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

Comparing

Comparing ValueObjects is easy. Just use the built in equals function. You get this out of the box if you extend any of the above abstract classes. If all the values match, then they are equal (Entities being the exception, only the "id" matters for comparison).

$float_a = new Float(0.121);
$float_b = new Float(0.121);
$float_a->equals($float_b); //true

Serializing

As you've seen above, you can access the value of any value object and you can navigate complex valueobjects to extract their tree structure. This means you can serialize a value object and store it in a Datebase to deserialize and use later.

Now, the thing is, writing these serializers is a pain in the ass. That's why we've created generic serializer/deserializer classes that tranforms these ValueObjects into their base data structures, and back. This serializer wil only work with our abstract classes, so if you extend those, then you can serialize a ValueObject.

For AbstractSingleValue based ValueObjects, it returns the base value, for AbstractComposite and the more complex ValueObjects, it returns the tree structure as an array with key => values. Here's how it works.

use EventSourced\ValueObject\Serializer\Serializer;

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

Deserializing

Once you've serialized a ValueObject, you'll want to deserialize it at some future time. To do that, pass the serialized result to the deserialize function, type hinting the ValueObject class you want it to recreate, and you'll get the full ValueObject back. This works for simple and complex, such as collections and indexes.

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);

Error Messages

One thing you've probably noticed, we haven't said anything about error messages that report to the user on what went wrong. Well, there's a reason. ValueObjects are not error reporters, they are not intended to return human readable errors.

There are may reasons for this, but the main one is that error messages are usually application specific, it's next to impossible to write generic error messages that are usable in every context. So we didn't try to solve that problem, instead we focussed on making the ValueObjects act as guards against bad input, it's the applications responsibility to not send bad data and to report errors in a context sensitive manner.

That's not to say it doesn't report what went wrong. Invalid VOs automatically return an exception that includes the ValueObjects class and the value that caused the crash. This makes it easy to repeat the error and figure out exactly what went wrong.

Accessing Validation errors

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

Extensions

There is possibility to add custom value objects from third-party libraries.
In order to do that you should provide YourClassSerializer class with 2 interfaces implemented:

  • EventSourced\ValueObject\Contracts\Deserializer
  • EventSourced\ValueObject\Contracts\Serializer.

Below is an example for currency:

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

Then you should register it under extenstions.php in following way:

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

Deserializing method parameters

A handy feature we've added is to deserialize serialized VOs into a callable method.

Take a class that has a method which takes in value objects as arguments.

class PhoneBook 
{
    public function addPhoneNumber(PhoneNumber $number, Name $name, Address $address)
    {
        //...
    }
}

Wouldn't it be handy to deserialize these arguments and call this method, without having to wrap all the arguments in a single object? Well, you can!

$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(); calls the method on that object, with all the arguments taken from the the $payload object. Like the object deserializer, it will throw an exception if it the arguments can't be deserialized.

dql/valueobjects 适用场景与选型建议

dql/valueobjects 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 5.55k 次下载、GitHub Stars 达 10, 最近一次更新时间为 2016 年 08 月 29 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 dql/valueobjects 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 dql/valueobjects 我们能提供哪些服务?
定制开发 / 二次开发

基于 dql/valueobjects 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 5.55k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 11
  • 点击次数: 7
  • 依赖项目数: 2
  • 推荐数: 0

GitHub 信息

  • Stars: 10
  • Watchers: 1
  • Forks: 4
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2016-08-29