-
Notifications
You must be signed in to change notification settings - Fork 11.3k
[5.3] Implement object casting #13706
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
Conversation
I like it. Very much. |
I like it, because as always Taylor, I didn't need to read a single word you wrote. The code sells itself. |
Assuming this behaves as expected when casting the object to JSON, I think it's great! |
@pstephan1187 you can implement |
For a moment I was like hmm, this seems ok... Then it clicked. THIS IS AMAZING. |
Does everything get casted to a string? or would it be possible to cast to say, integers, booleans, etc? Edit: I guess it'd be part of the |
Wouw awesome! (y) |
Alex, things are cast to whatever you want. But I'm not sure I understand the question. On Wed, May 25, 2016 at 3:13 PM -0700, "Alex Bowers" notifications@github.com wrote: Does everything get casted to a string? or would it be possible to cast to say, integers, booleans, etc? — |
It is my understanding, that since for example booleans aren't a thing in mysql, but instead it is an integer, there is currently the value of either One thing you can currently do in Eloquent is use
If you want, for example to have an address and one of the fields is |
You state that
But your code example doesn't show an |
I don't see a large need for interface because your code will error anyways if you don't define the methods. So adding an interface is just changing the error message, not really doing anything useful and it's just another file to load off of disk. On Wed, May 25, 2016 at 3:21 PM -0700, "Alex Bowers" notifications@github.com wrote: You state that Value objects MUST implement two methods But your code example doesn't show an implements contract. Will one be enforced for this? — |
Will one be available to use, for those of us that like to use them to ensure that methods are setup properly, and to make the code headers read well? |
I really like this and would be happy as is, but I agree with @alexbowers here that I would really like an interface. I know it might not be very zonda, but I feel like since we have the tools to make the contract explicit, might as well. Plus it opens up interoperability via |
The approach taken by Mongolid, an ODM for MongoDB, designed by @Zizaco works "out of the box" (without the need for a new interface, using class User extends Eloquent
{
public function address()
{
return $this->embedsOne(Address::class, 'address'); // class name, field name
}
} The attribute $address = new Address;
$address->fill($user->address);
return $address; Also, calling There's a lot more logic involved but the main concept is this. It might be a way to go about it. For reference: http://leroy-merlin-br.github.io/mongolid/relationships/ |
An interface adds no value though. It just changes the error message. Does anyone have a use case for the interface other than changing the error message. I don't really want to get distracted by something so minor so maybe lets focus on actual implementation. On Wed, May 25, 2016 at 3:41 PM -0700, "Troy Pavlek" notifications@github.com wrote: I really like this and would be happy as is, but I agree with @alexbowers here that I would really like an interface. I know it might not be very zonda, but I feel like since we have the tools to make the contract explicit, might as well. Plus it opens up interoperability via illuminate/contracts so I don't necessarily need to depend on all of Laravel to provide decent value objects, but I can show that they're compatible. — |
Although I love the idea of having a embeddables feature, like Doctrine 2 has, I don't like this implementation. It's too intrusive, but I guess this is due to Eloquent's fundamental of being kinda magic-ish. I don't like having this behaviour, as actual code, in my value objects (VOs). There's a difference between the configuration and not intrusive way that Doctrine 2 uses and creating actual public methods on a value object that can be called from anywhere. Since it's just an array that's passed in, you'd have to additional validation just to check if the array has all the correct values. Unlike when you use a normal or named constructor (factory method) in which you get implicit validation and you create a stronger boundary. With the current implementation there's no way to use one of the constructors on the VO that might have the needed validation to keep it consistent and valid. In my opinion this type of feature doesn't fit on Eloquent. The idea behind this concept of a VO, which is literally just a good ol' object, is to package certain behaviour up in neat little chunk of code that will always have the same, it'll always be deterministic and thus immutable, but because of Eloquent's inherent openness it doesn't seem like a good fit. A better (seeming) solution could simply be to make a method on the model that instantiates the VO. public function address() {
return new Address($this->lineOne, $this->lineTwo);
} Edit 1: |
I have to agree, having something similar to the current way that relationships works would be pretty neat. To me, casting to an object seems odd (i think). it feels more like what the relationships setup, in terms of the properties created and such. |
@alexbowers except for that this doesn't need any framework support. A value object is simply just, an object. It shouldn't need outside support. If it does, if it's not boxed off (or so to speak), then there's something wrong. |
Taking the address example, this could be solved by creating It appears then this is attempting to refactor that multi-value getter and setter out to its own VO to simplify the model? (Is that the intended problem this PR is trying to solve?) I like it, as I have done the mutator methods many times in the past and it always felt convoluded and dirty. Update: thinking on this some more, really the only place this makes sense is in one of two situations:
I'm thinking that this concept is best used only after thinking long and hard if it is the right solution. |
@mikebronner could you give me a concrete example of what these methods could look like? To me, it all just seems like this can be handled by a simple method on the model itself, just code, not anything the framework has to do. |
Eloquent doesn't need to able to change the properties on the VO, because they're immutable. If they're mutable, there's no need to use a value object. |
@mitchellvanw using the address example above:
And the Address class would be a simple VO, or perhaps a non-eloquent model using |
I would love to see this in Laravel. I must say I like the Doctrine embeddables a bit better. Here you have to define both the constructor and two other methods to make it work. Also: When you add up city/zip for your address there are suddenly four fields that need to be handled and it will get messy with more method parameters and more boilerplate code to be written. The address example is what Symfony uses as well, so it's tempting to compare… Also: PHPMD will squawk at the unused All in all though, I would love to see this feature in Laravel. |
There an other ways to inject the constructor properties. For example we could allow defining which attributes should be injected in the cast definition. On Wed, May 25, 2016 at 5:02 PM -0700, "Torkil Johnsen" notifications@github.com wrote: I would love to see this in Laravel. I must say I like the Doctrine embeddables a bit better. Here you have to define both the constructor and two other methods to make it work. Also: When you add up city/zip for your address there are suddenly four fields that need to be handled and it will get messy with more method parameters and more boilerplate code to be written. The address example is what Symfony uses as well, so it's tempting to compare… Also: PHPMD will squawk at the unused $model parameter in fromModelAttributes() ;) All in all though, I would love to see this feature in Laravel. — |
Eh nevermind that won't be that great. On Wed, May 25, 2016 at 5:14 PM -0700, "Taylor Otwell" taylorotwell@gmail.com wrote: There an other ways to inject the constructor properties. For example we could allow defining which attributes should be injected in the cast definition. On Wed, May 25, 2016 at 5:02 PM -0700, "Torkil Johnsen" notifications@github.com wrote: I would love to see this in Laravel. I must say I like the Doctrine embeddables a bit better. Here you have to define both the constructor and two other methods to make it work. Also: When you add up city/zip for your address there are suddenly four fields that need to be handled and it will get messy with more method parameters and more boilerplate code to be written. The address example is what Symfony uses as well, so it's tempting to compare… Also: PHPMD will squawk at the unused $model parameter in fromModelAttributes() ;) All in all though, I would love to see this feature in Laravel. — |
@mitchellvanw I'm not sure if your argument that you won't have your constructor validation is valid. You said this:
In PHP 7 you could just do this: return new static(
$attributes['line_one'] ?? null,
$attributes['line_two'] ?? null,
); You don't really need to do any additional validation if the attributes exist in the mapping method because you can just send So, I'm not sure it's correct to say you need to check if the attributes exist. All you're doing in this method is creating a new instance of the object. No validation is needed. You can still let your constructor do all that. |
@taylorotwell I think regardless, we are talking about value objects here and there are advantages to them being immutable. |
@taylorotwell Sometimes it does not worth the trouble of having a database-design in the first normal form. When the decision made, right or wrong, I am going to have composite values in some columns, e.g. using json. Each json value in a column has its own schema, and to detect what is the schema, one solution is to have another column, namely ExampleSuppose you need to make a survey-monkey-like application. There are different types of questions in a single questionnaire, but in your app, the idea of having one or multiple tables per each question type is an overkill. So here is what the questions table looks like:
And in the PHP side, I would like to not have any switch-case at all when I need to, for example validate an answer against the question schema:
So when I have an Question::where('id', 10)->schema->validate($answer); No switch-case at all 😄 |
Definitely any class that implements these two methods should be implementing an |
All discussion of an interface can basically stop because it’s not going to happen, heh. There is no point to making an interface other to calm people’s superstitions that it somehow makes the code better. It doesn’t. Period. So, you can discuss other things here but not that :) |
@taylorotwell You wouldn't know this as you don't use an IDE, but using an Interface helps massively when creating new code, as the stub methods are added automatically. |
@joshbrw I can understand why that seems like a reasonable requirement, but coding to satisfy the functionality of an IDE is fundamentally wrong. What do you do with all your specialized code if you switch IDEs or your IDE changes behavior. That's a no-win scenario for me. |
Going to table this for now as it's apparently fairly divisive and I'm not sure I want to take on the complexity in Eloquent for something that isn't more unanimous. |
@taylorotwell I think it's sad that this was closed… For a proper example though, try using Google's Address Data Service format when making a value object. It has roughly these fields:
I might have said earlier that it get's messy with a lot of getters, setters and a huge constructor, but I'm going back on that as I see it as more important to get this feature into the codebase. Check out what the value object looks like in commerceguys/addressing, in their Address class, which can basically be used as an embeddable in Doctrine. Ross Tuck also has a nice article on the embeddables feature in Doctrine, which he calls "the promised land". I think that says it all. Please reconsider! :) |
This PR implements the ability to cast Eloquent values to arbitrary value objects. You may also cast an "attribute" that doesn't actually exist in the database which will allow you to create aggregate value objects that consist of multiple actual fields. Modifying the value object will affect the base model's attributes as you access them, so the model will always "feel" updated.
Value objects MUST implement two methods:
The
fromModelAttributes
will be called when Eloquent needs to create an instance of the value object. The model instance and raw attribute array will be passed to the method and you may construct the value object however you wish. You have total freedom here.The
toModelAttributes
method will be called by Eloquent when it needs to persist the value object's state back into the model. In this method you should return an array that maps back onto the Eloquent object's actual attributes that are stored in the database.Using the example value object above, the following is perfectly valid:
Eloquent will sync the value object's state back into its attributes using the
toModelAttributes
automatically before saving.If a value object is set to
null
, all of the attributes returned bytoModelAttributes
will be set tonull
:When a value object cast object is resolved, the same instance will be returned each time you access it: