byrcsc/laravel-cartographer · 1.x
Relationship detection.
How relations are resolved by invoking model methods, which relation types are recognised, what strict mode changes, and what to do about methods with side effects.
Cartographer does not guess relations from naming conventions or parse them out of source. It builds the relation object and asks it for its keys.
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}That produces an edge from posts to users labelled author, with
author_id as the foreign key, because the BelongsTo object reports exactly
that. A custom foreign key, a custom pivot table, a non-standard morph name —
all of them come out right, because Eloquent is the one answering.
What gets invoked
For each model, Cartographer collects the methods that are all of:
- public,
- non-static,
- declared on the model class itself,
- and take no required parameters.
Each one is called on a fresh instance inside a try. A method that throws is
skipped and the rest continue. A return value that is not a Relation is
discarded.
Declared on the class itself is the constraint worth remembering. Methods
inherited from a parent class are not inspected, so a relation defined on an
abstract BaseModel and shared by its subclasses produces no edges. Relations
brought in through a trait are inspected, because PHP reports the composing
class as the declaring class.
Building a relation runs no query
$this->belongsTo(User::class) constructs a query builder; it does not execute
one. The package never calls get(), first(), or count() on a relation, and
the test suite asserts that detecting relations across the fixture models
produces an empty query log.
What does happen is that your method bodies run. A relation method that only returns a relation is harmless. A public zero-argument method that writes a file, hits an API, or mutates static state will do that during generation.
Two ways out:
// config/cartographer.php
// Leave the model out entirely.
'exclude_models' => [
App\Models\Telemetry::class,
],
// Or only invoke methods that declare a Relation return type.
'relations' => [
'strict_types_only' => true,
],Strict mode
With strict_types_only enabled, a method is only invoked when its declared
return type is a Relation subclass. Union types qualify when every member
does, which covers a method that returns BelongsTo|HasOne.
// Invoked in strict mode.
public function author(): BelongsTo { ... }
public function alternate(): BelongsTo|HasOne { ... }
// Not invoked in strict mode.
public function untypedAuthor() { ... }
public function flush(): void { ... }Strict mode is off by default so untyped codebases work without changes. Turn it on when your relation methods are typed — it removes the side-effect problem entirely, and it makes generation cheaper by skipping every other method on the model.
The trade-off is silent omission: an untyped relation method disappears from the diagram with no warning. If you enable strict mode, type every relation method.
Recognised relation types
| Eloquent method | Type name | What is read from the relation |
|---|---|---|
belongsTo | belongs_to | Foreign key, owner key |
hasOne | has_one | Foreign key, local key |
hasMany | has_many | Foreign key, local key |
belongsToMany | belongs_to_many | Pivot table, both pivot keys, parent key |
morphTo | morph_to | Foreign key, owner key, morph name |
morphOne | morph_one | Foreign key, local key, morph name |
morphMany | morph_many | Foreign key, local key, morph name |
morphToMany | morph_to_many | Pivot table, both pivot keys, parent key, morph name |
morphedByMany | morphed_by_many | Pivot table, both pivot keys, parent key, morph name |
hasOneThrough | has_one_through | Foreign key, local key, intermediate model |
hasManyThrough | has_many_through | Foreign key, local key, intermediate model |
The type name is what --exclude-relations and relations.exclude match
against. Any other Relation subclass — a custom relation of your own — is
recorded under its class basename and drawn with the default one-to-many
cardinality.
The morph name is derived from the morph type column: commentable_type
becomes commentable. It is what appears in the edge label.
Polymorphic relations need both sides
A morphTo has no single related model, so there is nothing to point an edge
at. Cartographer resolves the targets by looking for the inverse: every
discovered model that declares a morphOne or morphMany back to this model
with the same morph name becomes an edge target.
// Attachment
public function attachable(): MorphTo { return $this->morphTo(); }
// Post
public function attachments(): MorphMany { return $this->morphMany(Attachment::class, 'attachable'); }
// Comment
public function attachments(): MorphMany { return $this->morphMany(Attachment::class, 'attachable'); }attachments }o--|| comments : "attachable (morph)"
attachments }o--|| posts : "attachable (morph)"
comments ||--o{ attachments : "attachable (morph)"
posts ||--o{ attachments : "attachable (morph)"A morphTo whose inverse is never declared produces no edges at all. That is
not a bug report waiting to happen — there is genuinely nothing in the codebase
saying which models the relation can point at.
One subtlety follows from this. Excluding morph_many removes the
morphMany edges but not the morph_to edges, because target resolution
reads the inverse relations before exclusions are applied. Exclude morph to
remove the whole family.
Relations that produce no edge
An edge needs both ends to be entities. A relation is dropped when:
- The related model was not discovered — it lives outside
paths, or it is inexclude_models. - The related model's table does not exist on the connection.
- Its type is excluded by
--exclude-relationsorrelations.exclude.
None of these warn. A missing edge between two models that are both in the diagram usually means the relation method threw; a missing edge to a model that is not in the diagram means the model was never discovered.
What to read next
- Scoping a diagram for excluding relation types and seeding from models.
- Diagram syntax for how each type maps to a Mermaid cardinality.
- Configuration for
strict_types_onlyand the exclusion keys.