›
byrcsc/laravel-cartographer · 1.x
Generate and check a named diagram for each application subsystem.
One diagram per repository stops working somewhere around forty tables, and past a certain size no hosted renderer will draw it at all. The fix is a full diagram as the reference, plus a small diagram for each subsystem your team actually talks about.
Groups are how you declare that set once, in config, instead of remembering a different set of flags for each one.
// config/cartographer.php
'groups' => [
'billing' => [App\Models\Invoice::class, App\Models\Payment::class],
'catalog' => [App\Models\Product::class, App\Models\Category::class],
],A bare run now writes the full diagram plus one file per group:
php artisan cartographer:erd INFO Generated 24 entities and 41 edges. Written to [/srv/app/docs/erd.md].
INFO Generated 5 entities and 6 edges for group [billing]. Written to [/srv/app/docs/erd/billing.md].
INFO Generated 4 entities and 4 edges for group [catalog]. Written to [/srv/app/docs/erd/catalog.md].Every group is regenerated on every run, which is the point. A set kept by hand drifts one file at a time; a set declared in config cannot.
By default, beside the main diagram in a directory named after it:
output | Group billing lands at |
|---|---|
docs/erd.md | docs/erd/billing.md |
docs/schema.md | docs/schema/billing.md |
docs/erd.mmd | docs/erd/billing.mmd |
docs/erd | docs/erd-billing.md |
The last row is the edge case: with no extension on the main output, its own name is already taken by a file, so the group cannot live in a directory of that name and gets a suffix instead.
The longer group form sets an explicit path:
'groups' => [
'billing' => [
'models' => [App\Models\Invoice::class, App\Models\Payment::class],
'output' => 'docs/finance/billing.md',
],
],The short form and the long form are the same thing. Use the short one until a group needs its own destination.
A relation whose far end is outside the group keeps its edge, and the model on the far side is drawn as an empty box:
erDiagram
invoices }o--|| users : "user"
invoices {
bigint id PK
bigint user_id FK
}
usersYou can see that an invoice belongs to a user without the billing diagram growing a copy of the user table and everything hanging off it. A stub entity carries a name and nothing else: no columns, and no edges of its own.
This is what makes a group readable. Without stubs, a group would either lie about its boundaries or drag in half the schema to be honest about them.
php artisan cartographer:erd --group=billingWrites that group's file and nothing else, at its configured path. Useful while you are settling on a group's model list, and for splitting a check across CI jobs.
A group is already a scope, so it cannot be narrowed further:
ERROR A group is already a scope, so [--group] cannot be combined with
[--only], [--except], [--models] or [--depth]. ERROR A group writes to its own configured path, so [--group] cannot be
combined with [--output].Both are rejections rather than silent precedence rules, because either combination has two plausible readings and guessing between them would write a file somewhere you did not ask for.
An unknown name lists what exists:
ERROR Unknown group [invoicing]. Configured groups: billing, catalog.php artisan cartographer:checkRegenerates everything the config declares, the full diagram and every group, and fails listing each file that has drifted. A group added to config but never generated counts as stale, not as an error, so the fix is always the same command.
ERROR Missing: [/srv/app/docs/erd/catalog.md].
ERROR 1 of 3 diagram(s) out of date. Regenerate with: php artisan cartographer:erd--group=billing narrows the check the same way it narrows generation. Use it
to split the work across CI jobs, never as the only check: a bare
cartographer:check is what proves every committed file is current. See
continuous integration.
For a diagram that is not worth committing, --only and --except scope a
single run without touching the group files:
php artisan cartographer:erd --only=Order,Invoice --stdout
php artisan cartographer:erd --except=Telemetry--only scopes the diagram the way a group does, stubs and all. --except
removes those models entirely, so no stub is left behind. Details in scoping a
diagram.
Any run that narrows the models, names an output, or prints to stdout produces that one diagram alone and leaves the configured group files untouched. That is what keeps an experiment from rewriting your committed set.
| Diagram | Contents | Reader |
|---|---|---|
| Full reference | Everything, --columns=all | Anyone tracing a column |
| Subsystem | A group of five to fifteen models | Someone changing that subsystem |
| Orientation | Everything, --columns=none | Someone new to the codebase |
Name groups after what people say in conversation, not after directories.
billing, catalog, and identity are groups; Models/Concerns is not.
The orientation diagram is the one people underrate. With --columns=none the
output is entity names and edges, which fits on a screen for applications where
the full diagram does not.
--format=mmd writes the raw Mermaid with no Markdown wrapper, which is what
you want when the diagram goes inside a page you already maintain:
php artisan cartographer:erd \
--only=Invoice,Payment --columns=keys \
--format=mmd \
--output=docs/diagrams/billing.mmdMost static site generators can include a file into a fenced block. If yours cannot, generate to stdout and assemble the page in a script:
{
cat docs/partials/billing-intro.md
echo '```mermaid'
php artisan cartographer:erd --only=Invoice --format=mmd --stdout
echo '```'
} > docs/billing.md--stdout suppresses warnings and the summary line, so nothing but the diagram
reaches the pipe.
php artisan cartographer:erd --group=billing --format=svgGroups and image export compose: the group's configured path has its extension replaced with the format, and the Mermaid source is written beside the image so the check still covers it. See exporting images.
Each run introspects exactly one connection. An application whose models are split across connections needs one run per connection, and the model list needs splitting to match:
php artisan cartographer:erd \
--connection=mysql \
--output=docs/erd-app.md
php artisan cartographer:erd \
--connection=reporting \
--only=Warehouse,FactOrder \
--output=docs/erd-reporting.mdWithout the scope, the second run would try every discovered model against the reporting connection and warn about each table that is not there.
There is no cross-connection diagram. An edge needs both tables on the connection being read, so a relation that spans connections produces no edge on either side. Groups do not change that: a group is a model list, not a connection.