diff --git a/config/modeltyper.php b/config/modeltyper.php index 5da0a25..45d1060 100644 --- a/config/modeltyper.php +++ b/config/modeltyper.php @@ -82,6 +82,20 @@ */ 'use-enums' => false, + /* + |-------------------------------------------------------------------------- + | Emit Plain TypeScript Enums Instead of Const Enums + |-------------------------------------------------------------------------- + | + | Determines whether enums emitted by use-enums use the plain "enum" syntax + | instead of "const enum". Const enum members are inlined by the TypeScript + | compiler and are not accessible at runtime; plain enums produce a real + | object, so their values can be read at runtime. + | + | Requires use-enums set to true + */ + 'plain-enums' => false, + /* |-------------------------------------------------------------------------- | Use TypeScript Types Instead of Interfaces diff --git a/readme.md b/readme.md index 3340887..e58dee2 100644 --- a/readme.md +++ b/readme.md @@ -256,6 +256,7 @@ protected function firstName(): Attribute - --global : Generate typescript interfaces in a global namespace named models - --json : Output the result as json - --use-enums : Use typescript enums instead of object literals +- --plain-enums : Emit a plain enum instead of a const enum when --use-enums is set, so values are readable at runtime - --use-types : Use typescript types instead of interfaces - --plurals : Output model plurals - --no-relations : Do not include relations @@ -492,6 +493,10 @@ get detected and bring in your enum class with your comments: > ModelTyper uses Object Literals by default instead of TS > Enums [for opinionated reasons](https://maxheiber.medium.com/alternatives-to-typescript-enums-50e4c16600b1). But you can > use `--use-enums` option to use TS Enums instead of Object Literals. +> +> With `--use-enums`, ModelTyper emits `const enum` by default. Its members are inlined by the TypeScript compiler, so +> they aren't readable at runtime (e.g. `Object.values(...)` won't work). Add `--plain-enums` alongside `--use-enums` +> to emit a plain `enum` instead, which produces a real object you can read from at runtime. `app/Enums/UserRoleEnum.php` diff --git a/src/Actions/GenerateCliOutput.php b/src/Actions/GenerateCliOutput.php index c5d4f73..064c3e3 100644 --- a/src/Actions/GenerateCliOutput.php +++ b/src/Actions/GenerateCliOutput.php @@ -37,7 +37,7 @@ class GenerateCliOutput * * @throws \ReflectionException */ - public function __invoke(Collection $models, array $mappings, bool $global = false, bool $useEnums = false, bool $useTypes = false, bool $plurals = false, bool $apiResources = false, bool $optionalRelations = false, bool $noRelations = false, bool $noHidden = false, bool $noCounts = false, bool $optionalCounts = false, bool $noExists = false, bool $optionalExists = false, bool $noSums = false, bool $optionalSums = false, bool $optionalNullables = false, bool $fillables = false, string $fillableSuffix = 'Fillable'): string + public function __invoke(Collection $models, array $mappings, bool $global = false, bool $useEnums = false, bool $plainEnums = false, bool $useTypes = false, bool $plurals = false, bool $apiResources = false, bool $optionalRelations = false, bool $noRelations = false, bool $noHidden = false, bool $noCounts = false, bool $optionalCounts = false, bool $noExists = false, bool $optionalExists = false, bool $noSums = false, bool $optionalSums = false, bool $optionalNullables = false, bool $fillables = false, string $fillableSuffix = 'Fillable'): string { $modelBuilder = app(BuildModelDetails::class); $colAttrWriter = app(WriteColumnAttribute::class); @@ -181,8 +181,8 @@ public function __invoke(Collection $models, array $mappings, bool $global = fal collect($this->enumReflectors) ->unique(fn (ReflectionClass $reflector) => $reflector->getName()) - ->each(function (ReflectionClass $reflector) use ($useEnums) { - $this->output .= app(WriteEnumConst::class)($reflector, $this->indent, false, $useEnums); + ->each(function (ReflectionClass $reflector) use ($useEnums, $plainEnums) { + $this->output .= app(WriteEnumConst::class)($reflector, $this->indent, false, $useEnums, $plainEnums); }); collect($this->imports) diff --git a/src/Actions/GenerateJsonOutput.php b/src/Actions/GenerateJsonOutput.php index a82ef9f..a69104d 100644 --- a/src/Actions/GenerateJsonOutput.php +++ b/src/Actions/GenerateJsonOutput.php @@ -35,7 +35,7 @@ class GenerateJsonOutput * * @throws ReflectionException */ - public function __invoke(Collection $models, array $mappings, bool $useEnums = false, bool $noCounts = false, bool $optionalCounts = false, bool $noExists = false, bool $optionalExists = false, bool $noSums = false, bool $optionalSums = false): string + public function __invoke(Collection $models, array $mappings, bool $useEnums = false, bool $plainEnums = false, bool $noCounts = false, bool $optionalCounts = false, bool $noExists = false, bool $optionalExists = false, bool $noSums = false, bool $optionalSums = false): string { $modelBuilder = app(BuildModelDetails::class); $colAttrWriter = app(WriteColumnAttribute::class); @@ -119,8 +119,8 @@ public function __invoke(Collection $models, array $mappings, bool $useEnums = f })->toArray(); }); - $this->output['enums'] = collect($this->enumReflectors)->map(function ($enum) use ($enumWriter, $useEnums) { - $enumConst = $enumWriter(reflection: $enum, jsonOutput: true, useEnums: $useEnums); + $this->output['enums'] = collect($this->enumReflectors)->map(function ($enum) use ($enumWriter, $useEnums, $plainEnums) { + $enumConst = $enumWriter(reflection: $enum, jsonOutput: true, useEnums: $useEnums, plainEnums: $plainEnums); return [ $enumConst['name'] => [ diff --git a/src/Actions/Generator.php b/src/Actions/Generator.php index b2ad6eb..968b0d1 100644 --- a/src/Actions/Generator.php +++ b/src/Actions/Generator.php @@ -19,7 +19,7 @@ class Generator * @throws ModelTyperException * @throws ReflectionException */ - public function __invoke(?string $specificModel = null, bool $global = false, bool $json = false, bool $useEnums = false, bool $useTypes = false, bool $plurals = false, bool $apiResources = false, bool $optionalRelations = false, bool $noRelations = false, bool $noHidden = false, bool $noCounts = false, bool $optionalCounts = false, bool $noExists = false, bool $optionalExists = false, bool $noSums = false, bool $optionalSums = false, bool $timestampsDate = false, bool $optionalNullables = false, bool $fillables = false, string $fillableSuffix = 'Fillable'): string + public function __invoke(?string $specificModel = null, bool $global = false, bool $json = false, bool $useEnums = false, bool $plainEnums = false, bool $useTypes = false, bool $plurals = false, bool $apiResources = false, bool $optionalRelations = false, bool $noRelations = false, bool $noHidden = false, bool $noCounts = false, bool $optionalCounts = false, bool $noExists = false, bool $optionalExists = false, bool $noSums = false, bool $optionalSums = false, bool $timestampsDate = false, bool $optionalNullables = false, bool $fillables = false, string $fillableSuffix = 'Fillable'): string { $models = app(GetModels::class)( model: $specificModel, @@ -37,6 +37,7 @@ public function __invoke(?string $specificModel = null, bool $global = false, bo global: $global, json: $json, useEnums: $useEnums, + plainEnums: $plainEnums, useTypes: $useTypes, plurals: $plurals, apiResources: $apiResources, @@ -63,12 +64,12 @@ public function __invoke(?string $specificModel = null, bool $global = false, bo * * @throws ReflectionException */ - protected function display(Collection $models, bool $global = false, bool $json = false, bool $useEnums = false, bool $useTypes = false, bool $plurals = false, bool $apiResources = false, bool $optionalRelations = false, bool $noRelations = false, bool $noHidden = false, bool $noCounts = false, bool $optionalCounts = false, bool $noExists = false, bool $optionalExists = false, bool $noSums = false, bool $optionalSums = false, bool $timestampsDate = false, bool $optionalNullables = false, bool $fillables = false, string $fillableSuffix = 'Fillable'): string + protected function display(Collection $models, bool $global = false, bool $json = false, bool $useEnums = false, bool $plainEnums = false, bool $useTypes = false, bool $plurals = false, bool $apiResources = false, bool $optionalRelations = false, bool $noRelations = false, bool $noHidden = false, bool $noCounts = false, bool $optionalCounts = false, bool $noExists = false, bool $optionalExists = false, bool $noSums = false, bool $optionalSums = false, bool $timestampsDate = false, bool $optionalNullables = false, bool $fillables = false, string $fillableSuffix = 'Fillable'): string { $mappings = app(GetMappings::class)(setTimestampsToDate: $timestampsDate); if ($json) { - return app(GenerateJsonOutput::class)(models: $models, mappings: $mappings, useEnums: $useEnums, noCounts: $noCounts, optionalCounts: $optionalCounts, noExists: $noExists, optionalExists: $optionalExists, noSums: $noSums, optionalSums: $optionalSums); + return app(GenerateJsonOutput::class)(models: $models, mappings: $mappings, useEnums: $useEnums, plainEnums: $plainEnums, noCounts: $noCounts, optionalCounts: $optionalCounts, noExists: $noExists, optionalExists: $optionalExists, noSums: $noSums, optionalSums: $optionalSums); } return app(GenerateCliOutput::class)( @@ -76,6 +77,7 @@ protected function display(Collection $models, bool $global = false, bool $json mappings: $mappings, global: $global, useEnums: $useEnums, + plainEnums: $plainEnums, useTypes: $useTypes, plurals: $plurals, apiResources: $apiResources, diff --git a/src/Actions/WriteEnumConst.php b/src/Actions/WriteEnumConst.php index f2eacb1..16476d6 100644 --- a/src/Actions/WriteEnumConst.php +++ b/src/Actions/WriteEnumConst.php @@ -11,7 +11,7 @@ class WriteEnumConst * * @return array{type: string, name: string}|string */ - public function __invoke(ReflectionClass $reflection, string $indent = '', bool $jsonOutput = false, bool $useEnums = false): array|string + public function __invoke(ReflectionClass $reflection, string $indent = '', bool $jsonOutput = false, bool $useEnums = false, bool $plainEnums = false): array|string { $entry = ''; @@ -30,7 +30,8 @@ public function __invoke(ReflectionClass $reflection, string $indent = '', bool if ($cases->isNotEmpty()) { if ($useEnums) { - $entry .= "{$indent}export const enum {$reflection->getShortName()} {" . PHP_EOL; + $enumKeyword = $plainEnums ? 'enum' : 'const enum'; + $entry .= "{$indent}export {$enumKeyword} {$reflection->getShortName()} {" . PHP_EOL; } else { $entry .= "{$indent}const {$reflection->getShortName()} = {" . PHP_EOL; } diff --git a/src/Commands/ModelTyperCommand.php b/src/Commands/ModelTyperCommand.php index 3a7e458..ca4784b 100644 --- a/src/Commands/ModelTyperCommand.php +++ b/src/Commands/ModelTyperCommand.php @@ -32,6 +32,7 @@ class ModelTyperCommand extends Command {--global : Generate typescript interfaces in a global namespace named models} {--json : Output the result as json} {--use-enums : Use typescript enums instead of object literals} + {--plain-enums : Emit a plain typescript enum instead of a const enum when --use-enums is set, so values are readable at runtime} {--use-types : Use typescript types instead of interfaces} {--plurals : Output model plurals} {--no-relations : Do not include relations} @@ -79,6 +80,7 @@ public function handle(Generator $generator): int global: $this->getConfig('global'), json: $this->getConfig('json'), useEnums: $this->getConfig('use-enums'), + plainEnums: $this->getConfig('plain-enums'), useTypes: $this->getConfig('use-types'), plurals: $this->getConfig('plurals'), apiResources: $this->getConfig('api-resources'), diff --git a/test/Tests/Feature/Actions/WriteEnumConstTest.php b/test/Tests/Feature/Actions/WriteEnumConstTest.php index 0d83c93..be87448 100644 --- a/test/Tests/Feature/Actions/WriteEnumConstTest.php +++ b/test/Tests/Feature/Actions/WriteEnumConstTest.php @@ -44,4 +44,30 @@ public function test_action_can_be_executed_and_returns_array() $this->assertEquals('Roles', $result['name']); $this->assertIsString($result['type']); } + + public function test_action_generates_const_enum_when_use_enums_is_enabled() + { + $action = app(WriteEnumConst::class); + $reflectionModel = $this->resolveClassAsReflection(Roles::class); + + $result = $action(reflection: $reflectionModel, useEnums: true); + + $expected = $this->getExpectedContent('enum-const.ts', true); + + $this->assertIsString($result); + $this->assertEquals($expected, $result); + } + + public function test_action_generates_plain_enum_when_plain_enums_is_enabled() + { + $action = app(WriteEnumConst::class); + $reflectionModel = $this->resolveClassAsReflection(Roles::class); + + $result = $action(reflection: $reflectionModel, useEnums: true, plainEnums: true); + + $expected = $this->getExpectedContent('enum-plain.ts', true); + + $this->assertIsString($result); + $this->assertEquals($expected, $result); + } } diff --git a/test/Tests/Feature/Console/ModelTyperCommandTest.php b/test/Tests/Feature/Console/ModelTyperCommandTest.php index 3441ddd..3cfb5fa 100644 --- a/test/Tests/Feature/Console/ModelTyperCommandTest.php +++ b/test/Tests/Feature/Console/ModelTyperCommandTest.php @@ -104,6 +104,17 @@ public function test_command_generates_use_enums_when_option_is_enabled() ])->expectsOutput($expected); } + public function test_command_generates_plain_enums_when_option_is_enabled() + { + $expected = $this->getExpectedContent('user-plain-enums.ts'); + + $this->artisan(ModelTyperCommand::class, [ + '--model' => User::class, + '--use-enums' => true, + '--plain-enums' => true, + ])->expectsOutput($expected); + } + public function test_command_generates_plurals_when_option_is_enabled() { $expected = $this->getExpectedContent('user-plurals.ts'); diff --git a/test/input/expectations/enum-const.ts b/test/input/expectations/enum-const.ts new file mode 100644 index 0000000..4bbe89d --- /dev/null +++ b/test/input/expectations/enum-const.ts @@ -0,0 +1,10 @@ +export const enum Roles { + /** Can do anything */ + ADMIN = 'admin', + /** Standard readonly */ + USER = 'user', + /** Value that needs string escaping */ + USERCLASS = 'App\\Models\\User', +} + +export type RolesEnum = `${Roles}` diff --git a/test/input/expectations/enum-plain.ts b/test/input/expectations/enum-plain.ts new file mode 100644 index 0000000..6f444a8 --- /dev/null +++ b/test/input/expectations/enum-plain.ts @@ -0,0 +1,10 @@ +export enum Roles { + /** Can do anything */ + ADMIN = 'admin', + /** Standard readonly */ + USER = 'user', + /** Value that needs string escaping */ + USERCLASS = 'App\\Models\\User', +} + +export type RolesEnum = `${Roles}` diff --git a/test/input/expectations/user-plain-enums.ts b/test/input/expectations/user-plain-enums.ts new file mode 100644 index 0000000..bc7a4e6 --- /dev/null +++ b/test/input/expectations/user-plain-enums.ts @@ -0,0 +1,36 @@ +export interface User { + // columns + id: number + name: string + email: string + email_verified_at: string | null + password?: string + remember_token?: string | null + created_at: string | null + updated_at: string | null + // mutators + role_traditional: string + role_new: string + role_enum: RolesEnum + role_enum_traditional: RolesEnum + score: number + score_nullable: number | null + role_or_string: Roles | stringEnum + // relations + notifications: DatabaseNotification[] + // counts + notifications_count: number + // exists + notifications_exists: boolean +} + +export enum Roles { + /** Can do anything */ + ADMIN = 'admin', + /** Standard readonly */ + USER = 'user', + /** Value that needs string escaping */ + USERCLASS = 'App\\Models\\User', +} + +export type RolesEnum = `${Roles}`