├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── TODO.md ├── composer.json ├── config └── eloquent-model-generator.php ├── example ├── example.mwb └── example.sql └── src ├── Commands └── EloquentModelGeneratorCommand.php ├── EloquentModelGeneratorServiceProvider.php ├── Generators ├── Generator.php ├── ModelGenerator.php ├── RelationsGenerator.php └── TraitGenerator.php ├── Parser └── RelationsParser.php ├── Relations ├── TableRelations.php └── Types │ ├── BelongsToManyRelation.php │ ├── BelongsToRelation.php │ ├── HasManyRelation.php │ └── HasOneRelation.php └── Traits └── HelperTrait.php /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Open to PR's 2 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | #Thanks everyone for contributing! 2 | 3 | ##Project Owner 4 | - user11001 5 | 6 | ##Contributers 7 | - wPatrick 8 | - smskin 9 | - rjacobsen2012 10 | - heskymatic 11 | - believer-ufa 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) pepijnolivier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eloquent Model Generator 2 | 3 | 4 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/pepijnolivier/eloquent-model-generator.svg?style=flat-square)](https://packagist.org/packages/pepijnolivier/eloquent-model-generator) 5 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/pepijnolivier/eloquent-model-generator/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/pepijnolivier/eloquent-model-generator/actions?query=workflow%3Arun-tests+branch%3Amain) 6 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/pepijnolivier/eloquent-model-generator/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/pepijnolivier/eloquent-model-generator/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/pepijnolivier/eloquent-model-generator.svg?style=flat-square)](https://packagist.org/packages/pepijnolivier/eloquent-model-generator) 8 | 9 | 10 | 11 | This Laravel package will generate models with their appropriate Eloquent relations based on an existing database schema. 12 | 13 | For automatically generating database migrations for your schema, see [kitloong/laravel-migrations-generator](https://github.com/kitloong/laravel-migrations-generator) 14 | 15 | ## Requirements 16 | 17 | - PHP 8.1+ 18 | - Laravel 8+ 19 | 20 | ## Installation 21 | 22 | You can install the package via composer: 23 | 24 | ```bash 25 | composer require --dev pepijnolivier/eloquent-model-generator 26 | ``` 27 | 28 | You can publish the config file with: 29 | 30 | ```bash 31 | php artisan vendor:publish --tag="eloquent-model-generator-config" 32 | ``` 33 | 34 | This is the contents of the published config file: 35 | 36 | ```php 37 | 'App\Models\Generated', 51 | 'trait_namespace' => 'App\Models\Generated\Relations', 52 | 53 | /* 54 | |-------------------------------------------------------------------------- 55 | | Output Path 56 | |-------------------------------------------------------------------------- 57 | | 58 | | Path where the models will be created. 59 | | 60 | */ 61 | 'model_path' => 'app/Models/Generated', 62 | 'trait_path' => 'app/Models/Generated/Relations', 63 | 64 | /* 65 | |-------------------------------------------------------------------------- 66 | | Extend Model 67 | |-------------------------------------------------------------------------- 68 | | 69 | | Extend the base model. 70 | | 71 | */ 72 | 'extend' => Model::class, 73 | ]; 74 | 75 | ``` 76 | 77 | 78 | ## Usage 79 | 80 | ```php 81 | php artisan generate:models 82 | ``` 83 | 84 | ## Testing 85 | 86 | ```bash 87 | composer test 88 | ``` 89 | 90 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 91 | 92 | ## Security Vulnerabilities 93 | 94 | Please see [SECURITY](SECURITY.md) for details. 95 | 96 | ## Credits 97 | 98 | - [Pepijn Olivier](https://github.com/pepijnolivier) 99 | - [All Contributors](../../contributors) 100 | 101 | ## License 102 | 103 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 104 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Just open an issue 2 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - timestamps 2 | - softdeletes 3 | - specify database 4 | - detect hasManyThrough etc (see other generator packages for that ?) 5 | - morph (...type + ...id) 6 | - belongsToThrough (hoeveel levels diep ?) ~> alles ! 7 | - hasManyThrough (hoeveel levels diep ?) 1,2, 3, 4 of 5 excluding pivots 8 | => theorie: zal mogelijk conflicting relation names geven dus moet de laatste zijn om te implementeren 9 | => https://stackoverflow.com/questions/21699050/many-to-many-relationships-in-laravel-belongstomany-vs-hasmanythrough 10 | - config automatically excludes tables to proces (migrations, etc) 11 | - consider relations less verbose if automatically detectable via column names 12 | 13 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pepijnolivier/eloquent-model-generator", 3 | "description": "Eloquent Model Generator", 4 | "keywords": [ 5 | "pepijnolivier", 6 | "laravel", 7 | "eloquent-model-generator" 8 | ], 9 | "homepage": "https://github.com/pepijnolivier/eloquent-model-generator", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Pepijn Olivier", 14 | "email": "olivier.pepijn@gmail.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1", 20 | "illuminate/contracts": "^8.0|^9.0|^10.0", 21 | "illuminate/support": "^8.0|^9.0|^10.0", 22 | "kitloong/laravel-migrations-generator": "^6.10", 23 | "nette/php-generator": "^4.0", 24 | "spatie/laravel-package-tools": "^1.12.0" 25 | }, 26 | "require-dev": { 27 | "laravel/pint": "^1.0", 28 | "nunomaduro/collision": "~7", 29 | "nunomaduro/larastan": "^2.0.1", 30 | "orchestra/testbench": "^8.0", 31 | "pestphp/pest": "^2.0", 32 | "pestphp/pest-plugin-arch": "^2.0", 33 | "pestphp/pest-plugin-laravel": "^2.0", 34 | "phpstan/extension-installer": "^1.1", 35 | "phpstan/phpstan-deprecation-rules": "^1.0", 36 | "phpstan/phpstan-phpunit": "^1.0" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Pepijnolivier\\EloquentModelGenerator\\": "src/", 41 | "Pepijnolivier\\EloquentModelGenerator\\Database\\Factories\\": "database/factories/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "Pepijnolivier\\EloquentModelGenerator\\Tests\\": "tests/" 47 | } 48 | }, 49 | "scripts": { 50 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 51 | "analyse": "vendor/bin/phpstan analyse", 52 | "test": "vendor/bin/pest", 53 | "test-coverage": "vendor/bin/pest --coverage", 54 | "format": "vendor/bin/pint" 55 | }, 56 | "config": { 57 | "sort-packages": true, 58 | "allow-plugins": { 59 | "pestphp/pest-plugin": true, 60 | "phpstan/extension-installer": true 61 | } 62 | }, 63 | "extra": { 64 | "laravel": { 65 | "providers": [ 66 | "Pepijnolivier\\EloquentModelGenerator\\EloquentModelGeneratorServiceProvider" 67 | ], 68 | "aliases": { 69 | "EloquentModelGenerator": "Pepijnolivier\\EloquentModelGenerator\\Facades\\EloquentModelGenerator" 70 | } 71 | } 72 | }, 73 | "minimum-stability": "stable", 74 | "prefer-stable": true 75 | } 76 | -------------------------------------------------------------------------------- /config/eloquent-model-generator.php: -------------------------------------------------------------------------------- 1 | 'App\Models\Generated', 15 | 'trait_namespace' => 'App\Models\Generated\Relations', 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Output Path 20 | |-------------------------------------------------------------------------- 21 | | 22 | | Path where the models will be created. 23 | | 24 | */ 25 | 'model_path' => 'app/Models/Generated', 26 | 'trait_path' => 'app/Models/Generated/Relations', 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | Extend Model 31 | |-------------------------------------------------------------------------- 32 | | 33 | | Extend the base model. 34 | | 35 | */ 36 | 'extend' => Model::class, 37 | ]; 38 | -------------------------------------------------------------------------------- /example/example.mwb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pepijnolivier/Eloquent-Model-Generator/8692aec24bb4c9bbf6fb86d2d2d599ab243de539/example/example.mwb -------------------------------------------------------------------------------- /example/example.sql: -------------------------------------------------------------------------------- 1 | # ************************************************************ 2 | # Sequel Pro SQL dump 3 | # Version 4096 4 | # 5 | # http://www.sequelpro.com/ 6 | # http://code.google.com/p/sequel-pro/ 7 | # 8 | # Host: 127.0.0.1 (MySQL 5.6.24) 9 | # Database: l5-generators 10 | # Generation Time: 2015-12-08 10:24:44 +0000 11 | # ************************************************************ 12 | 13 | 14 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 15 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 16 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 17 | /*!40101 SET NAMES utf8 */; 18 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 19 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 20 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 21 | 22 | 23 | # Dump of table comments 24 | # ------------------------------------------------------------ 25 | 26 | DROP TABLE IF EXISTS `comments`; 27 | 28 | CREATE TABLE `comments` ( 29 | `id` int(11) NOT NULL AUTO_INCREMENT, 30 | `post_id` int(11) NOT NULL, 31 | `user_id` int(11) NOT NULL, 32 | `comment` varchar(45) NOT NULL, 33 | `created_at` timestamp NOT NULL DEFAULT '1970-01-01 01:01:01', 34 | `updated_at` timestamp NOT NULL DEFAULT '1970-01-01 01:01:01', 35 | PRIMARY KEY (`id`), 36 | KEY `comments_post_id_idx` (`post_id`), 37 | KEY `comments_user_id_idx` (`user_id`), 38 | CONSTRAINT `comments_post_id` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION, 39 | CONSTRAINT `comments_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION 40 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 41 | 42 | 43 | 44 | # Dump of table groups 45 | # ------------------------------------------------------------ 46 | 47 | DROP TABLE IF EXISTS `groups`; 48 | 49 | CREATE TABLE `groups` ( 50 | `id` int(11) NOT NULL AUTO_INCREMENT, 51 | `name` varchar(45) NOT NULL, 52 | `created_at` timestamp NOT NULL DEFAULT '1970-01-01 01:01:01', 53 | `updated_at` timestamp NOT NULL DEFAULT '1970-01-01 01:01:01', 54 | PRIMARY KEY (`id`) 55 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 56 | 57 | 58 | 59 | # Dump of table nationalities 60 | # ------------------------------------------------------------ 61 | 62 | DROP TABLE IF EXISTS `nationalities`; 63 | 64 | CREATE TABLE `nationalities` ( 65 | `id` int(11) NOT NULL AUTO_INCREMENT, 66 | `name` varchar(45) NOT NULL, 67 | `created_at` timestamp NOT NULL DEFAULT '1970-01-01 01:01:01', 68 | `updated_at` timestamp NOT NULL DEFAULT '1970-01-01 01:01:01', 69 | PRIMARY KEY (`id`) 70 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 71 | 72 | 73 | 74 | # Dump of table phones 75 | # ------------------------------------------------------------ 76 | 77 | DROP TABLE IF EXISTS `phones`; 78 | 79 | CREATE TABLE `phones` ( 80 | `user_id` int(11) NOT NULL, 81 | `number` decimal(10,0) DEFAULT NULL, 82 | `created_at` timestamp NOT NULL DEFAULT '1970-01-01 01:01:01', 83 | `updated_at` timestamp NOT NULL DEFAULT '1970-01-01 01:01:01', 84 | PRIMARY KEY (`user_id`), 85 | CONSTRAINT `phone_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION 86 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 87 | 88 | 89 | 90 | # Dump of table posts 91 | # ------------------------------------------------------------ 92 | 93 | DROP TABLE IF EXISTS `posts`; 94 | 95 | CREATE TABLE `posts` ( 96 | `id` int(11) NOT NULL AUTO_INCREMENT, 97 | `title` varchar(45) NOT NULL, 98 | `created_at` timestamp NOT NULL DEFAULT '1970-01-01 01:01:01', 99 | `updated_at` timestamp NOT NULL DEFAULT '1970-01-01 01:01:01', 100 | PRIMARY KEY (`id`) 101 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 102 | 103 | 104 | 105 | # Dump of table user_group 106 | # ------------------------------------------------------------ 107 | 108 | DROP TABLE IF EXISTS `user_group`; 109 | 110 | CREATE TABLE `user_group` ( 111 | `user_id` int(11) NOT NULL, 112 | `group_id` int(11) NOT NULL, 113 | KEY `user_group_user_id_idx` (`user_id`), 114 | KEY `user_group_group_id_idx` (`group_id`), 115 | CONSTRAINT `user_group_group_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION, 116 | CONSTRAINT `user_group_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION 117 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 118 | 119 | 120 | 121 | # Dump of table users 122 | # ------------------------------------------------------------ 123 | 124 | DROP TABLE IF EXISTS `users`; 125 | 126 | CREATE TABLE `users` ( 127 | `id` int(11) NOT NULL AUTO_INCREMENT, 128 | `nationality_id` int(11) DEFAULT NULL, 129 | `first_name` varchar(45) NOT NULL, 130 | `last_name` varchar(45) NOT NULL, 131 | `email` varchar(45) DEFAULT NULL, 132 | `biography` longtext, 133 | `ratio` float DEFAULT NULL, 134 | `age` int(11) DEFAULT NULL, 135 | `birthdate` timestamp NULL DEFAULT NULL, 136 | `is_married` tinyint(1) DEFAULT NULL, 137 | `created_at` timestamp NOT NULL DEFAULT '1970-01-01 01:01:01', 138 | `updated_at` timestamp NOT NULL DEFAULT '1970-01-01 01:01:01', 139 | PRIMARY KEY (`id`), 140 | KEY `users_nationality_id_idx` (`nationality_id`), 141 | CONSTRAINT `users_nationality_id` FOREIGN KEY (`nationality_id`) REFERENCES `nationalities` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION 142 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 143 | 144 | 145 | 146 | 147 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 148 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 149 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 150 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 151 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 152 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 153 | -------------------------------------------------------------------------------- /src/Commands/EloquentModelGeneratorCommand.php: -------------------------------------------------------------------------------- 1 | schema = $this->getSchema(); 30 | $this->parser = new RelationsParser($this->schema); 31 | $this->generator = new Generator($this->schema, $this->parser); 32 | 33 | $this->generate(); 34 | 35 | $this->info('All done'); 36 | return self::SUCCESS; 37 | } 38 | 39 | private function generate() 40 | { 41 | $tableNames = $this->schema->getTableNames()->toArray(); 42 | foreach ($tableNames as $table) { 43 | try { 44 | $this->generator->handle($table); 45 | } catch(\Exception $e) { 46 | Log::error($e->getMessage()); 47 | $this->error("\nFailed to generate model for table $table"); 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * Get DB schema by the database connection name. 54 | * 55 | * @throws \Exception 56 | */ 57 | protected function getSchema(): Schema 58 | { 59 | $driver = DB::getDriverName(); 60 | 61 | if (!$driver) { 62 | throw new Exception('Failed to find database driver.'); 63 | } 64 | 65 | switch ($driver) { 66 | case Driver::MYSQL(): 67 | return $this->schema = app(MySQLSchema::class); 68 | 69 | case Driver::PGSQL(): 70 | return $this->schema = app(PgSQLSchema::class); 71 | 72 | case Driver::SQLITE(): 73 | return $this->schema = app(SQLiteSchema::class); 74 | 75 | case Driver::SQLSRV(): 76 | return $this->schema = app(SQLSrvSchema::class); 77 | 78 | default: 79 | throw new Exception('The database driver in use is not supported.'); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/EloquentModelGeneratorServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('eloquent-model-generator') 15 | ->hasConfigFile() 16 | ->hasCommand(EloquentModelGeneratorCommand::class); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Generators/Generator.php: -------------------------------------------------------------------------------- 1 | parser->getRelationsForTable($table); 22 | $this->generateModelAndTrait($table, $relationsForTable); 23 | } 24 | 25 | 26 | private function generateModelAndTrait(string $tableName, TableRelations $relations=null) 27 | { 28 | $modelName = $this->generateModelNameFromTableName($tableName); 29 | $traitName = "Has{$modelName}Relations"; 30 | 31 | $modelFolder = $this->getModelFolder(); 32 | $this->createFolderIfNotExists($modelFolder); 33 | 34 | [$modelFile, $modelClass] = (new ModelGenerator( 35 | $modelName, 36 | $this->getModelNamespaceString() 37 | ))->handle(); 38 | 39 | $hasRelationships = $relations->hasRelationships() ?? false; 40 | if($hasRelationships) { 41 | [$traitFile, $trait] = (new TraitGenerator( 42 | $traitName, 43 | $this->getModelNamespaceString(), 44 | $this->getTraitNamespaceString(), 45 | $relations 46 | ))->handle(); 47 | 48 | $modelClass->addTrait("{$this->getTraitNamespaceString()}\\$traitName"); 49 | $modelClass->getNamespace()->addUse("{$this->getTraitNamespaceString()}\\$traitName"); 50 | 51 | $traitFolder = $this->getTraitFolder(); 52 | $traitPath = "{$traitFolder}/Has{$modelName}Relations.php"; 53 | $this->createFolderIfNotExists($traitFolder); 54 | file_put_contents($traitPath, (string) $traitFile); 55 | } 56 | 57 | $modelPath = "{$modelFolder}/{$modelName}.php"; 58 | file_put_contents($modelPath, (string) $modelFile); 59 | } 60 | 61 | public function getTables(): array 62 | { 63 | return $this->schema->getTableNames()->toArray(); 64 | } 65 | 66 | protected function createFolderIfNotExists($path) 67 | { 68 | if (!is_dir($path)) { 69 | mkdir($path); 70 | } 71 | } 72 | 73 | private function getModelNamespaceString(): string 74 | { 75 | return config( 76 | 'eloquent_model_generator.model_namespace', 77 | 'App\Models\Generated' 78 | ); 79 | } 80 | 81 | private function getTraitNamespaceString(): string 82 | { 83 | return config( 84 | 'eloquent_model_generator.trait_namespace', 85 | 'App\Models\Generated\Relations' 86 | ); 87 | } 88 | 89 | private function getModelFolder(): string 90 | { 91 | return config( 92 | 'eloquent_model_generator.model_path', 93 | 'app/Models/Generated' 94 | ); 95 | } 96 | 97 | private function getTraitFolder(): string 98 | { 99 | return config( 100 | 'eloquent_model_generator.trait_path', 101 | 'app/Models/Generated/Relations' 102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Generators/ModelGenerator.php: -------------------------------------------------------------------------------- 1 | addNamespace($this->modelNamespaceString); 19 | $modelNamespace->addUse(Model::class); 20 | $modelClass = $modelNamespace->addClass($this->modelName); 21 | 22 | $modelClass 23 | ->setExtends(Model::class) 24 | ->addComment("Generated") 25 | ; 26 | 27 | return [$modelFile, $modelClass]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Generators/RelationsGenerator.php: -------------------------------------------------------------------------------- 1 | relationsMap = []; 26 | } 27 | 28 | public function handle() 29 | { 30 | $this->addBelongsToMethods(); 31 | $this->addHasOneMethods(); 32 | 33 | $this->addHasManyMethods(); 34 | $this->addBelongsToManyMethods(); 35 | 36 | // @todo 37 | // morph methods 38 | // through methods 39 | } 40 | 41 | private function addHasOneMethods() 42 | { 43 | $hasOneRules = $this->relations->getHasOneRelations(); 44 | 45 | /** @var HasOneRelation $relation */ 46 | foreach($hasOneRules as $relation) { 47 | 48 | $functionName = $this->getUniqueFunctionName($relation->getFunctionName()); 49 | 50 | $body = sprintf( 51 | 'return $this->hasOne(%s::class, \'%s\', \'%s\');', 52 | $relation->getEntityClass(), 53 | $relation->getForeignKey(), 54 | $relation->getLocalKey() 55 | ); 56 | 57 | $this->classLike 58 | ->addMethod($functionName) 59 | ->addBody($body); 60 | 61 | $this->namespace->addUse($this->modelNamespaceString . "\\" . $relation->getEntityClass()); 62 | } 63 | } 64 | 65 | private function addBelongsToManyMethods() { 66 | 67 | $belongsToManyRules = $this->relations->getBelongsToManyRelations(); 68 | 69 | /** @var BelongsToManyRelation $relation */ 70 | foreach($belongsToManyRules as $relation) { 71 | 72 | $functionName = $this->getUniqueFunctionName($relation->getFunctionName()); 73 | 74 | $body = sprintf( 75 | 'return $this->belongsToMany(%s::class, \'%s\', \'%s\', \'%s\');', 76 | $relation->getEntityClass(), 77 | $relation->getTable(), 78 | $relation->getForeignPivotKey(), 79 | $relation->getRelatedPivotKey(), 80 | ); 81 | 82 | $this->classLike 83 | ->addMethod($functionName) 84 | ->addBody($body); 85 | 86 | $this->namespace->addUse($this->modelNamespaceString . "\\" . $relation->getEntityClass()); 87 | } 88 | } 89 | 90 | private function addHasManyMethods() { 91 | 92 | $hasManyRules = $this->relations->getHasManyRelations(); 93 | 94 | /** @var HasManyRelation $relation */ 95 | foreach($hasManyRules as $relation) { 96 | 97 | $functionName = $this->getUniqueFunctionName($relation->getFunctionName()); 98 | 99 | $body = sprintf( 100 | 'return $this->hasMany(%s::class, \'%s\', \'%s\');', 101 | $relation->getEntityClass(), 102 | $relation->getForeignKey(), 103 | $relation->getLocalKey() 104 | ); 105 | 106 | $this->classLike 107 | ->addMethod($functionName) 108 | ->addBody($body); 109 | 110 | $this->namespace->addUse($this->modelNamespaceString . "\\" . $relation->getEntityClass()); 111 | } 112 | } 113 | 114 | private function addBelongsToMethods() 115 | { 116 | $belongsToRules = $this->relations->getBelongsToRelations(); 117 | 118 | /** @var BelongsToRelation $relation */ 119 | foreach($belongsToRules as $relation) { 120 | 121 | $functionName = $this->getUniqueFunctionName($relation->getFunctionName()); 122 | 123 | $body = sprintf( 124 | 'return $this->belongsTo(%s::class, \'%s\', \'%s\');', 125 | $relation->getEntityClass(), 126 | $relation->getForeignKey(), 127 | $relation->getOwnerKey() 128 | ); 129 | 130 | $this->classLike 131 | ->addMethod($functionName) 132 | ->addBody($body); 133 | 134 | $this->namespace->addUse($this->modelNamespaceString . "\\" . $relation->getEntityClass()); 135 | } 136 | } 137 | 138 | private function getUniqueFunctionName(string $functionName): string 139 | { 140 | if (isset($this->relationsMap[$functionName])) { 141 | $foundUnique = false; 142 | $counter = 1; 143 | 144 | while(!$foundUnique) { 145 | ++$counter; 146 | 147 | $newFunctionName = $functionName . $counter; 148 | 149 | if (!isset($this->relationsMap[$newFunctionName])) { 150 | $functionName = $newFunctionName; 151 | $foundUnique = true; 152 | } 153 | } 154 | } 155 | 156 | $this->relationsMap[$functionName] = true; 157 | return $functionName; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Generators/TraitGenerator.php: -------------------------------------------------------------------------------- 1 | addNamespace($this->traitNamespaceString); 23 | $traitNamespace->addUse(HasRelationships::class); 24 | $trait = $traitNamespace->addTrait($this->traitName); 25 | 26 | $trait->addComment("Generated"); 27 | $trait->addTrait(HasRelationships::class); 28 | 29 | (new RelationsGenerator( 30 | $traitNamespace, 31 | $trait, 32 | $this->relations, 33 | $this->modelNamespaceString 34 | ))->handle(); 35 | 36 | return [$traitFile, $trait]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Parser/RelationsParser.php: -------------------------------------------------------------------------------- 1 | schema = $schema; 29 | $this->relationsPerTable = $this->parse(); 30 | } 31 | 32 | public function getRelationsForTable(string $table): ?TableRelations 33 | { 34 | return $this->relationsPerTable[$table] ?? null; 35 | } 36 | 37 | /** @return TableRelations[] */ 38 | public function getRelationsPerTable(): array 39 | { 40 | return $this->relationsPerTable; 41 | } 42 | 43 | private function parse() 44 | { 45 | $tables = $this->schema->getTableNames(); 46 | 47 | /** @var TableRelations[] $relationsByTable */ 48 | $relationsByTable = []; 49 | 50 | // first create empty ruleset for each table 51 | foreach ($tables as $tableName) { 52 | $relationsByTable[$tableName] = new TableRelations($tableName); 53 | } 54 | 55 | /** @var string $tableName */ 56 | foreach ($tables as $tableName) { 57 | $table = $this->schema->getTable($tableName); 58 | $foreign = $this->getForeignKeysForTable($tableName); 59 | $primary = $this->getPrimaryKeysForTable($tableName); 60 | 61 | // @improvement we should probably pass in the table everywhere, instead of the table name... 62 | $isManyToMany = $this->isManyToManyTable($tableName); 63 | 64 | if ($isManyToMany === true) { 65 | $this->addManyToManyRelations($tableName, $relationsByTable); 66 | } 67 | 68 | foreach ($foreign as $fk) { 69 | $isOneToOne = $this->isOneToOne($fk, $primary); 70 | 71 | if ($isOneToOne) { 72 | $this->addOneToOneRules($tableName, $fk, $relationsByTable); 73 | } else { 74 | $this->addOneToManyRules($tableName, $fk, $relationsByTable); 75 | } 76 | } 77 | } 78 | 79 | return $relationsByTable; 80 | } 81 | 82 | private function addOneToManyRules(string $tableName, ForeignKey $fk, array &$relationsByTable) 83 | { 84 | // $table belongs to $FK 85 | // FK hasMany $table 86 | 87 | $tableNames = $this->schema->getTableNames()->toArray(); 88 | $fkTable = $fk->getForeignTableName(); 89 | 90 | $localColumns = $fk->getLocalColumns(); 91 | $foreignColumns = $fk->getForeignColumns(); 92 | 93 | if(count($localColumns) > 1) { 94 | // throw new \Exception('Composite local keys are not supported'); 95 | return; 96 | } 97 | 98 | if(count($foreignColumns) > 1) { 99 | // throw new \Exception('Composite foreign columns are not supported'); 100 | return; 101 | } 102 | 103 | $field = $localColumns[0]; 104 | $references = $foreignColumns[0]; 105 | 106 | if(in_array($fkTable, $tableNames)) { 107 | $hasManyModel = $this->generateModelNameFromTableName($tableName); 108 | $hasManyFunctionName = $this->getPluralFunctionName($hasManyModel); 109 | 110 | // @toconsider: if it's a table with only 2 columns, and they are both the FK 111 | // then it's just a pure pivot table. We might not want to add a relation for that. 112 | 113 | $relation = new HasManyRelation($hasManyFunctionName, $hasManyModel, $field, $references); 114 | 115 | /** @var TableRelations $tableRelations */ 116 | $tableRelations = $relationsByTable[$fkTable]; 117 | $tableRelations->addHasManyRelation($relation); 118 | } 119 | if(in_array($tableName, $tableNames)) { 120 | $belongsToModel = $this->generateModelNameFromTableName($fkTable); 121 | $belongsToFunctionName = $this->getSingularFunctionName($belongsToModel); 122 | $relation = new BelongsToRelation($belongsToFunctionName, $belongsToModel, $field, $references); 123 | 124 | /** @var TableRelations $tableRelations */ 125 | $tableRelations = $relationsByTable[$tableName]; 126 | $tableRelations->addBelongsToRelation($relation); 127 | } 128 | } 129 | 130 | private function addOneToOneRules(string $tableName, ForeignKey $fk, array &$relationsByTable) 131 | { 132 | //$table belongsTo $FK 133 | //$FK hasOne $table 134 | 135 | $tableNames = $this->schema->getTableNames()->toArray(); 136 | 137 | $fkTable = $fk->getForeignTableName(); 138 | $localColumns = $fk->getLocalColumns(); 139 | $foreignColumns = $fk->getForeignColumns(); 140 | 141 | if(count($localColumns) > 1) { 142 | // throw new \Exception('Composite local keys are not supported'); 143 | return; 144 | } 145 | 146 | if(count($foreignColumns) > 1) { 147 | // throw new \Exception('Composite foreign columns are not supported'); 148 | return; 149 | } 150 | 151 | // switched ???? 152 | $field = $localColumns[0]; 153 | $references = $foreignColumns[0]; 154 | 155 | if(in_array($fkTable, $tableNames)) { 156 | $modelName = $this->generateModelNameFromTableName($tableName); 157 | $functionName = $this->getSingularFunctionName($modelName); 158 | 159 | $relation = new HasOneRelation($functionName, $modelName, $field, $references); 160 | 161 | /** @var TableRelations $tableRelations */ 162 | $tableRelations = $relationsByTable[$fkTable]; 163 | $tableRelations->addHasOneRelation($relation); 164 | } 165 | if(in_array($tableName, $tableNames)) { 166 | $belongsToModel = $this->generateModelNameFromTableName($fkTable); 167 | $belongsToFunctionName = $this->getSingularFunctionName($belongsToModel); 168 | $relation = new BelongsToRelation($belongsToFunctionName, $belongsToModel, $field, $references); 169 | 170 | /** @var TableRelations $tableRelations */ 171 | $tableRelations = $relationsByTable[$tableName]; 172 | $tableRelations->addBelongsToRelation($relation); 173 | } 174 | } 175 | 176 | /** 177 | * @param string $tableName 178 | * @param TableRelations[] $rules 179 | * @return void 180 | */ 181 | private function addManyToManyRelations(string $tableName, array &$relationsByTable) 182 | { 183 | $tableNames = $this->schema->getTableNames()->toArray(); 184 | $foreign = $this->getForeignKeysForTable($tableName); 185 | 186 | //$FK1 belongsToMany $FK2 187 | //$FK2 belongsToMany $FK1 188 | 189 | /** @var ForeignKey $fk1 */ 190 | $fk1 = $foreign[0]; 191 | 192 | /** @var ForeignKey $fk2 */ 193 | $fk2 = $foreign[1]; 194 | 195 | if((count($fk1->getLocalColumns()) > 1) || (count($fk2->getLocalColumns()) > 1)) { 196 | // throw new \Exception('Composite primary keys are not supported'); 197 | return; 198 | } 199 | 200 | $fk1Table = $fk1->getForeignTableName(); 201 | $fk1Field = $fk1->getLocalColumns()[0]; 202 | 203 | $fk2Table = $fk2->getForeignTableName(); 204 | $fk2Field = $fk2->getLocalColumns()[0]; 205 | 206 | if (in_array($fk1Table, $tableNames)) { 207 | $belongsToManyModel = $this->generateModelNameFromTableName($fk2Table); 208 | $belongsToManyFunctionName = $this->getPluralFunctionName($belongsToManyModel); 209 | $through = $tableName; 210 | 211 | $relation = new BelongsToManyRelation($belongsToManyFunctionName, $belongsToManyModel, $through, $fk1Field, $fk2Field); 212 | 213 | /** @var TableRelations $tableRelations */ 214 | $tableRelations = $relationsByTable[$fk1Table]; 215 | $tableRelations->addBelongsToManyRelation($relation); 216 | } 217 | if (in_array($fk2Table, $tableNames)) { 218 | $belongsToManyModel = $this->generateModelNameFromTableName($fk1Table); 219 | $belongsToManyFunctionName = $this->getPluralFunctionName($belongsToManyModel); 220 | 221 | $through = $tableName; 222 | $relation = new BelongsToManyRelation($belongsToManyFunctionName, $belongsToManyModel, $through, $fk2Field, $fk1Field); 223 | 224 | /** @var TableRelations $tableRelations */ 225 | $tableRelations = $relationsByTable[$fk2Table]; 226 | $tableRelations->addBelongsToManyRelation($relation); 227 | } 228 | } 229 | 230 | //if FK is also a primary key, and there is only one primary key, we know this will be a one to one relationship 231 | private function isOneToOne(ForeignKey $fk, Collection $primary) 232 | { 233 | if (count($primary) !== 1) { 234 | return false; 235 | } 236 | 237 | /** @var Index $prim */ 238 | foreach ($primary as $prim) { 239 | 240 | $primaryColumns = $prim->getColumns(); 241 | $foreignColumns = $fk->getLocalColumns(); 242 | 243 | if(count($primaryColumns) > 1) { 244 | // throw new \Exception('Composite primary keys are not supported'); 245 | return false; 246 | } 247 | 248 | if(count($foreignColumns) > 1) { 249 | // throw new \Exception('Composite foreign keys are not supported'); 250 | return false; 251 | } 252 | 253 | $primaryKeyColumn = $primaryColumns[0]; 254 | $foreignKeyColumn = $foreignColumns[0]; 255 | 256 | if ($primaryKeyColumn === $foreignKeyColumn) { 257 | // dd($primaryKeyColumn, $foreignKeyColumn, $prim, $fk); 258 | // dd($fk, $primary); 259 | return true; 260 | } 261 | } 262 | } 263 | 264 | // does this table have exactly two foreign keys that are 265 | // - either both primary, 266 | // - or neither are primary 267 | // ... and no other tables in the database refer to this table? 268 | // then it's cleary a pivot / many-to-many table! 269 | private function isManyToManyTable(string $tableName): bool 270 | { 271 | $tableNames = $this->schema->getTableNames(); 272 | 273 | $foreignKeys = $this->getForeignKeysForTable($tableName); 274 | $primaryKeys = $this->getPrimaryKeysForTable($tableName); 275 | 276 | //ensure we only have two foreign keys 277 | if (count($foreignKeys) === 2) { 278 | //ensure our foreign keys are not also defined as primary keys 279 | $primaryKeyCountThatAreAlsoForeignKeys = 0; 280 | 281 | foreach ($foreignKeys as $foreignKey) { 282 | foreach ($primaryKeys as $primaryKey) { 283 | if ($primaryKey->getName() === $foreignKey->getName()) { 284 | ++$primaryKeyCountThatAreAlsoForeignKeys; 285 | } 286 | } 287 | } 288 | 289 | if ($primaryKeyCountThatAreAlsoForeignKeys === 1) { 290 | // one of the keys foreign keys was also a primary key 291 | // this is not a many to many. (many to many is only possible when both or none of the foreign keys are also primary) 292 | return false; 293 | } 294 | 295 | // ensure no other tables refer to this one 296 | foreach ($tableNames as $compareTable) { 297 | if ($tableName !== $compareTable) { 298 | $compareFK = $this->getForeignKeysForTable($compareTable); 299 | 300 | /** @var ForeignKey $compareForeignKey */ 301 | foreach ($compareFK as $compareForeignKey) { 302 | if ($compareForeignKey->getForeignTableName() === $tableName) { 303 | return false; 304 | } 305 | } 306 | } 307 | } 308 | 309 | return true; 310 | } 311 | 312 | return false; 313 | } 314 | 315 | /** 316 | * @param string $tableName 317 | * @return Collection 318 | */ 319 | private function getForeignKeysForTable(string $tableName): Collection 320 | { 321 | return $this->schema->getTableForeignKeys($tableName); 322 | } 323 | 324 | /** 325 | * @param string $tableName 326 | * @return Collection 327 | */ 328 | private function getPrimaryKeysForTable(string $tableName): Collection 329 | { 330 | $table = $this->schema->getTable($tableName); 331 | $primaryKeys = $table->getIndexes()->filter(function($index) { 332 | return $index->getType() == IndexType::PRIMARY(); 333 | }); 334 | 335 | return $primaryKeys; 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/Relations/TableRelations.php: -------------------------------------------------------------------------------- 1 | relations); 24 | } 25 | 26 | public function addBelongsToRelation(BelongsToRelation $relation) 27 | { 28 | $this->relations['belongsTo'][] = $relation; 29 | } 30 | 31 | public function addHasOneRelation(HasOneRelation $relation) 32 | { 33 | $this->relations['hasOne'][] = $relation; 34 | } 35 | 36 | public function addHasManyRelation(HasManyRelation $relation) 37 | { 38 | $this->relations['hasMany'][] = $relation; 39 | } 40 | 41 | public function addBelongsToManyRelation(BelongsToManyRelation $relation) 42 | { 43 | $this->relations['belongsToMany'][] = $relation; 44 | } 45 | 46 | /** 47 | * @return BelongsToRelation[] 48 | */ 49 | public function getBelongsToRelations(): array 50 | { 51 | return Arr::get($this->relations, 'belongsTo', []); 52 | } 53 | 54 | /** 55 | * @return HasOneRelation[] 56 | */ 57 | public function getHasOneRelations(): array 58 | { 59 | return Arr::get($this->relations, 'hasOne', []); 60 | } 61 | 62 | /** 63 | * @return HasManyRelation[] 64 | */ 65 | public function getHasManyRelations(): array 66 | { 67 | return Arr::get($this->relations, 'hasMany', []); 68 | } 69 | 70 | /** 71 | * @return BelongsToManyRelation[] 72 | */ 73 | public function getBelongsToManyRelations(): array 74 | { 75 | return Arr::get($this->relations, 'belongsToMany', []); 76 | } 77 | 78 | 79 | public function getTableName(): string 80 | { 81 | return $this->tableName; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Relations/Types/BelongsToManyRelation.php: -------------------------------------------------------------------------------- 1 | functionName; 20 | } 21 | 22 | public function getEntityClass(): string 23 | { 24 | return $this->entityClass; 25 | } 26 | 27 | public function getTable(): ?string 28 | { 29 | return $this->table; 30 | } 31 | 32 | public function getForeignPivotKey(): ?string 33 | { 34 | return $this->foreignPivotKey; 35 | } 36 | 37 | public function getRelatedPivotKey(): ?string 38 | { 39 | return $this->relatedPivotKey; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/Relations/Types/BelongsToRelation.php: -------------------------------------------------------------------------------- 1 | functionName; 19 | } 20 | 21 | public function getEntityClass(): string 22 | { 23 | return $this->entityClass; 24 | } 25 | 26 | public function getForeignKey(): ?string 27 | { 28 | return $this->foreignKey; 29 | } 30 | 31 | public function getOwnerKey(): ?string 32 | { 33 | return $this->ownerKey; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Relations/Types/HasManyRelation.php: -------------------------------------------------------------------------------- 1 | functionName; 19 | } 20 | 21 | public function getEntityClass(): string 22 | { 23 | return $this->entityClass; 24 | } 25 | 26 | public function getForeignKey(): ?string 27 | { 28 | return $this->foreignKey; 29 | } 30 | 31 | public function getLocalKey(): ?string 32 | { 33 | return $this->localKey; 34 | } 35 | 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/Relations/Types/HasOneRelation.php: -------------------------------------------------------------------------------- 1 | functionName; 19 | } 20 | 21 | public function getEntityClass(): string 22 | { 23 | return $this->entityClass; 24 | } 25 | 26 | public function getForeignKey(): ?string 27 | { 28 | return $this->foreignKey; 29 | } 30 | 31 | public function getLocalKey(): ?string 32 | { 33 | return $this->localKey; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Traits/HelperTrait.php: -------------------------------------------------------------------------------- 1 |