byrcsc/laravel-cartographer · 1.x
PHP API.
The classes behind the command, their constructors and methods, the data objects they return, and how to drive the pipeline yourself.
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.
Classes
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 = [],
): 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.
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;$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');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;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 onlyWhich 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
| Exception | Extends | Thrown by |
|---|---|---|
SchemaReadException | RuntimeException | SchemaReader::read() |
GraphAssemblyException | RuntimeException | GraphAssembler::assemble() |
InvalidArgumentException | — | MermaidRenderer::render() on a bad mode or format |
RuntimeException | — | AtomicFileWriter::write() |
The command catches InvalidArgumentException and RuntimeException, which
covers all four, prints the message, and exits 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.
- Diagram syntax for the text
MermaidRendererproduces.