byrcsc/laravel-cartographer · 1.x
Diagram syntax.
The exact text the renderer emits — file structure, the cardinality for each relation type, edge labels, column lines and key markers, and how unsafe names are rewritten.
Everything the renderer produces is described here. Read it when you are parsing the output, reviewing a diff, or wondering why a name came out differently from the table it represents.
File structure
--format=markdown wraps the diagram so it renders on GitHub and GitLab:
<!-- Generated by byrcsc/laravel-cartographer. -->
```mermaid
erDiagram
...
```--format=mmd emits the diagram alone, starting at erDiagram — the input
mmdc expects.
Inside the diagram, the order is fixed:
erDiagram- One line per edge, indented four spaces
- One blank line, if there is at least one edge and one entity
- One block per entity, indented four spaces
The file ends with exactly one newline. There is no date, no version stamp, and no host name, so regenerating without changes produces a byte-identical file.
Edge lines
posts }o--|| users : "author"The format is source cardinality target : "label", where source and target are
table identifiers.
Edges are sorted by source table, then target table, then relation type, then relation name.
Cardinality by relation type
| Relation type | Cardinality | Reads as |
|---|---|---|
belongs_to, morph_to | }o--|| | Many to exactly one |
has_one, morph_one, has_one_through | ||--o| | One to zero-or-one |
has_many, morph_many, has_many_through | ||--o{ | One to zero-or-many |
belongs_to_many, morph_to_many, morphed_by_many | }o--o{ | Many to many |
Any other Relation subclass | ||--o{ | One to zero-or-many |
The cardinality describes what Eloquent says about the relation, not what the
database enforces. A belongsTo on a nullable foreign key still renders as
many-to-exactly-one; nullability is on the column line instead.
Both directions appear when both models declare the relation. posts ||--o{ comments and comments }o--|| posts are the same foreign key from each side,
and they are not collapsed — a missing inverse is usually worth seeing.
Labels
The label is the relation method name, with a suffix for two families:
| Relation type | Label |
|---|---|
| Anything ordinary | "author" — the method name |
Any morph* type | "attachable (morph)" — the morph name |
Any *_through type | "postComments (through)" — the method name |
Polymorphic edges are labelled with the morph name rather than the method name,
because the morph name is what both sides share. attachable appears on the
morphTo and on every morphMany pointing back at it, which is what makes the
pair readable.
Labels are escaped: carriage returns and newlines become spaces, and " becomes
". A quote in a method name cannot break the diagram.
Entity blocks
posts {
bigint author_id FK
text body "nullable"
bigint category_id FK "nullable"
timestamp created_at "nullable"
bigint id PK
timestamp published_at "nullable"
varchar title
timestamp updated_at "nullable"
}Entities are sorted by table name. Columns are sorted by name, not by their position in the table.
An entity with no columns to show — --columns=none, or every column excluded —
is emitted as a bare name:
posts
tags
usersColumn lines
The format is type name markers "nullable", with the last two parts appearing
only when they apply.
| Part | Source |
|---|---|
| Type | The normalized column type; see schema introspection |
| Name | The column name, rewritten if it is not a valid identifier |
| Markers | PK, FK, UK, comma-separated in that order |
"nullable" | Present on nullable columns in all mode only |
bigint user_id FK, UK
bigint category_id FK "nullable"PK suppresses UK on the same column. Composite keys mark every column they
contain.
Names that are rewritten
Mermaid identifiers are narrower than SQL identifiers, so some names cannot be used as written.
Table names
A table name is used as its own identifier when it matches
[A-Za-z_][A-Za-z0-9_]* and is not one of Mermaid's reserved words —
direction, end, erdiagram, in any casing.
Otherwise the identifier becomes entity_ plus the first twelve characters of
the name's SHA-1, and the real name is carried as a display alias:
entity_69a135d7d611["order items"] ||--o{ entity_7a92f3d26362["end"] : "items"The alias is what a Mermaid renderer shows, so the diagram still reads correctly. The hash is derived from the name alone, so the same table produces the same identifier on every run.
If a generated identifier collides with a table that is genuinely called
entity_69a135d7d611, a numeric suffix is appended: entity_69a135d7d611_2.
Real table names always win the unsuffixed form.
Column names and types
Column names and type names keep only A-Z, a-z, 0-9, _, (, ), [,
], and -. Every other character becomes an underscore.
A result that does not start with a letter or * is prefixed: column_ for
column names, type_ for types. So 2fa_secret renders as column_2fa_secret,
and an unrecognised type spelled !custom renders as type__custom.
Column identifiers that collide after rewriting are given a numeric suffix
within their entity, so select value and select@value render as
select_value and select_value_2.
Rewriting affects identifiers only. Table display aliases keep the real name, and nothing is renamed in your database.
Entities with no model
Pivot tables are rendered like any other entity, with their columns and key markers:
post_tag {
bigint id PK
bigint post_id FK, UK
bigint tag_id FK, UK
}They carry no edges of their own — the many-to-many edge is drawn directly
between the two models — and they have no model class, so columns.exclude
cannot filter them.
What to read next
- Schema introspection for where types and markers come from.
- Relationship detection for where edges and labels come from.
- PHP API to render a graph yourself.