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

PHP API.

Call the classes behind cartographer:erd from application code.

Every stage of the command is a class you can use directly. Reach for the API when one Artisan invocation is not the right shape: several diagrams from one schema read, a graph inspected before it is rendered, or a diagram assembled inside a command of your own.

All classes live in the Byrcsc\Cartographer namespace.

The pipeline in one script

use Byrcsc\Cartographer\AtomicFileWriter;
use Byrcsc\Cartographer\GraphAssembler;
use Byrcsc\Cartographer\MermaidRenderer;
use Byrcsc\Cartographer\ModelDiscovery;
use Byrcsc\Cartographer\RelationshipDetector;
use Byrcsc\Cartographer\SchemaReader;

$models = (new ModelDiscovery([app_path('Models')]))->discover();
$schema = app(SchemaReader::class)->read();

$assembler = new GraphAssembler(new RelationshipDetector);
$renderer = new MermaidRenderer;
$writer = app(AtomicFileWriter::class);

$scopes = [
    'docs/erd.md' => [[], null],
    'docs/erd-publishing.md' => [['Post'], 1],
];

foreach ($scopes as $path => [$seeds, $depth]) {
    $graph = $assembler->assemble($models, $schema, $seeds, $depth);

    $writer->write(base_path($path), $renderer->render($graph, 'keys'));
}

The schema is read once and reused. That is the main reason to drop to the API: cartographer:erd reads the schema on every invocation, which is fine for a handful of diagrams and wasteful for twenty.

The whole pipeline in one call

DiagramGenerator is what both commands run. Hand it a GenerationSettings and it discovers models, reads the schema once, and returns every diagram the settings declare:

use Byrcsc\Cartographer\DiagramGenerator;
use Byrcsc\Cartographer\GenerationSettings;

$settings = GenerationSettings::fromArray(config('cartographer'));

foreach (app(DiagramGenerator::class)->generate($settings, base_path()) as $diagram) {
    echo "{$diagram->path}: {$diagram->entityCount} entities, {$diagram->edgeCount} edges\n";
}

Use this when you want the command's behaviour with a different destination for the output. Use the stage classes above when you want a different pipeline.

Classes

DiagramGenerator

public function __construct(SchemaReader $schemaReader, DiagramExporter $exporter);

/** @return non-empty-list<GeneratedDiagram> */
public function generate(
    GenerationSettings $settings,
    string $basePath,
    ?Closure $warn = null,
    bool $render = true,
): array;

Returns one GeneratedDiagram per target, in target order: the main diagram first, then each configured group. Models are discovered and the schema is read once however many diagrams come out of them.

$warn receives one message per skipped model, unmatched discovery path, and crossed renderer limit. Model-level warnings are reported against the first target only, because they describe the models rather than any one diagram.

$render set to false produces the Mermaid source and the paths without rendering an artifact. That is what cartographer:check uses, and it is why checking an exported diagram needs no mermaid-cli.

$diagrams = app(DiagramGenerator::class)->generate(
    $settings,
    base_path(),
    fn (string $message) => logger()->warning($message),
    render: false,
);

GenerationSettings

public static function fromArray(array $config): self;

public function with(GenerationOverrides $overrides): self;

/** @return non-empty-list<DiagramTarget> */
public function targets(): array;

public function groupOutput(DiagramGroup $group): string;
public function mermaidConfig(): array;
public function themePreset(): ?ThemePreset;
public function rendersImages(): bool;
public function sourcePathFor(string $output): ?string;

public static function resolvePath(string $output, string $basePath): string;

Everything a run needs, already validated. fromArray reads the package config shape and throws InvalidArgumentException with the messages listed in configuration. with layers command line overrides on top, where a null on the override leaves the configured value alone.

targets() is where a group becomes a file. It throws when --group is combined with a scope option or --output, and when the named group is not configured.

use Byrcsc\Cartographer\GenerationOverrides;

$settings = GenerationSettings::fromArray(config('cartographer'))
    ->with(new GenerationOverrides(format: 'svg', theme: 'dracula'));

Constants worth naming: FORMATS, IMAGE_FORMATS, COLUMN_MODES, DEFAULT_OUTPUT, DEFAULT_FORMAT, and DEFAULT_COLUMN_MODE.

ModelDiscovery

public function __construct(
    array $paths,
    array $excludeModels = [],
    ?Closure $warn = null,
);

public function discover(): array;

$paths are glob patterns matched against directories. $excludeModels are model class names. $warn receives one string message per pattern that matches no directory; pass null to discard warnings.

discover() returns a list of model class names, sorted, with duplicates removed.

