Laravel Enum casting error Call to undefined method App\\Enums\\UserType::from()

Viewed 1493

When I try to cast a attribute from my model to one of my enums, it throws an erro:

Call to undefined method App\Enums\UserType::from()

I can not find anything about the required find() method.

I followed the instructions here.

My Enum UserType:

namespace App\Enums;

enum UserType
{
    case CUSTOMER;
    case PARTNER;
    case ADMIN;
}

And my User-model:

namespace App\Models;

use App\Enums\UserType;
use FaarenTech\LaravelCustomUuids\Interfaces\HasCustomUuidInterface;
use FaarenTech\LaravelCustomUuids\Models\UuidModel;
use Illuminate\Auth\MustVerifyEmail;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableInterface;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableInterface;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Auth\Authenticatable;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Auth\Passwords\CanResetPassword;

class User extends UuidModel implements
    AuthorizableInterface,
    AuthenticatableInterface,
    CanResetPasswordInterface,
    HasCustomUuidInterface
{
    use
        HasApiTokens,
        HasFactory,
        Notifiable,
        Authenticatable,
        Authorizable,
        CanResetPassword,
        MustVerifyEmail,
        SoftDeletes;

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'type' => UserType::class
    ];
}
2 Answers

You need to map each enum to a scalar value (int, float, string, bool), this way Laravel can convert from the (database stored) value -> enum.

e.g.

enum UserType: string
{
    case CUSTOMER = 'CUSTOMER';
    case PARTNER = 'PARTNER';
    case ADMIN = 'ADMIN';
}

See Backed Enums in PHP:

Backed enums implement an internal BackedEnum interface, which exposes two additional methods:

from(int|string): self will take a scalar and return the corresponding Enum Case. If one is not found, it will throw a ValueError. This is mainly useful in cases where the input scalar is trusted and a missing enum value should be considered an application-stopping error.

Sometimes you don't want to write case SOMETHING = 'SOMETHING'; because it looks redundant, so I created small trait, that overwrites Illuminate's HasAttributes methods:

<?php

namespace App\Traits;

use BackedEnum;
use Exception;
use UnitEnum;

trait CastsEnums
{
    /**
     * Cast string from database to PHP Enum properly.
     *
     * @param $key
     * @param $value
     * @return mixed|void
     */
    protected function getEnumCastableAttributeValue($key, $value)
    {
        if (is_null($value)) {
            return;
        }

        $castType = $this->getCasts()[$key];

        if ($value instanceof $castType) {
            return $value;
        }

        if ($castType instanceof BackedEnum) {
            return $castType::from($value);
        }

        return $this->getEnumByName($value, $castType);
    }

    /**
     * Rewrite original method, so we can use enums names if values are not presented.
     *
     * @param $key
     * @param $value
     * @return void
     */
    protected function setEnumCastableAttribute($key, $value)
    {
        $enumClass = $this->getCasts()[$key];

        if (!isset($value)) {
            $this->attributes[$key] = null;
        } elseif ($value instanceof $enumClass) {
            $this->attributes[$key] = $value->value ?? $value->name;
        } elseif ($enumClass instanceof BackedEnum) {
            $this->attributes[$key] = $enumClass::from($value)->value;
        } else {
            $this->attributes[$key] = $this->getEnumByName($value, $enumClass);
        }
    }

    private function getEnumByName(string $name, string $enumClass): UnitEnum
    {
        foreach ($enumClass::cases() as $case) {
            if ($case->name === $name) {
                return $case;
            }
        }

        throw new Exception('Enum case not found');
    }
}

Once you've added this trait to your project, you should use it in your model (or in all models, adding it to parent model) that needs cast to non-backed enums.

If you use SQL-enums, make sure that you have same enum cases in PHP!

Related