What Loom detects¶
A per-primitive summary of the code shapes Loom reads and the ones it doesn't. For what each field means, see the schema. For symptoms and fixes, see Why was my code missed.
Loom parses source without running your app. It scans app/, plus routes/ and the route files that providers and bootstrap/app.php load for routes, and bootstrap/app.php for the scheduler. Anything decided at runtime is out of reach.
| Primitive | Detected | Not detected |
|---|---|---|
| Events | Classes in app/Events/. Any class passed to event(...), broadcast(...), broadcast_if/unless(...) or Event::dispatch(...). Classes named in a model's $dispatchesEvents, listed as dispatched by the model. X::dispatch(), dispatchIf, dispatchUnless for classes in app/Events/. |
X::dispatch() on an event outside app/Events/. Dynamic targets (event($e)). Anonymous classes. $dispatchesEvents entries without a literal hook name and Foo::class value. ShouldBroadcast is not reported as a flag. |
| Listeners | Concrete classes in app/Listeners/ with a public handle* or __invoke method that takes a first parameter, as Laravel's discovery does. The method can be declared, inherited from a parent, or provided by a trait (including as renames and insteadof). The event is the class in the first parameter's type, including nullable and union types; the method name is recorded as written. $listen arrays. Event::listen(...) (facade imported, fully qualified or the bare Event alias) and container-resolved dispatchers ($this->app['events'], app(), resolve(), make(), or a variable assigned from them). Event arrays. Subscribers via $subscribe or Event::subscribe(...) (any of those facade spellings), in return-array and $events->listen(...) styles. [Listener::class, 'method'], Closure::fromCallable([...]) and Listener::method(...). |
Dispatcher passed as a typed boot() parameter or held in a property. Dynamic event names. $obj->method(...) and 'Class::method' strings. Untyped or builtin-typed parameters (the listener is listed with no event). Abstract classes, traits, interfaces, and methods made non-public with as protected. Handlers inherited from a vendor class. Registrations inside nested closures in subscribe(). |
| Closure listeners | Arrow functions and closures in $listen, listen() calls and subscribers. Events keyed by ::class or string. Event::listen(function (OrderPlaced $e) {...}) with the event inferred from the first parameter type (union types give one entry per class). Events and jobs dispatched inside the body, including inside closures nested in it. |
Inferred closures with an untyped, object or otherwise non-class first parameter (skipped). Queue status (always false). Back-links from handled_by and dispatched_from. |
| Observers | #[ObservedBy(...)] (single and array). Model::observe(...), static::observe(...), self::observe(...). Event::listen('eloquent.{hook}: {Model}', ...) strings for model_events. Hook methods the observer declares, inherits from a parent or gets from a trait, of any visibility, for the events Eloquent lets observers subscribe to (retrieved through forceDeleted, plus trashed on Laravel 12 and later). |
parent::observe(...), $this->observe(...), dynamic arguments. booting and booted methods (Laravel doesn't register them on an observer, though Event::listen('eloquent.booted: ...') strings are read). Hooks inherited from a vendor class. Closure handlers in model_events. Container-form eloquent.* registrations. |
| Jobs | Classes in app/Jobs/. Classes passed to dispatch(new X), Bus::dispatch(...) or X::dispatch() (including conditional and fluent-chain forms). Class-level scalar $connection, $queue, $delay, $tries, $timeout, $backoff. |
queued through vendor parent classes. backoff() and retryUntil() methods. Non-literal property values. ShouldBeUnique, Batchable and similar flags. |
| Dispatches | event(), broadcast(), Event::dispatch(), dispatch(), Bus::dispatch(), dispatch_sync(), X::dispatchSync(), X::dispatchAfterResponse(), Bus::dispatchSync/dispatchNow/dispatchAfterResponse(), ->afterResponse(), Bus::batch(...)->dispatchAfterResponse(), Queue::push/pushOn/later/laterOn/bulk (recorded with a mode), Bus::chain([...]) and Bus::batch([...]) (one dispatch per literal job), X::dispatch/dispatchIf/dispatchUnless(), inside class methods, and inside closures and arrow functions within them (DB::transaction(fn () => ...), each, tap, afterCommit, closures assigned to a variable), which count for the enclosing method. Closures passed to Event::listen and Route::get belong to that listener or route instead. Queue, connection, integer delay, afterCommit, locale, mailer options. Dynamic targets go to unresolved_dispatches. |
Closures that are registered with Queue::before/after/failing or other registration APIs, and first-class callable syntax ($this->send(...)). Script-level code outside a route closure. Queue::pushRaw. ShouldQueue and the sync queue driver are not evaluated against mode. Container-resolved dispatchers. Non-literal delays. Modifiers set on a separate statement. |
| Mailables | Classes in app/Mail/. Classes passed to Mail::send/sendNow/queue/onQueue/later/laterOn, including Mail::to(...)->send(...) chains. Class-level queue properties. |
Mail::raw, ->html()/->text() sends. Variable targets (listed as unresolved). queued through vendor parents. backoff() and retryUntil(). |
| Notifications | Classes in app/Notifications/. Classes passed to $x->notify(...), notifyNow, Notification::send/sendNow, Notification::route(...)->notify(...). Channels from a via() that returns a literal array. Literal channel filters on send. |
via() inherited from a parent or trait. Conditional or computed via(), reported via channels_dynamic. shouldSend(). Message content. Receiver types. Facade-level modifiers before send. |
| Schedule | Kernel::schedule(), ->withSchedule(...) in bootstrap/app.php, Schedule:: calls in routes/console.php, and Schedule::call/command/job/exec under app/. ->group(...). Standard frequency helpers, sub-minute helpers (as frequency), and common constraints and flags. |
Macros and unknown helpers (cron is null). Variable arguments. repeatEvery(), evenWhenPaused(), ping and output hooks. Closure bodies. |
| Routes | Route::get/post/put/patch/delete/options/any/match in routes/*.php and in route files loaded by loadRoutesFrom(), Route::group() with a file path, or withRouting(web:, api:, commands:) when the path is a literal, __DIR__, base_path(), app_path() or a concatenation of these. Route::resource and apiResource. Groups with prefix, name, controller and middleware, including the group that loads a route file (Route::...->group($path), withRouting(web:, api:) with the web and api middleware and the api prefix). Tuple, invokable, Class@method and closure actions. Events and jobs dispatched in the controller method, or in the closure for a closure action. |
Middleware group and alias expansion, withoutMiddleware. names(), parameters(), scoped(), shallow(), nested resource names. Route::resources([...]). Variable actions. Attribute routes. Route files loaded by a computed, relative or out-of-root path (listed by loom:scan -v). Prefix, name or middleware of a loading group when the value is not a literal (listed by loom:scan -v), and the loading group's domain, where and namespace. |
Rules that apply everywhere¶
- Targets must be
::classreferences,new Xexpressions or string literals. Variables, concatenation and container lookups don't resolve. - Loom walks the directories in
scan.paths(defaultapp/) and skips files matchingscan.exclude. Convention directories such asEvents/andJobs/resolve inside each scan path. - Loom locates classes through the PSR-4 map in
composer.json(App\toapp/when there is none). A class it can't find on disk, or finds outside the scan paths, is dropped. - Files with syntax errors are skipped.
loom:scanprints their count, and-vlists them. - Anything inside
vendor/is treated as opaque, soqueuedandvia()inherited from a package don't show. - Dispatch links from listeners, jobs and observers come from the registered handler method only. Routes attribute by controller method.
- Handler
dispatches[]lists only events and jobs. Mail and notification sends inside a handler appear inmailables[].sent_fromandnotifications[].notified_frominstead. - Route middleware follows Laravel's merge rules. Calling
middleware()twice on a group chain (Route::middleware('a')->middleware('b')->group(...)) keeps onlyb. Nested groups add the inner list after the outer one, and->middleware()calls on a single route accumulate.