On Thu, May 15, 2025, at 22:11, Larry Garfield wrote:
> On Thu, May 15, 2025, at 2:56 PM, Rob Landers wrote:
> > On Thu, May 15, 2025, at 17:32, Tim Düsterhus wrote:
> >> Hi
> >> 
> >> Am 2025-05-15 14:14, schrieb Rob Landers:
> >> > For example, if you have a Money type, you'd want to be able to ensure 
> >> > it cannot be negative when updating via `with()`. This is super 
> >> > important for ensuring constraints are met during the clone.
> >> 
> >> That's why the assignments during cloning work exactly like regular 
> >> property assignments, observing visibility and property hooks.
> >> 
> >> The only tiny difference is that an “outsider” is able to change a 
> >> `public(set) readonly` property after a `__clone()` method ran to 
> >> completion and relied on the property in question not changing on the 
> >> cloned object after it observed its value. This seems not to be 
> >> something relevant in practice, because why would the exact value of the 
> >> property only matter during cloning, but not at any other time?
> >> 
> >> Best regards
> >> Tim Düsterhus
> >> 
> >
> > Hey Tim,
> >
> >> why would the exact value of the 
> >> property only matter during cloning, but not at any other time?
> >
> > For example, queueing up patches to store/db to commit later; during 
> > the clone, it may register various states to ensure the patches are 
> > accurate from that point. That's just one example, though, and it 
> > suggests calling __clone *before* setting the values is the right 
> > answer.
> >
> > I think Larry's idea of just using hooks for validation is also pretty 
> > good. As Larry said, the only thing you can really do is throw an 
> > exception, and the same would be true in a constructor as well.
> >
> > — Rob
> 
> The limit of hooks is that they're single-property.  So depending on how your 
> derived properties are implemented, it may be insufficient.  I could easily 
> write such an example (the hooks RFC included some), but how contrived they 
> are, I don't know.
> 
> --Larry Garfield
> 

Yeah, the validation won't be too automatable (ie, in a base class) without at 
least having reusable hooks. It's important to be mindful that there are 
approximately three different paradigms when it comes to validating objects 
(and they apply to frameworks differently) in PHP.

-- validate on serialization --

This paradigm is mostly used in symfony via doctrine/serializer. An object is 
allowed to be in an "invalid" state and is usually constructed in a "zero" 
state, and then state is applied over the lifetime of the application. Only 
during serialization to the wire is it usually validated. So, it usually looks 
something like this:

$user = new User();
$user->name = "Rob"
$user->id = 123;

When you receive an object in a function, you will likely have to validate that 
specific properties are set before operating on it, otherwise you can end up 
with bugs. The nice thing about this style is that you can build up an object's 
state over a longer period (such as processing a form input or a database query 
result).

-- validate on construction --

This paradigm is commonly used in value objects and more functional-style PHP. 
The idea is that an object must be valid by the time it is constructed and does 
not allow partially formed objects to exist. All properties are validated in 
the constructor, and invalid combinations throw exceptions immediately. This 
tends to lead to more robust code, particularly when the object represents a 
meaningful invariant (e.g., `Money`, `EmailAddress`, `Uuid`). Here's what it 
looks like:

$user = new User(name: "Rob", id: 123);

The dowside is that its harder to build objects piece by piece and usually 
requires factories, DTOs, and builder patterns for times when not all the data 
is available upfront. This approach is usually seen in functional, DDD (domain 
driven design), or strict typing contexts and plays nicely with immutability. 
This style is less-common in popular PHP frameworks like Symfony and Laravel, 
which tend to favor more flexible object construction.

-- validate on mutation --

This paradigm is popular in both Symfony and Laravel and favors an 
active-record-esqe approach. In this paradigm, each mutator (setter) is 
responsible for ensuring the property remains valid at the point of 
modification. The downside is that inter-property constraints require redundant 
checks that can be difficult to maintain. The nice thing is that they're easy 
to enforce.

Of course, these can be mixed-and-matched as needed/desired. The downside with 
the cloning method here is that it really puts "validation on construction" on 
a back foot. Developers will likely have absolutely no way to perform 
validation without rewriting their entire class structures and will make 
validation during construction basically impossible for immutable objects.

— Rob

Reply via email to