$models = (new ModelDiscovery(
    [app_path('Models'), base_path('src/Domain/*/Models')],
    [App\Models\Telemetry::class],
    fn (string $message) => logger()->warning($message),
))->discover();

SchemaReader

public function __construct(
    DatabaseManager $database,
    ?string $configuredConnection = null,
    SchemaTypeNormalizer $typeNormalizer = new SchemaTypeNormalizer,
);

public function read(?string $connection = null): DatabaseSchema;

public function normalizeType(string $type): string;

The container binds this as a singleton preconfigured with cartographer.connection, so app(SchemaReader::class) is the usual way in. read() takes an explicit connection that overrides both.

read() throws SchemaReadException when the connection cannot be opened or introspected, with the driver exception attached as the previous exception.

normalizeType() exposes the type mapping on its own, for a column you looked up elsewhere. It applies the non-key branch of the integer rule.

RelationshipDetector

public function __construct(bool $strictTypesOnly = false);

public function detect(string $modelClass): array;

Returns a list of Relationship objects for one model, sorted by method name. Methods that throw are skipped.

foreach ((new RelationshipDetector)->detect(App\Models\Post::class) as $relation) {
    printf(
        "%s: %s -> %s (fk=%s pivot=%s morph=%s)\n",
        $relation->name,
        $relation->type,
        $relation->relatedModel ?? 'null',
        $relation->foreignKey ?? '-',
        $relation->pivotTable ?? '-',
        $relation->morphName ?? '-',
    );
}
attachments: morph_many -> App\Models\Attachment (fk=attachable_id pivot=- morph=attachable)
author: belongs_to -> App\Models\User (fk=author_id pivot=- morph=-)
category: belongs_to -> App\Models\Category (fk=category_id pivot=- morph=-)
comments: has_many -> App\Models\Comment (fk=post_id pivot=- morph=-)
tags: belongs_to_many -> App\Models\Tag (fk=- pivot=post_tag morph=-)

This is the class to use when you want relation metadata for something other than a diagram, a documentation generator, a lint rule, a dependency report.

GraphAssembler

public function __construct(
    RelationshipDetector $detector,
    ?Closure $warn = null,
);

public function assemble(
    array $models,
    DatabaseSchema $schema,
    array $seeds = [],
    ?int $depth = null,
    array $excludeRelations = [],
    ?array $scope = null,
): Graph;

$seeds are short or fully qualified model class names. $depth is a hop limit, or null for unlimited. $excludeRelations are type names and the through and morph group aliases.

$scope is what a group and --only are built on. A non-null list limits the diagram to those models, and a relation leaving the scope keeps its edge with the foreign entity emitted as a stub: a GraphEntity with isStub true, no columns, and no edges of its own. null means the whole application.

$graph = $assembler->assemble(
    $models,
    $schema,
    scope: ['App\Models\Invoice', 'App\Models\Payment'],
);

Throws GraphAssemblyException for a negative depth, an unknown seed, an ambiguous seed, and a seed whose table does not exist. $warn receives one message per skipped model.

MermaidRenderer

public function __construct(array $columnExcludes = []);

public function render(
    Graph $graph,
    string $columns = 'all',
    string $format = 'markdown',
): string;

public function diagram(Graph $graph, string $columns): string;

public function wrap(string $diagram, string $format): string;

$columnExcludes maps model class names to lists of column names, matching cartographer.columns.exclude. $columns is all, keys, or none; $format is markdown or mmd. Either one outside its set throws InvalidArgumentException.

$diagram = (new MermaidRenderer([
    App\Models\User::class => ['remember_token'],
]))->render($graph, 'keys', 'mmd');

render() is diagram() followed by wrap(). The two are separate because an image export needs the bare Mermaid source to hand the renderer and the wrapper only applies to text output.

DiagramExporter and MermaidCliExporter

interface DiagramExporter
{
    public function export(string $mermaid, ExportRequest $request): string;
}

MermaidCliExporter is the bundled implementation, and it is what the container resolves DiagramExporter to. Swap it in a service provider to render through something else:

$this->app->bind(DiagramExporter::class, MyExporter::class);
final class MermaidCliExporter implements DiagramExporter
{
    public const int EXPORT_MAX_TEXT_SIZE = 100_000_000;
    public const int EXPORT_MAX_EDGES = 1_000_000;
    public const float TIMEOUT_SECONDS = 300.0;

    public function __construct(MermaidCliLocator $locator);
}

It writes the source and a config file to a temporary directory, runs mermaid-cli, and returns the rendered bytes. The two size constants are why an export is not subject to the limits hosted renderers enforce. Every failure is a GenerationException: a missing binary, a non-zero exit, a timeout, or a successful run that wrote nothing.

