Crell / Serde

Robust Serde (serialization/deserialization) library for PHP 8.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Better support for composite value objects

cnastasi opened this issue · comments

Detailed description

Given this scenario

readonly class Name
{
    public function __construct(public string $value) {
        // validation logic
    }
}

readonly class Age
{
    public function __construct(public int $value) {
        // validation logic
    }
}

readonly class Person
{
      public function __construct(public Name $name, public Age $age) {}
}

If I try to serialize the class Person then I will have this JSON

{"name":{"value":"John Smith"},"age":{"value":42}}

Trying with the flatten: true attribute in both properties, this is the result

{"value":"John Smith"}

But I would prefer an output like this

{"name":"John Smith", "age": 42}

Context

When working extensively with value objects and wrapping primitives, it's common to wrap fields like Email, Url, Age, etc. These are values that need specific validation and can be reused to compose more complex value objects or entities. I believe those adopting a DDD approach in PHP could share similar needs.

Possible implementation

I'm still studying how the library internally works, but I'm not yet ready to give an implementation suggestion.
But I can give some thought about a possible behaviour.

The first approach that came to my mind was this one: add the Field serializedName argument to specify a new name.

readonly class Person
{
    public function __construct(
        #[Field(flatten: true, serializedName: 'name')]
        public Name $name,
        #[Field(flatten: true, serializedName: 'age')]
        public Age $age
    ) {}
}

Alternatively, I suggest a new Attribute:

#[Attribute(Attribute::TARGET_CLASS)]
readonly class ValueObject
{
    public function __construct(public string $field = 'value') {}
}

So we can define the metadata directly inside the Value Object, instead of repeating it every time it's used.

#[ValueObject]
readonly class Name
{
    public function __construct(public string $value) {
        // validation logic
    }
}

What do you think? I would love to hear your thoughts on this proposal.

Hm. Yes, currently the flattening logic implicitly assumes that every name is unique, and will do exactly what you're seeing otherwise.

I'm not sure if it will work, but try putting a serializedName on the property of the value object. If it does what I think it will do, that should "just work." If not, then there's something not using the serialized name correctly and that's a bug to fix so that it "just works." 😄

Also be aware that the constructor is not called with deserializing, so any validation you do there will be ignored. The best way to handle that would be to move the validation to a private method, call that from the constructor, and then tag the validation method #[PostLoad] so that Serde will invoke it automatically as well. Add exceptions to taste.

Already tried serializedName without success, so that's a bug, I assume.

About the validation, thanks for the hint!

While diving a bit deeper into the library and watching this presentation I realised that maybe relying on __unserialize and __serialize can be a viable alternative to customize the structure a bit better.

The upside I see in this approach is that project classes can have greater degree of control in the process, which would solve the use case described in this issue.

The downside is that it would require serialize and deserialize the entire object, which depending on the context, may become a bit repetitive.

I am still pondering this, but haven't had an opportunity to dig into the code. I've been over on Tukio making it snazzier. 😄 Will look into this when I am able.

Proposed fix here: #48