›
›
›
  1. docs
  2. ›
  3. byrcsc/laravel-cartographer
1.x
Browse documentationOpenClose

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • How a diagram is built
  • Model discovery
  • Relationship detection
  • Schema introspection
  • Scoping a diagram

Operations

  • Diagrams per subsystem
  • Exporting images
  • Themes and fonts
  • Renderer limits
  • Keeping the diagram current
  • Continuous integration

Reference

  • Configuration
  • Console commands
  • Diagram syntax
  • PHP API
  • Testing
  • Troubleshooting

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • How a diagram is built
  • Model discovery
  • Relationship detection
  • Schema introspection
  • Scoping a diagram

Operations

  • Diagrams per subsystem
  • Exporting images
  • Themes and fonts
  • Renderer limits
  • Keeping the diagram current
  • Continuous integration

Reference

  • Configuration
  • Console commands
  • Diagram syntax
  • PHP API
  • Testing
  • Troubleshooting

byrcsc/laravel-cartographer · 1.x

Relationship detection.

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.

What gets invoked

For each model, Cartographer collects methods that meet every condition:

  • public,
  • non-static,
  • declared on the model class itself,
  • and take no required parameters.

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.

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 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.

Recognised relation types

Eloquent methodType nameWhat is read from the relation
belongsTobelongs_toForeign key, owner key
hasOnehas_oneForeign key, local key
hasManyhas_manyForeign key, local key
belongsToManybelongs_to_manyPivot table, both pivot keys, parent key
morphTomorph_toForeign key, owner key, morph name
morphOnemorph_oneForeign key, local key, morph name
morphManymorph_manyForeign key, local key, morph name
morphToManymorph_to_manyPivot table, both pivot keys, parent key, morph name
morphedByManymorphed_by_manyPivot table, both pivot keys, parent key, morph name
hasOneThroughhas_one_throughForeign key, local key, intermediate model
hasManyThroughhas_many_throughForeign 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.

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 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.

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 appears in exclude_models.
  • The related model's table does not exist on the connection.
  • Its type is excluded by --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.

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_only and the exclusion keys.
PreviousModel discoveryNextSchema introspection
View source

On this page

  1. What gets invoked
  2. Building a relation runs no query
  3. Strict mode
  4. Recognised relation types
  5. Polymorphic relations need both sides
  6. Relations that produce no edge
  7. What to read next