vicgutt/laravel-models-finder
A Laravel package that helps you find and discover Eloquent models in your application. It scans your project for model classes, making them easy to locate, list, and work with—handy for tooling, admin panels, and package development.
This is a small, single-purpose utility: it walks your autoloadable files and returns the Eloquent model classes it finds. That's it. Everything you'll do with it is downstream of that one call, so the entire onboarding is figuring out (a) which paths it scans and (b) what shape the result comes back in.
Install and publish
composer require vicgutt/laravel-models-finder --dev
php artisan vendor:publish --tag="models-finder-config"
Consider --dev unless you actually call the finder at runtime (e.g. to build a permissions or search registry on boot). Keeping it dev-only removes it from your production dependency surface entirely.
Three places to look, in order
config/models-finder.php — the scanned paths and exclusion list. This is 90% of the package's behavior. Default it to app_path(), and add package model directories explicitly rather than scanning vendor wholesale.find(), all(), get()) and the return type (array of FQCNs vs. Collection). The README is short; reading the class takes two minutes and removes all ambiguity.First use case: verify discovery in Tinker
// Confirm what the finder actually sees before wiring it into anything
$models = app(\VicGUTT\ModelFinder\ModelFinder::class)->find();
// or the facade, whatever the class exposes
collect($models)->each(fn ($m) => dump($m, (new $m)->getTable()));
Run this on a real project with models in app/Models, a couple of package models in vendor, and at least one abstract base model. You will immediately see the two failure modes you need to design around: abstract classes leaking in, and package models either missing or duplicated.
1. Wrap it in your own registry — always
The single most valuable pattern with a 0-star, single-maintainer dependency is a one-class firewall:
final class ModelRegistry
{
/** @return array<class-string<\Illuminate\Database\Eloquent\Model>> */
public function all(): array
{
return once(fn () => collect($this->finder->find())
->filter(fn (string $c) => is_subclass_of($c, Model::class))
->reject(fn (string $c) => (new ReflectionClass($c))->isAbstract())
->values()
->all());
}
}
Bind it as a singleton. Now the package is an implementation detail you can replace with File::allFiles() + class_exists() in an afternoon if it's abandoned.
2. Cache the result in production
Filesystem scanning is I/O bound and pointless on every request. If you must run it at runtime, cache:
Cache::rememberForever('model-registry', fn () => $registry->all());
…and bust it in your deploy script (php artisan cache:clear in the release step, or key the cache on a build hash). If it's dev-only, don't cache at all — stale model lists during development are worse than the scan cost.
3. Build a cross-cutting feature that would otherwise be a hardcoded list
This is where the package earns its keep. Good fits:
view-any, create, update, delete per model.Searchable / having a slug.$fillable count, whether a factory exists, whether a policy exists.models:list internal Artisan command — genuinely useful for onboarding and for catching models nobody knew existed.public function handle(ModelRegistry $registry): void
{
$this->table(['Model', 'Connection', 'Table'], collect($registry->all())->map(fn ($m) => [
$m, (new $m)->getConnectionName(), (new $m)->getTable(),
]));
}
4. Reflection-driven filtering, not name filtering
Filter on traits and interfaces rather than string-matching class names:
$softDeletable = collect($registry->all())
->filter(fn ($m) => in_array(SoftDeletes::class, class_uses_recursive($m)));
This makes a "prune only soft-deletable models" command correct by construction, rather than by remembering to update a list.
5. Tests that fail when someone adds a model without the boilerplate
it('has a factory, a policy and a table for every model', function () {
foreach (app(ModelRegistry::class)->all() as $model) {
expect(class_exists("Database\\Factories\\".class_basename($model)."Factory"))->toBeTrue();
expect((new $model)->getTable())->not->toBeEmpty();
}
});
This is a cheap architectural guardrail and the highest-value thing you'll get out of the package.
6. Package development
Point the config at the package's own model directory and the host app's, then assert integration in the package test suite — it's a quick way to verify your package's models are discoverable by the host app's generic tooling.
Risk assessment comes first. 0 stars, 0 dependents, single maintainer, last release March 2023. That's not a disqualifier for a ~200-line dev utility, but it does mean: pin the version, commit the lock file, keep it in require-dev, and wrap it (pattern 1). Do not let it become load-bearing infrastructure.
Autoloading is the #1 source of wrong results. The finder resolves FQCNs from file paths plus namespace mapping. Non-PSR-4 directories, classmap-only packages, and freshly created model dirs will produce false negatives. composer dump-autoload after adding a namespace, and if something is missing, compare the raw file list against the resolved class list to see exactly where it drops.
Abstract base models will appear. Most Laravel codebases have an abstract BaseModel or Model base class sitting in the models directory. It extends Model, so it's a match. Always reject non-instantiable classes (isAbstract(), !isInstantiable()), or new $model will throw.
Instantiating models is not free. getTable() on a fresh instance runs boot(), bootTraits(), and may touch config, register observers, or fire global scope registration. Don't do it inside nested loops over hundreds of models. Cache the table map once.
Connection collisions. Dedupe and key on getConnectionName().'.'.getTable(), not the table alone — multi-tenant or multi-connection apps will have duplicate table names across connections.
Scan scope. Pointing at base_path() will walk vendor/, storage/, and node_modules. Even scoped to app_path(), database/migrations (which are classes but not models) can get pulled in if included. Keep the config paths tight and the exclude list explicit; this is also the primary extension point — if the package doesn't support a filter callback, subclass the finder or filter in your registry.
Production boot order. If you're calling the finder during app boot (permission registration, route model binding helpers), remember the autoloader may not have every class loaded — class_exists($class) will trigger autoload, which is usually fine, but a broken autoload entry turns a deploy into a fatal error rather than a silently missing model.
Debugging recipe. When a model doesn't show up: (1) class_exists(FQCN) in Tinker, (2) check the config path covers its directory, (3) check the exclude globs match the path, (4) confirm namespace matches directory. That's the whole failure surface — the package is too small to hide bugs anywhere else.
Extension points are thin. Treat config paths/excludes as the only supported customization. If you need custom filtering, do it in your wrapper; don't fork the package for a 10-line filter.
How can I help you explore Laravel packages today?