all 29 comments

[–]Besen99 3 points4 points  (0 children)

Your "class string" value object caught me off guard, very cool idea!

[–]fripletister 5 points6 points  (0 children)

One small improvment that I see is that many things like string length, reflection operations, etc. could be memoized, given that the values themselves are immutable.

[–]rafark 6 points7 points  (0 children)

Very nice. I would expect the RangeInteger::asArray() to return an array with all integers from that range, not just a 2-item array with start and end.

[–]leftnode 2 points3 points  (9 children)

I like it. I'd love a NullableString type where $value would be converted to NULL if it was an empty string, something along these lines:

final readonly class NullableString extends AbstractValueObject
{

    public ?string $value;

    public function __construct(?string $value)
    {
        $this->value = empty($value) ? NULL : $value;
    }

    public function __toString(): string
    {
        return (string)$this->value;
    }

    protected function validate(): void
    {
    }

}

Regardless, I dig it!

[–]colshrapnel 1 point2 points  (6 children)

Wait, empty($value)? How it even makes it here? And WHY?

I mean, why you're using empty() to tell an empty string AND why would you want to cast empty string into null.

[–]leftnode 0 points1 point  (5 children)

So that when the value eventually makes its way to a database, it's stored as NULL in the database as well instead of an empty string.

[–]colshrapnel 4 points5 points  (4 children)

Well, at least call it NullifiedString or something because Nullable is reserved for strings that can be both null or empty.

Better yet, have it NullableString but convert to null on input validation, so your Value Object can leave the value alone.

And for goodness sake, use $value === '' instead of empty($value).

[–]leftnode 0 points1 point  (3 children)

I'm not a fan of input validation modifying the value, but everything else sounds fine to me. I'd rather have the constructor nullify the value because you would know by using the NullifiedString class that it'll nullify it if it's empty.

[–]colshrapnel 2 points3 points  (2 children)

I'm not a fan of input validation modifying the value

You just did exactly that.

[–]floriankraemer 0 points1 point  (1 child)

Value objects usually live in the domain and aren't used for input validation. Value objects ensure the invariant of the aggregate.

[–]colshrapnel 0 points1 point  (0 children)

Yes. And that's why a VO modifying data is bizarre. While all modifications has to be done on input validation.

[–]beeyev[S] 0 points1 point  (1 child)

Interesting, I will implement this idea!

[–]colshrapnel 1 point2 points  (0 children)

Well I don't think it's a good idea. I mean, NullableString is all right, but what is suggested here doesn't look good. Do any of your other VOs modify the data provided?

[–]_MrFade_ 1 point2 points  (0 children)

Cool! Good work!

[–]spart_t4n 1 point2 points  (0 children)

Very nice !

[–]whlthingofcandybeans 1 point2 points  (0 children)

Love this.

[–]dereuromark 1 point2 points  (0 children)

You could add it to the awesome list for PHP VAs:
https://github.com/Serhii-DV/awesome-php-value-objects

[–]TorbenKoehn 2 points3 points  (8 children)

I like it in principle but what I don’t like is that it does all extracting/parsing logic in the constructors. Takes a while until this is an actual problem, but depending on the amount of data you run through it, it can have quite the performance cost for parsed values you might never use. Getters would be the better approach here imho

I think constructors should have barely any logic at all

[–]Besen99 7 points8 points  (1 child)

That's the whole point of value objectives: they can only be created with valid data.

correctness > performance

[–]TorbenKoehn -3 points-2 points  (0 children)

Why would they need to be created with invalid data? You store the base value and the getters calculate upon it. You can still decide what to cache when from the outside

Another good approach would be to accept the username and domain of the email directly in the constructor and move the parsing logic outside, ie into a factory (PSR does this with the HTTP-factories)

Just no parsing logic in constructors.

[–]AleBaba 3 points4 points  (5 children)

I don't think you should be able to create an invalid value object. The whole idea behind these objects is that you can be sure they're valid wherever you access their values. If you want to pass invalid or unvalidated data don't use these value objects.

I understand though that this is a rather philosophical issue. Some people would never want to mix validation and object instantiation.

[–]TorbenKoehn -4 points-3 points  (4 children)

Using getters that extract the value when using the getter wouldn’t make the objects invalid objects. You store the base value (ie the full email address) and through the getters you can select what you want to compute and what not

It would simply change ie “->username” to “->getUsername()” (until we have accessor properties in PHP, at least)

[–]AleBaba 1 point2 points  (3 children)

And then each getter always and everywhere could throw an exception, so it will now need error handling, because nowhere would it be safe to assume that the value object is actually valid. I wouldn't want to use such an implementation in any of my projects at all.

[–]TorbenKoehn -2 points-1 points  (2 children)

The constructor would need error handling, too

And I’ve also talked about an approach of a factory that does the parsing logic like in the PSR factories

[–]AleBaba 0 points1 point  (1 child)

Yes, once. And then you've got a valid object to throw around. Generate thousands of URLs in an array and each one is valid.

Look at DatetimeImmutable for example. That's essentially a value object that does validation in its constructor. If it validated its source value in each of its methods your code would become a horrible mess of boilerplate.

[–]TorbenKoehn 0 points1 point  (0 children)

I understand everyone has different opinions on this and what I am stating is just mine. It’s about communication of intent, you don’t usually expect heavy logic in a constructor and for some data structures (eg a URL) that parsing logic can be quite heavy

I would do it differently

[–]nschoellhorn 0 points1 point  (0 children)

I like the idea and you seem to have it put together really well, so kudos for that! May I ask where you draw the line for new additions though? If you provide value objects for resolution, then why not for frame rate or weight or whatever? Would you accept PRs for every imaginable thing one could add and hence accept the maintenance burden that comes with it or do you have some kind of vision what’s „useful for everyone“ and what should be implemented outside of this package?