MermaidCliLocator

public const string INSTALL_HINT;

public function locate(?string $configured, string $basePath): string;

Returns a path to a mermaid-cli binary, searching the configured path, then node_modules/.bin/mmdc under $basePath, then PATH. Recognises mmdc, mmdc.cmd, and mmdc.exe. Throws GenerationException when a configured path is not executable, and when nothing is found anywhere.

ThemeResolver and ThemePreset

final class ThemeResolver
{
    public const string DEFAULT_THEME = 'light';
    public const string DEFAULT_FONT = 'mono';

    /** @var array<string, string> */
    public const array FONTS;

    /** @var array<string, array{font: string, themeVariables: array<string, string>}> */
    public const array BUILT_IN;

    /** @param array<string, ThemePreset> $presets */
    public function resolve(array $presets, string $theme, ?string $font = null): ThemePreset;
}
final readonly class ThemePreset
{
    public function __construct(
        public string $name,
        public array $themeVariables,
        public string $font = ThemeResolver::DEFAULT_FONT,
    );

    public function mermaidConfig(): array;
    public function backgroundColor(): ?string;
    public function withFont(string $font): self;
}

Pure transformation: no filesystem and no renderer, so a theme is resolvable and testable without mermaid-cli installed. resolve() prefers a user preset over a built-in of the same name and throws InvalidArgumentException listing what is available when a name matches neither.

mermaidConfig() returns ['theme' => 'base', 'themeVariables' => [...]] with the font stack merged in as fontFamily. backgroundColor() is the canvas a PNG rasterizes against: background, then mainBkg, then null.

RendererLimits

public const int DEFAULT_MAX_TEXT_SIZE = 50000;
public const int DEFAULT_MAX_EDGES = 500;

public function __construct(
    ?int $maxTextSize = self::DEFAULT_MAX_TEXT_SIZE,
    ?int $maxEdges = self::DEFAULT_MAX_EDGES,
);

/** @return list<string> */
public function warnings(string $diagram, int $edgeCount): array;

warnings() takes the raw Mermaid source, without any Markdown wrapper, and returns a message per limit crossed. An empty list means everything is within its limits. null on either constructor argument switches that check off; a non-positive integer throws InvalidArgumentException.

Nothing here modifies the diagram. See renderer limits.

AtomicFileWriter

public function __construct(Filesystem $files);

public function write(string $path, string $contents): void;

Creates missing directories, writes to a temporary file in the destination directory, and moves it into place. On any failure it removes the temporary file and throws RuntimeException with the message Unable to write ERD to [path].

Paths are used exactly as given; there is no project-root resolution, which the command does before calling this.

Data objects

Every one is final readonly with public properties.

Graph

public array $entities;   // list<GraphEntity>
public array $edges;      // list<GraphEdge>

public function entity(string $name): ?GraphEntity;

GraphEntity

public string $name;         // table name
public ?string $modelClass;  // null for pivot tables
public TableSchema $schema;
public bool $isStub;         // true for a model outside the scope

A stub is how a scoped diagram stays honest about its boundaries. It carries a name and nothing else, so an edge leaving a group or an --only set still points somewhere without dragging the far side of the schema in.

GeneratedDiagram

public string $path;         // where the artifact goes
public string $mermaid;      // the raw Mermaid source
public ?string $contents;    // the rendered artifact, null when render: false
public string $comparison;   // what cartographer:check compares
public int $entityCount;
public int $edgeCount;
public array $warnings;      // list<string>
public ?string $group;       // null for the main diagram
public ?string $sourcePath;  // the .mmd beside an exported image

public function artifact(): string;
public function comparedPath(): string;

One result from DiagramGenerator::generate(). artifact() returns $contents and throws GenerationException when the run did not render one. comparedPath() is $sourcePath when there is one and $path otherwise, which is how an image is checked through its source rather than its bytes.

DiagramGroup and DiagramTarget

final readonly class DiagramGroup
{
    public string $name;
    public array $models;    // list<class-string<Model>>
    public ?string $output;  // null lands it beside the main diagram
}

final readonly class DiagramTarget
{
    public string $output;
    public ?array $scope;   // list<string>|null, null is the whole application
    public ?string $group;  // null for the main diagram
}

A DiagramGroup is a validated config entry. A DiagramTarget is one file a run will produce, and GenerationSettings::targets() turns the groups into them.

ExportRequest

public string $format;             // svg or png
public string $basePath;
public ?string $binary;            // cartographer.export.mermaid_cli
public array $mermaidConfig;       // from ThemePreset::mermaidConfig()
public ?string $backgroundColor;   // from ThemePreset::backgroundColor()

