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

[8.x] ArrayObject + Collection Custom Casts #36245

Merged
merged 5 commits into from
Feb 13, 2021
Merged

[8.x] ArrayObject + Collection Custom Casts #36245

merged 5 commits into from
Feb 13, 2021

Conversation

taylorotwell
Copy link
Member

@taylorotwell taylorotwell commented Feb 12, 2021

This PR implements an opt-in AsArrayObject and AsCollection custom cast.

Background

Of course, Laravel already includes an ability to cast a JSON / TEXT column that contains JSON to an array or collection like so:

$casts = ['options' => 'array'];

However, this does have some downsides. First, the following code is impossible using the simple array cast:

$user = User::find(1);

$user->options['foo'] = 'bar';

$user->save();

Many developers probably expect this to work, but it doesn't. It is impossible to mutate a specific property of the primitive array returned by this cast. You must mutate the entire array:

$user = User::find(1);

$user->options = ['foo' => 'bar'];

$user->save();

AsArrayObject + AsCollection

However, these new casts utilize Eloquent's custom cast feature which implements more intelligent management and caching of objects. The AsArrayObject cast will cast the underlying JSON string to a PHP ArrayObject instance. This class is included in the standard library of PHP and allows an object to behave like an array. Such an approach makes the following possible:

// Within model...
$casts = ['options' => AsArrayObject::class];

// Manipulating the options...
$user = User::find(1);

$user->options['foo']['bar'] = 'baz';

$user->save();

However, it should be noted that an ArrayObject can not be fed into array functions like array_map, so you must use $user->options->toArray() to access the raw underlying array if you wish to use the array in this way. You may also use $user->options->collect() to get a Collection instance from the ArrayObject.

Similar benefits are derived from the AsCollection cast - this cast is similar to the existing collection class, but because of the better object caching and management of custom casts, you are able to mutate singular properties on the collection or even push to the collection.

Final Thoughts

The existing array and collection casts would not be removed from the framework. However, it may be wise to primarily document these new casts (if they are accepted) in the future as they are a bit more robust and developer friendly.

@harmlessprince
Copy link

This is brilliant

@iamharis010
Copy link

This is excellent Taylor. Thanks!

@ahbanavi
Copy link

This is very handy while working with arrays, It's Awsome.

@ryangjchandler
Copy link
Contributor

Yes! Currently hand-rolling these for a better DX. Would love to see them as a first-class cast in the framework so there's some consistency between projects.

@mattstauffer
Copy link
Contributor

You know I love it :)

@timrspratt
Copy link
Contributor

timrspratt commented Feb 12, 2021

Came up against this exact thing a few days ago. Nice.

What about the encrypted:array and encrypted:collection options?

Could any of the current string based cast names be moved to this format too (still supported but documented in the same way these may be to provide consistency with things like dates and times that return carbon instances)?

@taylorotwell
Copy link
Member Author

@timrspratt technically every cast can be turned into a custom cast - but there would be little benefit in doing so in practical terms. Especially for primitives like integers, etc.

@debjit
Copy link

debjit commented Feb 12, 2021

You saved a lot of extra lines in my code.

*/
public function collect()
{
return collect($this->getArrayCopy());
Copy link
Member

Choose a reason for hiding this comment

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

Because the ArrayObject is already Traversable, you don't need to get an ->getArrayCopy() here.

Copy link

Choose a reason for hiding this comment

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

I believe one of my project is exactly waiting for that

@sdbruder
Copy link

sdbruder commented Feb 12, 2021

huge 👍 +1. please.

@olivernybroe
Copy link
Contributor

Really love this!
Hope to see this as default at some point even.

It could be awesome if we even could extend this so when using AsCollection I can specify a cast for the item. (not sure what the syntax should be)

@foremtehan
Copy link
Contributor

foremtehan commented Feb 12, 2021

How can i get ride this error when the data have special characters ?

"JSON: Malformed UTF-8 characters, possibly incorrectly encoded."

I faced this issue when i cast my field to 'array'

$this->expectExceptionMessage('Unable to encode attribute [objectAttribute] for model [Illuminate\Tests\Database\EloquentModelCastingStub] to JSON: Malformed UTF-8 characters, possibly incorrectly encoded.');

@martinduefrandsen
Copy link

Very helpful addition!

@taylorotwell
Copy link
Member Author

Added encrypted counterparts.

@GrahamCampbell GrahamCampbell changed the title ArrayObject + Collection Custom Casts [8.x] ArrayObject + Collection Custom Casts Feb 13, 2021
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes {
Copy link
Contributor

Choose a reason for hiding this comment

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

This anonymous class makes me feel warm and fuzzy

@stephenjude
Copy link

This will come in very handy

@junkystu
Copy link

I've bumped into this a couple of time. Love the solution!

@andrey-helldar
Copy link
Contributor

@taylorotwell, you are getting close to what I already suggested over a year ago. At least this way we will come to this goal.

P.S.: sorry for the trigger. You yourself asked the opinion of others 🙂

@morrislaptop
Copy link
Contributor

This is great! For a slightly more "typed" approach I created laravel-popo-caster just last week. Would love any feedback

@ayimdomnic
Copy link

This is brilliant @taylorotwell .. I’ve been doing a hack around it

return new class implements CastsAttributes {
public function get($model, $key, $value, $attributes)
{
return new ArrayObject(json_decode($attributes[$key], true));

Choose a reason for hiding this comment

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

ArrayObject construct will only take an array, or object. json_decode() can possibly return bool, null among others.

(also worth noting json_decode() does not handle invalid json, which you either have to use the exception flag for, or json_last_error())

https://www.php.net/manual/en/function.json-decode.php#refsect1-function.json-decode-returnvalues

Copy link
Member Author

Choose a reason for hiding this comment

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

Then PHP will throw an error if it is not an error - which is what I would want to happen in this case. The column assigned this cast should always contain something that can be decoded to an array.

Copy link

@honzahana honzahana Feb 18, 2021

Choose a reason for hiding this comment

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

That is bad. Throws an exception web DB column is nullable (and item is null).

brandoncbang added a commit to brandoncbang/form-admin that referenced this pull request Sep 1, 2021
FormEntry Factory class was causing errors on line #66. This was happening because in Laravel, a column cast to an array only allows mutation of the entire array, rather than one property.

Details here: laravel/framework#36245
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.