Skip to content
Merged

5.0.0 #122

Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -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
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -121,4 +167,4 @@ If you discover any security related issues, please email [email protected] 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.
12 changes: 6 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
39 changes: 34 additions & 5 deletions src/GenerateDiagramCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Comment on lines +75 to +100
Copy link

Copilot AI Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The command handler has multiple return points and duplicated file-write logic for text and structured outputs. Consider refactoring into smaller methods (e.g., handleTextOutput & handleStructuredOutput) to improve readability and reduce duplication.

Suggested change
// 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);
// Handle text output if the option is set or format is 'text'
if ($this->option('text-output') || $this->option('format') === self::FORMAT_TEXT) {
$this->handleTextOutput($graph);
return;
}
// Handle structured output for .txt files
$outputFileName = $this->getOutputFileName();
if (pathinfo($outputFileName, PATHINFO_EXTENSION) === 'txt') {
$this->handleStructuredOutput($models, $outputFileName);

Copilot uses AI. Check for mistakes.

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
Expand All @@ -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');
Expand Down
81 changes: 78 additions & 3 deletions src/GraphBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -30,6 +31,68 @@ 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) {
$columnType = config('erd-generator.use_column_types') ? ' (' . $column->getType()->getName() . ')' : '';
$output .= "- `" . $column->getName() . "`" . $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 {
Expand All @@ -49,6 +112,11 @@ protected function getTableColumnsFromModel(EloquentModel $model)
} catch (\Throwable $e) {
}

try {
return Schema::getColumns($model->getTable());
} catch (\Throwable $e) {
}

return [];
}

Expand All @@ -61,11 +129,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'] ?? '';
}
Comment on lines +139 to +145
Copy link

Copilot AI Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Handling two different column representations (object vs array) here adds complexity. It might be cleaner to normalize the column data in getTableColumnsFromModel so downstream code only deals with a single, consistent structure.

Suggested change
if (is_object($column)) {
$name = $column->getName();
$typeName = $column->getType()->getName();
} else { // it's an array!
$name = $column['name'] ?? '';
$typeName = $column['type_name'] ?? '';
}
$name = $column['name'];
$typeName = $column['type_name'];

Copilot uses AI. Check for mistakes.

$label = $name;
if (config('erd-generator.use_column_types')) {
$label .= ' ('.$column->getType()->getName().')';
$label .= ' ('. $typeName .')';
}
$table .= '<tr width="100%"><td port="' . $column->getName() . '" align="left" width="100%" bgcolor="'.config('erd-generator.table.row_background_color').'"><font color="'.config('erd-generator.table.row_font_color').'" >' . $label . '</font></td></tr>' . PHP_EOL;
$table .= '<tr width="100%"><td port="' . $name . '" align="left" width="100%" bgcolor="'.config('erd-generator.table.row_background_color').'"><font color="'.config('erd-generator.table.row_font_color').'" >' . $label . '</font></td></tr>' . PHP_EOL;
}
}

Expand Down
Loading
Loading