Scan configuration¶
Keys under scan in config/loom.php decide which directories loom:scan walks. Publish the file to change them:
Keys¶
| Key | Default | Meaning |
|---|---|---|
scan.paths |
['app'] |
Directories to scan, relative to the project root. A * matches within one path segment (Modules/*). Paths outside the project root are rejected. |
scan.route_paths |
['routes'] |
Directories holding route files, relative to the project root. * globs are allowed (Modules/*/routes). Read for routes[] and for dispatches inside route closures. Independent of scan.paths. |
scan.discover_routes |
true |
Also read route files that Laravel loads from a path: loadRoutesFrom(), Route::group() with a file path and the web, api and commands paths of withRouting() in bootstrap/app.php. See Route files. false reads only scan.route_paths; loom:scan --no-discover-routes does the same for one run. |
scan.psr4_paths |
false |
When true, also scans every directory in autoload.psr-4 of your composer.json. autoload-dev is not included. |
scan.exclude |
[] |
Globs relative to the project root. A match removes the file, or everything under the matching directory, from every scanner. |
loom:scan --path=DIR replaces scan.paths for one run, and --route-path=DIR replaces scan.route_paths. scan.psr4_paths and scan.exclude still apply. --no-discover-routes turns off route-file discovery for the run.
How directories resolve¶
Convention directories resolve inside each scan directory: Events/, Listeners/, Jobs/, Mail/ and Notifications/ are looked for directly under every entry of scan.paths. Everything else (dispatch sites, $listen arrays, observers, Schedule:: calls, Console/Kernel.php) is found anywhere under a scan directory.
| Layout | scan.paths |
|---|---|
| Default Laravel | ['app'] |
Modules/Billing/Events/... |
['app', 'Modules/*'] |
Modules/Billing/app/Events/... |
['app', 'Modules/*/app'] |
lib/Domain/Events/... |
['lib/Domain'] |
Modules/Billing/routes/web.php |
Nothing, when a provider loads it with loadRoutesFrom(). Otherwise scan.route_paths: ['routes', 'Modules/*/routes'] |
Directories that don't exist are ignored. If none exist, loom:scan exits with 1.
Route files¶
Laravel loads a route file with require, from any path. Loom reads the files under scan.route_paths (default routes/) and, while scan.discover_routes is on, every file these calls load by a path it can resolve statically:
| Call | Path argument |
|---|---|
$this->loadRoutesFrom($path) in a service provider |
$path |
Route::group($attributes, $path) and Route::...->group($path) |
$path, or an array of paths |
->withRouting(web:, api:, commands:) in bootstrap/app.php |
a path, or for web and api an array of paths |
->withCommands([...]) |
file entries; directories are command paths and are skipped |
Paths resolve when they are built from string literals, . concatenation, __DIR__, DIRECTORY_SEPARATOR, dirname(), base_path() and app_path() (assuming the default app/ directory). A discovered route file is searched for further loading calls.
Loom looks for these calls in the files under scan.paths, the route files, bootstrap/app.php, and the providers listed in bootstrap/providers.php and config/app.php (located through composer.json PSR-4), so a module provider outside scan.paths is still found when the app registers it.
The final set is scan.route_paths plus the discovered files, minus scan.exclude. A path that cannot be followed is never dropped silently: the summary line counts it and loom:scan -v lists the file, line and reason.
| Reason | Meaning |
|---|---|
| path is not statically resolvable | The expression uses a variable, a method call, a config value or another runtime value. |
| relative path depends on the working directory | A path such as 'routes/x.php' with no __DIR__ or base_path(). |
| path is outside the project root | The file is not under the project root. |
| no such file | The path resolves but names no file. |
Group attributes of the loading call¶
A loaded file's routes inherit the group around the loading call, as they do in Laravel, which requires the file inside that group:
| Loading call | Applied to the file's routes |
|---|---|
Route::middleware('web')->prefix('shop')->name('shop.')->group($path) |
prefix shop, name prefix shop., middleware web |
Route::group(['prefix' => 'x', 'as' => 'x.', 'middleware' => 'auth'], $path) |
the same three attributes from the array |
$this->loadRoutesFrom($path) inside a Route::...->group(function () {...}) |
the attributes of that enclosing group |
withRouting(web: $path) |
middleware web |
withRouting(api: $path) |
middleware api and prefix api, or the literal apiPrefix: argument |
Route::controller(C::class)->group($path) |
default controller for bare method-name actions |
loadRoutesFrom() on its own adds nothing, because Laravel does not wrap it in a group. Groups declared inside the loaded file nest inside the loading group, and a file that loads another file passes the combined attributes on. A file loaded from two places is read once per loading group, so it can appear under two prefixes. Middleware is recorded by name, so web and api are not expanded. domain, where and namespace of the loading group are not recorded.
An attribute that is not a literal (->prefix($prefix), ->middleware($list), Route::group($attributes, ...)) is not guessed: the file is still read and its routes carry the attributes that did resolve. The summary line counts these and loom:scan -v lists them under "Group attributes not applied". With scan.discover_routes off no loading call is followed, so no group is applied.
Locating classes¶
When a dispatch names a class that isn't in a convention directory, Loom finds its file through the PSR-4 map in your composer.json (autoload.psr-4), the same way Composer's autoloader does: longest matching prefix first, each directory of that prefix in order, then the empty prefix. Without a composer.json, or with no psr-4 entry, Loom assumes App\ maps to app/.
A located file must be inside a scan directory and not excluded, otherwise the class is dropped.
Exclude globs¶
| Pattern | Matches |
|---|---|
app/Legacy |
The directory and everything under it |
app/*/Fakes/*.php |
One directory level between app/ and Fakes/ |
**/Fakes |
Any Fakes directory at any depth |
app/**/*.generated.php |
Generated files at any depth under app/ |
* and ? never cross a /; ** does. Matching is case-sensitive. Excludes also apply to route files, routes/console.php and bootstrap/app.php.