Everything DiagramExporter::export() needs beyond the Mermaid source itself.

GraphEdge

public string $source;      // source table name
public string $target;      // target table name
public string $type;        // belongs_to, morph_many, ...
public string $name;        // relation method name
public ?string $morphName;
public ?string $pivotTable;

Relationship

public string $name;
public string $type;
public ?string $relatedModel;      // null for morph_to
public ?string $foreignKey;
public ?string $ownerKey;
public ?string $pivotTable;
public ?string $pivotForeignKey;
public ?string $pivotRelatedKey;
public ?string $morphName;
public ?string $intermediateModel; // through relations only

Which properties are populated depends on the type; see the table in relationship detection.

DatabaseSchema and TableSchema

final readonly class DatabaseSchema
{
    public array $tables;  // list<TableSchema>

    public function table(string $name): ?TableSchema;
}

final readonly class TableSchema
{
    public string $name;
    public array $columns;      // list<SchemaColumn>
    public array $indexes;      // list<SchemaIndex>
    public array $foreignKeys;  // list<SchemaForeignKey>

    public function column(string $name): ?SchemaColumn;
    public function hasPrimaryColumn(string $name): bool;
    public function hasUniqueColumn(string $name): bool;
    public function foreignKeyFor(string $column): ?SchemaForeignKey;
}

$indexes holds primary and unique indexes only. Plain indexes are read during introspection, they affect type normalization, but they are not kept, because nothing downstream renders them.

SchemaColumn, SchemaIndex, SchemaForeignKey

final readonly class SchemaColumn
{
    public string $name;
    public string $type;      // normalized
    public bool $nullable;
}

final readonly class SchemaIndex
{
    public string $name;
    public array $columns;    // list<string>
    public bool $unique;
    public bool $primary;
}

final readonly class SchemaForeignKey
{
    public string $name;
    public array $columns;             // list<string>
    public string $referencedTable;
    public array $referencedColumns;   // list<string>
}

The name on an index or a foreign key is derived, not the database's own name. It is built from the columns so that the same structure produces the same name on every driver:

index names: primary:id
fk names: foreign:author_id->users(id) | foreign:category_id->categories(id)

Use it for sorting and comparison. Do not use it to drop a constraint.

SchemaTypeNormalizer

public function normalize(
    string $typeName,
    string $nativeType,
    bool $keyColumn,
): string;

$typeName is the driver's type name, $nativeType its full declaration (tinyint(1)), and $keyColumn whether the column takes part in any index or foreign key. Pass a custom instance to SchemaReader to change the mapping.

Exceptions

ExceptionExtendsThrown by
SchemaReadExceptionRuntimeExceptionSchemaReader::read()
GraphAssemblyExceptionRuntimeExceptionGraphAssembler::assemble()
GenerationExceptionRuntimeExceptionMermaidCliLocator::locate(), MermaidCliExporter::export(), GeneratedDiagram::artifact()
InvalidArgumentExceptionnoneMermaidRenderer::render(), GenerationSettings, ThemeResolver, ThemePreset, RendererLimits
RuntimeExceptionnoneAtomicFileWriter::write()

Both commands catch InvalidArgumentException and RuntimeException, which covers all five, print the message, and exit 1.

Stability

These classes are the package's public surface, and the test suite asserts that each one exists under its documented name. Constructor signatures and return types are covered by semantic versioning; the private helpers inside them are not.

What to read next

  • How a diagram is built for what each class contributes.
  • Testing for asserting on generated diagrams.
  • Exporting images for the renderer behind MermaidCliExporter.
  • Diagram syntax for the text MermaidRenderer produces.
PreviousDiagram syntaxNextTesting
View source

On this page

  1. The pipeline in one script
  2. The whole pipeline in one call
  3. Classes
  4. DiagramGenerator
  5. GenerationSettings
  6. ModelDiscovery
  7. SchemaReader
  8. RelationshipDetector
  9. GraphAssembler
  10. MermaidRenderer
  11. DiagramExporter and MermaidCliExporter
  12. MermaidCliLocator
  13. ThemeResolver and ThemePreset
  14. RendererLimits
  15. AtomicFileWriter
  16. Data objects
  17. Graph
  18. GraphEntity
  19. GeneratedDiagram
  20. DiagramGroup and DiagramTarget
  21. ExportRequest
  22. GraphEdge
  23. Relationship
  24. DatabaseSchema and TableSchema
  25. SchemaColumn, SchemaIndex, SchemaForeignKey
  26. SchemaTypeNormalizer
  27. Exceptions
  28. Stability
  29. What to read next