Сегодня я хотел бы обсудить с вами несколько путей, в которых можно использовать такой тип как enums в Laravel. Особенно с той точки зрения, что такого типа как Enum в PHP не существует.
Итак, что же такое enum?
Enum - в программировании тип данных, чьё множество значений представляет собой ограниченный список идентификаторов.
Если вкратце, то Enum - это тип данных для категоризации именованных значений. Они могут использоваться в самых разных местах. Например, вместо того, чтобы жёстко прописывать в коде статусы сообщений в блоге, их можно вынести в отдельный класс Enum.
QuickAdminPanel - сервис по генерации админки на Laravel, написанный Povilas Korop. И если вы в нём прописали поля, которые содержат выпадающие списки, то он их выведет в константы следующим образом:
<?php
namespace App\Models;
class User extends Authenticatable
{
use SoftDeletes, Notifiable, HasFactory;
public $table = 'users';
const LANGUAGE_SELECT = [
'ru_ru' => 'Russian',
'en_us' => 'English',
];
const SMS_DAYS_BEFORE_SELECT = [
'0' => 'The same day',
'1' => 'Before 1 day',
'2' => 'Before 2 days',
'3' => 'Before 3 days',
'7' => 'Before week',
'31' => 'Before month',
];
const MAIL_DAYS_BEFORE_SELECT = [
'0' => 'The same day',
'1' => 'Before 1 day',
'2' => 'Before 2 days',
'3' => 'Before 3 days',
'7' => 'Before week',
'31' => 'Before month',
];
}
В этом примере у класса User есть три выпадающих списка, значения которых прописаны в константах. Это нарушает целый ряд принципов чистого кода. С одной стороны мы нагружаем модели ненужной информацией, нарушаем принцип единичной ответственности и к тому же работаем с массивами, которые нельзя ни типизировать, ни использовать дополнительные методы, ни получить преимущества подсветки синтаксиса.
PHP предлагает очень простую имплементацию SPL, но в реальности она не очень сильно помогает. С другой стороны есть очень популярный пакет bensampo/laravel-enum, который я часто использую в своих проектах. Это действительно здорово.
Сейчас мы попробуем его в деле, заменив обычные массивы в выпадающих списках и перенесём их в enums.
Итак, следуя принципам DDD и Porto, создадим родительский абстрактный класс, от которого мы в дальнейшем будем наследоваться:
<?php
namespace Parents\Enums;
class Enum extends \BenSampo\Enum\Enum
{
}
Теперь попробуем сделать наш первый простой enum - выпадающий список поддерживаемых языков:
const LANGUAGE_SELECT = [
'ru_ru' => 'Russian',
'en_us' => 'English',
];
Как вы видите, мы на входе имеем массив, где ключ - это значение выпадающего списка, а значение массива - его текст. Создадим для этого выпадающего списка свой первый класс:
<?php
namespace Domains\Users\Enums;
use BenSampo\Enum\Contracts\LocalizedEnum;
class LanguageEnum extends \Parents\Enums\Enum implements LocalizedEnum
{
const Russian = 'ru_ru';
const English = 'en_us';
}
Как видите, мы сделали здесь наоборот, в названии контанты мы используем текст выпадающего списка. Это сделано для того, чтобы обойти ограничения в поддержках типов данных. Текст он всегда является строкой. А тип INT мы в названии контанты использовать не можем. А значение списка может хранить в себе в том числе и целочисленный тип.
Итак, как же мы можем использовать этот класс. Например, при создании пользователя в blade мы можем использовать следуюшее:
<select class="form-control {{ $errors->has('language') ? 'is-invalid' : '' }}" name="language" id="language">
<option value disabled {{ old('language', null) === null ? 'selected' : '' }}>{{ trans('global.pleaseSelect') }}</option>
@foreach(\Domains\Users\Enums\LanguageEnum::getInstances() as $key => $label)
<option value="{{ $label->value }}" {{ old('language', 'ru_ru') === (string) $label->value ? 'selected' : '' }}>{{$label->description}}</option>
@endforeach
</select>
Для получения всех имеющихся значений мы используем метод \Domains\Users\Enums\LanguageEnum::getInstances(). На выходе мы получаем массив объектов. У этих объектов есть три важных свойства: value, key, description.
В нашем случае value - это значение выпадающего списка. А description - это переведённый текст для отображения на экране. Как же нам перевести значения? Это очень просто. Для этого создадим в папке с переводами resources/lang/en/enums.php файл enums.php:
<?php
use Domains\Users\Enums\LanguageEnum;
use Domains\Users\Enums\SmsDayBefore;
return [
LanguageEnum::class => [
LanguageEnum::Russian => 'Russian',
LanguageEnum::English => 'English',
],
SmsDayBefore::class => [
SmsDayBefore::SAME_DAY => 'The same day',
SmsDayBefore::BEFORE_ONE => 'Before 1 day',
SmsDayBefore::BEFORE_TWO => 'Before 2 days',
SmsDayBefore::BEFORE_THREE => 'Before 3 days',
SmsDayBefore::BEFORE_WEEK => 'Before week',
SmsDayBefore::BEFORE_MONTH => 'Before month'
],
];
Также в blade файле если нам понадобится получить значение списка, мы используем такую конструкцию:
{{ \Domains\Users\Enums\LanguageEnum::fromValue($user->language)?->description ?? '' }}
Но это лишь вершина айсберга тех преимуществ, которые мы получаем. При обращении к полю language модели User мы можем сразу получать объект класса LanguageEnum. Для этого в User.php нам надо прописать следующее:
protected $casts = [
'is_admin' => 'boolean', // Example standard laravel cast
'user_type' => LanguageEnum::class, // Example enum cast
];
Далее где-то в контроллеме мы получаем следующее:
$user = User::first();
$user->language // Instance of LanguageEnum
Как быть с миграциями? И здесь тоже есть очень удобный инструмент:
use \Domains\Users\Enums\LanguageEnum;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class UpdateUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table): void {
$table->enum('type', LanguageEnum::getValues())
->default(LanguageEnum::Russian);
});
}
}
И что не менее важно, мы получаем и дополнительные преимущества по валидации:
$this->validate($request, [
'language' => ['required', new EnumValue(LanguageEnum::class)],
]);
Помните тот массив с ключами в виде int?
const SMS_DAYS_BEFORE_SELECT = [
'0' => 'The same day',
'1' => 'Before 1 day',
'2' => 'Before 2 days',
'3' => 'Before 3 days',
'7' => 'Before week',
'31' => 'Before month',
];
Сделаем из него enum:
<?php
namespace Domains\Users\Enums;
class SmsDayBefore extends \Parents\Enums\Enum implements \BenSampo\Enum\Contracts\LocalizedEnum
{
const SAME_DAY = 0;
const BEFORE_ONE = 1;
const BEFORE_TWO = 2;
const BEFORE_THREE = 3;
const BEFORE_WEEK = 7;
const BEFORE_MONTH = 31;
public static function parseDatabase($value)
{
return (int) $value;
}
}
Как видите, мы в значениях храним не строку, а int! И мы хотим, чтобы операции с БД также осуществлялись в формате int. Здесь нам и поможет метод parseDatabase.
Этот подход в целом исключает необходимость дублирования кода, даёт больше преимуществ в работе с константами по сравнению с обычными массивами. Я знаю что это всё не идеально и надеюсь, что в будущем мы увидим поддержку enum в php.