20. Enums. Enum в PostgreSQL. Laravel створює фейкові enums. Macros в Laravel
Відео версія
(YouTube - для зворотнього зв'язку)
Enum НЕ МАЄ проблем з підтримкою в PostgreSQL
Enum в PostgreSQL - це окремий тип. Кожен enum - окремий тип.
PostgreSQL не вміє видаляти enum value. Просто перейменовуйте його.
Laravel не вміє створювати PostgreSQL enum, зате фейкає їх. Будьте уважні!
Скалярні enum більш зручні для розробника.
Macros дозволяють додавати свої функції до сутностей Laravel. Це дуже корисно, але вносить "магію" в код, тому будьте обережні.
Не треба боятися писати фільтри, підгрупи й будь-яку логіку, пов'язану з enum в класі enum.
Сервіс-провайдер для створення колонок при міграції будь-якого типу:
<?php
namespace App\Providers;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Grammars\Grammar;
use Illuminate\Support\Fluent;
use Illuminate\Support\ServiceProvider;
class MigrationServiceProvider extends ServiceProvider
{
public function boot(): void
{
Grammar::macro('typeRaw', function (Fluent $column) {
return $column->get('raw_type');
});
Blueprint::macro('typeColumn', function (string $type, string $columnName) {
return $this->addColumn('raw', $columnName, ['raw_type' => $type]);
});
}
}
Обов'язково реєструємо провайдер:
return [
'providers' => ServiceProvider::defaultProviders()->merge([
App\Providers\MigrationServiceProvider::class,
])->toArray(),
];
Тепер у міграціях можемо використовувати:
Schema::create('some-table', function (Blueprint $table) {
$table->typeColumn('some_custom_type', 'type');
});
Для створення enum використовуйте прямий запит до БД:
DB::unprepared("CREATE TYPE some_type AS ENUM ('some_option1','some_option2')");
Не забувайте видаляти міграції в down функції:
DB::unprepared('DROP TYPE some_type');
Інтерфейс для enums у БД:
<?php
namespace App\Contracts;
interface EnumDB extends \UnitEnum
{
public static function dbType(): string;
}
- dbType повинен повертати назву enum типу в PostgreSQL.
Тестовий трейт на основі інтерфейсу:
<?php
namespace App\Traits\Tests;
use App\Contracts\EnumDB;
use Illuminate\Support\Facades\DB;
use ReflectionClass;
use ReflectionException;
use Tests\TestCase;
use OpenApi\Attributes\Schema;
/**
* @mixin TestCase
*/
trait TestEnum
{
/**
* @return string|EnumDB
*/
abstract private function testedEnum(): string|EnumDB;
/**
* @test
*/
public function enumEqualWithDatabase(): void
{
$enum = $this->testedEnum();
$dbType = $enum::dbType();
$cases = $enum::cases();
$valuesInDB = DB::select("SELECT unnest(enum_range(NULL::{$dbType})) as value");
$valuesInDB = array_map(fn ($item) => $item->value, $valuesInDB);
$this->assertEquals(count($valuesInDB), count($cases));
foreach ($cases as $case) {
$enumValue = $case->value ?? $case->name;
$this->assertContains($enumValue, $valuesInDB);
}
}
/**
* @test
*
* @throws ReflectionException
*/
public function enumHasOpenApiSchema(): void
{
$enum = $this->testedEnum();
$reflector = new ReflectionClass($enum);
$schemaAttributes = $reflector->getAttributes(Schema::class);
$this->assertNotEmpty($schemaAttributes);
}
}
Приклад enum:
<?php
namespace Modules\Device\Enums;
use OpenApi\Attributes as OA;
use App\Contracts\EnumDB;
#[OA\Schema(type: 'string')]
enum DeviceType: string implements EnumDB
{
case Mobile = 'mobile';
case Tablet = 'tablet';
case Desktop = 'desktop';
case Bot = 'bot';
public static function dbType(): string
{
return 'device_type';
}
}
#[OA\Schema(type: 'string')]
- створює OpenAPI схему enum.
І тест буде виглядати так:
<?php
namespace Modules\Device\Tests\Unit\Enums;
use App\Contracts\EnumDB;
use App\Traits\Tests\TestEnum;
use Modules\Device\Enums\DeviceType;
use Tests\TestCase;
class DeviceTypeTest extends TestCase
{
use TestEnum;
private function testedEnum(): string|EnumDB
{
return DeviceType::class;
}
}
І якщо використовуєте власні OpenAPI атрибути, ось зручний для enums.
<?php
namespace OpenAPI\Properties;
use OpenApi\Attributes as OA;
use ReflectionClass;
use ReflectionException;
class PropertyEnum extends OA\Property
{
/**
* @throws ReflectionException
*/
public function __construct(
string $property,
string|\UnitEnum $enum,
bool $nullable = false,
?string $example = null,
) {
$className = (new ReflectionClass($enum))->getShortName();
$ref = "#/components/schemas/{$className}";
$example = $example ?? ($enum::cases()[0]->value || $enum::cases()[0]->name);
if ($nullable) {
return parent::__construct(
property: $property,
example: $example,
nullable: true,
anyOf: [
new OA\Schema(
ref: $ref
),
new OA\Schema(
type: 'null',
),
]
);
}
parent::__construct(
property: $property,
ref: $ref,
type: 'enum',
example: $example
);
}
}