20. Enums. Enum in PostgreSQL. Laravel creates fake enums. Macros in Laravel

Video version
(Leave your feedback on YouTube)

Enum HAS NO issues with support in PostgreSQL

Enum in PostgreSQL is a separate type. Each enum is a separate type.

PostgreSQL can't delete enum value. Just rename it.

Laravel can't create PostgreSQL enums, but it fakes them. Be careful!

Backed enums are more convenient for the developer.

Macros allow adding your functions to Laravel entities. This is very useful but introduces "magic" into the code, so be careful.

Don't be afraid to write filters, subsets, and any logic related to enums in the enum class.

Service provider for creating columns in migrations of any type:

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]);
        });
    }
}

Be sure to register the provider:

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

Now in migrations, we can use:

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

To create an enum, use a direct query to the DB:

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

Don't forget to delete the migrations in the down function:

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

Interface for enums in the DB:

app/Contracts/EnumDB.php
<?php

namespace App\Contracts;

interface EnumDB extends \UnitEnum
{
    public static function dbType(): string;
}
  • dbType should return the name of the enum type in PostgreSQL.

Test trait based on the interface:

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);
    }
}

Example 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')] creates an OpenAPI schema for the enum.

And the test will look like this:

<?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;
    }
}

And if you use custom OpenAPI attributes, here's a convenient one for 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
        );
    }
}