├── LICENSE.md ├── README.md ├── composer.json ├── config └── laravel-erd.php ├── phpcs.xml ├── resources └── views │ └── index.blade.php ├── routes └── web.php └── src ├── Commands └── LaravelERDCommand.php ├── Controllers └── LaravelERDController.php ├── LaravelERD.php ├── LaravelERDFacade.php └── LaravelERDServiceProvider.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) :vendor_name 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 | # Laravel ERD Generator 2 | 3 | Automatically generate interactive ERD from Models relationships in Laravel. 4 | - This package provides a CLI to automatically generate interactive ERD by looking into relationships setup inside Models. 5 | - The tool's interface is available to get JSON data of relationships and table schema from Models to be used by visual charts libraries of your choice such as d3.js etc 6 | - Output is an interactive ERD (Entity Relation Diagram) powered by HTML and JS (GOJS). 7 | 8 | 9 | | Lang | Link | 10 | | :------ | :------------------------------------------------------------------------------------------------------------------------------------- | 11 | | Details | [Medium Article](https://medium.com/web-developer/laravel-automatically-generate-interactive-erd-from-eloquent-relations-83fe65440716) | 12 | | Demo | [Online Demo](https://kevincobain2000.github.io/laravel-blog/erd/) | 13 | 14 | 15 | ## Requirements 16 | 17 | | Lang | Version | 18 | | :------ | :--------- | 19 | | PHP | 7.4 or 8.0 | 20 | | Laravel | 6.* or 8.* | 21 | 22 | ## Installation 23 | 24 | You can install the package via composer: 25 | 26 | ```bash 27 | composer require kevincobain2000/laravel-erd --dev 28 | ``` 29 | 30 | 31 | You can publish the config file with: 32 | 33 | ```bash 34 | php artisan vendor:publish --provider="Kevincobain2000\LaravelERD\LaravelERDServiceProvider" 35 | ``` 36 | 37 | ## Usage 38 | 39 | You can access the ERD in ``localhost:3000/erd`` 40 | 41 | or generate a static HTML 42 | 43 | ```php 44 | php artisan erd:generate 45 | ``` 46 | 47 | ERD HTML is generated inside ``docs/``. 48 | 49 | ### Sample 50 | 51 | #### Screenshot 52 | 53 | ![Image](https://i.imgur.com/tYk1CuC.png) 54 | 55 | #### Get JSON output 56 | 57 | ```php 58 | use Kevincobain2000\LaravelERD\LaravelERD; 59 | 60 | $modelsPath = base_path('app/Models'); 61 | 62 | $laravelERD = new LaravelERD(); 63 | $linkDataArray = $laravelERD->getLinkDataArray($modelsPath); 64 | $nodeDataArray = $laravelERD->getNodeDataArray($modelsPath); 65 | $erdData = json_encode( 66 | [ 67 | "link_data" => $linkDataArray, 68 | "node_data" => $nodeDataArray, 69 | ], 70 | JSON_PRETTY_PRINT 71 | ); 72 | var_dump($erdData); 73 | ``` 74 | 75 | Sample JSON output 76 | 77 | ```js 78 | { 79 | "link_data": [ 80 | { 81 | "from": "comments", 82 | "to": "users", 83 | "fromText": "1..1\nBT", 84 | "toText": "", 85 | "fromPort": "author_id", 86 | "toPort": "id", 87 | "type": "BelongsTo" 88 | }, 89 | { 90 | "from": "comments", 91 | "to": "posts", 92 | "fromText": "1..1\nBT", 93 | "toText": "", 94 | "fromPort": "post_id", 95 | "toPort": "id", 96 | "type": "BelongsTo" 97 | }, 98 | ... 99 | ... 100 | ], 101 | "node_data": [ 102 | { 103 | "key": "comments", 104 | "schema": [ 105 | { 106 | "name": "id", 107 | "isKey": true, 108 | "figure": "Hexagon", 109 | "color": "#be4b15", 110 | "info": "integer" 111 | }, 112 | { 113 | "name": "author_id", 114 | "isKey": false, 115 | "figure": "Decision", 116 | "color": "#6ea5f8", 117 | "info": "integer" 118 | }, 119 | ... 120 | ... 121 | } 122 | ... 123 | ] 124 | 125 | ``` 126 | 127 | ## Testing 128 | 129 | ```bash 130 | ./vendor/bin/phpunit 131 | ``` 132 | 133 | ## Changelog 134 | 135 | - Initial Release - POC 136 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kevincobain2000/laravel-erd", 3 | "description": "A tool to automatically generate interactive ERD relationships in Models for Laravel", 4 | "keywords": [ 5 | "kevincobain2000", 6 | "laravel erd generator", 7 | "laravel-erd" 8 | ], 9 | "homepage": "https://github.com/kevincobain2000/laravel-erd", 10 | "license": "MIT", 11 | "version": "1.7", 12 | "authors": [ 13 | { 14 | "name": "Pulkit Kathuria", 15 | "email": "kevincobain2000@gmail.com", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^7.4|^8.0|^8.1|^8.2|^8.3", 21 | "spatie/laravel-package-tools": "^1.4.3" 22 | }, 23 | "require-dev": { 24 | "brianium/paratest": "^6.2", 25 | "nunomaduro/collision": "^5.3", 26 | "orchestra/testbench": "^6.15", 27 | "phpunit/phpunit": "^9.3", 28 | "spatie/laravel-ray": "^1.23", 29 | "vimeo/psalm": "^4.8" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Kevincobain2000\\LaravelERD\\": "src" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Kevincobain2000\\LaravelERD\\Tests\\": "tests" 39 | } 40 | }, 41 | "scripts": { 42 | "psalm": "vendor/bin/psalm", 43 | "test": "./vendor/bin/testbench package:test --parallel --no-coverage", 44 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 45 | }, 46 | "config": { 47 | "sort-packages": true 48 | }, 49 | "extra": { 50 | "laravel": { 51 | "providers": [ 52 | "Kevincobain2000\\LaravelERD\\LaravelERDServiceProvider" 53 | ], 54 | "aliases": { 55 | "LaravelERD": "Kevincobain2000\\LaravelERD\\LaravelERDFacade" 56 | } 57 | } 58 | }, 59 | "minimum-stability": "dev", 60 | "prefer-stable": true 61 | } 62 | -------------------------------------------------------------------------------- /config/laravel-erd.php: -------------------------------------------------------------------------------- 1 | 'erd', 5 | 'middlewares' => [ 6 | //Example 7 | //\App\Http\Middleware\NotFoundWhenProduction::class, 8 | ], 9 | 'models_path' => base_path('app/Models'), 10 | 'docs_path' => base_path('docs/erd/'), 11 | 12 | "display" => [ 13 | "show_data_type" => false, 14 | 15 | /** 16 | * Controls how the lines between models are drawn. 17 | * 18 | * Valid values are: Normal, Orthogonal, or AvoidsNodes. 19 | * 20 | * AvoidsNodes can be very slow in larger diagrams! 21 | */ 22 | "routing" => 'AvoidsNodes', 23 | ], 24 | 25 | "from_text" => [ 26 | "BelongsTo" => "1..1\nBelongs To", 27 | "BelongsToMany" => "1..*\nBelongs To Many", 28 | "HasMany" => "1..*\nHas Many", 29 | "HasOne" => "1..1\nHas One", 30 | "ManyToMany" => "*..*\nMany To Many", 31 | "ManyToOne" => "*..1\nMany To One", 32 | "OneToMany" => "1..*\nOne To Many", 33 | "OneToOne" => "1..1\nOne To One", 34 | "MorphTo" => "1..1\n", 35 | "MorphToMany" => "1..*\n", 36 | ], 37 | "to_text" => [ 38 | "BelongsTo" => "", 39 | "BelongsToMany" => "", 40 | "HasMany" => "", 41 | "HasOne" => "", 42 | "ManyToMany" => "", 43 | "ManyToOne" => "", 44 | "OneToMany" => "", 45 | "OneToOne" => "", 46 | "MorphTo" => "", 47 | "MorphToMany" => "", 48 | ], 49 | ]; 50 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Standard Based on PSR2 4 | tests/* 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /resources/views/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ERD 13 | 14 | 15 | 28 |
29 | 42 |
43 |
44 |
45 |
46 | 349 | 350 | 351 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | name('laravel-erd.index') 19 | ->middleware(config('laravel-erd.middlewares')); -------------------------------------------------------------------------------- /src/Commands/LaravelERDCommand.php: -------------------------------------------------------------------------------- 1 | laravelERD = $laravelERD; 20 | parent::__construct(); 21 | } 22 | 23 | /** 24 | * Execute the console command. 25 | * 26 | * @return int 27 | */ 28 | public function handle() 29 | { 30 | $modelsPath = config('laravel-erd.models_path') ?? base_path('app/Models'); 31 | $destinationPath = config('laravel-erd.docs_path') ?? base_path('docs/erd/'); 32 | 33 | // extract data 34 | $linkDataArray = $this->laravelERD->getLinkDataArray($modelsPath); 35 | $nodeDataArray = $this->laravelERD->getNodeDataArray($modelsPath); 36 | 37 | if (! File::exists($destinationPath)) { 38 | File::makeDirectory($destinationPath, 0755, true); 39 | } 40 | File::put($destinationPath . '/index.html', 41 | view('erd::index') 42 | ->with([ 43 | 'routingType' => config('laravel-erd.display.routing') ?? 'AvoidsNodes', 44 | 45 | // pretty print array to json 46 | 'docs' => json_encode( 47 | [ 48 | "link_data" => $linkDataArray, 49 | "node_data" => $nodeDataArray, 50 | ] 51 | ), 52 | ]) 53 | ->render() 54 | ); 55 | 56 | $this->info("ERD data written successfully to $destinationPath"); 57 | 58 | return 0; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Controllers/LaravelERDController.php: -------------------------------------------------------------------------------- 1 | laravelERD = $laravelERD; 17 | } 18 | 19 | public function index() 20 | { 21 | $modelsPath = config('laravel-erd.models_path') ?? base_path('app/Models'); 22 | $linkDataArray = $this->laravelERD->getLinkDataArray($modelsPath); 23 | $nodeDataArray = $this->laravelERD->getNodeDataArray($modelsPath); 24 | 25 | return view('erd::index')->with([ 26 | 'routingType' => config('laravel-erd.display.routing') ?? 'AvoidsNodes', 27 | 28 | // pretty print array to json 29 | 'docs' => json_encode( 30 | [ 31 | "link_data" => $linkDataArray, 32 | "node_data" => $nodeDataArray, 33 | ] 34 | ), 35 | ]); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/LaravelERD.php: -------------------------------------------------------------------------------- 1 | map(function ($item) { 21 | $path = $item->getFilename(); 22 | $namespace = $this->extractNamespace($item->getRealPath()) . '\\'; 23 | $class = sprintf( 24 | '\%s%s', 25 | $namespace, 26 | strtr(substr($path, 0, strrpos($path, '.')), '/', '\\') 27 | ); 28 | return $class; 29 | }) 30 | ->filter(function ($class) { 31 | $valid = false; 32 | 33 | if (class_exists($class)) { 34 | $reflection = new ReflectionClass($class); 35 | $valid = $reflection->isSubclassOf(Model::class) && !$reflection->isAbstract(); 36 | } 37 | 38 | return $valid; 39 | }); 40 | } 41 | 42 | public function getLinkDataArray(string $modelsPath) 43 | { 44 | $linkDataArray = []; 45 | $modelNames = $this->getModelsNames($modelsPath); 46 | 47 | foreach ($modelNames as $modelName) { 48 | $model = app($modelName); 49 | $links = $this->getLinks($model); 50 | foreach ($links as $link) { 51 | $linkDataArray[] = $link; 52 | } 53 | } 54 | 55 | return $linkDataArray; 56 | } 57 | 58 | public function getNodeDataArray(string $modelsPath) 59 | { 60 | $nodeDataArray = []; 61 | $modelNames = $this->getModelsNames($modelsPath); 62 | $modelNames = $this->removeDuplicateModelNames($modelNames); 63 | 64 | foreach ($modelNames as $modelName) { 65 | $model = app($modelName); 66 | 67 | $nodeDataArray[] = $this->getNodes($model); 68 | } 69 | return $nodeDataArray; 70 | } 71 | 72 | function removeDuplicateModelNames($modelNames) 73 | { 74 | $finalModelNames = collect($modelNames) 75 | ->map(function($modelName) { 76 | $model = app($modelName); 77 | return [ 78 | 'model_name' => $modelName, 79 | 'table' => $model->getTable(), 80 | ]; 81 | }) 82 | ->unique('table') 83 | ->pluck('model_name'); 84 | 85 | return $finalModelNames->all(); 86 | } 87 | 88 | private function extractNamespace($file) 89 | { 90 | $ns = NULL; 91 | $handle = fopen($file, "r"); 92 | if ($handle) { 93 | while (($line = fgets($handle)) !== false) { 94 | if (strpos($line, 'namespace') === 0) { 95 | $parts = explode(' ', $line); 96 | $ns = rtrim(trim($parts[1]), ';'); 97 | break; 98 | } 99 | } 100 | fclose($handle); 101 | } 102 | return $ns; 103 | } 104 | 105 | /** 106 | * Relationships 107 | * 108 | * @param Model $model 109 | * @return array of relationships 110 | */ 111 | private function getRelationships(Model $model): array 112 | { 113 | $relationships = []; 114 | $model = new $model; 115 | 116 | foreach ((new ReflectionClass($model))->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { 117 | if ($method->class != get_class($model) 118 | || !empty($method->getParameters()) 119 | || $method->getName() == __FUNCTION__ 120 | ) { 121 | continue; 122 | } 123 | 124 | try { 125 | $return = $method->invoke($model); 126 | // check if not instance of Relation 127 | if (!($return instanceof Relation)) { 128 | continue; 129 | } 130 | $relationType = (new ReflectionClass($return))->getShortName(); 131 | $modelName = (new ReflectionClass($return->getRelated()))->getName(); 132 | 133 | $foreignKey = $return->getQualifiedForeignKeyName(); 134 | $parentKey = $return->getQualifiedParentKeyName(); 135 | 136 | $relationships[$method->getName()] = [ 137 | 'type' => $relationType, 138 | 'model' => $modelName, 139 | 'foreign_key' => $foreignKey, 140 | 'parent_key' => $parentKey, 141 | ]; 142 | } catch (QueryException $e) { 143 | // ignore 144 | } catch (TypeError $e) { 145 | // ignore 146 | } catch (Throwable $e) { 147 | // throw $e; 148 | //ignore 149 | } 150 | } 151 | 152 | return $relationships; 153 | } 154 | 155 | private function getNodes(Model $model): array 156 | { 157 | $nodeItems = []; 158 | $columns = Schema::getColumnListing($model->getTable()); 159 | 160 | foreach ($columns as $column) { 161 | $keyName = $model->getKeyName(); 162 | if (is_array($keyName)) { 163 | $isPrimaryKey = in_array($column, $keyName); 164 | } else { 165 | $isPrimaryKey = $column == $keyName; 166 | } 167 | 168 | $nodeItems[] = [ 169 | "name" => $column, 170 | "isKey" => $isPrimaryKey, 171 | "figure" => $isPrimaryKey ? "Hexagon" : "Decision", 172 | "color" => $isPrimaryKey ? "#be4b15" : "#6ea5f8", 173 | "info" => config('laravel-erd.display.show_data_type') ? Schema::getColumnType($model->getTable(), $column) : "", 174 | ]; 175 | } 176 | return [ 177 | "key" => $model->getTable(), 178 | "schema" => $nodeItems 179 | ]; 180 | } 181 | 182 | private function getLinks(Model $model) 183 | { 184 | $relationships = $this->getRelationships($model); 185 | $linkItems = []; 186 | foreach ($relationships as $relationship) { 187 | $fromTable = $model->getTable(); 188 | $toTable = app($relationship['model'])->getTable(); 189 | 190 | // check if is array for multiple primary key 191 | if (is_array($relationship['foreign_key']) || is_array($relationship['parent_key'])) { 192 | // TODO add support for multiple primary keys 193 | $fromPort = "."; 194 | $toPort = "."; 195 | } else { 196 | $isBelongsTo = ($relationship['type'] == "BelongsTo" || $relationship['type'] == "BelongsToMany"); 197 | $fromPort = $isBelongsTo ? $relationship["foreign_key"] : $relationship["parent_key"]; 198 | $toPort = $isBelongsTo ? $relationship["parent_key"] : $relationship["foreign_key"]; 199 | } 200 | 201 | $linkItems[] = [ 202 | "from" => $fromTable, 203 | "to" => $toTable, 204 | "fromText" => config('laravel-erd.from_text.'.$relationship['type']), 205 | "toText" => config('laravel-erd.to_text.'.$relationship['type']), 206 | "fromPort" => explode(".", $fromPort)[1], //strip tablename 207 | "toPort" => explode(".", $toPort)[1],//strip tablename 208 | "type" => $relationship['type'], 209 | ]; 210 | } 211 | return $linkItems; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/LaravelERDFacade.php: -------------------------------------------------------------------------------- 1 | name('laravel-erd') 16 | ->hasConfigFile('laravel-erd') 17 | ->hasViews() 18 | ->hasCommand(LaravelERDCommand::class) 19 | ->hasRoutes('web'); 20 | } 21 | } 22 | --------------------------------------------------------------------------------