Skip to content

Instantly share code, notes, and snippets.

@jhoff
Last active December 4, 2022 23:21
Embed
What would you like to do?
Laravel Model Enumeration Trait

Laravel Model Enumeration Trait

The Enum trait is a really useful way to allow you to pre-define all of the valid values for a given field on a model and enforce that their values are set appropriately. This basically allows you to treat a field as a menu without the database overhead of dealing with true enum fields or lookup tables.

Add the Enum trait to your model

namespace App;

use App\Traits\Enums;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use Enums;
    ...
}

Sample Usage

Model App\Post has an enumerated field called status that we want to enforce specific values for.

class Post extends Model
{
    use Enums;

    // Define all of the valid options in an array as a protected property that
    //    starts with 'enum' followed by the plural studly cased field name
    protected $enumStatuses = [
        'Draft',
        'Scheduled',
        'Published',
        'Archived'
    ];

    // Alternately, if you use an associative array, the keys can be used
    //    to set the value as well. The full string will still be stored in the database.
    /* protected $enumStatuses = [
        'dr' => 'Draft',
        'sc' => 'Scheduled',
        'pu' => 'Published',
        'ar' => 'Archived'
    ]; */
    ...

Once you've defined this $enum property on the model, any time that field is set on any instance, a validation process will run to enforce that the value is being set properly:

$post = new App\Post;
$post->status = 'Something Invalid';
// Throws an InvalidEnumException
$post = App\Post::first();
$post->status = 'Draft';
// Sets the value to Draft as expected
// Key values will always work to set the value as well,
//   so using the non-associative array example, this will set status to 'Draft'
$post = App\Post::create([
  'status' => 0
]);

// Using the associative array example, this will set status to 'Published'
$post = App\Post::create([
  'status' => 'pu'
]);

Enumerations work really well in blade files too. Simply use the getEnum static helper:

@foreach( App\Post::getEnum('status') as $key => $value)
    {{ $key }}: {{ $value }}
@endforeach

Or use them with the LaravelCollective form builder:

{{ Form::select('status', App\Post::getEnum('status')) }}
<?php
namespace App\Traits;
use Illuminate\Support\Str;
use App\Exceptions\InvalidEnumException;
trait Enums
{
/**
* Enum property getter
*
* @param string $field
* @return mixed|false
*/
public static function getEnum(string $field)
{
$instance = new static;
if ($instance->hasEnumProperty($field)) {
$property = $instance->getEnumProperty($field);
return $instance->$property;
}
return false;
}
/**
* Check for the presence of a property that starts
* with enum for the provided attribute
*
* @param string $field
* @param mixed $value
* @return $this
* @throws InvalidEnumException
*/
public function setAttribute($field, $value)
{
if ($this->hasEnumProperty($field)) {
if (!$this->isValidEnum($field, $value)) {
throw new InvalidEnumException("Invalid value for " . static::class . "::$field ($value)");
}
if ($this->isKeyedEnum($field, $value)) {
$value = $this->getKeyedEnum($field, $value);
}
}
return parent::setAttribute($field, $value);
}
/**
* Gets the expected enum property
*
* @param string $field
* @return string
*/
protected function getEnumProperty(string $field)
{
return 'enum' . Str::plural(Str::studly($field));
}
/**
* Gets the enum value by key
*
* @param string $field
* @param mixed $key
* @return mixed
*/
protected function getKeyedEnum(string $field, $key)
{
return static::getEnum($field)[$key];
}
/**
* Is an enum property defined for the provided field
*
* @param string $field
* @return boolean
*/
protected function hasEnumProperty(string $field)
{
$property = $this->getEnumProperty($field);
return isset($this->$property) && is_array($this->$property);
}
/**
* Is the provided value a key in the enum
*
* @param string $field
* @param mixed $key
* @return bool
*/
protected function isKeyedEnum(string $field, $key)
{
return in_array($key, array_keys(static::getEnum($field)), true);
}
/**
* Is the value a valid enum in any way
*
* @param string $field
* @param mixed $value
* @return bool
*/
protected function isValidEnum(string $field, $value)
{
return $this->isValueEnum($field, $value) ||
$this->isKeyedEnum($field, $value);
}
/**
* Is the provided value in the enum
*
* @param string $field
* @param mixed $value
* @return bool
*/
protected function isValueEnum(string $field, $value)
{
return in_array($value, static::getEnum($field));
}
}
<?php
namespace App\Exceptions;
use Exception;
class InvalidEnumException extends Exception
{
//
}
@lmartins
Copy link

Hi Jordan, this is really neat!
How do you display the "friendly" name on view where we are displaying the model entry? Like "status 1" instead of its DB value of 0?

@jhoff
Copy link
Author

jhoff commented Jun 18, 2018

@lmartins Sorry for the delay. The intention with this is to store the data in the database as a string. Storing them as integers introduces problems if you decide to change the array order or add and remove later.

@vsharper
Copy link

vsharper commented Sep 12, 2018

@jhoff

Hope I haven't necro-bumped this but I feel that the implementation is a little overkill for what you're trying to achieve.

I've found using an abstract class with constants gives you this flexibility without a hardcore Enum trait, as you've implemented.

I'm not criticising what you've done, nor am I saying that it's wrong, it's just that there may be an easier way to do it.

Usually, if I require enums, I would do something like this:

MyStatusEnum.php

<?php

namespace App\Enums;

abstract class MyStatusEnum
{
    const ON = 'TURN_IT_ON';
    const OFF = 'TURN_IT_OFF';
    const BROKEN = 'CANNOT_TURN_IT_ON_OR_OFF';
}

We're done...

Now, anytime I want to use it, we can do (In our model, or anywhere for that matter):

$status = MyStatusEnum::ON;

$status now has value 'TURN_IT_ON'.

To make it clearer, a real world implementation of a class that's created extending Laravel's Eloquent Model:

AnExampleModel.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class AnExampleModel extends Model
{

    public $fillable = [
        'status',
        'some_var'
    ];

    /**
     * @var App\Enums\MyStatusEnum
     */
    public $status; // My table column of type enum.

    /**
     * @var string $some_var
     */
    public $some_var;
}

Now, when I use this model in my code, I'll be able to do:

Somewhere in your code

$model = new AnExampleModel([
    'status' => MyStatusEnum::ON,
    'some_var' => 'foo'
]);

And you're done. $model->status will have value 'TURN_IT_ON'

@anthonygore
Copy link

Very useful, thanks.

One thing that doesn't quite work, though, is the form builder snippet. What you have is:

{{ Form::select('status', App\Post::getEnum('status')) }}

This will create a select where the values are the array keys, which will be 0, 1, 2 etc. This can be fixed by changing the keys to the values:

{!! Form::select('status', collect(App\Post::getEnum('status'))->mapWithKeys(function($item) { return [$item => $item]; }) !!}

@jhoff
Copy link
Author

jhoff commented May 23, 2020

Wow I totally forgot this gist existed.

I've since added this trait to a small collection of others, which you can find here: https://gitlab.com/two-thirds/eloquent-traits

@vsharper, I'm pretty sure you're missing the point here. Using a separate class with a bunch of consts ( consteses? consti? ) enforces absolutely nothing when the attribute is set or changed. I could still set 'status' to 'potato' and that abstract class isn't gonna stop it. And to make matters worse, you've added this completely separate file for an array of values that has absolutely no ties back to the model it pertains to.

The whole point in using this trait is that there is at least some level of enforcement when data is shoved into a new model.

@anthonygore thanks for the feedback. There newer version listed above has a getSelectEnum method that does just what you're looking for.

@robertosanval
Copy link

Great! Thanks for this gist!

I'd like to make an appreciation, in the model class I think is better to do it this way:

public const STATUSES = [
    'received'  => 'REC',
    'published' => 'PUB',
    'reported'  => 'REP',
    'deleted'   => 'DEL',
];

protected array $enumStatuses = self::STATUSES;

With this way you can get the available statuses from a controller like this:

$user = new User();
$user->setAttribute('status', Picture::STATUSES['received']);
$user->save();

What do you think?

@Sophist-UK
Copy link

Oh how I wish this was included in Laravel or published as a package!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment