beyondcode/laravel-er-diagram-generator
Generate entity-relationship diagrams directly from your Laravel models. This package inspects your Eloquent relationships and produces a visual ER diagram with Graphviz, making database structure easy to understand and share with your team.
1. Install the package (dev-only) and Graphviz.
composer require --dev beyondcode/laravel-er-diagram-generator
# Graphviz is an external binary, not a Composer dependency
brew install graphviz # macOS
sudo apt-get install graphviz # Debian/Ubuntu
dot -V # verify it's on PATH before anything else
2. Publish the config and read it end to end.
php artisan vendor:publish --provider="BeyondCode\ErdGenerator\ErdGeneratorServiceProvider"
Everything you'll tune day-to-day lives in config/erd-generator.php: which paths are scanned for models, which models are excluded, where output lands, and (in 6.x) ignore_columns for suppressing noisy columns. If you're upgrading from 5.x, the published file will not contain the new keys — re-publish or hand-merge.
3. Run it once, unscoped, as a baseline.
php artisan erd:generate
Expect storage/erd/erd.png (or whatever output.path / format you configure). Open it, and immediately look for the things you don't recognize: models you forgot existed, edges that go nowhere, and dense clusters that will be unreadable later. That first render is your survey.
4. Do your first real run scoped to one domain.
A whole-app diagram on a 60-model codebase is a wall of boxes. Point generate.paths at a single subdirectory and add your baseline exclusions first:
// config/erd-generator.php
'generate' => [
'paths' => [
app_path('Models/Billing'),
],
'exclude' => [
App\Models\Billing\InvoiceLineSnapshot::class, // internal, not domain-meaningful
App\Models\Concerns\BaseModel::class, // abstract, will break discovery
],
'ignore_columns' => [
'id', 'created_at', 'updated_at', 'deleted_at', 'remember_token',
],
],
'output' => [
'path' => storage_path('erd'),
],
Set the ignore list before you show the diagram to anyone. Audit timestamps and surrogate keys add visual noise and hide the relationships, which are the point.
Keep a small script that re-runs generation per domain by mutating config in-process. This is the single highest-leverage pattern — scoped diagrams are readable, global ones aren't.
// scripts/erd.php — run with: php artisan tinker scripts/erd.php (or as a command)
use Illuminate\Support\Facades\{Artisan, File};
$domains = [
'billing' => app_path('Models/Billing'),
'identity' => app_path('Models/Identity'),
'catalog' => app_path('Models/Catalog'),
];
foreach ($domains as $name => $path) {
$out = storage_path("erd/{$name}");
File::ensureDirectoryExists($out);
config()->set('erd-generator.generate.paths', [$path]);
config()->set('erd-generator.output.path', $out);
Artisan::call('erd:generate');
$this->info("Generated {$name}");
}
The day-to-day workflow this package actually earns its keep on: before you answer "how big is this refactor?", scope the diagram to the models around the table in question and look at inbound edges. A users or tenants table with twenty edges is a different estimate than one with three. This is a ten-minute answer to a question that otherwise costs a day of grepping for belongsTo.
Generate on the branch, commit the .svg under docs/erd/, and reference it in the PR description.
php artisan erd:generate && git add docs/erd/*.svg
Prefer SVG over PNG as the committed format: it's text, so reviewers can actually read a git diff to see which nodes/edges appeared or vanished, and it scales for documentation sites. PNG diffs are opaque binary changes — you get "file changed" and nothing else.
- name: Install Graphviz
run: sudo apt-get update && sudo apt-get install -y graphviz
- name: Prepare schema
run: |
php artisan migrate --force --database=sqlite
- name: Generate ERD
run: php artisan erd:generate
- uses: actions/upload-artifact@v4
with:
name: erd
path: storage/erd
Two things matter here: Graphviz pinned in the image (layout differs across versions — a floating latest will produce spurious diffs), and a real, migrated database available to the job, because 6.x reads column names and types from the live schema via getColumns(). A SQLite file is the cheapest way to satisfy that.
App\Models\CustAcct in a planning review is friction every single time. Alias the handful of core models to business vocabulary — "Customer", "Subscription", "Invoice" — and keep PHP class names for the long tail nobody outside engineering looks at. This is the difference between a diagram that gets used in roadmap conversations and one that lives in a docs/ folder nobody opens.
Exclude: abstract/base model classes, trait-carrying pseudo-models, framework/vendor models, snapshot/log models that exist only for internal plumbing, and pivot models that duplicate what belongsToMany already draws. Keep the list in the config, not scattered in the script, so it's reviewable in a PR.
The diagram reflects models, not the database. Raw SQL tables, legacy tables with no Eloquent class, and migrations that were never modelled are invisible. Never treat the output as the source of truth for schema — it's a navigation aid. If you need schema truth, read migrations or schema:dump.
6.x needs a reachable, migrated database. Because column data now comes from Laravel's getColumns(), running the generator against an empty or unmigrated database fails or produces bare tables. Onboarding tickets about "the diagram has no columns" are almost always "ran it before php artisan migrate."
Check $connection on every model. Models pinned to a non-default connection (protected $connection = 'analytics';) will be introspected against that connection. Generating against a testing SQLite while some models point elsewhere yields tables with missing or wrong columns.
Polymorphic relations are under-represented. morphTo cannot be statically resolved, so the target side of a morph relation typically degrades to a dangling edge or disappears. If your design leans on morphs, annotate the diagram manually — don't let stakeholders conclude the relationship doesn't exist.
Multiple relations to the same model collapse visually. Two belongsTo(User::class) for sender and receiver often render as one edge with no way to tell which is which. Same for self-referencing hierarchies. This is where the diagram quietly lies; keep a written note alongside it.
Missing Graphviz fails with an opaque error. Not a PHP exception with a helpful message — usually a shell-level failure. First diagnostic step for any "it doesn't work" report is dot -V.
config:cache will bite you in CI. Cached config from a prior step can mask config changes; run php artisan config:clear before generating, or generate outside the cached-config window entirely.
Large graphs degrade fast. Past ~150 nodes the single Graphviz layout becomes unreadable and slow, and per-table column introspection means one database round-trip per table. Scope by domain, exclude aggressively, and accept multiple targeted runs rather than chasing one giant diagram.
Upgrading 5.x → 6.x: gate on a .dot/output diff. Branch-bump, generate against the same database on both versions, and diff the outputs. Classify each diff as cosmetic (layout/type strings from getColumns()), expected (new nodes from aliasing/column changes), or suspicious (missing edges, dropped tables). Verify PHP and Laravel constraints in composer.json don't drop a line you're still on — the changelog is additive-sounding but it's a major bump.
Troubleshooting discovery: grep the output. If you're unsure whether a model was picked up, search the generated SVG for the node label. Faster than adding dd() calls or reading the generator source.
Keep it out of production. --dev plus composer install --no-dev in the deploy pipeline; there's no reason for the package or Graphviz to reach a runtime image.
Extension points are thin. There's no plugin API for custom relation rendering — your levers are config (paths, excludes, ignore_columns, aliases) and running the command programmatically with mutated config. That covers segmentation; anything deeper (custom node shapes, per-domain legends) belongs in post-processing the SVG or in a different tool.
How can I help you explore Laravel packages today?