Browse documentationOpen

byrcsc/laravel-cartographer · 1.x

Model discovery.

How configured paths become a list of model classes, which classes qualify, how globs handle modular layouts, and why a path that matches nothing only warns.

Discovery answers one question: which classes should become entities. It reads files, not the database, and it runs before anything else.

// config/cartographer.php
'paths' => [
    app_path('Models'),
],

Each entry is expanded as a glob against directories. Every matched directory is scanned recursively for .php files.

What qualifies as a model

A class becomes a candidate entity when all four hold:

  1. It is declared in a scanned .php file.
  2. The class exists once the autoloader has been asked for it.
  3. It subclasses Illuminate\Database\Eloquent\Model.
  4. It is not abstract.

Everything else is skipped in silence: traits, interfaces, enums, plain classes, anonymous classes, and files with no class in them at all. A file that declares two classes contributes both, if both qualify.

Class names are read from the file's PHP tokens, so a class is found by its declaration rather than by its filename. app/Models/Concerns/HasSlug.php containing a trait contributes nothing; app/Models/Catalog.php containing Product and Variant contributes two.

Discovery is where exclude_models is applied:

'exclude_models' => [
    App\Models\Telemetry::class,
    App\Models\PasswordReset::class,
],

An excluded model produces no entity and no edges. Relations that other models declare towards it are dropped too, because the edge has nowhere to land.

Modular and domain-driven layouts

Globs are the reason paths is a list of patterns rather than a list of directories:

'paths' => [
    app_path('Models'),
    base_path('src/Domain/*/Models'),
    base_path('modules/*/src/Models'),
],

src/Domain/*/Models matches src/Domain/Billing/Models, src/Domain/Catalog/Models, and any domain added later. Nothing needs updating when a new module appears.

Only *-style directory matching is available — the pattern is passed to PHP's glob() with directories only. There is no filtering by class name, namespace, or file name; a directory is either scanned entirely or not at all. Use exclude_models to remove individual classes.

Overlapping paths are safe

Files are keyed by their resolved real path, so a file reached through two patterns is read once. Listing app_path('Models') twice, or listing both a parent and a child directory, changes nothing about the result.

Ordering

The final list is sorted by fully qualified class name. Directory order, filesystem order, and the order of the paths array have no effect on the output. This is one half of why the diagram is stable between runs.

A path that matches nothing

A pattern matching no directory produces a warning and is skipped:

   WARN  Model discovery path matched no directories: /srv/app/src/Domain/*/Models

The run continues. This is deliberate: a glob for modules that do not exist yet in a given deployment should not break the command.

Warnings are suppressed under --stdout, so piped output stays clean. If you are debugging paths, run without --stdout.

When discovery finds nothing at all, the command fails instead:

   ERROR  No Eloquent models were discovered. Check the paths configured in [cartographer.paths].

Models are still filtered later

Discovery produces candidates, not entities. A discovered model is dropped from the graph at assembly time when either is true:

  • It cannot be instantiated. new Model is called to read the table name; if the constructor throws, the model is skipped with a warning naming the exception.

  • Its table does not exist on the target connection:

       WARN  Skipping model [App\Models\Ghost]: table [missing_ghosts] does not exist.

The second is the common one, and it usually means the database is behind the code. Run your migrations and generate again.

Validation

paths and exclude_models are validated when the command runs, and a bad value fails it:

ProblemMessage
paths is not an array[cartographer.paths] must be an array of strings.
paths contains a non-string or an empty string[cartographer.paths] must contain only non-empty strings.
exclude_models contains a class that is not a model[cartographer.exclude_models] must contain only Eloquent model class names.

exclude_models entries are checked with is_a(..., Model::class, true), so the class must be loadable. A typo in the class name fails the command rather than silently excluding nothing.