├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── changelog.md ├── composer.json ├── config └── laravel-typescript.php ├── contributing.md ├── docs └── index.md ├── license.md ├── phpunit.xml ├── readme.md └── src ├── Console └── Commands │ └── LaravelTypescriptCommand.php ├── Facades └── LaravelTypescript.php ├── Helpers ├── Attributes.php ├── Column.php ├── Generator.php ├── ModelInspectionResult.php ├── ModelInspector.php ├── Reflection.php └── Types.php ├── LaravelTypescript.php ├── LaravelTypescriptServiceProvider.php ├── Traits └── HasTypeGenerators.php ├── Types ├── ColumnType.php ├── Definition.php ├── EnumType.php ├── LaravelCastType.php ├── PhpType.php ├── RelationType.php └── Type.php └── Util ├── AccessorResolver.php └── Counter.php /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `Typescript Transformer` will be documented in this file. 4 | 5 | ## Version 1.0 6 | 7 | ### Added 8 | - Everything 9 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wovosoft/laravel-typescript", 3 | "description": "Transforms Laravel Models to Typescript Interfaces/Types", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Narayan Adhikary", 8 | "email": "wovosoft@gmail.com", 9 | "homepage": "https://wovosoft.com" 10 | } 11 | ], 12 | "homepage": "https://github.com/wovosoft/laravel-typescript", 13 | "keywords": [ 14 | "Laravel", 15 | "Typescript Transformer", 16 | "Typescript Interfaces", 17 | "Typescript Types", 18 | "php to typescript" 19 | ], 20 | "require": { 21 | "composer/class-map-generator": "*" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Wovosoft\\LaravelTypescript\\": "src/" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "Wovosoft\\LaravelTypescript\\Tests\\": "tests" 31 | } 32 | }, 33 | "extra": { 34 | "laravel": { 35 | "providers": [ 36 | "Wovosoft\\LaravelTypescript\\LaravelTypescriptServiceProvider" 37 | ], 38 | "aliases": { 39 | "LaravelTypescript": "Wovosoft\\LaravelTypescript\\Facades\\LaravelTypescript" 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /config/laravel-typescript.php: -------------------------------------------------------------------------------- 1 | resource_path('js/types/models.d.ts'), 5 | 'source_dir' => app_path('Models'), 6 | /** 7 | * Custom attributes should have return types defined. 8 | * But if it is not, then the return type should be this type. 9 | * And this value should be php supported return types. 10 | * like primitive types or any other classes. 11 | */ 12 | 'custom_attributes' => [ 13 | 'fallback_return_type' => 'string', 14 | /* 15 | * Return type resolver for the new style attribute's accessor method. 16 | * eg. prlDate():Attribute => Attribute::get(fn():return_type=>return_value) 17 | */ 18 | 'accessor_resolvers' => \Wovosoft\LaravelTypescript\Util\AccessorResolver::class, 19 | ], 20 | /** 21 | * Custom relations should have return types defined. 22 | * But if it is not, then the return type should be this type. 23 | * And this value should be php supported return types. 24 | * like primitive types or any other classes. 25 | * @see https://github.com/staudenmeir/eloquent-has-many-deep 26 | */ 27 | "counter" => \Wovosoft\LaravelTypescript\Util\Counter::class 28 | ]; 29 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome and will be fully credited. 4 | 5 | Contributions are accepted via Pull Requests on [Github](https://github.com/wovosoft/typescript-transformer). 6 | 7 | # Things you could do 8 | If you want to contribute but do not know where to start, this list provides some starting points. 9 | - Add license text 10 | - Remove rewriteRules.php 11 | - Set up TravisCI, StyleCI, ScrutinizerCI 12 | - Write a comprehensive ReadMe 13 | 14 | ## Pull Requests 15 | 16 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 17 | 18 | - **Document any change in behaviour** - Make sure the `readme.md` and any other relevant documentation are kept up-to-date. 19 | 20 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 21 | 22 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 23 | 24 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 25 | 26 | 27 | **Happy coding**! 28 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ## Story 2 | 3 | ```php 4 | ### First we need to get the model classes in a directory 5 | 6 | use Wovosoft\LaravelTypescript\Helpers; 7 | 8 | $modelClasses = Helpers\ModelInspector::getModelsIn(app_path('Models')); 9 | 10 | 11 | 12 | ### Then, these models are passed to the generator 13 | 14 | $contents = Helpers\Transformer::generate($modelClasses) 15 | 16 | ``` 17 | 18 | ### What happens in `Helpers\Transfer::generate` method 19 | 20 | Generates a collection of provided models including attributes, columns and relations 21 | in a parsable format. 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # The license 2 | 3 | Free to use 4 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel Typescript 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Total Downloads][ico-downloads]][link-downloads] 5 | [![Build Status][ico-travis]][link-travis] 6 | [![StyleCI][ico-styleci]][link-styleci] 7 | 8 | Transforms Laravel Models to Typescript Interfaces/Types 9 | 10 | ## Precautions 11 | 12 | This packages instantiates Models to retrieve it's casts, 13 | attributes, relations in some cases. So, if your models have 14 | some sensitive actions inside __constructor method, please 15 | be aware of using this package. 16 | 17 | ## Installation 18 | 19 | Via Composer 20 | 21 | ``` bash 22 | composer require --dev wovosoft/laravel-typescript 23 | ``` 24 | 25 | ## Publish Configuration 26 | 27 | Run the command given below. This will publish `laravel-typescript.php` config file. 28 | 29 | ```bash 30 | php artisan vendor:publish --provider="Wovosoft\LaravelTypescript\LaravelTypescriptServiceProvider" 31 | ``` 32 | 33 | Configure the configurations 34 | 35 | ```php 36 | return [ 37 | 'output_path' => resource_path('js/types/models.d.ts'), 38 | 'source_dir' => app_path('Models'), 39 | /** 40 | * Custom attributes should have return types defined. 41 | * But if it is not, then the return type should be this type. 42 | * And this value should be php supported return types. 43 | * like primitive types or any other classes 44 | */ 45 | "custom_attributes" => [ 46 | "fallback_return_type" => "string" 47 | ] 48 | ]; 49 | ``` 50 | 51 | ## Usage 52 | 53 | Run the command given below to generate typescript types. 54 | 55 | ```bash 56 | php artisan laravel-typescript:transform 57 | ``` 58 | 59 | Generated contents will be written in configured location. 60 | 61 | ## Advanced Usage 62 | 63 | Sometimes Models can be stored in different locations, like in some packages, some directories etc., 64 | in that case, please check the source of 65 | [./src/LaravelTypescript.php](https://github.com/wovosoft/laravel-typescript/blob/master/src/LaravelTypescript.php) 66 | 67 | You can just instantiate this class, and generate types for models in some other directories. 68 | 69 | ```php 70 | use Wovosoft\LaravelTypescript\Facades\LaravelTypescript; 71 | 72 | 73 | $dirs = [ 74 | "models" => app_path("Models"), 75 | "hrmPerson" => base_path("packages/wovosoft/hrms-person/src/Models"), 76 | ]; 77 | 78 | foreach ($dirs as $name => $dir) { 79 | LaravelTypescript::generate( 80 | sourceDir : $dir, 81 | outputPath: resource_path("js/types/$name.d.ts") 82 | ); 83 | 84 | echo "Generated $name.d.ts"; 85 | } 86 | ``` 87 | 88 | ## Note on New Model Attributes 89 | 90 | For new Model Attributes, return type of the Closure function should be defined, 91 | otherwise, it will generate value of 92 | `config('laravel-typescript.custom_attributes.fallback_return_type')` type for the related 93 | property. 94 | 95 | ```php 96 | use \Illuminate\Database\Eloquent\Model; 97 | use \Illuminate\Database\Eloquent\Casts\Attribute; 98 | 99 | class User extends Model{ 100 | public function isActive() : Attribute 101 | { 102 | return Attribute::get(fn(): bool =>$this->status==='active'); 103 | } 104 | 105 | public function getIsInactiveAttribute():bool 106 | { 107 | return $this->status==="inactive"; 108 | } 109 | } 110 | ``` 111 | 112 | ## Change log 113 | 114 | Please see the [changelog](changelog.md) for more information on what has changed recently. 115 | 116 | ## Contributing 117 | 118 | Please see [contributing.md](contributing.md) for details and a todolist. 119 | 120 | ## Security 121 | 122 | If you discover any security related issues, please create issues 123 | in [Issues Tracker](https://github.com/wovosoft/laravel-typescript/issues) 124 | 125 | ## Credits 126 | 127 | - [Narayan Adhikary][link-author] 128 | - [All Contributors][link-contributors] 129 | 130 | ## License 131 | 132 | MIT. Please see the [license file](license.md) for more information. 133 | 134 | [ico-version]: https://img.shields.io/packagist/v/wovosoft/laravel-typescript.svg?style=flat-square 135 | 136 | [ico-downloads]: https://img.shields.io/packagist/dt/wovosoft/laravel-typescript.svg?style=flat-square 137 | 138 | [ico-travis]: https://img.shields.io/travis/wovosoft/laravel-typescript/master.svg?style=flat-square 139 | 140 | [ico-styleci]: https://github.styleci.io/repos/661637738/shield?branch=master 141 | 142 | [link-packagist]: https://packagist.org/packages/wovosoft/laravel-typescript 143 | 144 | [link-downloads]: https://packagist.org/packages/wovosoft/laravel-typescript 145 | 146 | [link-travis]: https://travis-ci.org/wovosoft/laravel-typescript 147 | 148 | [link-styleci]: https://github.styleci.io/repos/661637738 149 | 150 | [link-author]: https://github.com/wovosoft 151 | 152 | [link-contributors]: ../../contributors 153 | -------------------------------------------------------------------------------- /src/Console/Commands/LaravelTypescriptCommand.php: -------------------------------------------------------------------------------- 1 | info('Successfully Generated Typescript Model Interfaces'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Facades/LaravelTypescript.php: -------------------------------------------------------------------------------- 1 | getName()); 34 | /** 35 | * In old style, get{Prop}Attribute() method's returns type can be 36 | * Enum Type, we do not need to care about it. 37 | */ 38 | if ( 39 | $methodName->startsWith('get') 40 | && $methodName->endsWith('Attribute') 41 | && $methodName->value() !== 'getAttribute' 42 | ) { 43 | return true; 44 | } 45 | 46 | /** 47 | * in new style, custom attribute defining method always returns Attribute::class, 48 | * so, when it returns other than Attribute::class, it is not a custom attribute 49 | */ 50 | if ($reflectionMethod->getReturnType() instanceof ReflectionUnionType) { 51 | return false; 52 | } 53 | 54 | /** 55 | * If it returns Attribute::class it is a custom attribute 56 | */ 57 | return $reflectionMethod->getReturnType()?->getName() === Attribute::class; 58 | } 59 | 60 | /** 61 | * Determines if a method is used to define model relation or not 62 | */ 63 | public static function isRelation(ReflectionMethod $method): bool 64 | { 65 | return $method->hasReturnType() 66 | && $method->getReturnType() instanceof ReflectionNamedType 67 | && is_subclass_of($method->getReturnType()->getName(), Relation::class); 68 | } 69 | 70 | /** 71 | * @description Determines if the attribute is of new style or old style 72 | * 73 | * @param ReflectionMethod $method 74 | * 75 | * @return bool 76 | */ 77 | public static function isNewStyled(ReflectionMethod $method): bool 78 | { 79 | if (is_null($method->getReturnType())) { 80 | return false; 81 | } 82 | 83 | $name = str($method->getName()); 84 | 85 | /** 86 | * First we check if it is of old style attribute 87 | * old style attributes are defined like, 'get{PropName}Attribute()' 88 | */ 89 | if ( 90 | $name->startsWith('get') 91 | && $name->endsWith('Attribute') 92 | && $name->value() !== 'getAttribute' 93 | ) { 94 | return false; 95 | } 96 | 97 | /** 98 | * New style attributes don't return multiple values, 99 | * it returns only one, and it is of type. 100 | * 101 | * @link \Illuminate\Database\Eloquent\Casts\Attribute::class 102 | */ 103 | if ($method->getReturnType() instanceof ReflectionUnionType) { 104 | return false; 105 | } 106 | 107 | /** 108 | * If return type is exactly of Illuminate\Database\Eloquent\Casts\Attribute::class, 109 | * then it is of new style attribute 110 | */ 111 | return $method->getReturnType()->getName() === Attribute::class; 112 | } 113 | 114 | /** 115 | * @description Get Reflection of new style attributes 'get' method 116 | * This method should be called for new style attributes only. 117 | * 118 | * @note In most cases, it is safe to retrieve types in this way, 119 | * because calling new-styled attribute doesn't make any database/php operations, 120 | * it just returns an instance of Attribute::class 121 | * @note It is safe to check if the method defines new style attribute before calling this function 122 | * 123 | * @param ReflectionMethod $method 124 | * 125 | * @throws ReflectionException 126 | * @throws Exception 127 | * 128 | * @return ReflectionFunction 129 | */ 130 | public static function getReflectionOfNewStyleAttribute(ReflectionMethod $method): ReflectionFunction 131 | { 132 | $model = ModelInspector::parseModel($method->getDeclaringClass()->getName()); 133 | 134 | return new ReflectionFunction($model->{$method->getName()}()->get); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Helpers/Column.php: -------------------------------------------------------------------------------- 1 | name; 20 | } 21 | 22 | public function getType(): string { 23 | return $this->type; 24 | } 25 | 26 | public function getTypeName(): string { 27 | return $this->type_name; 28 | } 29 | 30 | public function getNotNull(): bool { 31 | return !$this->nullable; 32 | } 33 | 34 | public static function fromArray(array $data): static { 35 | $item = new static(); 36 | $item->name = $data['name']; 37 | $item->type_name = $data['type_name']; 38 | $item->type = $data['type']; 39 | $item->collation = $data['collation']; 40 | $item->nullable = $data['nullable']; 41 | $item->default = $data['default']; 42 | $item->comment = $data['comment']; 43 | $item->auto_increment = $data['auto_increment']; 44 | $item->generation = $data['generation']; 45 | return $item; 46 | } 47 | 48 | public function toArray(): array { 49 | return [ 50 | 'name' => $this->name, 51 | 'type_name' => $this->type_name, 52 | 'type' => $this->type, 53 | 'collation' => $this->collation, 54 | 'nullable' => $this->nullable, 55 | 'default' => $this->default, 56 | 'comment' => $this->comment, 57 | 'auto_increment' => $this->auto_increment, 58 | 'generation' => $this->generation, 59 | ]; 60 | } 61 | 62 | public function toJson(): false|string { 63 | return json_encode($this->toArray()); 64 | } 65 | 66 | public function toCollection(): Collection { 67 | return collect($this->toArray()); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Helpers/Generator.php: -------------------------------------------------------------------------------- 1 | 34 | * @throws ReflectionException 35 | * 36 | */ 37 | public function getDefinitions(): Collection 38 | { 39 | return $this 40 | ->getColumnDefinitions() 41 | ->merge($this->getCustomAttributeDefinitions()) 42 | ->merge($this->getRelationDefinitions()) 43 | ->keyBy('name'); 44 | } 45 | 46 | /** 47 | * @description Generates interface 48 | * 49 | * @return string 50 | * @throws ReflectionException 51 | * 52 | */ 53 | public function __toString(): string 54 | { 55 | return $this->toTypescript(); 56 | } 57 | 58 | /** 59 | * @description Generates interface 60 | * 61 | * @throws ReflectionException 62 | */ 63 | public function toTypescript(): string 64 | { 65 | $typings = $this 66 | ->getDefinitions() 67 | ->implode(function (Definition $def, string $key) { 68 | return "\t\t$key" . ($def->isUndefinable ? '?' : '') . ": $def;"; 69 | }, PHP_EOL); 70 | 71 | $reflection = Reflection::model($this->result->getModel()); 72 | 73 | return str($typings) 74 | ->prepend("\texport interface " . $reflection->getShortName() . ' {' . PHP_EOL) 75 | ->append(PHP_EOL . "\t}"); 76 | } 77 | 78 | /** 79 | * @description Returns database column definitions 80 | * 81 | * @return Collection 82 | * @throws ReflectionException 83 | * 84 | * @throws Exception 85 | */ 86 | private function getColumnDefinitions(): Collection 87 | { 88 | $modelReflection = Reflection::model($this->result->getModel()); 89 | $model = ModelInspector::parseModel($this->result->getModel()); 90 | 91 | return $this->result 92 | ->getColumns() 93 | ->map(function (Column $column, string $key) use ($model, $modelReflection) { 94 | /* 95 | * Database columns can be cast in different format 96 | */ 97 | if ($model->hasCast($key)) { 98 | /* 99 | * When, the cast is of type Enum it should be rendered as union type 100 | * @todo : Rendered union type can be stored in separate scope rather then being 101 | * rendered as values directly. 102 | */ 103 | if (is_string($model->getCasts()[$key]) && enum_exists($model->getCasts()[$key])) { 104 | $type = EnumType::toType($model->getCasts()[$key]); 105 | } elseif (LaravelCastType::isBuiltIn($model->getCasts()[$key])) { 106 | $type = LaravelCastType::getType($model->getCasts()[$key]); 107 | } else { 108 | $type = PhpType::toType($model->getCasts()[$key]); 109 | } 110 | } else { 111 | $type = ColumnType::toType($column->getTypeName()); 112 | } 113 | 114 | return new Definition( 115 | namespace : $modelReflection->getNamespaceName(), 116 | name : $column->getName(), 117 | model : $modelReflection->getName(), 118 | modelShortName: $modelReflection->getShortName(), 119 | types : [$type], 120 | isRequired : $column->getNotnull(), 121 | isUndefinable : false 122 | ); 123 | }); 124 | } 125 | 126 | /** 127 | * @return Collection 128 | */ 129 | private function getCustomAttributeDefinitions(): Collection 130 | { 131 | return $this->result 132 | ->getCustomAttributes() 133 | ->mapWithKeys(function (ReflectionMethod $method) { 134 | $decClass = $method->getDeclaringClass(); 135 | $types = $this->getAttributeReturnTypes($method); 136 | 137 | /* 138 | * When there is return type is not defined 139 | */ 140 | if ($types->isEmpty()) { 141 | $types->add( 142 | new Type( 143 | name : config('laravel-typescript.custom_attributes.fallback_return_type'), 144 | isMultiple: false 145 | ) 146 | ); 147 | } 148 | 149 | return [ 150 | $this->qualifyAttributeName($method) => new Definition( 151 | namespace : $decClass->getNamespaceName(), 152 | name : $this->qualifyAttributeName($method), 153 | model : $decClass->getName(), 154 | modelShortName: $decClass->getShortName(), 155 | types : $types, 156 | isRequired : $this->isRequiredReturnType($method), 157 | isUndefinable : true 158 | ), 159 | ]; 160 | }); 161 | } 162 | 163 | /** 164 | * @description Returns definitions of relations 165 | * 166 | * @return Collection 167 | * @throws ReflectionException 168 | * 169 | */ 170 | private function getRelationDefinitions(): Collection 171 | { 172 | if ($this->result->getModel() instanceof Model) { 173 | $model = $this->result->getModel(); 174 | } else { 175 | $modelClass = $this->result->getModel(); 176 | $model = new $modelClass(); 177 | } 178 | 179 | $modelReflection = Reflection::model($model); 180 | 181 | return $this->result 182 | ->getRelations() 183 | ->mapWithKeys(function (ReflectionMethod $method) use ($model, $modelReflection) { 184 | try { 185 | $relation = $method->invoke($model); 186 | $relatedModel = get_class($relation->getRelated()); 187 | 188 | } catch (Throwable) { 189 | return []; 190 | } 191 | 192 | $relatedModelReflection = Reflection::model($relatedModel); 193 | 194 | /** 195 | * When Model and Related Morels are from the same namespace, 196 | * only short name is enough 197 | * When Model and Related Model are from different namespaces, 198 | * full namespace name should be used. 199 | */ 200 | 201 | $typeName = Reflection::isSameNamespace($relatedModelReflection, $modelReflection) 202 | ? $relatedModelReflection->getShortName() 203 | : $relatedModelReflection->getName(); 204 | 205 | return [ 206 | $this->qualifyAttributeName($method) => new Definition( 207 | namespace : $modelReflection->getNamespaceName(), 208 | name : $this->qualifyAttributeName($method), 209 | model : get_class($model), 210 | modelShortName: $modelReflection->getShortName(), 211 | types : [ 212 | new Type( 213 | name : $typeName, 214 | isMultiple: RelationType::getReturnCountType($method->getReturnType()->getName()) 215 | ), 216 | ], 217 | //model relations are not set by their method nemo, 218 | //so in typescript it should be nullable (not required) and undefinable 219 | isRequired : false, 220 | isUndefinable : true 221 | ) 222 | ]; 223 | }); 224 | } 225 | 226 | /** 227 | * @description Returns Collection of Return Types (Type) 228 | * 229 | * @param ReflectionMethod $method 230 | * 231 | * @return Collection 232 | * @throws ReflectionException 233 | * 234 | */ 235 | private function getAttributeReturnTypes(ReflectionMethod $method): Collection 236 | { 237 | if (Attributes::isNewStyled($method)) { 238 | $type = Attributes::getReflectionOfNewStyleAttribute($method)->getReturnType(); 239 | } else { 240 | $type = $method->getReturnType(); 241 | } 242 | 243 | if ($type instanceof ReflectionNamedType) { 244 | $types = [$type]; 245 | } else { 246 | $types = $type?->getTypes(); 247 | } 248 | 249 | return collect($types) 250 | ->map(function (ReflectionNamedType $type) { 251 | if ($type->isBuiltin()) { 252 | /** 253 | * @todo In php it is not possible to define array's member type 254 | * but in docblock it is possible. So, do it later. 255 | */ 256 | //if ($type->getName() === "array") { 257 | // $doc = new DocBlock($method->getDocComment()); 258 | // if ($doc->hasTag('return')) { 259 | // /** @var DocBlock\Tag\ReturnTag $returnTag */ 260 | // $returnTag = $doc->getTagsByName('return')[0]; 261 | // dump($returnTag->getTypes()); 262 | // } 263 | //} 264 | 265 | return PhpType::toType($type->getName() ?: config('laravel-typescript.custom_attributes.fallback_return_type')); 266 | } elseif (enum_exists($type->getName())) { 267 | return EnumType::toType($type->getName()); 268 | } /** 269 | * When the return type is of type Model, it's qualified namespace should be used. 270 | * The returning model should be rendered in separate interface. 271 | */ 272 | elseif (ModelInspector::isOfModelType($type->getName())) { 273 | return Type::model(name: $type->getName()); 274 | } 275 | 276 | $resolver = config('laravel-typescript.custom_attributes.accessor_resolvers'); 277 | if (is_callable($resolver)) { 278 | return $resolver($type->getName()); 279 | } 280 | 281 | return Type::any(); 282 | }); 283 | } 284 | 285 | /** 286 | * @description Returns Prop name to be generated 287 | * 288 | * @param ReflectionMethod $method 289 | * 290 | * @return string 291 | */ 292 | private function qualifyAttributeName(ReflectionMethod $method): string 293 | { 294 | $name = str($method->getName()); 295 | 296 | if (Attributes::isRelation($method) || Attributes::isNewStyled($method)) { 297 | return $name->snake()->value(); 298 | } 299 | 300 | return $name->after('get')->before('Attribute')->snake()->value(); 301 | } 302 | 303 | /** 304 | * @description Determines if a props value is required or nullable 305 | * 306 | * @throws ReflectionException 307 | */ 308 | private function isRequiredReturnType(ReflectionMethod $method): bool 309 | { 310 | /* 311 | * The New style attribute returns an instance of Attribute, 312 | * which has two methods get and set. We only need to care about the 313 | * return types of get method. 314 | * Because it is set dynamically as a Closure and the returning type is 315 | * added in the Closure, the attribute method should be called to 316 | * achieve the instance and then get the returning type from that 317 | * instance of get method. 318 | * 319 | * And according to the implementation logic, the method (prop():Attribute) should only return 320 | * the instance of Attribute, it shouldn't perform any other action. 321 | * So, it is safe to call it to have the instance of Attribute. 322 | */ 323 | if (Attributes::isNewStyled($method)) { 324 | /* 325 | * NOTE: When $model->newStyleAttribute or $model->new_style_attribute 326 | * is called, it is being resolved directly by the model itself. 327 | * So, we have to call it as a function to get the return type of the 328 | * callback named get. 329 | */ 330 | 331 | return !Attributes::getReflectionOfNewStyleAttribute($method)->getReturnType()?->allowsNull(); 332 | } 333 | 334 | return !$method->getReturnType()?->allowsNull(); 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/Helpers/ModelInspectionResult.php: -------------------------------------------------------------------------------- 1 | |Model $model 13 | * @param Collection $columns 14 | * @param Collection $custom_attributes 15 | * @param Collection $relations 16 | */ 17 | public function __construct( 18 | private string|Model $model, 19 | private Collection $columns, 20 | private Collection $custom_attributes, 21 | private Collection $relations 22 | ) { 23 | } 24 | 25 | /** 26 | * @description Generates the model interface 27 | * 28 | * @return string 29 | */ 30 | public function __toString(): string { 31 | return $this->getGenerator(); 32 | } 33 | 34 | /** 35 | * @return Model|string 36 | */ 37 | public function getModel(): Model|string { 38 | return $this->model; 39 | } 40 | 41 | /** 42 | * @return Collection 43 | */ 44 | public function getColumns(): Collection { 45 | return $this->columns; 46 | } 47 | 48 | /** 49 | * @return Collection 50 | */ 51 | public function getCustomAttributes(): Collection { 52 | return $this->custom_attributes; 53 | } 54 | 55 | /** 56 | * @return Collection 57 | */ 58 | public function getRelations(): Collection { 59 | return $this->relations; 60 | } 61 | 62 | public function getGenerator(): Generator { 63 | return new Generator($this); 64 | } 65 | 66 | public function toTypescript(): string { 67 | return (string) $this->getGenerator(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Helpers/ModelInspector.php: -------------------------------------------------------------------------------- 1 | |Model|null $model 32 | */ 33 | public function __construct( 34 | private string|Model|null $model = null 35 | ) { 36 | } 37 | 38 | private static array $defaultRelations = [ 39 | BelongsTo::class, 40 | BelongsToMany::class, 41 | HasMany::class, 42 | HasManyThrough::class, 43 | HasOne::class, 44 | HasOneOrMany::class, 45 | HasOneThrough::class, 46 | MorphMany::class, 47 | MorphOne::class, 48 | MorphOneOrMany::class, 49 | MorphPivot::class, 50 | MorphTo::class, 51 | MorphToMany::class, 52 | Pivot::class, 53 | Relation::class 54 | ]; 55 | 56 | /** 57 | * @description Returns the list of model-classes in a directory 58 | * 59 | * @param string|array $directories 60 | * 61 | * @return Collection> 62 | * 63 | * @link https://github.com/composer/class-map-generator 64 | */ 65 | public static function getModelsIn(string|array $directories): Collection 66 | { 67 | if (is_string($directories)) { 68 | $directories = [$directories]; 69 | } 70 | 71 | return collect($directories) 72 | ->map(fn(string $dir) => array_keys(ClassMapGenerator::createMap($dir))) 73 | ->collapse() 74 | ->filter(fn($class) => static::isOfModelType($class)); 75 | } 76 | 77 | /** 78 | * @description Checks if the provided class/object is of type Model 79 | * 80 | * @note the parameter $model's type is set to be string|Model, because 81 | * we need to check any kind of object/class to be checked if it 82 | * is of type Model or not. If string|Model is used, strings of any 83 | * type will be passed, but objects other than Model won't be passed 84 | * for testing whether it is of type Model or not. 85 | * 86 | * @param mixed $model 87 | * 88 | * @return bool 89 | */ 90 | public static function isOfModelType(mixed $model): bool 91 | { 92 | if (is_string($model) && class_exists($model)) { 93 | return is_subclass_of($model, Model::class); 94 | } 95 | 96 | return $model instanceof Model; 97 | } 98 | 99 | /** 100 | * @description Returns new instance of the Model Inspector class. 101 | * 102 | * @note If value for $model is provided @method inspectionFor() doesn't need to be used 103 | * 104 | * @param class-string|Model|null $model 105 | * 106 | * @return static 107 | */ 108 | public static function new(string|Model|null $model = null): static 109 | { 110 | return new static($model); 111 | } 112 | 113 | /** 114 | * @description Used to set Model class for inspection 115 | * 116 | * @param class-string|Model $model 117 | * 118 | * @return $this 119 | */ 120 | public function inspectionFor(string|Model $model): static 121 | { 122 | $this->model = $model; 123 | 124 | return $this; 125 | } 126 | 127 | /** 128 | * @description Return model inspection result which contains 129 | * list of database columns, custom attributes, and relations. 130 | * 131 | * @return ModelInspectionResult 132 | * @throws \Exception 133 | * 134 | * @throws ReflectionException 135 | */ 136 | public function getInspectionResult(): ModelInspectionResult 137 | { 138 | $this->isModelSet(); 139 | 140 | return new ModelInspectionResult( 141 | model : $this->model, 142 | columns : $this->getColumns(), 143 | custom_attributes: $this->getCustomAttributes(), 144 | relations : $this->getRelations() 145 | ); 146 | } 147 | 148 | /** 149 | * @description Returns Collection of Database columns 150 | * 151 | * @return Collection 152 | * 153 | * @throws \Exception 154 | */ 155 | private function getColumns(): Collection 156 | { 157 | $this->isModelSet(); 158 | 159 | $model = static::parseModel($this->model); 160 | 161 | $columns = Schema::getColumns($model->getTable()); 162 | 163 | /** 164 | * Model fields name should be exact like column name. 165 | * Fields which are marked as hidden, should not be generated. 166 | * So, those fields are being forgotten (omitted) from the collection. 167 | */ 168 | return collect($columns) 169 | ->when(!empty($model->getHidden()), fn(Collection $cols) => $cols->forget($model->getHidden())) 170 | ->map(fn($col) => Column::fromArray($col)); 171 | } 172 | 173 | /** 174 | * @description Returns methods which are used to define Custom Attributes 175 | * 176 | * @return Collection 177 | * @throws ReflectionException 178 | * 179 | */ 180 | private function getCustomAttributes(): Collection 181 | { 182 | return $this->getMethods()->filter(fn(ReflectionMethod $rf) => Attributes::isAttribute($rf)); 183 | } 184 | 185 | /** 186 | * @description Returns methods of a given model, which are used to define relations 187 | * 188 | * @return Collection 189 | * @throws ReflectionException 190 | * 191 | * @throws \Exception 192 | */ 193 | private function getRelations(): Collection 194 | { 195 | $this->isModelSet(); 196 | 197 | return $this->getMethods() 198 | ->filter(fn(ReflectionMethod $rf) => Attributes::isRelation($rf)); 199 | } 200 | 201 | 202 | /** 203 | * @throws ReflectionException 204 | */ 205 | private function getMethods(): Collection 206 | { 207 | return collect((new ReflectionClass($this->model))->getMethods(ReflectionMethod::IS_PUBLIC)); 208 | } 209 | 210 | /** 211 | * @param class-string|Model $model 212 | * 213 | * @return Model 214 | * @throws \Exception 215 | * 216 | */ 217 | public static function parseModel(string|Model $model): Model 218 | { 219 | if (is_string($model)) { 220 | if (!is_subclass_of($model, Model::class)) { 221 | throw new \Exception("$model is not a valid Model Class"); 222 | } 223 | 224 | return new $model(); 225 | } 226 | 227 | return $model; 228 | } 229 | 230 | /** 231 | * @throws \Exception 232 | */ 233 | private function isModelSet(): bool 234 | { 235 | if (!isset($this->model)) { 236 | throw new \Exception('Model Not Set'); 237 | } 238 | 239 | return true; 240 | } 241 | 242 | public static function getQualifiedNamespace(string $name): string 243 | { 244 | return str($name)->replace('\\', '.')->value(); 245 | } 246 | 247 | public static function getDefaultRelations(): array 248 | { 249 | return static::$defaultRelations; 250 | } 251 | 252 | public static function isDefaultRelation(string $relation): bool 253 | { 254 | return in_array($relation, static::$defaultRelations); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/Helpers/Reflection.php: -------------------------------------------------------------------------------- 1 | getNamespaceName() === $ro2->getNamespaceName(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Helpers/Types.php: -------------------------------------------------------------------------------- 1 | GenericType::string(comment: 'ASCII String'), 11 | 'bigint' => GenericType::number(), 12 | 'int8' => GenericType::number(), 13 | 'bool' => GenericType::boolean(), 14 | 'binary' => GenericType::unknown(comment: 'Binary Data'), 15 | 'blob' => GenericType::unknown(comment: 'Binary Large Object'), 16 | 'boolean' => GenericType::boolean(), 17 | 'timestamp' => GenericType::string(comment: 'Timestamp'), 18 | 'character' => GenericType::string(comment: 'Character'), 19 | 'varchar' => GenericType::string(comment: 'Variable Character'), 20 | 'char' => GenericType::string(comment: 'Character'), 21 | 'date' => GenericType::string(comment: 'Date'), 22 | 'date_mutable' => GenericType::mutableDate(), 23 | 'date_immutable' => GenericType::immutableDate(), 24 | 'dateinterval' => GenericType::string(comment: 'date interval'), 25 | 'datetime' => GenericType::string(comment: 'Datetime'), 26 | 'datetime_immutable' => GenericType::immutableDatetime(), 27 | 'datetimetz_mutable' => GenericType::mutableDatetimeZ(), 28 | 'datetimetz_immutable' => GenericType::immutableDatetimeZ(), 29 | 'decimal' => GenericType::number(), 30 | 'float' => GenericType::float(), 31 | 'guid' => GenericType::string(comment: 'GUID'), 32 | 'integer' => GenericType::number(), 33 | 'json' => GenericType::json(), 34 | 'simple_array' => GenericType::any(isMultiple: true, comment: 'Simple Array'), 35 | 'smallint' => GenericType::number(comment: "Small Integer"), 36 | 'string' => GenericType::string(), 37 | 'text' => GenericType::string(comment: "Text"), 38 | 'time_mutable' => GenericType::mutableTime(), 39 | 'time_immutable' => GenericType::immutableTime(), 40 | 'serial' => GenericType::number(comment: "Serial"), 41 | 'serial8' => GenericType::number(comment: "Serial 8"), 42 | 'money' => GenericType::number(comment: "Money"), 43 | 'xml' => GenericType::string(comment: "XML"), 44 | 'uuid' => GenericType::string(comment: "UUID"), 45 | 'citext' => GenericType::string(comment: "Case Insensitive Text"), 46 | 'macaddr' => GenericType::string(comment: "MAC Address"), 47 | 'inet' => GenericType::string(comment: "Internet Address"), 48 | 'cidr' => GenericType::string(comment: "CIDR"), 49 | 'tsvector' => GenericType::string(comment: "Text Search Vector"), 50 | 'tsquery' => GenericType::string(comment: "Text Search Query"), 51 | 'tinyint' => GenericType::number(comment: "Tiny Integer"), 52 | 'mediumint' => GenericType::number(comment: "Medium Integer"), 53 | 'numeric' => GenericType::number(comment: "Numeric"), 54 | 'dec' => GenericType::number(comment: "Decimal"), 55 | 'double' => GenericType::number(comment: "Double"), 56 | 'double_precision' => GenericType::number(comment: "Double Precision"), 57 | 'real' => GenericType::number(comment: "Real"), 58 | 'bit' => GenericType::number(comment: "Bit"), 59 | 'enum' => GenericType::string(comment: "Enum"), 60 | 'set' => GenericType::string(comment: "Set"), 61 | 'smalldatetime' => GenericType::string(comment: "Small Datetime"), 62 | 'datetime2' => GenericType::string(comment: "Datetime2"), 63 | 'datetimeoffset' => GenericType::string(comment: "Datetime Offset"), 64 | 'time' => GenericType::string(comment: "Time"), 65 | 'smallmoney' => GenericType::number(comment: "Small Money"), 66 | 'image' => GenericType::unknown(comment: "Image"), 67 | 'ntext' => GenericType::string(comment: "NText"), 68 | 'uniqueidentifier' => GenericType::string(comment: "Unique Identifier"), 69 | 'rowversion' => GenericType::string(comment: "Row Version"), 70 | 'geography' => GenericType::string(comment: "Geography"), 71 | 'geometry' => GenericType::string(comment: "Geometry"), 72 | 'null' => GenericType::unknown(comment: "Null"), 73 | ]; 74 | 75 | return $transformers[$key] ?? GenericType::any(); 76 | } 77 | 78 | private function __construct() { 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /src/LaravelTypescript.php: -------------------------------------------------------------------------------- 1 | {path,contents} 21 | * @throws ReflectionException 22 | */ 23 | public function generate( 24 | string|array $sourceDir, 25 | string $outputPath, 26 | ): array { 27 | File::ensureDirectoryExists(dirname($outputPath)); 28 | 29 | $contents = $this->toTypescript($sourceDir); 30 | 31 | File::put(path: $outputPath, contents: $contents); 32 | 33 | return [ 34 | $outputPath, 35 | $contents, 36 | ]; 37 | } 38 | 39 | /** 40 | * @param string|array $sourceDir 41 | * 42 | * @return string 43 | * @throws ReflectionException 44 | */ 45 | public function toTypescript(string|array $sourceDir): string { 46 | 47 | return ModelInspector::getModelsIn($sourceDir) 48 | ->map(fn(string $modelClass) => [ 49 | 'namespace' => (new ReflectionClass($modelClass))->getNamespaceName(), 50 | 'model' => $modelClass, 51 | ]) 52 | ->groupBy('namespace') 53 | ->mapWithKeys(fn(Collection $models, string $namespace) => [ 54 | $namespace => $models->pluck('model'), 55 | ]) 56 | ->map(function (Collection $modelClasses, string $namespace) { 57 | $namespace = ModelInspector::getQualifiedNamespace($namespace); 58 | 59 | return "declare namespace $namespace {" 60 | .PHP_EOL 61 | .$modelClasses 62 | ->map( 63 | function (string $modelClass) { 64 | return ModelInspector::new($modelClass) 65 | ->getInspectionResult() 66 | ->getGenerator() 67 | ->toTypescript(); 68 | } 69 | ) 70 | ->implode(fn(string $content) => $content, PHP_EOL.PHP_EOL) 71 | .PHP_EOL 72 | .'}' 73 | .PHP_EOL; 74 | }) 75 | ->implode(fn(string $content) => $content, PHP_EOL.PHP_EOL); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/LaravelTypescriptServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 19 | $this->bootForConsole(); 20 | } 21 | } 22 | 23 | /** 24 | * Register any package services. 25 | * 26 | * @return void 27 | */ 28 | public function register(): void 29 | { 30 | $this->mergeConfigFrom(__DIR__ . '/../config/laravel-typescript.php', 'laravel-typescript'); 31 | 32 | // Register the service the package provides. 33 | $this->app->singleton('laravel-typescript', function ($app) { 34 | return LaravelTypescript::new(); 35 | }); 36 | } 37 | 38 | /** 39 | * Get the services provided by the provider. 40 | * 41 | * @return array 42 | */ 43 | public function provides(): array 44 | { 45 | return ['laravel-typescript']; 46 | } 47 | 48 | /** 49 | * Console-specific booting. 50 | * 51 | * @return void 52 | */ 53 | protected function bootForConsole(): void 54 | { 55 | // Publishing the configuration file. 56 | $this->publishes([ 57 | __DIR__ . '/../config/laravel-typescript.php' => config_path('laravel-typescript.php'), 58 | ], 'laravel-typescript.config'); 59 | 60 | // Registering package commands. 61 | $this->commands([ 62 | LaravelTypescriptCommand::class, 63 | ]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Traits/HasTypeGenerators.php: -------------------------------------------------------------------------------- 1 | getName(); 14 | } 15 | 16 | return Types::getGenericType($type); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Types/Definition.php: -------------------------------------------------------------------------------- 1 | value pairs 11 | */ 12 | class Definition 13 | { 14 | /** 15 | * @param string $namespace 16 | * @param string $name 17 | * @param string $model 18 | * @param string $modelShortName 19 | * @param array|Collection $types 20 | * @param bool $isRequired 21 | * @param bool $isUndefinable 22 | */ 23 | public function __construct( 24 | public string $namespace, 25 | public string $name, 26 | public string $model, 27 | public string $modelShortName, 28 | public array|Collection $types, 29 | public bool $isRequired, 30 | public bool $isUndefinable 31 | ) 32 | { 33 | if (is_array($types)) { 34 | $this->types = collect($types); 35 | } 36 | } 37 | 38 | public function getTypes(): Collection 39 | { 40 | return $this->types; 41 | } 42 | 43 | public function __toString(): string 44 | { 45 | return $this->types->implode(fn(Type $type) => (string)$type, ' | ') 46 | . (!$this->isRequired ? ' | null' : ''); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Types/EnumType.php: -------------------------------------------------------------------------------- 1 | $enum 12 | * 13 | * @return Type 14 | */ 15 | public static function toType(string $enum): Type 16 | { 17 | if (enum_exists($enum)) { 18 | return Type::new( 19 | name: collect($enum::cases())->implode(fn ($option) => "\"$option->value\"", ' | ') 20 | ); 21 | } 22 | 23 | return Type::any(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Types/LaravelCastType.php: -------------------------------------------------------------------------------- 1 | before(':')->value(), [ 10 | 'int', 'integer', 'real', 'float', 'double', 'decimal', 'string', 11 | 'bool', 'boolean', 'object', 'array', 'json', 'collection', 'date', 12 | 'datetime', 'custom_datetime', 'immutable_date', 'immutable_custom_datetime', 13 | 'immutable_datetime', 'timestamp', 14 | ]); 15 | } 16 | 17 | /** 18 | * @param string $castType 19 | * 20 | * @return Type 21 | * 22 | * @link vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php 23 | */ 24 | public static function getType(string $castType): Type 25 | { 26 | return match (str($castType)->before(':')->value()) { 27 | 'int', 28 | 'integer', 29 | 'real', 30 | 'float', 31 | 'double', 32 | 'decimal' => Type::number(), 33 | 'string' => Type::string(), 34 | 'bool', 35 | 'boolean' => Type::boolean(), 36 | 'object' => Type::object(), 37 | 'array' => Type::array(), 38 | 'json', 39 | 'collection' => Type::json(), 40 | 'date' => Type::string(comment: 'date string'), 41 | 'datetime' => Type::string(comment: 'datetime string'), 42 | 'custom_datetime' => Type::string(comment: 'custom_datetime string'), 43 | 'immutable_date' => Type::string(comment: 'immutable_date string'), 44 | 'immutable_custom_datetime' => Type::string(comment: 'immutable_custom_datetime string'), 45 | 'immutable_datetime' => Type::string(comment: 'immutable_datetime string'), 46 | 'timestamp' => Type::string(comment: 'timestamp string'), 47 | default => Type::string(comment: 'no specific type') 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Types/PhpType.php: -------------------------------------------------------------------------------- 1 | Type::number(), 27 | 28 | /* 29 | * @todo : docblock should be checked to have exact array of type 30 | */ 31 | self::ARRAY => Type::any(isMultiple: true), 32 | /* 33 | * @todo : Rather than just generating a generic object interface, 34 | * more detailed interface can be generated in future versions. 35 | */ 36 | self::OBJECT => Type::object(), 37 | self::STRING => Type::string(), 38 | self::BOOLEAN, self::BOOL => Type::boolean(), 39 | //self::NULL => Type::any(), 40 | default => Type::any() 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Types/RelationType.php: -------------------------------------------------------------------------------- 1 | run($relationClass); 31 | } 32 | 33 | return match ($relationClass) { 34 | //HasOne::class, 35 | //HasOneThrough::class, 36 | //BelongsTo::class, 37 | //MorphOne::class, 38 | //MorphTo::class, 39 | //MorphPivot::class => RelationType::One, 40 | 41 | HasManyThrough::class, 42 | HasMany::class, 43 | BelongsToMany::class, 44 | MorphMany::class, 45 | MorphToMany::class => RelationType::Many, 46 | 47 | HasOneOrMany::class, 48 | MorphOneOrMany::class => RelationType::OneOrMany, 49 | 50 | default => RelationType::One 51 | }; 52 | } 53 | 54 | /** 55 | * @param string $relationClass 56 | * @return RelationType 57 | * @todo : Implement Custom relations 58 | */ 59 | public static function getCustomReturnCountType(string $relationClass): RelationType 60 | { 61 | return match ($relationClass) { 62 | default => RelationType::One 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Types/Type.php: -------------------------------------------------------------------------------- 1 | name)->replace('\\', '.')->value(); 25 | } 26 | 27 | public function getName(): string 28 | { 29 | return $this->name; 30 | } 31 | 32 | public function getIsMultiple(): bool|RelationType 33 | { 34 | return $this->isMultiple; 35 | } 36 | 37 | public function __toString(): string 38 | { 39 | $name = $this->getQualifiedName(); 40 | 41 | /** 42 | * When the type is of Relation, it can be of one, multiple or one_many in number. 43 | */ 44 | if ($this->isMultiple instanceof RelationType) { 45 | return match ($this->getIsMultiple()) { 46 | RelationType::Many => $name . '[]', 47 | RelationType::OneOrMany => implode(' | ', [$name, $name . '[]']), 48 | //RelationType::One => $name, 49 | default => $name 50 | } . ($this->comment ? " /** $this->comment **/" : ''); 51 | } 52 | 53 | return $name . ($this->getIsMultiple() ? '[]' : '') . ($this->comment ? " /** $this->comment **/" : ''); 54 | } 55 | 56 | public static function new(string $name, bool $isMultiple = false, ?string $comment = null): static 57 | { 58 | return new static( 59 | name : $name, 60 | isMultiple: $isMultiple, 61 | comment : $comment 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Util/AccessorResolver.php: -------------------------------------------------------------------------------- 1 | Type::immutableDatetime(), 17 | Carbon::class, 18 | CarbonMutable::class => Type::mutableDatetime(), 19 | default => Type::any() 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Util/Counter.php: -------------------------------------------------------------------------------- 1 | RelationType::One, 21 | "Staudenmeir\EloquentHasManyDeep\HasManyDeep" => RelationType::Many, 22 | default => RelationType::One 23 | }; 24 | } 25 | } 26 | --------------------------------------------------------------------------------