├── RoboFile.php ├── composer.json ├── config └── modeltyper.php └── src ├── Actions ├── BuildModelDetails.php ├── DetermineAccessorType.php ├── GenerateCliOutput.php ├── GenerateJsonOutput.php ├── Generator.php ├── GetMappings.php ├── GetModels.php ├── MapReturnType.php ├── MatchCase.php ├── RunModelInspector.php ├── WriteColumnAttribute.php ├── WriteEnumConst.php └── WriteRelationship.php ├── Commands ├── ModelTyperCommand.php └── ShowModelTyperMappingsCommand.php ├── Constants └── TypescriptMappings.php ├── Exceptions ├── AbstractModelException.php ├── CommandException.php └── ModelTyperException.php ├── Listeners └── RunModelTyperCommand.php ├── ModelTyperServiceProvider.php ├── Overrides └── ModelInspector.php └── Traits ├── ClassBaseName.php └── ModelRefClass.php /RoboFile.php: -------------------------------------------------------------------------------- 1 | [...$array, ...glob(trim($pattern))], 13 | [] 14 | ); 15 | 16 | $this->taskWatch()->monitor($files, function (FilesystemEvent $event) use ($commands) { 17 | $this->taskExec($commands)->run(); 18 | })->run(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fumeapp/modeltyper", 3 | "description": "Generate TypeScript interfaces from Laravel Models", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "kevin olson", 9 | "email": "acidjazz@gmail.com" 10 | } 11 | ], 12 | "contributors": [ 13 | "kevin olson ", 14 | "tanner Campbell " 15 | ], 16 | "require": { 17 | "php": "^8.2", 18 | "illuminate/support": "^11.33.0|^12.0", 19 | "illuminate/database": "^11.33.0|^12.0", 20 | "illuminate/console": "^11.33.0|^12.0" 21 | }, 22 | "require-dev": { 23 | "consolidation/robo": "^5.1.0", 24 | "larastan/larastan": "^3.0.2", 25 | "laravel/pint": "^1.18.3", 26 | "orchestra/testbench": "^9.6.1|^10.0", 27 | "phpstan/phpstan-deprecation-rules": "^2.0.1", 28 | "phpunit/phpunit": "^11.4.4", 29 | "totten/lurkerlite": "^1.3" 30 | }, 31 | "conflict": { 32 | "laravel/framework": "<11.33.0", 33 | "nesbot/carbon": "<3.5.0" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "FumeApp\\ModelTyper\\": "src/" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Tests\\": "test/Tests/", 43 | "App\\": "test/laravel-skeleton/app/", 44 | "Database\\Factories\\": "test/laravel-skeleton/database/factories/", 45 | "Database\\Seeders\\": "test/laravel-skeleton/database/seeders/" 46 | } 47 | }, 48 | "extra": { 49 | "laravel": { 50 | "providers": [ 51 | "FumeApp\\ModelTyper\\ModelTyperServiceProvider" 52 | ] 53 | } 54 | }, 55 | "scripts": { 56 | "pint": [ 57 | "@php vendor/bin/pint --ansi" 58 | ], 59 | "test": [ 60 | "@php vendor/bin/phpunit --colors --display-errors --testdox" 61 | ], 62 | "test-watch": [ 63 | "@php vendor/bin/robo watch 'src, test/Tests' 'clear && composer test'" 64 | ], 65 | "test-coverage": [ 66 | "@php XDEBUG_MODE=coverage ./vendor/bin/phpunit --colors=always --testdox --coverage-text" 67 | ], 68 | "stan": [ 69 | "@php vendor/bin/phpstan analyse --verbose --ansi" 70 | ], 71 | "post-autoload-dump": [ 72 | "@clear", 73 | "@prepare" 74 | ], 75 | "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", 76 | "prepare": "@php vendor/bin/testbench package:discover --ansi" 77 | }, 78 | "scripts-descriptions": { 79 | "pint": "Run the Pint Linter and Fixer.", 80 | "test": "Run the PHPUnit tests.", 81 | "test-coverage": "Run the PHPUnit tests with code coverage.", 82 | "stan": "Run PHPStan analyzer." 83 | }, 84 | "config": { 85 | "optimize-autoloader": true, 86 | "preferred-install": "dist", 87 | "sort-packages": true 88 | }, 89 | "minimum-stability": "stable", 90 | "prefer-stable": true 91 | } 92 | -------------------------------------------------------------------------------- /config/modeltyper.php: -------------------------------------------------------------------------------- 1 | false, 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Output TypeScript Definitions to a File 20 | |-------------------------------------------------------------------------- 21 | | 22 | | Specifies whether to output the TypeScript definitions to a file. If set 23 | | to true, the definitions will be saved to the specified file path. 24 | */ 25 | 'output-file' => false, 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Path for Output TypeScript Definitions File 30 | |-------------------------------------------------------------------------- 31 | | 32 | | Defines the file path where the TypeScript definitions will be saved. 33 | | 34 | | Requires output-file set to true 35 | */ 36 | 'output-file-path' => './resources/js/types/models.d.ts', 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Generate TypeScript Interfaces in a Global Namespace 41 | |-------------------------------------------------------------------------- 42 | | 43 | | Specifies whether to generate TypeScript interfaces within a global 44 | | namespace. This helps in organizing the interfaces and 45 | | avoiding naming conflicts. 46 | | 47 | | Uses config 'global-namespace' as Namespace 48 | */ 49 | 'global' => false, 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Global Namespace Name for TypeScript Interfaces 54 | |-------------------------------------------------------------------------- 55 | | 56 | | Defines the name of the global namespace where the TypeScript interfaces 57 | | will be generated. This helps in maintaining a clear structure for the 58 | | TypeScript codebase. 59 | | 60 | | Requires global set to true 61 | */ 62 | 'global-namespace' => 'models', 63 | 64 | /* 65 | |-------------------------------------------------------------------------- 66 | | Output the Result in JSON Format 67 | |-------------------------------------------------------------------------- 68 | | 69 | | Specifies whether to output the TypeScript definitions in JSON format. This 70 | | can be useful for further processing or integration with other tools. 71 | */ 72 | 'json' => false, 73 | 74 | /* 75 | |-------------------------------------------------------------------------- 76 | | Use TypeScript Enums Instead of Object Literals 77 | |-------------------------------------------------------------------------- 78 | | 79 | | Determines whether to use TypeScript enums instead of object literals for 80 | | representing certain data structures. Enums provide a more type-safe and 81 | | expressive way to define sets of related constants. 82 | */ 83 | 'use-enums' => false, 84 | 85 | /* 86 | |-------------------------------------------------------------------------- 87 | | Output Plural Form for Models 88 | |-------------------------------------------------------------------------- 89 | | 90 | | Specifies whether to output the plural form of model names. This can be 91 | | useful for consistency and clarity when dealing with collections of models. 92 | | 93 | | Uses Laravel Pluralizer 94 | */ 95 | 'plurals' => false, 96 | 97 | /* 98 | |-------------------------------------------------------------------------- 99 | | Exclude Model Relationships 100 | |-------------------------------------------------------------------------- 101 | | 102 | | Determines whether to exclude model relationships from the TypeScript 103 | | definitions. This can be useful if relationships are not needed in the 104 | | TypeScript codebase. 105 | */ 106 | 'no-relations' => false, 107 | 108 | /* 109 | |-------------------------------------------------------------------------- 110 | | Make Model Relationships Optional 111 | |-------------------------------------------------------------------------- 112 | | 113 | | Specifies whether to make model relationships optional in the TypeScript 114 | | definitions. This allows for more flexibility in handling models that may 115 | | or may not have related data. 116 | */ 117 | 'optional-relations' => false, 118 | 119 | /* 120 | |-------------------------------------------------------------------------- 121 | | Exclude Hidden Model Attributes 122 | |-------------------------------------------------------------------------- 123 | | 124 | | Determines whether to exclude hidden model attributes from the TypeScript 125 | | definitions. Hidden attributes are typically not needed in the client-side 126 | | code. 127 | */ 128 | 'no-hidden' => false, 129 | 130 | /* 131 | |-------------------------------------------------------------------------- 132 | | Output Timestamps as Date Object Types 133 | |-------------------------------------------------------------------------- 134 | | 135 | | Specifies whether to output timestamps as Date object types. This allows 136 | | for more accurate and type-safe handling of date and time values in the 137 | | TypeScript code. 138 | */ 139 | 'timestamps-date' => false, 140 | 141 | /* 142 | |-------------------------------------------------------------------------- 143 | | Make Nullable Attributes Optional 144 | |-------------------------------------------------------------------------- 145 | | 146 | | Determines whether to make nullable attributes optional in the TypeScript 147 | | definitions. This provides better handling of attributes that may not have 148 | | a value. 149 | */ 150 | 'optional-nullables' => false, 151 | 152 | /* 153 | |-------------------------------------------------------------------------- 154 | | Output api.MetApi Interfaces 155 | |-------------------------------------------------------------------------- 156 | | 157 | | Specifies whether to output TypeScript interfaces for api.MetApi resources. 158 | */ 159 | 'api-resources' => false, 160 | 161 | /* 162 | |-------------------------------------------------------------------------- 163 | | Output Fillable Model Attributes 164 | |-------------------------------------------------------------------------- 165 | | 166 | | Specifies whether to output fillable model attributes in the TypeScript 167 | | definitions. Fillable attributes are those that can be mass-assigned. 168 | */ 169 | 'fillables' => false, 170 | 171 | /* 172 | |-------------------------------------------------------------------------- 173 | | Suffix for Fillable Model Attributes 174 | |-------------------------------------------------------------------------- 175 | | 176 | | Defines a suffix to be added to fillable model attributes in the TypeScript 177 | | definitions. This can help in distinguishing fillable attributes from 178 | | other attributes. 179 | | 180 | | Requires fillables set to true 181 | */ 182 | 'fillable-suffix' => 'fillable', 183 | 184 | /* 185 | |-------------------------------------------------------------------------- 186 | | Override or Add New Type Mappings 187 | |-------------------------------------------------------------------------- 188 | | 189 | | Custom mappings allow you to add support for types that are considered 190 | | unknown or override existing mappings. You can also add mappings for your 191 | | custom casts. 192 | | 193 | | Example: 194 | | 'App\Casts\YourCustomCast' => 'string | null', 195 | | 'binary' => 'Blob', 196 | | 'bool' => 'boolean', 197 | | 'point' => 'CustomPointInterface', 198 | | 'year' => 'string', 199 | */ 200 | 'custom_mappings' => [ 201 | // 'binary' => 'Blob', 202 | ], 203 | 204 | /* 205 | |-------------------------------------------------------------------------- 206 | | Define Custom Relationships 207 | |-------------------------------------------------------------------------- 208 | | 209 | | Custom relationships allow you to add support for relationships from 210 | | external packages that are not a part of the Laravel core. Note that 211 | | relationship method names are case sensitive. 212 | | 213 | | Singular: relationships that return a single model 214 | | Plural: relationships that return multiple models 215 | | 216 | | Example: 217 | | 'singular' => [ 218 | | 'belongsToThrough', 219 | | ], 220 | */ 221 | 'custom_relationships' => [ 222 | 'singular' => [ 223 | // 'belongsToThrough', 224 | ], 225 | 226 | 'plural' => [ 227 | // 228 | ], 229 | ], 230 | 231 | /* 232 | |-------------------------------------------------------------------------- 233 | | Case for Model Attributes and Relationships 234 | |-------------------------------------------------------------------------- 235 | | Options: snake, camel, pascal 236 | | Defines the case style for model attributes and relationships in the 237 | | TypeScript definitions. For keeping a consistent naming 238 | | convention throughout the codebase. 239 | */ 240 | 'case' => [ 241 | 'columns' => 'snake', 242 | 'relations' => 'snake', 243 | ], 244 | 245 | /* 246 | |-------------------------------------------------------------------------- 247 | | Included Models 248 | |-------------------------------------------------------------------------- 249 | | 250 | | The include models list allows you to allowlist certain models from being 251 | | generated. 252 | */ 253 | 'included_models' => [ 254 | // Only these models are allowed 255 | ], 256 | 257 | /* 258 | |-------------------------------------------------------------------------- 259 | | Excluded Models 260 | |-------------------------------------------------------------------------- 261 | | 262 | | The exclude models list allows you to ignore certain models from 263 | | generating TypeScript definitions. 264 | */ 265 | 'excluded_models' => [ 266 | // Models to ignore 267 | ], 268 | ]; 269 | -------------------------------------------------------------------------------- /src/Actions/BuildModelDetails.php: -------------------------------------------------------------------------------- 1 | getModelDetails($modelFile); 26 | 27 | if ($modelDetails === null) { 28 | return null; 29 | } 30 | 31 | $reflectionModel = $this->getRefInterface($modelDetails); 32 | $laravelModel = $reflectionModel->newInstance(); 33 | $databaseColumns = $laravelModel->getConnection()->getSchemaBuilder()->getColumnListing($laravelModel->getTable()); 34 | 35 | $name = $this->getClassName($modelDetails['class']); 36 | $columns = collect($modelDetails['attributes'])->filter(fn ($att) => in_array($att['name'], $databaseColumns)); 37 | $nonColumns = collect($modelDetails['attributes'])->filter(fn ($att) => ! in_array($att['name'], $databaseColumns)); 38 | $relations = collect($modelDetails['relations']) 39 | ->when($includedModels, function ($relations, $includedModels) { 40 | return $relations->filter(fn (array $relation) => in_array($relation['related'], $includedModels)); 41 | }) 42 | ->when($excludedModels, function ($relations, $excludedModels) { 43 | return $relations->filter(fn (array $relation) => ! in_array($relation['related'], $excludedModels)); 44 | }); 45 | 46 | $interfaces = collect($laravelModel->interfaces ?? [])->map(fn ($interface, $key) => [ 47 | 'name' => $key, 48 | 'type' => $interface['type'] ?? 'unknown', 49 | 'nullable' => $interface['nullable'] ?? false, 50 | 'import' => $interface['import'] ?? null, 51 | 'forceType' => true, 52 | ]); 53 | 54 | $imports = $interfaces 55 | ->filter(fn (array $interface): bool => isset($interface['import'])) 56 | ->map(fn (array $interface): array => ['import' => $interface['import'], 'type' => $interface['type']]) 57 | ->unique() 58 | ->values(); 59 | 60 | // Override all columns, mutators and relationships with custom interfaces 61 | $columns = $this->overrideCollectionWithInterfaces($columns, $interfaces); 62 | 63 | $nonColumns = $this->overrideCollectionWithInterfaces($nonColumns, $interfaces); 64 | 65 | $relations = $this->overrideCollectionWithInterfaces($relations, $interfaces); 66 | 67 | return [ 68 | 'reflectionModel' => $reflectionModel, 69 | 'name' => $name, 70 | 'columns' => $columns, 71 | 'nonColumns' => $nonColumns, 72 | 'relations' => $relations, 73 | 'interfaces' => $interfaces, 74 | 'imports' => $imports, 75 | ]; 76 | } 77 | 78 | /** 79 | * @return array{"class": class-string<\Illuminate\Database\Eloquent\Model>, database: string, table: string, policy: class-string|null, attributes: \Illuminate\Support\Collection, relations: \Illuminate\Support\Collection, events: \Illuminate\Support\Collection, observers: \Illuminate\Support\Collection, collection: class-string<\Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model>>, builder: class-string<\Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model>>}|null 80 | */ 81 | private function getModelDetails(SplFileInfo $modelFile): ?array 82 | { 83 | $modelFile = Str::of(app()->getNamespace()) 84 | ->append($modelFile->getRelativePathname()) 85 | ->replace('.php', '') 86 | ->toString(); 87 | 88 | return app(RunModelInspector::class)($modelFile); 89 | } 90 | 91 | private function overrideCollectionWithInterfaces(Collection $columns, Collection $interfaces): Collection 92 | { 93 | return $columns->filter(function ($column) use ($interfaces) { 94 | $includeColumn = true; 95 | 96 | $interfaces->each(function ($interface, $key) use ($column, &$includeColumn) { 97 | if ($key === $column['name']) { 98 | $includeColumn = false; 99 | } 100 | }); 101 | 102 | return $includeColumn; 103 | }); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Actions/DetermineAccessorType.php: -------------------------------------------------------------------------------- 1 | $reflectionModel 19 | * 20 | * @throws Exception 21 | */ 22 | public function __invoke(ReflectionClass $reflectionModel, string $mutator): ReflectionMethod 23 | { 24 | $mutator = Str::studly($mutator); 25 | 26 | // Try traditional 27 | try { 28 | return $reflectionModel->getMethod('get' . $mutator . 'Attribute'); 29 | } catch (ReflectionException $e) { 30 | } 31 | 32 | // Try new 33 | try { 34 | return $reflectionModel->getMethod($mutator); 35 | } catch (ReflectionException $e) { 36 | } 37 | 38 | throw new Exception('Accessor method for ' . $mutator . ' on model ' . $reflectionModel->getName() . ' does not exist'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Actions/GenerateCliOutput.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | protected array $enumReflectors = []; 26 | 27 | /** 28 | * @var array> 29 | */ 30 | protected array $imports = []; 31 | 32 | /** 33 | * Output the command in the CLI. 34 | * 35 | * @param Collection $models 36 | * @param array $mappings 37 | */ 38 | public function __invoke(Collection $models, array $mappings, bool $global = false, bool $useEnums = false, bool $plurals = false, bool $apiResources = false, bool $optionalRelations = false, bool $noRelations = false, bool $noHidden = false, bool $optionalNullables = false, bool $fillables = false, string $fillableSuffix = 'Fillable'): string 39 | { 40 | $modelBuilder = app(BuildModelDetails::class); 41 | $colAttrWriter = app(WriteColumnAttribute::class); 42 | $relationWriter = app(WriteRelationship::class); 43 | 44 | if ($global) { 45 | $namespace = Config::get('modeltyper.global-namespace', 'models'); 46 | $this->output .= 'export {}' . PHP_EOL . 'declare global {' . PHP_EOL . " export namespace {$namespace} {" . PHP_EOL . PHP_EOL; 47 | $this->indent = ' '; 48 | } 49 | 50 | $models->each(function (SplFileInfo $model) use ($mappings, $modelBuilder, $colAttrWriter, $relationWriter, $plurals, $apiResources, $optionalRelations, $noRelations, $noHidden, $optionalNullables, $fillables, $fillableSuffix, $useEnums) { 51 | $entry = ''; 52 | $modelDetails = $modelBuilder( 53 | modelFile: $model, 54 | includedModels: Config::get('modeltyper.included_models', []), 55 | excludedModels: Config::get('modeltyper.excluded_models', []), 56 | ); 57 | 58 | if ($modelDetails === null) { 59 | // skip iteration if model details could not be resolved 60 | return; 61 | } 62 | 63 | [ 64 | 'reflectionModel' => $reflectionModel, 65 | 'name' => $name, 66 | 'columns' => $columns, 67 | 'nonColumns' => $nonColumns, 68 | 'relations' => $relations, 69 | 'interfaces' => $interfaces, 70 | 'imports' => $imports, 71 | ] = $modelDetails; 72 | 73 | $this->imports = array_merge($this->imports, $imports->toArray()); 74 | 75 | $entry .= "{$this->indent}export interface {$name} {" . PHP_EOL; 76 | 77 | if ($columns->isNotEmpty()) { 78 | $entry .= "{$this->indent} // columns" . PHP_EOL; 79 | $columns->each(function ($att) use (&$entry, $reflectionModel, $colAttrWriter, $noHidden, $optionalNullables, $mappings, $useEnums) { 80 | [$line, $enum] = $colAttrWriter(reflectionModel: $reflectionModel, attribute: $att, mappings: $mappings, indent: $this->indent, noHidden: $noHidden, optionalNullables: $optionalNullables, useEnums: $useEnums); 81 | if (! empty($line)) { 82 | $entry .= $line; 83 | if ($enum) { 84 | $this->enumReflectors[] = $enum; 85 | } 86 | } 87 | }); 88 | } 89 | 90 | if ($nonColumns->isNotEmpty()) { 91 | $entry .= "{$this->indent} // mutators" . PHP_EOL; 92 | $nonColumns->each(function ($att) use (&$entry, $reflectionModel, $colAttrWriter, $noHidden, $optionalNullables, $mappings, $useEnums) { 93 | [$line, $enum] = $colAttrWriter(reflectionModel: $reflectionModel, attribute: $att, mappings: $mappings, indent: $this->indent, noHidden: $noHidden, optionalNullables: $optionalNullables, useEnums: $useEnums); 94 | if (! empty($line)) { 95 | $entry .= $line; 96 | if ($enum) { 97 | $this->enumReflectors[] = $enum; 98 | } 99 | } 100 | }); 101 | } 102 | 103 | if ($interfaces->isNotEmpty()) { 104 | $entry .= "{$this->indent} // overrides" . PHP_EOL; 105 | $interfaces->each(function ($interface) use (&$entry, $reflectionModel, $colAttrWriter, $mappings) { 106 | [$line] = $colAttrWriter(reflectionModel: $reflectionModel, attribute: $interface, mappings: $mappings, indent: $this->indent); 107 | $entry .= $line; 108 | }); 109 | } 110 | 111 | if ($relations->isNotEmpty() && ! $noRelations) { 112 | $entry .= "{$this->indent} // relations" . PHP_EOL; 113 | $relations->each(function ($rel) use (&$entry, $relationWriter, $optionalRelations, $plurals) { 114 | $entry .= $relationWriter(relation: $rel, indent: $this->indent, optionalRelation: $optionalRelations, plurals: $plurals); 115 | }); 116 | } 117 | 118 | $entry .= "{$this->indent}}" . PHP_EOL; 119 | 120 | if ($plurals) { 121 | $plural = Str::plural($name); 122 | $entry .= "{$this->indent}export type $plural = {$name}[]" . PHP_EOL; 123 | 124 | if ($apiResources) { 125 | $entry .= "{$this->indent}export interface {$name}Results extends api.MetApiResults { data: $plural }" . PHP_EOL; 126 | } 127 | } 128 | 129 | if ($apiResources) { 130 | $entry .= "{$this->indent}export interface {$name}Result extends api.MetApiResults { data: $name }" . PHP_EOL; 131 | $entry .= "{$this->indent}export interface {$name}MetApiData extends api.MetApiData { data: $name }" . PHP_EOL; 132 | $entry .= "{$this->indent}export interface {$name}Response extends api.MetApiResponse { data: {$name}MetApiData }" . PHP_EOL; 133 | } 134 | 135 | if ($fillables) { 136 | $fillableAttributes = $reflectionModel->newInstanceWithoutConstructor()->getFillable(); 137 | $fillablesUnion = implode(' | ', array_map(fn ($fillableAttribute) => "'$fillableAttribute'", $fillableAttributes)); 138 | $entry .= "{$this->indent}export type {$name}{$fillableSuffix} = Pick<$name, $fillablesUnion>" . PHP_EOL; 139 | } 140 | 141 | $entry .= PHP_EOL; 142 | 143 | $this->output .= $entry; 144 | }); 145 | 146 | collect($this->enumReflectors) 147 | ->unique(fn (ReflectionClass $reflector) => $reflector->getName()) 148 | ->each(function (ReflectionClass $reflector) use ($useEnums) { 149 | $this->output .= app(WriteEnumConst::class)($reflector, $this->indent, false, $useEnums); 150 | }); 151 | 152 | collect($this->imports) 153 | ->unique() 154 | ->each(function ($import) { 155 | $importTypeWithoutGeneric = Str::before($import['type'], '<'); 156 | $entry = "import { {$importTypeWithoutGeneric} } from '{$import['import']}'" . PHP_EOL; 157 | $this->output = $entry . $this->output; 158 | }); 159 | 160 | if ($global) { 161 | $this->output .= ' }' . PHP_EOL . '}' . PHP_EOL . PHP_EOL; 162 | } 163 | 164 | return substr($this->output, 0, strrpos($this->output, PHP_EOL)); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Actions/GenerateJsonOutput.php: -------------------------------------------------------------------------------- 1 | > 16 | */ 17 | protected array $output = []; 18 | 19 | /** 20 | * @var array 21 | */ 22 | protected array $enumReflectors = []; 23 | 24 | use ClassBaseName; 25 | use ModelRefClass; 26 | 27 | /** 28 | * Output the command in the CLI as JSON. 29 | * 30 | * @param Collection $models 31 | * @param array $mappings 32 | */ 33 | public function __invoke(Collection $models, array $mappings, bool $useEnums = false): string 34 | { 35 | $modelBuilder = app(BuildModelDetails::class); 36 | $colAttrWriter = app(WriteColumnAttribute::class); 37 | $relationWriter = app(WriteRelationship::class); 38 | $enumWriter = app(WriteEnumConst::class); 39 | 40 | $models->each(function (SplFileInfo $model) use ($modelBuilder, $colAttrWriter, $relationWriter, $mappings, $useEnums) { 41 | $modelDetails = $modelBuilder( 42 | modelFile: $model, 43 | includedModels: Config::get('modeltyper.included_models', []), 44 | excludedModels: Config::get('modeltyper.excluded_models', []), 45 | ); 46 | 47 | if ($modelDetails === null) { 48 | // skip iteration if model details could not be resolved 49 | return; 50 | } 51 | 52 | [ 53 | 'reflectionModel' => $reflectionModel, 54 | 'name' => $name, 55 | 'columns' => $columns, 56 | 'nonColumns' => $nonColumns, 57 | 'relations' => $relations, 58 | 'interfaces' => $interfaces, 59 | ] = $modelDetails; 60 | 61 | $this->output['interfaces'][$name] = $columns 62 | ->merge($nonColumns) 63 | ->merge($interfaces) 64 | ->map(function ($att) use ($reflectionModel, $colAttrWriter, $mappings, $useEnums) { 65 | [$property, $enum] = $colAttrWriter(reflectionModel: $reflectionModel, mappings: $mappings, attribute: $att, jsonOutput: true, useEnums: $useEnums); 66 | if ($enum) { 67 | $this->enumReflectors[] = $enum; 68 | } 69 | 70 | return $property; 71 | })->toArray(); 72 | 73 | $this->output['relations'] = $relations->map(function ($rel) use ($relationWriter, $name) { 74 | $relation = $relationWriter(relation: $rel, jsonOutput: true); 75 | 76 | return [ 77 | $relation['type'] => [ 78 | 'name' => $relation['name'], 79 | 'type' => 'export type ' . $relation['type'] . ' = ' . 'Array<' . $name . '>', 80 | ], 81 | ]; 82 | })->toArray(); 83 | }); 84 | 85 | $this->output['enums'] = collect($this->enumReflectors)->map(function ($enum) use ($enumWriter, $useEnums) { 86 | $enumConst = $enumWriter(reflection: $enum, jsonOutput: true, useEnums: $useEnums); 87 | 88 | return [ 89 | $enumConst['name'] => [ 90 | 'name' => $enumConst['name'], 91 | 'type' => $enumConst['type'], 92 | ], 93 | ]; 94 | })->toArray(); 95 | 96 | return json_encode($this->output, \JSON_PRETTY_PRINT) . PHP_EOL; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Actions/Generator.php: -------------------------------------------------------------------------------- 1 | isEmpty()) { 28 | throw new ModelTyperException('No models found.'); 29 | } 30 | 31 | return $this->display( 32 | models: $models, 33 | global: $global, 34 | json: $json, 35 | plurals: $plurals, 36 | apiResources: $apiResources, 37 | optionalRelations: $optionalRelations, 38 | noRelations: $noRelations, 39 | noHidden: $noHidden, 40 | timestampsDate: $timestampsDate, 41 | optionalNullables: $optionalNullables, 42 | useEnums: $useEnums, 43 | fillables: $fillables, 44 | fillableSuffix: $fillableSuffix 45 | ); 46 | } 47 | 48 | /** 49 | * Return the command output. 50 | * 51 | * @param Collection $models 52 | */ 53 | protected function display(Collection $models, bool $global = false, bool $json = false, bool $useEnums = false, bool $plurals = false, bool $apiResources = false, bool $optionalRelations = false, bool $noRelations = false, bool $noHidden = false, bool $timestampsDate = false, bool $optionalNullables = false, bool $fillables = false, string $fillableSuffix = 'Fillable'): string 54 | { 55 | $mappings = app(GetMappings::class)(setTimestampsToDate: $timestampsDate); 56 | 57 | if ($json) { 58 | return app(GenerateJsonOutput::class)(models: $models, mappings: $mappings, useEnums: $useEnums); 59 | } 60 | 61 | return app(GenerateCliOutput::class)( 62 | models: $models, 63 | mappings: $mappings, 64 | global: $global, 65 | useEnums: $useEnums, 66 | plurals: $plurals, 67 | apiResources: $apiResources, 68 | optionalRelations: $optionalRelations, 69 | noRelations: $noRelations, 70 | noHidden: $noHidden, 71 | optionalNullables: $optionalNullables, 72 | fillables: $fillables, 73 | fillableSuffix: $fillableSuffix 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Actions/GetMappings.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function __invoke(bool $setTimestampsToDate = false): array 16 | { 17 | $mappings = TypescriptMappings::$mappings; 18 | 19 | if ($setTimestampsToDate) { 20 | $mappings['datetime'] = 'Date'; 21 | $mappings['immutable_datetime'] = 'Date'; 22 | $mappings['immutable_custom_datetime'] = 'Date'; 23 | $mappings['date'] = 'Date'; 24 | $mappings['immutable_date'] = 'Date'; 25 | $mappings['timestamp'] = 'Date'; 26 | } 27 | 28 | return array_change_key_case(array_merge( 29 | $mappings, 30 | Config::get('modeltyper.custom_mappings', []), 31 | ), CASE_LOWER); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Actions/GetModels.php: -------------------------------------------------------------------------------- 1 | |null $includedModels 18 | * @param list|null $excludedModels 19 | * @return Collection 20 | */ 21 | public function __invoke(?string $model = null, ?array $includedModels = null, ?array $excludedModels = null): Collection 22 | { 23 | $modelShortName = $this->resolveModelFilename($model); 24 | 25 | if (! empty($includedModels)) { 26 | $includedModels = array_map(fn ($includedModel) => $this->resolveModelFilename($includedModel), $includedModels); 27 | } 28 | 29 | if (! empty($excludedModels)) { 30 | $excludedModels = array_map(fn ($excludedModel) => $this->resolveModelFilename($excludedModel), $excludedModels); 31 | } 32 | 33 | return collect(File::allFiles(app_path())) 34 | ->filter(fn (SplFileInfo $file) => $file->getExtension() === 'php') 35 | ->filter(function (SplFileInfo $file) { 36 | $tokens = token_get_all(file_get_contents($file->getRealPath())); 37 | 38 | $isClassOrAbstract = false; 39 | foreach ($tokens as $token) { 40 | if ($token[0] == T_CLASS) { 41 | $isClassOrAbstract = true; 42 | break; 43 | } 44 | if ($token[0] == T_ABSTRACT) { 45 | $isClassOrAbstract = true; 46 | break; 47 | } 48 | } 49 | 50 | return $isClassOrAbstract; 51 | }) 52 | ->filter(function (SplFileInfo $file) { 53 | $class = app()->getNamespace() . str_replace( 54 | ['/', '.php'], 55 | ['\\', ''], 56 | Str::after($file->getPathname(), app_path() . DIRECTORY_SEPARATOR) 57 | ); 58 | 59 | return class_exists($class) && (new ReflectionClass($class))->isSubclassOf(EloquentModel::class); 60 | }) 61 | ->when($includedModels, function ($files, $includedModels) { 62 | return $files->filter(fn (SplFileInfo $file) => in_array($file->getBasename('.php'), $includedModels)); 63 | }) 64 | ->when($excludedModels, function ($files, $excludedModels) { 65 | return $files->filter(fn (SplFileInfo $file) => ! in_array($file->getBasename('.php'), $excludedModels)); 66 | }) 67 | ->when($modelShortName, function ($files, $modelShortName) { 68 | return $files->filter(fn (SplFileInfo $file) => $file->getBasename('.php') === $modelShortName); 69 | }) 70 | ->values(); 71 | } 72 | 73 | private function resolveModelFilename(?string $model): string|false 74 | { 75 | if ($model === null) { 76 | return false; 77 | } 78 | 79 | return Str::contains($model, '\\') ? Str::afterLast($model, '\\') : $model; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Actions/MapReturnType.php: -------------------------------------------------------------------------------- 1 | $mappings 13 | * 14 | * @throws \ErrorException 15 | */ 16 | public function __invoke(string $returnType, array $mappings): string 17 | { 18 | if (strlen($returnType) === 0) { 19 | throw new ErrorException('Empty string'); 20 | } 21 | 22 | $returnType = explode(' ', $returnType)[0]; 23 | $returnType = explode('(', $returnType)[0]; 24 | $returnType = strtolower($returnType); 25 | 26 | if ($returnType[0] === '?') { 27 | return $mappings[str_replace('?', '', $returnType)] . ' | null'; 28 | } 29 | 30 | if (! isset($mappings[$returnType])) { 31 | return 'unknown'; 32 | } 33 | 34 | return $mappings[$returnType]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Actions/MatchCase.php: -------------------------------------------------------------------------------- 1 | Str::snake($value), 16 | 'camel' => Str::camel($value), 17 | 'pascal' => Str::studly($value), 18 | default => $value, 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Actions/RunModelInspector.php: -------------------------------------------------------------------------------- 1 | app = $app ?? app(); 13 | } 14 | 15 | /** 16 | * Run internal Laravel ModelInspector class. 17 | * 18 | * @see https://github.com/laravel/framework/blob/11.x/src/Illuminate\Database\Eloquent\ModelInspector.php 19 | * 20 | * @param class-string<\Illuminate\Database\Eloquent\Model> $model 21 | * @return array{"class": class-string<\Illuminate\Database\Eloquent\Model>, database: string, table: string, policy: class-string|null, attributes: \Illuminate\Support\Collection, relations: \Illuminate\Support\Collection, events: \Illuminate\Support\Collection, observers: \Illuminate\Support\Collection, collection: class-string<\Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model>>, builder: class-string<\Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model>>}|null 22 | */ 23 | public function __invoke(string $model): ?array 24 | { 25 | try { 26 | return app(ModelInspector::class)->inspect($model); 27 | } catch (\Illuminate\Contracts\Container\BindingResolutionException $th) { 28 | return null; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Actions/WriteColumnAttribute.php: -------------------------------------------------------------------------------- 1 | $reflectionModel 21 | * @param array{name: string, type: string, increments: bool, nullable: bool, default: mixed, unique: bool, fillable: bool, hidden?: bool, appended: mixed, cast?: string|null, forceType?: bool} $attribute 22 | * @param array $mappings 23 | * @return array{array{name: string, type: string}, ReflectionClass|null}|array{string, ReflectionClass|null}|array{null, null} 24 | */ 25 | public function __invoke(ReflectionClass $reflectionModel, array $attribute, array $mappings, string $indent = '', bool $jsonOutput = false, bool $noHidden = false, bool $optionalNullables = false, bool $useEnums = false): array 26 | { 27 | $enumRef = null; 28 | $returnType = app(MapReturnType::class); 29 | 30 | $case = Config::get('modeltyper.case.columns', 'snake'); 31 | $name = app(MatchCase::class)($case, $attribute['name']); 32 | 33 | $type = 'unknown'; 34 | 35 | if ($noHidden && isset($attribute['hidden']) && $attribute['hidden']) { 36 | return [null, null]; 37 | } 38 | 39 | if (isset($attribute['forceType'])) { 40 | $name = $attribute['name']; 41 | $type = $attribute['type']; 42 | } else { 43 | if (! is_null($attribute['cast']) && $attribute['cast'] !== $attribute['type']) { 44 | if (isset($mappings[strtolower($attribute['cast'])])) { 45 | $type = $returnType($attribute['cast'], $mappings); 46 | } else { 47 | if ($attribute['type'] === 'json' || $this->getClassName($attribute['cast']) === 'AsCollection' || $this->getClassName($attribute['cast']) === 'AsArrayObject') { 48 | $type = $returnType('json', $mappings); 49 | } else { 50 | if ($attribute['cast'] === 'accessor' || $attribute['cast'] === 'attribute') { 51 | /** @var \ReflectionMethod $accessorMethod */ 52 | $accessorMethod = app(DetermineAccessorType::class)($reflectionModel, $name); 53 | 54 | $accessorMethodReturnType = $accessorMethod->getReturnType(); 55 | 56 | if (! is_null($accessorMethodReturnType) && $accessorMethodReturnType instanceof ReflectionNamedType) { 57 | if ($accessorMethodReturnType->getName() === 'Illuminate\Database\Eloquent\Casts\Attribute') { 58 | $closure = call_user_func($accessorMethod->getClosure($reflectionModel->newInstance()), 1); 59 | 60 | if (! is_null($closure->get)) { 61 | $rt = (new ReflectionFunction($closure->get))->getReturnType(); 62 | 63 | if (! is_null($rt) && $rt instanceof ReflectionNamedType) { 64 | $type = $returnType($rt->getName(), $mappings); 65 | $enumRef = $this->resolveEnum($rt->getName()); 66 | 67 | if ($enumRef) { 68 | $type = $this->getClassName($rt->getName()); 69 | } 70 | 71 | if ($rt->allowsNull()) { 72 | $attribute['nullable'] = true; 73 | } 74 | } 75 | } 76 | } else { 77 | $type = $this->getClassName($accessorMethodReturnType->getName()); 78 | $enumRef = $this->resolveEnum($accessorMethodReturnType->getName()); 79 | } 80 | } 81 | } else { 82 | if (Str::contains($attribute['cast'], '\\')) { 83 | $reflection = (new ReflectionClass($attribute['cast'])); 84 | if ($reflection->isEnum()) { 85 | $type = $this->getClassName($attribute['cast']); 86 | $enumRef = $reflection; 87 | } 88 | } else { 89 | $cleanStr = Str::of($attribute['cast'])->before(':')->lower()->toString(); 90 | 91 | if (isset($mappings[$cleanStr])) { 92 | $type = $returnType($cleanStr, $mappings); 93 | } else { 94 | dump('Unknown cast type: ' . $attribute['cast']); 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } else { 101 | $type = $returnType($attribute['type'], $mappings); 102 | } 103 | } 104 | 105 | if ($useEnums) { 106 | $type = $enumRef && $type ? ($type . 'Enum') : $type; 107 | } 108 | 109 | if ($attribute['nullable']) { 110 | $type .= ' | null'; 111 | } 112 | 113 | if ((isset($attribute['hidden']) && $attribute['hidden']) || ($optionalNullables && $attribute['nullable'])) { 114 | $name = "{$name}?"; 115 | } 116 | 117 | if ($jsonOutput) { 118 | return [[ 119 | 'name' => $name, 120 | 'type' => $type, 121 | ], $enumRef]; 122 | } 123 | 124 | return ["{$indent} {$this->ensurePropertyIsValid($name)}: {$type}" . PHP_EOL, $enumRef]; 125 | } 126 | 127 | protected function resolveEnum(string $returnTypeName): ?ReflectionClass 128 | { 129 | try { 130 | $reflection = new ReflectionClass($returnTypeName); 131 | 132 | if ($reflection->isEnum()) { 133 | return $reflection; 134 | } 135 | } catch (ReflectionException $e) { 136 | } 137 | 138 | return null; 139 | } 140 | 141 | /** 142 | * Transforms invalid javascript property to valid one by surrounding it with quotes. 143 | * 144 | * This function checks if the property starts with a number or symbols. If it does, it 145 | * surrounds the identifier with double quotes to make it a valid string key. 146 | */ 147 | private function ensurePropertyIsValid(string $identifier): string 148 | { 149 | $firstCharacter = substr($identifier, 0, 1); 150 | 151 | if (! ctype_digit($identifier) && ctype_digit($firstCharacter)) { 152 | return "'$identifier'"; 153 | } 154 | 155 | if (preg_match('/^[^a-zA-Z0-9_$]/', $firstCharacter)) { 156 | return "'$identifier'"; 157 | } 158 | 159 | return $identifier; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Actions/WriteEnumConst.php: -------------------------------------------------------------------------------- 1 | getDocComment(); 19 | 20 | $comments = []; 21 | $docBlock = $reflection->getDocComment(); 22 | if ($docBlock) { 23 | $pattern = "#(@property+\s*[a-zA-Z0-9, ()_].*)#"; 24 | preg_match_all($pattern, $docBlock, $matches, PREG_PATTERN_ORDER); 25 | $comments = array_map(fn ($match) => trim(str_replace('@property', '', $match)), $matches[0]); 26 | } 27 | 28 | $cases = collect($reflection->getConstants()); 29 | 30 | if ($cases->isNotEmpty()) { 31 | if ($useEnums) { 32 | $entry .= "{$indent}export const enum {$reflection->getShortName()} {" . PHP_EOL; 33 | } else { 34 | $entry .= "{$indent}const {$reflection->getShortName()} = {" . PHP_EOL; 35 | } 36 | 37 | $cases->each(function ($case) use ($indent, &$entry, $comments, $useEnums) { 38 | $name = $case->name; 39 | $value = is_string($case->value) ? "'" . addslashes($case->value) . "'" : $case->value; 40 | 41 | // write comments if they exist 42 | if (! empty($comments)) { 43 | foreach ($comments as $comment) { 44 | if (str_starts_with($comment, $name)) { 45 | $comment = str_replace($name, '', $comment); 46 | $comment = preg_replace('/[^a-zA-Z0-9\s]/', '', $comment); 47 | $comment = trim($comment); 48 | $entry .= "{$indent} /** $comment */" . PHP_EOL; 49 | break; 50 | } 51 | } 52 | } 53 | 54 | if ($useEnums) { 55 | $entry .= "{$indent} {$name} = {$value}," . PHP_EOL; 56 | } else { 57 | $entry .= "{$indent} {$name}: {$value}," . PHP_EOL; 58 | } 59 | }); 60 | 61 | if ($useEnums) { 62 | $entry .= "{$indent}}" . PHP_EOL . PHP_EOL; 63 | $entry .= "{$indent}export type {$reflection->getShortName()}Enum = `\${{$reflection->getShortName()}}`" . PHP_EOL . PHP_EOL; 64 | } else { 65 | $entry .= "{$indent}} as const;" . PHP_EOL . PHP_EOL; 66 | $entry .= "{$indent}export type {$reflection->getShortName()} = typeof {$reflection->getShortName()}[keyof typeof {$reflection->getShortName()}]" . PHP_EOL . PHP_EOL; 67 | } 68 | } 69 | 70 | if ($jsonOutput) { 71 | return [ 72 | 'name' => $reflection->getShortName(), 73 | 'type' => $entry, 74 | ]; 75 | } 76 | 77 | return $entry; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Actions/WriteRelationship.php: -------------------------------------------------------------------------------- 1 | getClassName($relation['related']); 25 | $optional = $optionalRelation ? '?' : ''; 26 | 27 | $relationType = match ($relation['type']) { 28 | 'BelongsToMany', 'HasMany', 'HasManyThrough', 'MorphToMany', 'MorphMany', 'MorphedByMany' => $plurals === true ? Str::plural($relatedModel) : (Str::singular($relatedModel) . '[]'), 29 | 'BelongsTo', 'HasOne', 'HasOneThrough', 'MorphOne', 'MorphTo' => Str::singular($relatedModel), 30 | default => $relatedModel, 31 | }; 32 | 33 | if (in_array($relation['type'], Config::get('modeltyper.custom_relationships.singular', []))) { 34 | $relationType = Str::singular($relation['type']); 35 | } 36 | 37 | if (in_array($relation['type'], Config::get('modeltyper.custom_relationships.plural', []))) { 38 | $relationType = Str::singular($relation['type']); 39 | } 40 | 41 | if ($jsonOutput) { 42 | return [ 43 | 'name' => "{$name}{$optional}", 44 | 'type' => $relationType, 45 | ]; 46 | } 47 | 48 | return "{$indent} {$name}{$optional}: {$relationType}" . PHP_EOL; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Commands/ModelTyperCommand.php: -------------------------------------------------------------------------------- 1 | option('model'), 67 | global: $this->getConfig('global'), 68 | json: $this->getConfig('json'), 69 | useEnums: $this->getConfig('use-enums'), 70 | plurals: $this->getConfig('plurals'), 71 | apiResources: $this->getConfig('api-resources'), 72 | optionalRelations: $this->getConfig('optional-relations'), 73 | noRelations: $this->getConfig('no-relations'), 74 | noHidden: $this->getConfig('no-hidden'), 75 | timestampsDate: $this->getConfig('timestamps-date'), 76 | optionalNullables: $this->getConfig('optional-nullables'), 77 | fillables: $this->getConfig('fillables'), 78 | fillableSuffix: $this->getConfig('fillable-suffix'), 79 | ); 80 | 81 | /** @var string|null $path */ 82 | $path = $this->argument('output-file'); 83 | 84 | if (is_null($path) && Config::get('modeltyper.output-file', false)) { 85 | $path = (string) Config::get('modeltyper.output-file-path', ''); 86 | } 87 | 88 | if (! is_null($path) && strlen($path) > 0) { 89 | $this->files->ensureDirectoryExists(dirname($path)); 90 | $this->files->put($path, $output); 91 | 92 | $this->info('Typescript interfaces generated in ' . $path . ' file'); 93 | 94 | return Command::SUCCESS; 95 | } 96 | 97 | $this->line($output); 98 | } catch (ModelTyperException $exception) { 99 | $this->error($exception->getMessage()); 100 | 101 | return Command::FAILURE; 102 | } 103 | 104 | return Command::SUCCESS; 105 | } 106 | 107 | private function getConfig(string $key): string|bool 108 | { 109 | if ($this->option('ignore-config')) { 110 | return $this->option($key); 111 | } 112 | 113 | return $this->option($key) ?: Config::get("modeltyper.{$key}"); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Commands/ShowModelTyperMappingsCommand.php: -------------------------------------------------------------------------------- 1 | option('timestamps-date') ?: Config::get('modeltyper.timestamps-date', false); 43 | 44 | $mappings = collect(app(GetMappings::class)(setTimestampsToDate: $timestampsAsDate)) 45 | ->map(fn (string $mappings, string $key): array => [$key, $mappings]) 46 | ->values() 47 | ->toArray(); 48 | 49 | $this->table(headers: ['From PHP Type', 'To TypeScript Type'], rows: $mappings); 50 | 51 | $this->info('Showing type conversion table using timestamps-date set to ' . ($timestampsAsDate ? 'true' : 'false')); 52 | } catch (\Throwable $throwable) { 53 | $this->error($throwable->getMessage()); 54 | 55 | return Command::FAILURE; 56 | } 57 | 58 | return Command::SUCCESS; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Constants/TypescriptMappings.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | public static array $mappings = [ 14 | 'array' => 'string[]', 15 | 'bigint' => 'number', 16 | 'bool' => 'boolean', 17 | 'boolean' => 'boolean', 18 | 'collection' => 'Record', 19 | 'date' => 'string', 20 | 'immutable_date' => 'string', 21 | 'datetime' => 'string', 22 | 'immutable_datetime' => 'string', 23 | 'immutable_custom_datetime' => 'string', 24 | 'decimal' => 'number', 25 | 'double' => 'number', 26 | 'encrypted' => 'string', 27 | 'float' => 'number', 28 | 'guid' => 'string', 29 | 'hashed' => 'string', 30 | 'integer' => 'number', 31 | 'json' => 'Record', 32 | 'numeric' => 'number', 33 | 'object' => 'Record', 34 | 'string' => 'string', 35 | 'text' => 'string', 36 | 'timestamp' => 'string', 37 | 38 | // mappings for Laravel 11 39 | 'char' => 'string', 40 | 'character' => 'string', 41 | 'enum' => 'string', 42 | 'int' => 'number', 43 | 'longtext' => 'string', 44 | 'mediumint' => 'number', 45 | 'mediumtext' => 'string', 46 | 'smallint' => 'number', 47 | 'tinyint' => 'boolean', 48 | 'time' => 'string', 49 | 'varchar' => 'string', 50 | 'year' => 'number', 51 | ]; 52 | } 53 | -------------------------------------------------------------------------------- /src/Exceptions/AbstractModelException.php: -------------------------------------------------------------------------------- 1 | artisan->call(ModelTyperCommand::class, [], $event->output); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ModelTyperServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 20 | __DIR__ . '/../config/modeltyper.php' => config_path('modeltyper.php'), 21 | ], 'config'); 22 | 23 | if ($this->app->runningInConsole()) { 24 | $this->commands([ 25 | ModelTyperCommand::class, 26 | ShowModelTyperMappingsCommand::class, 27 | ]); 28 | } 29 | 30 | $this->app->singleton(ModelTyperCommand::class, function ($app) { 31 | return new ModelTyperCommand($app['files']); 32 | }); 33 | 34 | if (! $this->app->runningUnitTests() && $this->app['config']->get('modeltyper.run-after-migrate', false) && $this->app['config']->get('modeltyper.output-file', false)) { 35 | $this->app['events']->listen(CommandFinished::class, RunModelTyperCommand::class); 36 | $this->app['events']->listen(MigrationsEnded::class, function () { 37 | RunModelTyperCommand::$shouldRun = true; 38 | }); 39 | } 40 | } 41 | 42 | /** 43 | * Register the application services. 44 | */ 45 | public function register(): void 46 | { 47 | $this->mergeConfigFrom( 48 | __DIR__ . '/../config/modeltyper.php', 49 | 'modeltyper' 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Overrides/ModelInspector.php: -------------------------------------------------------------------------------- 1 | relationMethods = collect(Arr::flatten(Config::get('modeltyper.custom_relationships', []))) 19 | ->map(fn (string $method): string => Str::trim($method)) 20 | ->merge($this->relationMethods) 21 | ->toArray(); 22 | 23 | parent::__construct($app ?? app()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Traits/ClassBaseName.php: -------------------------------------------------------------------------------- 1 | , database: string, table: string, policy: class-string|null, attributes: \Illuminate\Support\Collection, relations: \Illuminate\Support\Collection, events: \Illuminate\Support\Collection, observers: \Illuminate\Support\Collection, collection: class-string<\Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model>>, builder: class-string<\Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model>>} $info 13 | * @return \ReflectionClass<\Illuminate\Database\Eloquent\Model> 14 | */ 15 | public function getRefInterface(array $info): ReflectionClass 16 | { 17 | return new ReflectionClass($info['class']); 18 | } 19 | } 20 | --------------------------------------------------------------------------------