›
byrcsc/laravel-cartographer · 1.x
Cartographer calls Eloquent relation methods to find each edge and its keys.
Each edge in the diagram comes from an Eloquent relation method. Cartographer calls the method and reads the returned relation object.
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}This method produces an edge from posts to users labelled author. The
BelongsTo object also reports author_id as the foreign key. Custom foreign
keys, pivot tables, and morph names therefore come from Eloquent instead of a
naming convention.
For each model, Cartographer collects methods that meet every condition:
Cartographer calls each method on a new model instance. If a method throws an
exception, Cartographer skips it and continues. It also discards any returned
value that is not a Relation.
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.
$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,
],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 relation methods still work. Turn it on when every relation method declares a return type. Cartographer will then skip unrelated methods instead of calling them.
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.
| 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 --exclude-relations option and relations.exclude setting use the type
names in this table. A custom Relation subclass uses its class basename as the
type name and 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.
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 relation produces no edge when the inverse relation is missing.
Without an inverse, the code does not identify which models can be targets.
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.
An edge needs both ends to be entities. A relation is dropped when:
paths or appears in
exclude_models.--exclude-relations or relations.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.
strict_types_only and the exclusion
keys.