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.

Сервіс-провайдер для створення колонок при міграції будь-якого типу:

app/Providers/MigrationServiceProvider.php
<?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]);
        });
    }
}

Обов'язково реєструємо провайдер:

config/app.php
return [
    'providers' => ServiceProvider::defaultProviders()->merge([
        App\Providers\MigrationServiceProvider::class,
    ])->toArray(),
];

Тепер у міграціях можемо використовувати:

some migration
Schema::create('some-table', function (Blueprint $table) {
    $table->typeColumn('some_custom_type', 'type');
});

Для створення enum використовуйте прямий запит до БД:

some migration
DB::unprepared("CREATE TYPE some_type AS ENUM ('some_option1','some_option2')");

Не забувайте видаляти міграції в down функції:

some migration
DB::unprepared('DROP TYPE some_type');

Інтерфейс для enums у БД:

app/Contracts/EnumDB.php
<?php

namespace App\Contracts;

interface EnumDB extends \UnitEnum
{
    public static function dbType(): string;
}
  • dbType повинен повертати назву enum типу в PostgreSQL.

Тестовий трейт на основі інтерфейсу:

app/Traits/Tests/TestEnum.php
<?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.

OpenAPI/Properties/PropertyEnum.php
<?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
        );
    }
}