Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Serializer][PropertyAccess] Support immutable objects #19291

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
theofidry opened this issue Jul 5, 2016 · 5 comments
Closed

[Serializer][PropertyAccess] Support immutable objects #19291

theofidry opened this issue Jul 5, 2016 · 5 comments

Comments

@theofidry
Copy link
Contributor

theofidry commented Jul 5, 2016

The Serializer is amazing at transforming a data structure into another and ObjectNormalizer does a great job at figuring out how to denormalize and object. It avoids to have to rely on a lot of custom normalizers (unless performances are critical).

However it does not support immutable objects which is a bit sad as it forces user to create custom normalizers each time. Or rather to be clear, it does not support "withers", i.e. methods returning a new modified instance:

class Dummy
{
  public function withName(string $name): self
  {
    $clone = clone $this;
    $clone->name = $name;

    return $clone;
  }
}

As of now, the Serializer is using the property accessor:

/**
 * {@inheritdoc}
 */
protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = array())
{
    try {
        $this->propertyAccessor->setValue($object, $attribute, $value);
    } catch (NoSuchPropertyException $exception) {
        // Properties not found are ignored
    }
}

There would be two ways to do it:

  1. Add support for "wither" in the PropertyAccess
  2. Bypass the property access in this case

About 1) it's a bit tricky because when using setValue(&$object, 'prop', 'val), you would expect $object being modified, not having a new modified instance. However I would think it's still valid because you are passing $object reference, meaning what you want to set is the variable value and that you don't really care if the object is immutable or not.

  1. is more difficult than it looks. Because it would imply to have a "set attribute strategy": what should the serializer look for? Trying to set the attribute via the property accessor first and then falling back to the wither or vice versa? Besides there would be a lot of redundancy with the property accessor. PropertyAccessor is not just about setting the value, it is also about finding how to set it, i.e. figuring out what the mutator is, and we would have the exact same problem to solve and I don't really think this belongs to the Serializer...

I'm ready to work on it, but needs to hear people opinion on the topic :)

/cc @webmozart @dunglas

@theofidry theofidry changed the title [PropertyAccess] Support immutable objects [Serializer][PropertyAccess] Support immutable objects Jul 22, 2016
@kix
Copy link
Contributor

kix commented Jul 24, 2016

Looks like your link got somehow broken, here it is once again:

On the core subject: I personally think that PropertyAccess is somehow used for the wrong case here. It does fit at this moment, but not for your use case, unfortunately. Basically, what we want here is a hydrator instead, and such a hydrator should be able to deal not with withers, but basically with anything. Withers are just the style of immutability you've chosen to use; what if my models don't have these at all? I could be just using the constructor to pass the values into my instances, relying on other code to somehow clone the initial model. This style would require PropertyAccess to be able to access properties that are actually private and non-settable at all. (Also, how would you guess the argument order for the constructor here?)

Something like GeneratedHydrator would fit here, but what the ObjectNormalizer would then need here is a strategy to pick the best and most performant of the available hydration methods.

@theofidry
Copy link
Contributor Author

Looks like your link got somehow broken [...]

Fixed

On the core subject: I personally think that PropertyAccess is somehow used for the wrong case here. [...] Basically, what we want here is a hydrator instead, and such a hydrator should be able to deal not with withers, but basically with anything. [...] what if my models don't have these at all? I could be just using the constructor to pass the values into my instances [...]

I couple of thoughts on the fly: There is two problems at hand: instantiating the object and populating it. And in this part (setAttributeValue) we are talking of populating it (which is what an hydrator is about correct?). If you have no "mutator" (I'm putting quotes as I'm including withers or any equivalent here) but are putting everything in the constructor, there this case has already been taken care of at that point.

This style would require PropertyAccess to be able to access properties that are actually private and non-settable at all.

I'm not a big fan to "hack" things to set private values. Not that it's not a valid use-case but it can create some unexpected issues as you are setting your model state in a non conventional way. It's a scenario where I would rather see developers use custom serializers or if they are fine with this behaviour, enrich the serializer behaviour in their code base (i.e. not providing that in the core).

I personally think that PropertyAccess is somehow used for the wrong case here

I may have got the wrong idea on what the PropertyAccess is about and someone more knowledgeable may correct me, but to me it looks like PropertyAccess simply has two facets:

  • Reading a value from an element (array/object) (in which case it is an accessor)
  • Writing a value to an element (array/object) (in which case it is an hydrator)

For both, part of that process is figuring how to access/write the value which includes checking if the property is public or not, if there's a getter, is using camelcase or snake case and so on.

Withers are just the style of immutability you've chosen to use

100% agree with that. It is a convention like another which looks like widly adopted (I have no data to back it up, it is only from my personal experience) and I believe that if there is another popular convention, it should be supported as well (provided it's worth it).

So for those reasons I don't feel like it's misusing the PropertyAccess component :)

@kix
Copy link
Contributor

kix commented Jul 25, 2016

@theofidry, on second thought, the PropertyAccess is not actually the case for immutable models, since it's a constructor and not an accessor anymore for those. We never get to modify the exact model, we get a new one every time. It's just the naming, but the naming and its interface suggests that it should be accessing properties of a single object, whereas withSmth would return a new object each time. So it looks like some other component should be involved here :)

@theofidry
Copy link
Contributor Author

@kix I would say it still is. You are write, "wither" and so are actually non-static factories. However because they are non-static, they require an instantiated factory which is the instance itself.

To me this would still fall within the responsibility of an hydrator: you have an object and properties to populate, and you and a populated object. That the populated object is the same or not than the input one is nothing but a detail.

As said I wouldn't mind having it as a separate component or outside of Symfony, but the issue is that it relies on private methods of PropertyAccessor, because the lookup strategy to find the method is the same: https://github.com/symfony/property-access/blob/master/PropertyAccessor.php#L709

@theofidry
Copy link
Contributor Author

As discussed in off, there is not need for that as it can be solved by ensuring the object is properly populated when instantiating it. The only edge case that would not work would be if a property should be set with a withX but can't be set with a constructor/factory, in which case a "fix" is easy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants