vlados/laravel-unique-urls
Laravel package for generating and managing unique URLs, helping you create collision-free slugs and SEO-friendly permalinks for models and content with automatic uniqueness checks and customizable URL patterns.
This package generates and maintains unique, controller-backed URLs for your Eloquent models, driven by two artisan commands: urls:generate (writes the URL records) and urls:doctor (validates that every discovered model still resolves to a working handler).
php artisan vendor:publish for the package's config tag. Skim the published config first — model_paths (new in v2.2.0) and the handler/strategy defaults live there.php artisan urls:generate. On a fresh install with no configuration it scans app/ for models using the package's URL trait and creates a record per model.php artisan urls:doctor. It re-resolves everything and tells you which models have no reachable handler. As of v2.2.0 a clean run reports Everything is ok — checked 12 models in 2 paths, so you can confirm what it inspected rather than trusting a bare "ok".urlHandler() returning your controller and a urlStrategy() returning the slug path, run urls:generate --model=YourModel, and hit the generated route. Validate with urls:doctor --model=YourModel before running it across the whole app.The two files worth reading in the source are the config and LaravelUniqueUrlsController — the latter defines exactly how a URL is resolved at request time, and v2.2.0 aligned urls:doctor's checks to it.
Scanning is now module-aware. Previously urls:generate and urls:doctor used Spatie's ModelFinder with the default app_path(), so anything under Modules/Blog/app/Models was silently invisible. v2.2.0 adds Services\ModelDiscoveryService, which resolves scan directories together with their PSR-4 root and namespace prefix. Defaults are app/ plus every module under config('modules.paths.modules') (or base_path('Modules') if nwidart/laravel-modules isn't installed). Both the modern Modules/{Module}/app layout and the legacy Modules/{Module}/Models / Modules/{Module}/Entities layouts are covered.
Overriding discovery with model_paths. The new config key accepts three shapes:
'model_paths' => [
app_path(), // bare dir, namespace guessed from composer PSR-4
'Modules\Blog' => base_path('Modules/Blog/app'), // explicit namespace => path
['path' => base_path('src/Domain'), 'namespace' => 'App\Domain', 'base_path' => base_path('src')],
],
This list replaces the automatic scan entirely — it is not additive. Use it when your PSR-4 map is unusual, when you want to exclude a noisy module, or when models live outside app/ and Modules/.
CI pattern. Add php artisan urls:doctor --strict to your pipeline. Without --strict an empty scan still exits zero; with it, an empty whole-or-partial scan fails the build. Pair it with php artisan urls:generate in the same job so newly added models are covered.
Targeted runs. --model now behaves identically in both commands and accepts a fully qualified class name: php artisan urls:doctor --model="Modules\Blog\Models\Post". A bare name still resolves against App\Models first, then falls back to the discovered model set — so --model=Post keeps working after a model moves into a module. If a short name matches multiple discovered models, both commands list the candidates and refuse to guess. Use FQCNs in scripts to stay deterministic.
Handler conventions. urlHandler() is now validated through the same ControllerResolver the live request uses. __invoke() is checked for Livewire-style controllers; otherwise the declared method, with show() and index() as fallbacks. If you rely on the fallbacks, doctor will no longer flag you as broken — but you should still declare the method explicitly for clarity.
composer.json map, every class under it is dropped without error. This is the single most common cause of "why isn't my module being scanned". Mitigation: v2.2.0's doctor now names any path that resolves to zero models even when other paths found some — read those lines, they are almost always this bug. Run composer dump-autoload after changing PSR-4 config, since bare-directory namespace guessing reads the composer map.model_paths degrades to a smaller scan. Combine with --strict in CI to catch it.urls:doctor now uses class_uses_recursive(), so models inheriting the trait from a parent are checked too, and module models are no longer invisible. A project that moved models into modules should expect doctor to report more than before — it was blind, not clean. Don't treat the first post-upgrade run as a regression; reconcile the list once.urlHandler() is now instantiated, not just class_exists()-checked. Resolving goes through ControllerResolver, which builds the controller. A controller with an unresolvable constructor dependency is reported as an error. Keep handler constructors container-resolvable (or bind them), and don't assume doctor's check is cheap — it may boot services.urlStrategy() failures are deliberately silent. Doctor evaluates strategies against an empty model instance, so slug generation routinely fails on missing relations or null attributes. Those failures are swallowed on purpose — do not expect doctor to validate your slug logic. Test urlStrategy() yourself with real fixtures; use urls:generate --model=... on a seeded database to catch real strategy errors.--model=Post matching several models will now stop with a candidate list. In scripts always pass the FQCN.ModelDiscoveryService rather than fighting model_paths — it's the single seam both commands share, so a fix there covers generate and doctor at once.How can I help you explore Laravel packages today?