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.
Scope note: this is a baseline assessment — there is no prior assessment or repository/telemetry data on file. Everything below is derived from the v2.2.0 release notes and describes the package as a Laravel artisan tooling layer that generates and audits model URLs via urls:generate and urls:doctor.
Architecture fit
The package operates as a console-level tooling layer over Laravel's container and console APIs, with URL generation delegated to a per-model urlHandler() and slug behaviour to urlStrategy(). v2.2.0 introduces Services\ModelDiscoveryService, which is the architecturally significant change: model discovery was previously implicit (Spatie ModelFinder defaulting to app_path()) and is now an explicit, configurable, shared service consumed by both commands. That consolidation is the right shape — one source of truth for "which models does this application own" — and it turns a hardcoded assumption into an extension point via the new model_paths config key. Optional coupling to nwidart/laravel-modules is detected at runtime with a base_path('Modules') fallback, which keeps the package viable for both module-based and conventional apps. The urls:doctor redesign (resolving controllers through ControllerResolver and mirroring LaravelUniqueUrlsController's __invoke/show/index fallback logic) is a correctness improvement that aligns static analysis with runtime behaviour — a genuine architectural alignment rather than a cosmetic change.
Integration feasibility
High for conventional Laravel apps: no configuration change is required, and leaving model_paths unset preserves the previous app/-only behaviour as a strict subset. For modular apps the feasibility hinges on whether the project's PSR-4 map and directory layout match what the discovery service expects; the release supports both Modules/{Module}/app and legacy Modules/{Module}/Models / Entities layouts, which covers the common nwidart conventions and most hand-rolled variants. model_paths accepts three shapes (bare directory, Namespace => /dir pair, full descriptor), so teams can bypass namespace guessing entirely when their autoload map is unusual. Standard composer require / version bump; no migrations, no schema, no service provider re-registration implied.
Technical risk
urls:doctor. Resolution now builds the controller through the container rather than calling class_exists(). Constructors with side effects (auth checks, repository calls, HTTP clients, queue dispatches) will execute during a "read-only" audit. Failures are captured as errors rather than aborting the run, which is the right failure mode, but the side-effect surface is new and undocumented in terms of guarantees.urlStrategy() exceptions remain unreported because doctor runs against empty instances. This is defensible, but it means a green doctor run does not imply slug generation works. Doctor's signal should be read as "wiring is valid," not "URLs will generate."--strict escalates it to a non-zero exit — but only if teams opt in. Default exit codes on empty/partial scans remain a governance gap.--model=Post now falls back to discovered models when not in App\Models. Convenient interactively; in CI scripts it converts a previously hard failure into a potential collision error or, worse, a resolution to a different model than intended if only one match exists across modules.class_uses_recursive() widens coverage to models inheriting the trait from a parent, and modular models were previously invisible entirely. Modular projects should expect doctor to report strictly more after upgrade. Two changes move the other way (pinned Livewire component names and show()/index() fallbacks no longer flagged as broken), so some previously red findings will go green — teams should confirm that's genuine and not masking.Everything is ok now becomes Everything is ok — checked 12 models in 2 paths; anything doing exact-match parsing of doctor's stdout breaks.Key questions
model_paths schema?--strict empty scans? This determines whether doctor is usable as a CI gate at all.--json) output for dashboards and trend tracking, or is stdout parsing the only option?spatie/laravel-model-finder a hard dependency, and is it still required given the new discovery service?Stack fit
Fits any Laravel application already using artisan in local dev and CI, and is complementary to route:list, IDE helper generation, and static analysis rather than overlapping them. It slots naturally into a GitHub Actions / GitLab CI job that runs after composer install and before deploy. For module-based codebases (nwidart/laravel-modules, or in-house Modules/ or src/Domain/ PSR-4 layouts) this release is effectively the first version that is usable — prior versions quietly ignored those models.
Migration path
urls:doctor output (and exit code) on the branch you intend to upgrade.urls:doctor and diff against the baseline. Expect net-new findings for modular projects.model_paths explicitly with 'Namespace' => '/dir' pairs to remove the guessing step. Prefer explicit pairs for any non-standard layout.--model invocations in scripts to fully qualified class names; treat the short-name fallback as an interactive convenience only.--strict.Compatibility
No breaking API change is declared. Published config files predating model_paths fall back to automatic discovery, which is a superset of the old behaviour — so an unmodified config cannot lose coverage. The compatibility risks are behavioural and textual (wider reporting scope, changed doctor messaging, changed --model resolution semantics), not structural. Nothing here requires a coordinated change with application code.
Sequencing
Upgrade and observe first, gate later. Run the package upgrade in an isolated PR; land the doctor-output diff as the review artefact; fix namespace and path configuration; then, in a follow-on PR, adopt --strict as non-blocking (warning only) for one sprint before making it blocking. Keep urls:generate out of automated write paths until --model FQCN normalisation is complete.
Maintenance
The surface area grows modestly but meaningfully: model_paths has three accepted shapes and a "skip non-existent directories" rule, which will generate support questions. Documentation should show the exact three forms with one canonical example per project layout (plain Laravel, nwidart modules, custom PSR-4). Namespace guessing is coupled to composer.json's PSR-4 map, so it drifts whenever autoload configuration changes — explicit Namespace => path pairs are the lower-maintenance choice for anything non-standard.
Support
Expect a predictable wave of "doctor reports 60 new issues after upgrading" tickets from modular teams. Pre-empt with a release note and internal runbook stating that doctor was previously blind to module models, not clean. The corresponding positive change — pinned Livewire components and show()/index() fallbacks no longer reported broken — should be called out so teams don't assume the package regressed in the other direction.
Scaling
Cost scales with module count × model count, and now includes model and controller instantiation rather than cheap class_exists() checks. This should be negligible for apps in the low hundreds of models and worth measuring for modular monorepos. Recommend a one-off timing exercise on the largest application before wiring doctor into a per-PR job; if it's slow, schedule it nightly instead.
Failure modes
--strict.urlStrategy() failures are silent by design — a green doctor run can still ship broken slugs.model_paths entries are skipped without loud failure — a typo in config degrades coverage quietly.Ramp-up
Low for engineers: two artisan commands and one optional config key. Realistic estimate is roughly half a day to upgrade and triage a small modular app, and one to three days for a large modular monorepo where PSR-4 mismatches need cleanup and findings need bucketing. The single highest-value control is the pre/post doctor diff — it is cheap, it converts an opaque behaviour change into a reviewable list, and it gives the TPM a defensible answer when asked whether the upgrade "broke anything."
How can I help you explore Laravel packages today?