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:
<?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:
return [
'providers' => ServiceProvider::defaultProviders()->merge([
App\Providers\MigrationServiceProvider::class,
])->toArray(),
];
Now in migrations, we can use:
Schema::create('some-table', function (Blueprint $table) {
$table->typeColumn('some_custom_type', 'type');
});
To create an enum, use a direct query to the DB:
DB::unprepared("CREATE TYPE some_type AS ENUM ('some_option1','some_option2')");
Don't forget to delete the migrations in the down function:
DB::unprepared('DROP TYPE some_type');
Interface for enums in the DB:
<?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:
<?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.
<?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
);
}
}