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

[9.x] Add support for casting arrays containing enums #45621

Merged
merged 22 commits into from
Jan 19, 2023

Conversation

ralphjsmit
Copy link
Contributor

@ralphjsmit ralphjsmit commented Jan 12, 2023

Today I encountered a situation in an app where I had a database column in JSON that represented an array with enums.

Currently it is not possible to cast the values inside the array to real enums. The array would just contain the backed string or integer values. It would have been quite cumbersome to write a specific cast every time this situation happens.

After some searching I noticed that Spatie's Laravel Enum package does have support for this using the :array suffix, which I found nice.

This PR implements the casting for enums in an array:

class SomeModel extends Model
{
    protected $casts = [
        'user_type' => UserType::class,
        'user_types' => UserType::class . ':array',
    ];
}

Under the hood, when casting the value, this will store a JSON array with the backed values. When reading the attribute, the json is converted to an array and the backed values are converted to real enums.

Syntax improvements are welcome :)

Thanks!

@ralphjsmit ralphjsmit force-pushed the rjs/add-enum-arrayable-cast branch from 17e6c6b to da40aa8 Compare January 12, 2023 13:05
@ralphjsmit
Copy link
Contributor Author

I'm getting a failure on the MySQL tests on this line due to a space.

-    'string_status_array' => '["pending","done"]'
+    'string_status_array' => '["pending", "done"]'

Any idea what the preferred way would be to let the MySQL tests pass as well?

@ralphjsmit ralphjsmit changed the title [9.x] Add support for casting arrays with enums [9.x] Add support for casting arrays containing enums Jan 12, 2023
@ollieread
Copy link
Contributor

Appending a string to something like this feels a bit yucky, but I honestly can't think of a better solution.

The only one I've got, and I'm not entirely convinced by it, would be to have:

class SomeModel extends Model
{
    protected $casts = [
        'user_type'  => UserType::class,
        'user_types' => [UserType::class]
    ];
}

The cast handling code would need to be updated to look for an array of size 1, and if it was, create an array of values of that type. Though, this could possibly break the code out there that expects the cast type to be a string.

@driesvints
Copy link
Member

I'll have a look at this one today to see if we can test both major versions at the same time.

@driesvints
Copy link
Member

Seem to have found a solution 👍

@ralphjsmit
Copy link
Contributor Author

Thanks @driesvints! Was in doubt whether this issue was already more common or whether such a 'single time patch' solution was better :) Thanks! 🙂

@taylorotwell
Copy link
Member

taylorotwell commented Jan 18, 2023

One primary concern here is the same concern with the array cast - you can't modify it. Note caveats explained here: https://laravel.com/docs/9.x/eloquent-mutators#array-object-and-collection-casting

Ideally this would be an object cast like AsArrayObject or AsCollection that IMO have better behavior when working with arrays.

I coded an example implementation that I tested in my own app and it works as expected. Used like this:

protected $casts = [
    'options' => AsEnumCollection::class.':'.UserOption::class,
];

Code here:

<?php

namespace App;

use BackedEnum;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;

class AsEnumCollection implements Castable
{
    /**
     * Get the caster class to use when casting from / to this cast target.
     *
     * @param  array  $arguments
     * @return object|string
     */
    public static function castUsing(array $arguments)
    {
        return new class($arguments) implements CastsAttributes {
            protected $arguments;

            public function __construct(array $arguments)
            {
                $this->arguments = $arguments;
            }

            public function get($model, $key, $value, $attributes)
            {
                if (! isset($attributes[$key]) ||
                    is_null($attributes[$key])) {
                    return;
                }

                $data = json_decode($attributes[$key], true);

                if (! is_array($data)) {
                    return;
                }

                $enumClass = $this->arguments[0];

                return (new Collection($data))->map(function ($value) use ($enumClass) {
                    return is_subclass_of($enumClass, BackedEnum::class)
                            ? $enumClass::from($value)
                            : constant($enumClass.'::'.$value);
                });
            }

            public function set($model, $key, $value, $attributes)
            {
                return [$key => $value->map(function ($enum) {
                    return $enum instanceof BackedEnum
                        ? $enum->value
                        : $enum->name;
                })->toJson()];
            }

            public function serialize($model, string $key, $value, array $attributes)
            {
                return $value->map(function ($enum) {
                    return $enum instanceof BackedEnum
                        ? $enum->value
                        : $enum->name;
                })->toArray();
            }
        };
    }
}

@ralphjsmit
Copy link
Contributor Author

ralphjsmit commented Jan 19, 2023

@taylorotwell I updated the PR according to what you proposed (with a small change for null values to keep it consistent with the AsCollection cast). Updated the tests as well, so the PR is ready again.

Do also want to support an AsEnumArrayObject cast (in order to stay consistent with the AsArrayObject/AsCollection and the AsEncryptedArrayObjected/AsEncryptedCollection)? Or you don't fancy maintaining two similar casts? 🙂

@taylorotwell
Copy link
Member

Added support for AsEnumArrayObject.

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.

4 participants