Releases: vlados/laravel-unique-urls
Release list
v2.2.0
What's New
Module-aware model discovery
urls:generate and urls:doctor scanned app_path() only — the default of
Spatie's ModelFinder — so models living in a modular structure
(Modules/Blog/app/Models) silently dropped out of both commands.
Services\ModelDiscoveryService— resolves which directories to scan,
together with the PSR-4 root and namespace prefix each one maps to- Default scan:
app/plus every module underconfig('modules.paths.modules')
(orbase_path('Modules')when nwidart/laravel-modules
is not installed), covering both theModules/{Module}/applayout and the
olderModules/{Module}/ModelsandModules/{Module}/Entitiesones - New
model_pathsconfig key takes the list over completely, accepting a
bare directory (namespace guessed from the composer PSR-4 map), a
'Namespace' => '/directory'pair, or a fullpath/base_path/namespace
descriptor. Directories that do not exist are skipped.
urls:doctor says what it actually checked
An empty scan used to end in Everything is ok, which reads as health but only
meant there was nothing left to check.
- A clean run now ends with
Everything is ok — checked 12 models in 2 paths - Finding no models prints a warning listing the scanned paths instead
- Any single path that resolves to no models at all is named, even when other
paths found some. Aggregated away, that reads as health while a whole module
goes unchecked — almost always a wrong directory, or a namespace prefix that
does not match the project's PSR-4 map and silently drops every class under it - New
--strictflag turns an empty scan — whole or partial — into a failure
exit code for CI
--model behaves the same in both commands
urls:doctor --model="Modules\Blog\Models\Post" used to fail with
Target class [\App\Models\Modules\Blog\Models\Post] does not exist while
urls:generate accepted it. Both commands now take a fully qualified class
name, still resolve a bare name against App\Models, and report a missing class
as an error instead of throwing a container exception.
A bare name that is not in App\Models now falls back to the discovered models,
so --model=Post keeps working after App\Models\Post moves to
Modules\Blog\Models\Post. When the short name matches several models, both
commands say so and list the candidates rather than picking one.
urlHandler is validated the way a request resolves it
The controller from urlHandler() is now resolved through the
ControllerResolver — the component the live request uses — and the method check
mirrors LaravelUniqueUrlsController: __invoke() for the Livewire style,
otherwise the declared method with show() and index() as fallbacks.
Resolving a controller builds it, which class_exists() never did, so a
controller with an unresolvable dependency and a model that cannot be
instantiated are recorded as errors instead of ending the run. One broken model
no longer costs the coverage of every model after it.
A failure inside urlStrategy() stays deliberately silent. Doctor works with an
empty instance, and building a slug from one routinely fails on missing relations
or null attributes — reporting that would bury the real findings in noise. What
changed is only that such a failure no longer ends the run.
Behaviour Changes
- Doctor no longer reports a pinned Livewire component name as a missing class
- Doctor no longer reports a handler relying on the
show()/index()fallback
as broken - Doctor now also checks models that inherit the trait from a parent class
(class_uses()→class_uses_recursive()), so it may report on models it used
to skip
Upgrading
No configuration change is required: leaving model_paths unset — or absent from
an already published config file — keeps the automatic behaviour, which is a
superset of the previous app/-only scan.
A project that moved models into modules should expect urls:doctor to report
more than before — it was previously blind to them, not clean.
v2.1.1
v2.1.0
What's New
Pluggable ControllerResolver
Controller resolution is now delegated to a ControllerResolver contract, making the package fully extensible without modifying its source.
Contracts\ControllerResolver— interface with a singleresolve(string $controller): ?objectmethodResolvers\DefaultControllerResolver— ships with the package; resolves FQCNs viaclass_exists()and Livewire component names viaapp('livewire')->new()(only when Livewire is installed)- Bound as a singleton with
singletonIf, so apps can override it by binding their own implementation before the package boots
Upgrading from v2.0.x
No breaking changes. The default resolver handles both FQCNs and Livewire component names out of the box.
Full Changelog: v2.0.1...v2.1.0
v2.0.1
Documentation
- Restructured README into a focused quick-start (~100 lines) with detailed docs moved to
docs/folder - Added six dedicated docs pages: configuration, usage, bulk operations, artisan commands, testing, troubleshooting
- Fixed stale version line (now correctly shows PHP 8.2+ | Laravel 11, 12)
- Documented Livewire SFC component name resolution and previously undocumented
withoutGeneratingUrls()helper
Full Changelog: v2.0.0...v2.0.1
v2.0.0
Breaking Changes
- Minimum PHP version raised to 8.2 (was 8.1)
- Dropped Laravel 9 and 10 support — now requires Laravel 11 or 12
- Updated minimum versions for dev dependencies: Pest 3, PHPUnit 11, Larastan 3
What's New
Livewire SFC component name resolution
Livewire Single File Components (SFCs) use kebab-case names (e.g. counter) rather than fully-qualified class names. The handleRequest method now detects these and resolves them via app('livewire')->new(), preserving backward compatibility for FQCN-based controllers.
Supported Versions
- Laravel: 11, 12
- PHP: 8.2, 8.3, 8.4
Full Changelog: v1.2.0...v2.0.0
v1.2.0
What's New
Static URL generation toggle for bulk operations
Added static methods to HasUniqueUrls trait for globally disabling/enabling URL auto-generation during bulk imports:
Model::disableUrlGeneration()— suppress per-model URL generationModel::enableUrlGeneration()— re-enable per-model URL generationModel::withoutGeneratingUrls(callable $callback)— disable for the duration of a callback
This follows the same pattern as Laravel Scout's withoutSyncingToSearch(), allowing import jobs to skip per-row URL generation and handle it in batch at the end.
Example
Product::withoutGeneratingUrls(function () {
// Import thousands of products without generating URLs per-save
foreach ($products as $data) {
Product::updateOrCreate(['id' => $data->id], $data);
}
});
// Generate URLs in batch after import
Product::generateUrlsInBatch(Product::all());v1.1.3
Laravel 12 Compatibility
This release fixes compatibility with Laravel 12.
What Changed
The asJson() method signature changed in Laravel 12 to include a new $flags parameter:
// Laravel 11
protected function asJson($value): string|false
// Laravel 12
protected function asJson($value, $flags = 0): string|falseThis update ensures the package works with both Laravel 11 and Laravel 12.
Supported Versions
- Laravel: 9, 10, 11, 12
- PHP: 8.1, 8.2, 8.3, 8.4
Full Changelog: v1.1.2...v1.1.3
v1.1.2
Performance Improvement
This release moves the conflicting column validation from runtime to the urls:doctor command, eliminating redundant database schema queries on every model instantiation.
What Changed
Previously, every time a model using the HasUniqueUrls trait was instantiated, 2 schema queries were executed to check for conflicting url and urls columns. In loops or batch operations, this caused significant overhead.
Now, this validation only runs when you explicitly call:
php artisan urls:doctorMigration Guide
No changes required. The check is now part of the urls:doctor command, which you can run:
- During development to catch misconfigurations
- In your CI pipeline before deployment
Changes
- Removed
initializeHasUniqueUrls(),checkForConflictingAttributes(), andhasColumn()fromHasUniqueUrlstrait - Added
checkConflictingColumns()toUrlsDoctorCommand - Updated tests to verify the doctor command catches conflicts
Performance Impact
| Scenario | Before | After |
|---|---|---|
| Single model instantiation | 2 schema queries | 0 queries |
| Loop with 1000 models | 2000 schema queries | 0 queries |
Full Changelog: v1.1.1...v1.1.2
v1.1.1
v1.1.0
What's Changed
- Add Claude Code GitHub Workflow by @vlados in #150
- docs: add getSlug() method and usage examples to API documentation by @vlados in #151
- fix: change slug column from varchar(255) to text by @vlados in #152
- feat: add exception for models with conflicting url/urls columns by @vlados in #154
- Bump dependabot/fetch-metadata from 2.2.0 to 2.4.0 by @dependabot[bot] in #159
- Bump actions/checkout from 4 to 6 by @dependabot[bot] in #169
- Bump stefanzweifel/git-auto-commit-action from 5 to 7 by @dependabot[bot] in #162
Full Changelog: v0.4.1...v1.1.0