Photo by Safar Safarov on Unsplash

How to avoid ENUM data type in Laravel Eloquent

Ehsan Kiasaty

--

In your app, users might have different roles such as Admin, Author, publisher, etc. or hotel rooms might have different states like Free, Reserved, Booked.

there are two possible ways to implement this:

a) using enum columns: as you know, it’s not a good idea to use enum data type in the database. you can easily google for enum cons.

b) creating static tables: if we build a table for every enum data type, we end up having a considerable number of small tables with only a few rows, especially in large scale applications. Not much CRUD operations are going to be performed on these tables, they are almost constant small tables.

but there is a better way…

Creating a constant array in the Model instead of building a small database table

as the title suggests, you can create a small constant array in the eloquent model and use it instead of a small database table.

class User extends Model
{
/**
* Users' roles
*
* @var array
*/
public const ROLES = [
'admin' => 1,
'author' => 2
];
}

you can also have functions like getRole() and other methods like:

/**
* determins if the user role is Admin
* @return bool
*/
public function isAdmin()
{
return Auth::user()->role == self::ROLES['admin'];
}
/**
* determins if the user role is Author
* @return bool
*/
public function isAuthor()
{
return Auth::user()->role == self::ROLES['author'];
}

this is what I used to use in my applications, but there is a better way…

escape Enum using Eloquent accessors & mutators

In Laravel, you can have custom setters and getters in eloquent Models.

to learn more about how eloquent accessors & mutators work, read this:

class User extends Model
{
/**
* Users' roles
*
* @var array
*/
public const ROLES = [
1 => 'admin',
2 => 'author'
];
/**
* returns the id of a given role
*
* @param string $role user's role
* @return int roleID
*/
public static function getRoleID($role)
{
return array_search($role, self::ROLES);
}
/**
* get user role
*/
public function getRoleAttribute()
{
return self::ROLES[ $this->attributes['role_id'] ];
}
/**
* set user role
*/
public function setRoleAttribute($value)
{
$roleID = self::getRoleID($value);
if ($roleID) {
$this->attributes['role_id'] = $roleID;
}
}
}

That's it. by doing this, you see numbers in the database and on the other hand in the application, you see words like ‘admin’ and ‘author’.

let’s take a look at an example:

$user = \App\User::first();
echo $user->role; // output: 'admin'echo $user->role_id; // output: 1
$user->role = 'author';echo $user->role; // output: 'author'

or

class User extends Model
{
function isAdmin()
{
return $this->role == 'admin';
}
}

awesome, right? you can behave $user->role just like when there are strings saved in the database.

Note that when you query the database, you should pass the actual user id:

/**
* get the user by id and role
*
* @param integer $id userID
* @param string $role userRole
* @return App\User
*/
public static function findUser($id, $role)
{

return self::where('role', self::getRoleID($role) )->findOrFail($id);
}

as you see above, you should pass the actual user id. in the User class, you can use self::getRoleID() and outside of the User class, for example in controllers you can use \App\User::getRoleID()

Avoid Enums using traits in Laravel or Lumen

If you only have one or two enums, you are good to go with the previous method. simple and easy.

Otherwise, by using a trait you can avoid writing a lot of eloquent accessors & mutators. it adds a little complexity but it makes it more flexible and makes your models cleaner and shorter.

<?phpnamespace App\Traits;use Illuminate\Support\Str;trait EnumTrait
{
/**
* Set a given attribute on the model.
*
* @param string $key
* @param mixed $value
* @return mixed
*/
public function setAttribute($key, $value)
{
if ($enum = self::getEnum($key)) {
$value = Str::singular($value);
$id = array_search($value, constant("self::$enum"));
if ($id) {
$this->attributes[$key] = $id;
return $this;
}
} else {

return parent::setAttribute($key, $value);
}

}
/**
* Get an attribute from the model.
*
* @param string $key
* @return mixed
*/
public function getAttribute($key)
{
if ($enum = self::getEnum($key)) {
return constant("self::$enum")[$this->attributes[$key]];
}

$keyWithoutIdAtTheEnd = rtrim($key, '_id');
if(self::getEnum($keyWithoutIdAtTheEnd)) {
return $this->attributes[$keyWithoutIdAtTheEnd];
}

return parent::getAttribute($key);
}
/**
* Handle dynamic method calls into the model.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public static function __callStatic($method, $parameters)
{
$pattern = '/^get(([A-Z][a-z]+)+)I[dD]$/';
if( preg_match($pattern, $method, $matches) ) {
$key = Str::snake($matches[1]); if ($enum = self::getEnum($key)) { $value = strtolower($parameters[0]); $value = Str::singular($value);

return array_search($value, constant("self::$enum") );
}
}
return parent::__callStatic($method, $parameters);
}
/**
* Get the enum.
*
* @param string $key
* @return enum
*/
public static function getEnum($key)
{
return array_search($key, self::$enums);
}
}

and in the model:

use \App\Traits\EnumTrait;class User extends Model
{
use EnumTrait;
/**
* The attributes that are enum.
*
* @var array
*/
protected static $enums = [
'ROLES' => 'role'
];
/**
* Users' roles
*
* @var array
*/
public const ROLES = [
'admin' => 1,
'author' => 2
];
}

You only need to connect the attribute name to the array in $enums. the trait takes the rest.

conclusion

You can do this for any enum that pops up in your application. like user role, hotel room state, and so on.

If you have an improvement to this or have a better way to implement this in your mind, I would love to know that. email me at ehsan@kiasaty.com

--

--