Skip to content
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.3] [WIP] Custom Type Casting #13315

Closed
wants to merge 6 commits into from

Conversation

AdenFraser
Copy link
Contributor

@AdenFraser AdenFraser commented Apr 26, 2016

This is an early draft of some custom type casting functionality, discussed here:
laravel/ideas#9

The proposed implementation would allow Model Attributes to be cast to custom classes, with optional parameters.

For instance:

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'html_content' => HtmlString::class,
        'money' => '\App\Money,£',
    ];

Where the cast class is instantiated with the attribute value as the first parameter, followed by any additional parameters provided.

Assuming a casting class has a __toString() method, this will be used when inserting the attribute back into the database.

A simple custom class example could be automatic casting to HtmlString (useful for WYSIWYG stored content). Creating additional cast classes for Value Objects, such as Money, Currency, Url etc would be as straightforward as creating a class with a __construct and __toString().

class Money 
{
    protected $value;

    public function __construct($value, $symbol)
    {
        $this->value = $value;
        $this->symbol = $symbol;
    }

    public function withSymbol()
    {
          return $this->symbol.$this->value;
    }

    public function add($value)
    {
          $this->value  = $this->value + $value;

          return $this;
    }

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

This could certainly be made stricter, perhaps with a Castable interface to be implemented on castable objects but it works nicely as above.

This is an early draft to gather some feedback, thoughts?

:)

@AdenFraser
Copy link
Contributor Author

I've noticed this is breaking casting to 'datetime' (a Carbon instance) - fixing now.

@tomschlick
Copy link
Contributor

I think there needs to be a way for the cast class to access the model, or at least multiple fields.

For instance if you had a payments table utilizing the Money class example you may have different currencies per row. So it would be nice to have the amount field as well as the currency field both injected into the class so you can pass back the correct format.

@AdenFraser
Copy link
Contributor Author

I agree that would be useful, I'm not sure how best to implement that without making the process of creating a Casting class more conveluted. Thoughts?

@tomschlick
Copy link
Contributor

maybe something like this?

/**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'amount' => [Money::class, 'currency'],
        'amount' => '\App\Money,currency',
        'amount' => '\App\Money,£',
    ];

the thought is if the parameter == a field name then inject that field, if not then just inject that string.

@barryvdh
Copy link
Contributor

See my comment here: laravel/ideas#9 (comment)

This seems a bit confusing and not in line with other Laravel declarations (eg. routes/validators are My\Class@method, or money:£

@barryvdh
Copy link
Contributor

Also, probably better to use the Laravel App container to build your classes, instead of reflection, if you go this way.

@AdenFraser
Copy link
Contributor Author

AdenFraser commented Apr 26, 2016

@barryvdh Agree with your comments laravel/ideas#9 (comment)

@tomschlick I think to avoid unexpected conflicts of field values as parameters what you've proposed would need to be more inline with the way Routing and Middleware parameters work - where {field} is injected with it's dynamic value, for instance

protected $casts = [
    'amount' => '\App\Money,{currency}',
];

Extended to @barryvdh's suggestion would look like:

protected $casts = [
    'amount' => 'money,{currency}',
];

Which I think is clean and inline with Routing, Middleware, Validation, etc.

@AdenFraser
Copy link
Contributor Author

I do agree that building the class through the App container is the way to go!

@tomschlick
Copy link
Contributor

Yeah 👍 for that.

@GrahamCampbell
Copy link
Member

I do agree that building the class through the App container is the way to go!

Note that we cannot do that in an eloquent model. It cannot be coupled like that.

@JosephSilber
Copy link
Member

JosephSilber commented Apr 27, 2016

What would be the reason for resolving it out of the container? You should only ever cast to value objects, and those shouldn't ever have dependencies.

For example, if you want the ability to convert between different currencies at the current rate, you should have a separate service that handles that; your Money class should not be aware of that at all.

@AdenFraser
Copy link
Contributor Author

@JosephSilber I largely agree, although I wonder if there would be a use case where someone would like to resolve an interface implementation of 'Money'. I'm not to sure of the practicality of that though.

*/
protected function castExists($key)
{
if (! $this->castClass($key)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return $this->castClass($key);

@AdenFraser
Copy link
Contributor Author

I'll be attempting to wrap this up later this week, I'm currently transitioning between machines so a few things have had to go on the backburner.

@halaei
Copy link
Contributor

halaei commented May 11, 2016

If it is OK to use PHP serialization, It will be possible to support inheritance as well. Please take a look at #13511

@taylorotwell
Copy link
Member

Where are we at with this? :)

@AdenFraser
Copy link
Contributor Author

Sorry been hectic with a change in jobs. Will sink my teeth into this
properly this week.

Happy to open up write access to my fork (if that's possible?) If others
would like to assist in fleshing it out.

Definitely need to find time for this as I hope it can make it into 5.3 ;)
On 22 May 2016 22:58, "Taylor Otwell" notifications@github.com wrote:

Where are we at with this? :)


You are receiving this because you authored the thread.
Reply to this email directly or view it on GitHub
#13315 (comment)

@taylorotwell
Copy link
Member

I have an alternative implementation of this that I think addresses all concerns fairly elegantly. (Resolving stuff from IoC, mutating multiple fields into one aggregate value object, etc.) I'll try to get it pushed up as a PR today so y'all can take a look and see what you think.

@taylorotwell
Copy link
Member

While I have an implementation, I am still curious why people would prefer this approach over just defining getFooAttribute and setFooAttribute on their model and creating the value objects that way. What advantage would this approach give?

@JosephSilber
Copy link
Member

JosephSilber commented May 24, 2016 via email

@AdenFraser
Copy link
Contributor Author

@taylorotwell Awesome, I look forward to seeing what you decided to put together!

@halaei
Copy link
Contributor

halaei commented May 25, 2016

I have a question. If we change the casted object, will the model get affected as well? e.g:

$model->money = '10$';
$model->money->add(10)
echo $model->money;

is $model->money now 20$ or still 10$?

@AdenFraser
Copy link
Contributor Author

When the save action is used, it would be expected that your casted option
has a way of conversion from object back to string for database storage.

Presumably most people would want the new value to be stored in the
database, the same as money being an integer, adding 10 and saving would do.
On 25 May 2016 06:11, "Hamid Alaei Varnosfaderani" notifications@github.com
wrote:

I have a question. If we change the casted object, will the model get
affected as well? e.g:

$model->money = '10$';$model->money->add(10)echo $model->money;

is $model->money now 20$ or still 10$?


You are receiving this because you authored the thread.
Reply to this email directly or view it on GitHub
#13315 (comment)

@halaei
Copy link
Contributor

halaei commented May 25, 2016

@AdenFraser sorry don't find my answer. I wasn't talking about the saved value in the database, I am considering the in memory values. Let me put it this way, if you read $model->money twice, will you get the same object for the second time or will it be a new Money object that is equal to the previous one, but not the same. Or simply, will this assertion pass?

    assert($model->money === $model->money);

@AdenFraser
Copy link
Contributor Author

Once the value is casted, you will receive an object. So you'd have to
assert that $model->money is equal to a Money Object which has a value
property now set to 20.

In memory it would behave the same as accessory and mutations- the object
is what you would receive when you attempt to access the money property on
your model.
On 25 May 2016 11:54, "Hamid Alaei Varnosfaderani" notifications@github.com
wrote:

@AdenFraser https://github.com/AdenFraser sorry don't find my answer. I
wasn't talking about the saved value in the database, I am considering
the in memory values. Let me put it this way, if you read $model->money
twice, will you get the same object for the second time or will it be a
new Money object that is equal to the previous one, but not the same. Or
simply, will this assertion pass?

assert($model->money === $model->money);


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
#13315 (comment)

@taylorotwell
Copy link
Member

Yeah I think that value object casting would need to be cached and only done once per model like Hamid is saying.

On May 25, 2016, at 6:42 AM, Aden Fraser notifications@github.com wrote:

Once the value is casted, you will receive an object. So you'd have to
assert that $model->money is equal to a Money Object which has a value
property now set to 20.

In memory it would behave the same as accessory and mutations- the object
is what you would receive when you attempt to access the money property on
your model.
On 25 May 2016 11:54, "Hamid Alaei Varnosfaderani" notifications@github.com
wrote:

@AdenFraser https://github.com/AdenFraser sorry don't find my answer. I
wasn't talking about the saved value in the database, I am considering
the in memory values. Let me put it this way, if you read $model->money
twice, will you get the same object for the second time or will it be a
new Money object that is equal to the previous one, but not the same. Or
simply, will this assertion pass?

assert($model->money === $model->money);


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
#13315 (comment)


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub #13315 (comment)

@taylorotwell
Copy link
Member

#13706

Let's take up discussion here.

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

Successfully merging this pull request may close these issues.

None yet

8 participants