-
Notifications
You must be signed in to change notification settings - Fork 11.4k
[5.5] Eloquent object casting #18229
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
[5.5] Eloquent object casting #18229
Conversation
20bb3c9
to
36c982e
Compare
class User extends Model
{
protected $casts = [
'token' => 'secret',
'price' => 'money:price,currency',
];
public function castToSecret($value)
{
return Secret::fromHash($value);
}
// no "castFromSecret" here, because Secret has a __toString() method
public function castToMoney($amount, $currency)
{
return (new Money($amount))->setCurrency($currency);
}
public function castFromMoney($money)
{
return [$money->amount, $money->currency];
}
} class Secret
{
public $secret;
public function __construct($secret)
{
$this->secret = $secret;
}
public static function fromHash($secret)
{
return new static(base64_decode($secret));
}
public function __toString()
{
return base64_encode($this->secret);
}
} class Money
{
public $amount;
public $currency;
public function __construct($amount)
{
$this->amount = $amount;
}
public function setCurrency($currency)
{
$this->currency = $currency;
return $this;
}
} |
glad to see this come back to life |
This is going to be awesome! |
Relevant discussion: laravel/ideas#9 |
|
b36602b
to
36c982e
Compare
Couldn't the previous code in castAttribute use the new logic too? castToInt, castToFloat, castToString, castToBool, ...? Or perhaps just the non-scalar values like castToCollection, castToJson, castToDate, castToDateTime, castToTimestamp? |
Does the object properties have to be public ? We'd moslty want them Some basic example : class Identity
{
protected $firstName;
protected $lastName;
public function __construct($firstName, $lastName) {
$this->firstName = $firstName;
$this->lastName = $lastName;
}
} And +1 with sisve about dependency injection. It should be ideally handled as well. |
@RemiCollin: No, the properties' visibility doesn't matter, because your |
If you want to handle
You can use Laravel's
Yes, I already played around with that a little bit, but this is a separate PR in my opinion — to keep it simple. |
When I'm talking about null values I am thinking of null values in the database. How can I transform them into a meaningful value in php? The castAttribute method checks for nulls, and return nulls, before calling the new casting code. I would prefer if my castToShazaam() method would receive the null value and handle it itself. I don't like the pattern of calling resolve() or App::make() from within models (or any other code), it hides dependencies. I prefer it if method has a clear scope and intent, and only uses what has been provided; not call out and introduce dependencies to things that aren't obvious. |
@tillkruss : ok I see, at first I thought the intention was to handle this in base For DI, agree with @sisve that calling |
@sisve: All If that's not the case for you when testing this PR locally, please provide a working code sample for me to test against. |
I'm looking at the github source view, and it clearly shows the is_null check before doing calling castToClass. https://github.com/tillkruss/framework/blob/36c982ea23bd6759acfd088ad9972bcf16f813f7/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php#L474-L480 What you describe matches b36602b but that isn't part of this PR, according to the GitHub interface. |
Can we change mergeAttributesFromClassCasts into preferring the castFrom* methods instead of __toString? I think that if I write a castFromShazaam method, it should have priority over my Shazaam::__toString() may have another focus than giving values to persist into the database. |
@sisve: That makes sense, just switched it around. Thanks! Regarding the null-scenario: When the model is instantiated from a database record, the |
Value objects generally don't (or shouldn't) have dependencies. If you do need them, use the
These
Uh... really? Why? Calling
And how would that magically be any better? You're reaching into the very same container, and resolving a class dynamically. Dynamic service resolution is a very useful tool. Use it when applicable and stay happy 😀 |
@JosephSilber : you're right the result is the same, I didn't see it from that angle. About Value Objects not having dependencies, I'd agree 99%, though sometime it's nice to have this possibility. Also, while reading the examples above, I wonder if it wouldn't be better to explicitely return Something like this maybe: public function castFromMoney($money)
{
return [
'price' => $money->amount,
'currency' => $money->currency,
];
} |
@RemiCollin those column names shouldn't be hard coded. The casting method should not be aware of what columns the value is stored in. Imagine you have 2 money values in the same table:
Then you'd have this in your casts: $casts = [
'price' => 'money:price,price_currency',
'shipping' => 'money:shipping,shipping_currency',
]; As you can see, the actual column names have to stay abstract. It's up to the user to specify the columns. |
319365d
to
7af24a1
Compare
7af24a1
to
65c2f0a
Compare
: $object->__toString(); | ||
|
||
if ($attributeCount !== count($castedAttributes)) { | ||
throw new LogicException("Class cast {$attribute} must return {$attributeCount} attributes"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't be better to change this to:
"Class cast {$attribute} must return {$attributeCount} attribute(s)" ? I've seen it in some parts of the core
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, just did that. Thanks!
This is marvellous; I'd be very eager to read the documentation. |
Just curious, why the over thinking of dealing with money? I have always just stored a 0.00 if there is no value. |
80913c9
to
6139ad8
Compare
It's just an example. |
What level of SQL query-ability would we have with this? Based on the money example, I would love to do something like: |
This allows the object to decide how to handle `null` values.
0464e0c
to
22b4fe8
Compare
@DanielCoulbourne: Duplication for example, what if you model has 3 attributes that are all the same object, for example: |
@tillkruss I too am struggling to see why all of this can't just be a simple accessor. For example: <?php
class User {
public function getSecretAttribute()
{
return $this->buildOrGetInstance('secret', function() {
return new Secret($this->attributes['secret']);
});
}
public function setSecretAttribute($value)
{
$this->secret->setSecret($value);
$this->attributes['secret'] = $value;
}
} The My thoughts are just kinda "meh" towards this when we've already got something similar. Seems pretty over-engineered. |
@garygreen I like your pseudocode. Is there any available implementation of buildOrGetInstance? I guess the feature/possibility of accessor caching is missing in current eloquent object implementation. On the other hand @tillkruss implementation focuses more on a possibility of creating custom casts. As of custom casts - maybe it should be in a similar style like we currently have for custom validation rules creation |
That's true, to be honest I was kinda suprised to find out recently that when you access attributes that are cast to E.g.: $user->created_at; // Creates new carbon model....
$user->created_at; // Creates 2nd carbon model...
$user->created_at; // Creates 3rd carbon model... etc Seems a bit of a waste to keep creating new instances, but I guess it's useful if you want to mutate the date in some way... then again you can always use |
I downloaded the branch and tested it out. I have some serious problems with this so far. I'm not sure if I'm getting what this solves, maybe the example provided is not a good use case. Problems:1) Creates Variant interface for attributes$user = new User();
$user->price->amount; // property of non object error
$user->price->currency; // property of non object error
if($user->price instanceof Money){
$user->price->amount;
} else {
$user->price ?: 0;
} 2) Incomplete SymmetryThere are now 2 seemingly valid places to set the currency (which is fine) but they are not synced both ways. $user->price = (new Money(10))->setCurrency('BTC');
$user->currency = 'USD';
$user->price->currency; // BTC, does not sync from attribute to value object $user->price = (new Money(10))->setCurrency('BTC');
$user->price->currency = 'USD';
$user->price->currency; // USD
$user->currency; // USD, seems to sync from value object to attribute
// non-public internally referenced `$attributes` property is not updated
$user->attributes['currency'] // 'BTC' Anything internally referencing the attributes array is now getting incorrect data. |
@unstoppablecarl I completely agree with both points. Did you see my example above? It solves both those problems with simple getters and setters if you implement a very basic |
I think this is just more than I want to take on right now. |
It would be great to have this functionality out of the box in Laravel 5.7 |
I ended up using this on my models via a trait:
Then in my models I have $casts, like:
|
It could be extracted to trait then. |
Re-implementation of #13706 after a long discussion with @adamwathan and @JosephSilber.
Example classes are in the next comment below.