byrcsc/laravel-cartographer · 1.x
How a diagram is built.
The stages between the Artisan command and the written file, what each one contributes, and why the output is byte-identical between runs.
cartographer:erd runs six stages in order. Each one has a class behind it,
and each one either produces the input for the next or fails the command.
| Stage | Class | Produces |
|---|---|---|
| Find the models | ModelDiscovery | A sorted list of model class names |
| Read the schema | SchemaReader | Tables, columns, indexes, foreign keys |
| Resolve the relations | RelationshipDetector | Relation metadata per model |
| Assemble the graph | GraphAssembler | Entities and edges, scoped |
| Render it | MermaidRenderer | The diagram text |
| Write it | AtomicFileWriter | The file on disk |
Each class is usable on its own. See PHP API if you want to drive the pipeline yourself.
1. Find the models
Every configured path is expanded as a glob and scanned recursively for .php
files. Class declarations are read out of the file's tokens, then each candidate
is checked: it has to exist, subclass Illuminate\Database\Eloquent\Model, and
not be abstract.
Nothing here touches the database. Details in model discovery.
2. Read the schema
Laravel's schema builder is asked for tables, columns, indexes, and foreign keys
on the target connection. Column types are normalized so that int8, bigint,
and INTEGER from three different drivers all arrive as bigint.
Failures at this stage — a wrong connection name, bad credentials, a database that is not running — end the command with a message naming the connection. Details in schema introspection.
3. Resolve the relations
For each discovered model that has a table, Cartographer instantiates the model
and invokes its public zero-argument methods. Anything that returns a
Relation is kept, and its keys are read from the relation object.
This is the only stage that runs your code. It is also the reason relation
metadata is exact rather than inferred: belongsTo(User::class, 'author_id')
reports author_id because the relation object says so. Details in
relationship detection.
4. Assemble the graph
The graph is built in four steps:
- Entities. Each model is instantiated to read its table name, and matched against the schema. A model whose table does not exist is skipped with a warning.
- Edges. Each relation becomes an edge from the model's table to the related model's table. Relations pointing at models that were not discovered are dropped, as are excluded relation types.
- Selection. With no seeds, every model table is selected. With seeds, a breadth-first walk follows edges outward until the hop limit is reached.
- Pivot tables. Any pivot table named by a selected many-to-many edge is added as an entity, if that table exists in the schema.
Two consequences follow from step 2. A relation to a model outside your
configured paths produces no edge, because the target has no entity. And a
morphTo produces an edge only where another discovered model declares the
matching morphOne or morphMany back to it — the inverse side is what names
the possible targets.
Details in scoping a diagram.
5. Render and write
The renderer emits edges first, a blank line, then entity blocks. Table and column names that are not valid Mermaid identifiers are rewritten, and collisions are resolved with a numeric suffix. Details in diagram syntax.
The file is then written through a temporary file in the destination directory
and moved into place with rename(). A run that dies partway through leaves the
previous diagram intact rather than a half-written one. --stdout skips this
stage entirely.
Why the output is stable
Every collection is sorted before it reaches the renderer:
- Models are sorted by class name; discovered files are sorted by real path, so two paths matching the same directory produce one entry.
- Tables, columns, indexes, and foreign keys are sorted by name.
- Relations are sorted by method name.
- Entities are sorted by table name, and edges by source, target, type, and name.
Filesystem order, hash order, and the order the database happens to return rows in are all removed. Regenerating without changing the application produces a byte-identical file, which is what makes the diagram worth committing and worth checking in CI.
What never happens
- No rows are read. The package issues schema queries only. The test suite asserts that no query in a full run selects from a fixture table.
- No relation query runs. Building a relation object does not execute it,
and Cartographer never calls
get()orfirst()on one. - Nothing is written to the database.
- No file outside the output path is touched, apart from the temporary file next to it during the atomic write.
What to read next
- Model discovery for paths, globs, and what counts as a model.
- Relationship detection for what gets invoked and what that costs.
- Schema introspection for type normalization and key detection.