Skip to content

Releases: vlados/laravel-unique-urls

v2.2.0

Choose a tag to compare

@vlados vlados released this 15 Aug 15:46

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 under config('modules.paths.modules')
    (or base_path('Modules') when nwidart/laravel-modules
    is not installed), covering both the Modules/{Module}/app layout and the
    older Modules/{Module}/Models and Modules/{Module}/Entities ones
  • New model_paths config key takes the list over completely, accepting a
    bare directory (namespace guessed from the composer PSR-4 map), a
    'Namespace' => '/directory' pair, or a full path/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 --strict flag 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

Choose a tag to compare

@vlados vlados released this 15 Aug 15:46

Compatibility

  • Laravel 13 support — illuminate/contracts widened to ^11.0|^12.0|^13.0

Tagged at the time without release notes; backfilled for the record.

v2.1.0

Choose a tag to compare

@vlados vlados released this 12 Feb 11:53

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 single resolve(string $controller): ?object method
  • Resolvers\DefaultControllerResolver — ships with the package; resolves FQCNs via class_exists() and Livewire component names via app('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

Choose a tag to compare

@vlados vlados released this 12 Feb 10:28

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

Choose a tag to compare

@vlados vlados released this 12 Feb 10:09

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

Choose a tag to compare

@vlados vlados released this 10 Feb 13:37

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 generation
  • Model::enableUrlGeneration() — re-enable per-model URL generation
  • Model::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

Choose a tag to compare

@vlados vlados released this 11 Jan 08:52

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|false

This 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

Choose a tag to compare

@vlados vlados released this 11 Jan 08:35

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:doctor

Migration 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(), and hasColumn() from HasUniqueUrls trait
  • Added checkConflictingColumns() to UrlsDoctorCommand
  • 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

Choose a tag to compare

@vlados vlados released this 03 Dec 22:43

Full Changelog: v1.1.0...v1.1.1

v1.1.0

Choose a tag to compare

@vlados vlados released this 03 Dec 22:31

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