diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..cd8cdc2 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,58 @@ +name: run-tests + +on: + push: + branches: + - master + - dev + pull_request: + branches: + - master + +jobs: + php-tests: + runs-on: ubuntu-latest + + strategy: + fail-fast: true + matrix: + php: ['8.4', '8.3', '8.2'] + laravel: ['10.*', '11.*', '12.*'] + dependency-version: [prefer-stable] + exclude: + - php: 8.4 + laravel: 10.* + - php: 8.4 + laravel: 11.* + include: + - laravel: 10.* + testbench: 8.* + - laravel: 11.* + testbench: 9.* + - laravel: 12.* + testbench: 10.* + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Graphviz + run: sudo apt-get update && sudo apt-get install -y graphviz + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + coverage: none + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update + if [[ "${{ matrix.laravel }}" == "10.*" ]]; then composer require "doctrine/dbal:^3.3" --no-interaction --no-update; fi + composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction + + - name: Execute tests + run: vendor/bin/phpunit \ No newline at end of file diff --git a/README.md b/README.md index 9af2151..f4c1ed1 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,52 @@ Or use one of the other [output formats](https://www.graphviz.org/doc/info/outpu php artisan generate:erd output.svg --format=svg ``` +### Text Output + +If you want to generate a text representation of the ER diagram instead of an image, you can use the `--text-output` option: + +```bash +php artisan generate:erd output.txt --text-output +``` + +This will generate a text file with the GraphViz DOT representation of the ER diagram. + +### Structured Text Output for AI Models + +If you want to generate a structured text representation of the ER diagram that is more suitable for AI models, simply specify a filename with a `.txt` extension: + +```bash +php artisan generate:erd output.txt +``` + +This will automatically generate a Markdown file with a structured representation of the entities and their relationships, which can be used as context for AI models. + +#### Output Format + +The structured output format looks like this: + +```markdown +# Entity Relationship Diagram + +## Entities + +### User (`App\Models\User`) + +#### Attributes: +- `id` (integer) +- `name` (string) +- `email` (string) +... + +## Relationships + +### User Relationships +- **HasMany** `posts` to Post (Local Key: `id`, Foreign Key: `user_id`) +... +``` + +This format is particularly useful when providing context to AI models about your database structure. + ## Customization Please take a look at the published `erd-generator.php` configuration file for all available customization options. @@ -121,4 +167,4 @@ If you discover any security related issues, please email marcel@beyondco.de ins ## License -The MIT License (MIT). Please see [License File](LICENSE.md) for more information. +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. \ No newline at end of file diff --git a/composer.json b/composer.json index 20a0557..77bb9e1 100644 --- a/composer.json +++ b/composer.json @@ -16,16 +16,16 @@ } ], "require": { - "php": "^7.1|^8.0", - "doctrine/dbal": "~2.3|^3.3|^4.0", + "php": "^8.2", + "doctrine/dbal": "^3.3|^4.0", "phpdocumentor/graphviz": "^1.0", - "nikic/php-parser": "^2.0|^3.0|^4.0|^5.0" + "nikic/php-parser": "^4.0|^5.0" }, "require-dev": { "larapack/dd": "^1.0", - "orchestra/testbench": "~3.5|~3.6|~3.7|~3.8|^4.0|^7.0|^8.0|^9.0", - "phpunit/phpunit": "^7.0| ^8.0|^9.5.10|^10.5|^11.0.1", - "spatie/phpunit-snapshot-assertions": "^1.3|^4.2|^5.1" + "orchestra/testbench": "^8.0|^9.0|^10.0", + "phpunit/phpunit": "^9.5.10|^10.5|^11.0", + "spatie/phpunit-snapshot-assertions": "^4.2|^5.1" }, "autoload": { "psr-4": { diff --git a/src/GenerateDiagramCommand.php b/src/GenerateDiagramCommand.php index e060970..54946a6 100644 --- a/src/GenerateDiagramCommand.php +++ b/src/GenerateDiagramCommand.php @@ -19,7 +19,7 @@ class GenerateDiagramCommand extends Command * * @var string */ - protected $signature = 'generate:erd {filename?} {--format=png}'; + protected $signature = 'generate:erd {filename?} {--format=png} {--text-output : Output as text file instead of image}'; /** * The console command description. @@ -72,15 +72,39 @@ public function handle() $graph = $this->graphBuilder->buildGraph($models); - if ($this->option('format') === self::FORMAT_TEXT) { - $this->info($graph->__toString()); + // First check for text-output option + if ($this->option('text-output') || $this->option('format') === self::FORMAT_TEXT) { + $textOutput = $graph->__toString(); + + // If text-output option is set, write to file + if ($this->option('text-output')) { + $outputFileName = $this->getTextOutputFileName(); + file_put_contents($outputFileName, $textOutput); + $this->info(PHP_EOL); + $this->info('Wrote text diagram to ' . $outputFileName); + return; + } + + // Otherwise just output to console + $this->info($textOutput); return; } - $graph->export($this->option('format'), $this->getOutputFileName()); + // Then check for .txt extension in filename + $outputFileName = $this->getOutputFileName(); + if (pathinfo($outputFileName, PATHINFO_EXTENSION) === 'txt') { + // Generate structured text output for .txt files + $textOutput = $this->graphBuilder->generateStructuredTextRepresentation($models); + file_put_contents($outputFileName, $textOutput); + $this->info(PHP_EOL); + $this->info('Wrote structured ER diagram to ' . $outputFileName); + return; + } + + $graph->export($this->option('format'), $outputFileName); $this->info(PHP_EOL); - $this->info('Wrote diagram to ' . $this->getOutputFileName()); + $this->info('Wrote diagram to ' . $outputFileName); } protected function getOutputFileName(): string @@ -89,6 +113,11 @@ protected function getOutputFileName(): string static::DEFAULT_FILENAME . '.' . $this->option('format'); } + protected function getTextOutputFileName(): string + { + return $this->argument('filename') ?: static::DEFAULT_FILENAME . '.txt'; + } + protected function getModelsThatShouldBeInspected(): Collection { $directories = config('erd-generator.directories'); diff --git a/src/GraphBuilder.php b/src/GraphBuilder.php index 765c184..0f4730d 100644 --- a/src/GraphBuilder.php +++ b/src/GraphBuilder.php @@ -3,6 +3,7 @@ namespace BeyondCode\ErdGenerator; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Support\Facades\Schema; use phpDocumentor\GraphViz\Graph; use Illuminate\Support\Collection; use phpDocumentor\GraphViz\Node; @@ -30,6 +31,75 @@ public function buildGraph(Collection $models) : Graph return $this->graph; } + /** + * Generate a structured text representation of the ER diagram + * + * @param Collection $models + * @return string + */ + public function generateStructuredTextRepresentation(Collection $models) : string + { + $output = "# Entity Relationship Diagram\n\n"; + + // First list all models/entities with their attributes + $output .= "## Entities\n\n"; + + foreach ($models as $model) { + /** @var Model $model */ + $eloquentModel = app($model->getModel()); + $output .= "### " . $model->getLabel() . " (`" . $model->getModel() . "`)\n\n"; + + // Add table columns if available + if (config('erd-generator.use_db_schema')) { + $columns = $this->getTableColumnsFromModel($eloquentModel); + if (count($columns) > 0) { + $output .= "#### Attributes:\n\n"; + foreach ($columns as $column) { + if (is_object($column)) { + $name = $column->getName(); + $typeName = $column->getType()->getName(); + } else { + $name = $column['name'] ?? ''; + $typeName = $column['type_name'] ?? ''; + } + $columnType = config('erd-generator.use_column_types') ? ' (' . $typeName . ')' : ''; + $output .= "- `" . $name . "`" . $columnType . "\n"; + } + $output .= "\n"; + } + } + } + + // Then list all relationships + $output .= "## Relationships\n\n"; + + foreach ($models as $model) { + /** @var Model $model */ + if (count($model->getRelations()) > 0) { + $output .= "### " . $model->getLabel() . " Relationships\n\n"; + + foreach ($model->getRelations() as $relation) { + /** @var ModelRelation $relation */ + // Find the related model by comparing model class names + $relatedModelClass = $relation->getModel(); + + $relatedModel = $models->first(function ($m) use ($relatedModelClass) { + return $m->getModel() === $relatedModelClass; + }); + if ($relatedModel) { + $output .= "- **" . $relation->getType() . "** `" . $relation->getName() . "` to " . + $relatedModel->getLabel() . " (Local Key: `" . $relation->getLocalKey() . + "`, Foreign Key: `" . $relation->getForeignKey() . "`)\n"; + } + } + + $output .= "\n"; + } + } + + return $output; + } + protected function getTableColumnsFromModel(EloquentModel $model) { try { @@ -49,6 +119,11 @@ protected function getTableColumnsFromModel(EloquentModel $model) } catch (\Throwable $e) { } + try { + return Schema::getColumns($model->getTable()); + } catch (\Throwable $e) { + } + return []; } @@ -61,11 +136,18 @@ protected function getModelLabel(EloquentModel $model, string $label) if (config('erd-generator.use_db_schema')) { $columns = $this->getTableColumnsFromModel($model); foreach ($columns as $column) { - $label = $column->getName(); + if (is_object($column)) { + $name = $column->getName(); + $typeName = $column->getType()->getName(); + } else { // it's an array! + $name = $column['name'] ?? ''; + $typeName = $column['type_name'] ?? ''; + } + $label = $name; if (config('erd-generator.use_column_types')) { - $label .= ' ('.$column->getType()->getName().')'; + $label .= ' ('. $typeName .')'; } - $table .= '