vlados / laravel-unique-urls
A package for using and generating unique urls for each Eloquent model in Laravel
Fund package maintenance!
vlados
Installs: 1 141
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 1
Forks: 0
Open Issues: 1
pkg:composer/vlados/laravel-unique-urls
Requires
- php: ^8.1
- illuminate/contracts: ^9.0|^10.0|^11.0|^12.0
- spatie/laravel-model-info: ^1.4|^2.0
- spatie/laravel-package-tools: ^1.9.2|^2.0
- spatie/laravel-translatable: ^6.0|^7.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.8
- larastan/larastan: ^2.0|^3.0
- nunomaduro/collision: ^6.0|^7.0|^8.0
- nunomaduro/phpinsights: ^2.4
- orchestra/testbench: ^7.0|^8.0|^9.0|^10.0
- pestphp/pest: ^2.0|^3.0|^4.0
- pestphp/pest-plugin-faker: ^2.0|^3.0|^4.0
- pestphp/pest-plugin-laravel: ^2.0|^3.0|^4.0
- phpstan/extension-installer: ^1.1
- phpstan/phpstan-deprecation-rules: ^1.0|^2.0
- phpstan/phpstan-phpunit: ^1.0|^2.0
- phpunit/phpunit: ^10.0|^11.0|^12.0
- spatie/laravel-ray: ^1.26
- dev-main
- v1.1.1
- v1.1.0
- v1.0.0
- v0.4.1
- v0.4.0
- v0.3.2
- v0.3.1
- v0.3.0
- v0.2.0
- v0.1.0
- dev-depfu/update/composer/phpunit/phpunit-12.5.0
- dev-depfu/check/composer/illuminate/contracts-12.41.1
- dev-depfu/update/composer/orchestra/testbench-10.8.0
- dev-depfu/update/composer/pestphp/pest-4.1.6
- dev-depfu/update/composer/pestphp/pest-4.1.5
- dev-depfu/update/composer/orchestra/testbench-10.7.0
- dev-depfu/update/composer/phpunit/phpunit-12.4.4
- dev-depfu/update/composer/pestphp/pest-4.1.4
- dev-depfu/update/composer/phpunit/phpunit-12.4.3
- dev-depfu/update/composer/phpunit/phpunit-12.4.2
- dev-depfu/update/composer/pestphp/pest-4.1.3
- dev-depfu/update/composer/phpunit/phpunit-12.4.1
- dev-depfu/update/composer/pestphp/pest-4.1.2
- dev-depfu/update/composer/phpunit/phpunit-12.4.0
- dev-depfu/update/composer/pestphp/pest-4.1.1
- dev-depfu/update/composer/phpunit/phpunit-12.3.15
- dev-depfu/update/composer/pestphp/pest-plugin-faker-4.0.0
- dev-claude/issue-67-20251001-2050
- dev-claude/issue-13-20251001-2045
- dev-claude/issue-5-20251001-2037
- dev-add-claude-github-actions-1759346617443
- dev-depfu/update/composer/phpunit/phpunit-12.3.12
- dev-depfu/update/composer/phpunit/phpunit-11.5.38
- dev-depfu/update/composer/pestphp/pest-4.1.0
- dev-depfu/update/composer/phpunit/phpunit-12.3.8
- dev-depfu/update/composer/pestphp/pest-4.0.4
- dev-depfu/update/composer/phpunit/phpunit-11.5.35
- dev-depfu/update/composer/pestphp/pest-plugin-laravel-4.0.0
- dev-depfu/update/composer/phpunit/phpunit-12.3.6
- dev-depfu/update/composer/pestphp/pest-4.0.3
- dev-depfu/update/composer/pestphp/pest-4.0.2
- dev-depfu/update/composer/pestphp/pest-3.8.4
- dev-depfu/update/composer/pestphp/pest-4.0.0
- dev-depfu/update/composer/orchestra/testbench-10.6.0
- dev-depfu/update/composer/pestphp/pest-3.8.3
- dev-depfu/update/composer/phpunit/phpunit-10.5.52
- dev-depfu/update/composer/phpunit/phpunit-10.5.51
- dev-depfu/update/composer/phpunit/phpunit-12.3.3
- dev-depfu/update/composer/phpunit/phpunit-10.5.49
- dev-depfu/update/composer/phpunit/phpunit-12.3.0
- dev-depfu/update/composer/phpunit/phpunit-11.5.28
- dev-depfu/update/composer/phpunit/phpunit-12.2.8
- dev-depfu/update/composer/phpunit/phpunit-10.5.48
- dev-depfu/update/composer/phpunit/phpunit-11.5.26
- dev-depfu/update/composer/phpunit/phpunit-11.5.25
- dev-depfu/update/composer/nunomaduro/collision-8.8.2
- dev-depfu/update/composer/phpunit/phpunit-12.2.3
- dev-depfu/update/composer/orchestra/testbench-10.4.0
- dev-depfu/update/composer/phpunit/phpunit-11.5.23
- dev-depfu/update/composer/nunomaduro/collision-8.8.1
- dev-depfu/update/composer/phpunit/phpunit-12.2.1
- dev-depfu/update/composer/phpunit/phpunit-11.5.22
- dev-depfu/update/composer/phpunit/phpunit-11.5.21
- dev-depfu/update/composer/phpunit/phpunit-11.5.20
- dev-depfu/update/composer/orchestra/testbench-9.13.1
- dev-depfu/update/composer/orchestra/testbench-8.35.1
- dev-depfu/update/composer/phpunit/phpunit-11.5.18
- dev-depfu/update/composer/pestphp/pest-plugin-laravel-3.2.0
- dev-depfu/update/composer/orchestra/testbench-10.2.1
- dev-depfu/update/composer/pestphp/pest-3.8.2
- dev-depfu/update/composer/phpunit/phpunit-12.1.2
- dev-depfu/update/composer/phpunit/phpunit-12.1.0
- dev-depfu/update/composer/pestphp/pest-3.8.1
- dev-depfu/update/composer/nunomaduro/collision-8.8.0
- dev-depfu/update/composer/pestphp/pest-3.8.0
- dev-depfu/update/composer/phpunit/phpunit-11.5.15
- dev-depfu/update/composer/phpunit/phpunit-11.5.14
- dev-depfu/update/composer/nunomaduro/collision-7.12.0
- dev-depfu/update/composer/phpunit/phpunit-11.5.12
- dev-depfu/update/composer/orchestra/testbench-8.34.0
- dev-depfu/update/composer/phpunit/phpunit-11.5.11
- dev-depfu/update/composer/orchestra/testbench-10.0.0
- dev-depfu/update/composer/phpunit/phpunit-11.5.10
- dev-depfu/update/composer/phpunit/phpunit-11.5.9
- dev-depfu/update/composer/orchestra/testbench-9.11.0
- dev-depfu/update/composer/phpunit/phpunit-11.5.8
- dev-depfu/update/composer/orchestra/testbench-9.10.0
- dev-depfu/update/composer/phpunit/phpunit-12.0.2
- dev-depfu/update/composer/phpunit/phpunit-12.0.1
- dev-depfu/update/composer/phpunit/phpunit-10.5.45
- dev-depfu/update/composer/phpunit/phpunit-10.5.44
- dev-depfu/update/composer/phpunit/phpunit-10.5.43
- dev-depfu/update/composer/pestphp/pest-plugin-laravel-3.1.0
- dev-depfu/update/composer/pestphp/pest-3.7.3
- dev-depfu/update/composer/nunomaduro/collision-8.6.0
- dev-depfu/update/composer/pestphp/pest-3.7.2
- dev-depfu/update/composer/phpunit/phpunit-11.5.3
- dev-depfu/update/composer/phpunit/phpunit-10.5.40
- dev-depfu/update/composer/orchestra/testbench-9.8.0
- dev-depfu/update/composer/pestphp/pest-3.7.1
- dev-depfu/update/composer/phpunit/phpunit-11.5.0
- dev-depfu/update/composer/pestphp/pest-3.6.0
- dev-depfu/update/composer/phpunit/phpunit-11.4.4
- dev-depfu/update/composer/orchestra/testbench-9.6.1
- dev-depfu/update/composer/orchestra/testbench-8.28.0
- dev-depfu/update/composer/pestphp/pest-3.5.1
- dev-depfu/update/composer/phpunit/phpunit-10.5.38
- dev-depfu/update/composer/pestphp/pest-3.5.0
- dev-depfu/update/composer/phpunit/phpunit-10.5.37
- dev-depfu/update/composer/nunomaduro/collision-8.5.0
- dev-depfu/update/composer/pestphp/pest-plugin-laravel-3.0.0
- dev-depfu/update/composer/orchestra/testbench-8.26.0
- dev-depfu/update/composer/pestphp/pest-2.35.1
- dev-depfu/update/composer/orchestra/testbench-8.25.0
- dev-depfu/update/composer/phpunit/phpunit-10.5.30
- dev-depfu/update/composer/phpunit/phpunit-11.3.1
- dev-depfu/update/composer/nunomaduro/collision-8.4.0
- dev-depfu/update/composer/pestphp/pest-2.35.0
- dev-depfu/update/composer/phpunit/phpunit-11.3.0
- dev-depfu/update/composer/phpunit/phpunit-11.2.9
- dev-depfu/update/composer/phpunit/phpunit-10.5.29
- dev-depfu/update/composer/phpunit/phpunit-10.5.28
- dev-depfu/update/composer/nunomaduro/collision-8.3.0
- dev-depfu/update/composer/phpunit/phpunit-10.5.26
- dev-depfu/update/composer/phpunit/phpunit-10.5.25
- dev-depfu/update/composer/phpunit/phpunit-10.5.24
- dev-depfu/update/composer/phpunit/phpunit-10.5.22
- dev-depfu/update/composer/phpunit/phpunit-10.5.21
- dev-depfu/update/composer/pestphp/pest-2.34.8
- dev-depfu/update/composer/phpunit/phpunit-11.2.0
- dev-depfu/update/composer/orchestra/testbench-9.1.2
- dev-depfu/update/composer/orchestra/testbench-9.1.1
- dev-depfu/update/composer/orchestra/testbench-8.23.0
- dev-depfu/update/composer/pestphp/pest-plugin-laravel-2.4.0
- dev-depfu/update/composer/phpunit/phpunit-10.5.19
- dev-depfu/update/composer/orchestra/testbench-9.0.4
- dev-depfu/update/composer/phpunit/phpunit-10.5.18
- dev-depfu/update/composer/phpunit/phpunit-11.1.1
- dev-depfu/update/composer/pestphp/pest-2.34.7
- dev-depfu/update/composer/phpunit/phpunit-10.5.16
- dev-depfu/update/composer/orchestra/testbench-9.0.3
- dev-depfu/update/composer/pestphp/pest-2.34.5
- dev-depfu/update/composer/phpunit/phpunit-10.5.15
- dev-depfu/update/composer/orchestra/testbench-8.22.1
- dev-depfu/update/composer/orchestra/testbench-8.22.0
- dev-depfu/update/composer/pestphp/pest-2.34.2
- dev-depfu/update/composer/phpunit/phpunit-10.5.12
- dev-depfu/update/composer/nunomaduro/collision-8.1.1
- dev-depfu/update/composer/phpunit/phpunit-11.0.4
- dev-feature/add-rebuild-command
- dev-feature/48-implement-fallback-locale
This package is auto-updated.
Last update: 2025-12-06 08:35:39 UTC
README
Generate unique urls for blogs, ecommerce and platforms without prefix.
Supports Laravel 9-12 | PHP 8.1-8.4
✨ Features
-
✅ Auto-trim slashes - Automatically removes leading/trailing slashes to prevent 404 errors
-
✅ Custom exceptions - Detailed error messages with model context for easier debugging
-
✅ Optional validation - Enforce slug format rules and reserved slug protection
-
✅ Progress tracking - Visual progress bars for large URL generation operations
-
✅ Multi-language support - Different URLs for different languages, not just prefixes
-
✅ Automatic redirects - Old URLs automatically redirect to new ones when updated
-
✅ Hierarchical URLs - Support for URLs with parent relationships (e.g., category/product)
-
✅ Livewire integration - Full support for Livewire full-page components
-
✅ Batch operations - Efficient URL generation for thousands of models
Goals:
- When create or update a model to generate a unique url based on urlStrategy() function inside each model
- Possibility to have different urls for the different languages (not only a prefix in the beginning)
- If the url exists to create a new url with suffix _1, _2, etc.
- If we update the model to create a redirect from the old to the new url
- If there is a multiple redirects to redirect only to the last one
- Possibility to have an url depending on relations (category-name/product-name)
Installation
You can install the package via composer:
composer require vlados/laravel-unique-urls
You can publish and run the migrations with:
php artisan vendor:publish --tag="laravel-unique-urls-migrations"
php artisan migrate
Usage
Configuration
You can publish the config file with:
php artisan vendor:publish --tag="laravel-unique-urls-config"
This will create config/unique-urls.php with all available options:
return [ /* |-------------------------------------------------------------------------- | Available Languages |-------------------------------------------------------------------------- | | Define the available languages for URL generation. | Format: 'locale' => 'language_code' | */ 'languages' => [ 'bg_BG' => 'bg', 'en_US' => 'en', 'de_DE' => 'de', ], /* |-------------------------------------------------------------------------- | Redirect HTTP Code |-------------------------------------------------------------------------- | | HTTP status code used when redirecting from old URLs to new URLs. | Default: 301 (Permanent Redirect) | */ 'redirect_http_code' => 301, /* |-------------------------------------------------------------------------- | Auto-Trim Slashes |-------------------------------------------------------------------------- | | Automatically trim leading and trailing slashes from slugs. | This prevents 404 errors when urlStrategy() returns slugs with slashes. | Recommended: true | */ 'auto_trim_slashes' => true, /* |-------------------------------------------------------------------------- | Validate Slugs |-------------------------------------------------------------------------- | | Enable strict validation of slug format. When enabled, slugs must only | contain lowercase letters, numbers, and hyphens. Invalid characters | will throw an InvalidSlugException. | Default: false | */ 'validate_slugs' => false, /* |-------------------------------------------------------------------------- | Reserved Slugs |-------------------------------------------------------------------------- | | List of reserved slugs that cannot be used for URL generation. | Common examples: admin, api, login, etc. | */ 'reserved_slugs' => [ 'admin', 'api', 'login', 'logout', 'register', 'password', 'dashboard', ], /* |-------------------------------------------------------------------------- | Batch Size |-------------------------------------------------------------------------- | | Number of records to process in a single batch when generating URLs. | Larger values use more memory but may be faster. | Default: 500 | */ 'batch_size' => 500, /* |-------------------------------------------------------------------------- | Auto Generate on Create |-------------------------------------------------------------------------- | | Automatically generate URLs when a model is created. | Models can override this with isAutoGenerateUrls() method. | Default: true | */ 'auto_generate_on_create' => true, /* |-------------------------------------------------------------------------- | Create Redirects |-------------------------------------------------------------------------- | | Automatically create redirect entries when URLs are changed. | This ensures old URLs continue to work (recommended for SEO). | Default: true | */ 'create_redirects' => true, ];
Prepare your model
In your Model add these methods:
class MyModel extends Model { use Vlados\LaravelUniqueUrls\HasUniqueUrls; public function urlStrategy($language,$locale): string { return Str::slug($this->getAttribute('name'),"-",$locale); } public function urlHandler(): array { return [ // The controller used to handle the request 'controller' => CategoryController::class, // The method 'method' => 'view', // additional arguments sent to this method 'arguments' => [], ]; }
The method for handling the request:
public function view(Request $request, $arguments = []) { dd($arguments); }
Routes
And last, add this line at the end of your routes/web.php
Route::get('{urlObj}', [\Vlados\LaravelUniqueUrls\LaravelUniqueUrlsController::class, 'handleRequest'])->where('urlObj', '.*');
Batch import
If for example you have category tree and you need to import all the data before creating the urls, you can disable the automatic generation of the url on model creation
To disable automatically generating the urls on create or update overwrite the method isAutoGenerateUrls in the model:
public function isAutoGenerateUrls(): bool { return false; }
and call generateUrl() later like this:
YourModel::all()->each(function (YourModel $model) { $model->generateUrl(); });
or if you want to disable it on the go, use
$model = new TestModel(); $model->disableGeneratingUrlsOnCreate(); $model->name = "Test"; $model->save();
Livewire
To use Livewire full-page component to handle the request, first set in urlHandler() function in your model:
public function urlHandler(): array { return [ // The Livewire controller 'controller' => CategoryController::class, // The method should be empty 'method' => '', // additional arguments sent to the mount() function 'arguments' => [], ]; }
Example livewire component:
class LivewireComponentExample extends Component { private Url $urlModel; private array $url_arguments; public function mount(Url $urlObj, $arguments = []) { $this->urlModel = $urlObj; $this->url_arguments = $arguments; } public function render() { return view('livewire.view-category'); } }
API
| Methods | Description | Parameters |
|---|---|---|
| generateUrl() | Generate manually the URL for a single model | |
| generateUrlsInBatch() (static) | Generate URLs for multiple models with memory optimization | $models, $chunkSize = 500, $callback = null |
| getSlug() | Get the URL for a specific language in relative or absolute format | ?string $language = '', bool $relative = true |
| urlStrategy | The strategy for creating the URL for the model | $language, $locale |
| isAutoGenerateUrls() | Check if URLs should be generated automatically for the model | |
| disableGeneratingUrlsOnCreate() | Disable generating urls on creation for this instance | |
| Properties | ||
| relative_url | The url path, relative to the site url | |
| absolute_url | The absolute url, including the domain | |
| Relations | ||
| urls() | All the active urls, related to the current model |
Getting Model URLs
// Get relative URL for current locale $model->relative_url; // e.g., "my-product-name" // Get absolute URL for current locale $model->absolute_url; // e.g., "https://example.com/my-product-name" // Get URL for specific language (relative) $model->getSlug('en', true); // e.g., "my-product-name" // Get URL for specific language (absolute) $model->getSlug('en', false); // e.g., "https://example.com/my-product-name" // Get URL for current locale (defaults to app locale) $model->getSlug(); // e.g., "my-product-name"
Batch URL Generation
For processing large numbers of models efficiently, use the static generateUrlsInBatch() method:
use App\Models\Product; // Basic usage: generate URLs for all products without URLs $products = Product::whereDoesntHave('urls')->get(); $stats = Product::generateUrlsInBatch($products); // Returns: ['generated' => 150, 'skipped' => 0, 'failed' => 0] // With custom chunk size for memory optimization $products = Product::all(); $stats = Product::generateUrlsInBatch($products, chunkSize: 100); // With progress callback use Illuminate\Support\Facades\Log; $products = Product::all(); $stats = Product::generateUrlsInBatch( $products, chunkSize: 500, callback: function ($model, $processed, $total, $stats) { if ($processed % 100 === 0) { Log::info("URL generation progress: {$processed}/{$total}", $stats); } } ); // With progress bar in command $products = Product::all(); $bar = $this->output->createProgressBar($products->count()); $stats = Product::generateUrlsInBatch( $products, callback: function () use ($bar) { $bar->advance(); } ); $bar->finish(); $this->info("Generated: {$stats['generated']}, Skipped: {$stats['skipped']}, Failed: {$stats['failed']}");
Benefits:
- Automatic memory management with garbage collection
- Progress tracking via callback
- Error handling - continues processing even if some models fail
- Returns statistics about the operation
- Processes models in configurable chunk sizes
Commands
urls:generate
This command generates unique URLs for the specified model or all models that implement the HasUniqueUrls trait.
Usage:
php artisan urls:generate [options]
Options:
--model=ModelName- Generate URLs only for a specific model (accepts full class name or short name)--fresh- Truncate URLs table and regenerate all URLs from scratch--only-missing- Only generate URLs for models that don't have URLs yet--chunk-size=500- Number of records to process per chunk (default: 500)--force- Force generation even ifisAutoGenerateUrls()returns false
Examples:
# Generate URLs for all models php artisan urls:generate # Generate URLs only for Product model php artisan urls:generate --model="App\Models\Product" # Generate only missing URLs for Product model php artisan urls:generate --model=Product --only-missing # Fresh generation with custom chunk size php artisan urls:generate --fresh --chunk-size=1000 # Force generation for models with isAutoGenerateUrls() = false php artisan urls:generate --model=Product --force
Enhanced Output:
The command now provides detailed feedback:
$ php artisan urls:generate --model=Product Generating URLs for App\Models\Product... ├─ Found: 14,031 models ├─ With URLs: 14,030 ├─ Without URLs: 1 1/1 [============================] 100% | Generating... ├─ Generated: 1 URL └─ ✓ Completed ═══════════════════════════════════════ Summary ═══════════════════════════════════════ Generated: 1 URLs Duration: 0.5s ═══════════════════════════════════════ All done ✓
Warning for isAutoGenerateUrls():
If a model has isAutoGenerateUrls() returning false, you'll see:
⚠ App\Models\Product has isAutoGenerateUrls() = false URLs will not be generated automatically. Use --force flag to generate anyway, or enable in model.
urls:doctor
This command checks if all models have implemented the HasUniqueUrls trait and required functions correctly.
Usage:
php artisan urls:doctor [--model=ModelName]
Checks:
- Verifies all required methods are implemented with correct parameters
- Validates that urlHandler() returns correct controller and method
- Checks if urlStrategy() generates unique URLs for all languages
- Detects common implementation issues
Common Issues
URLs have leading/trailing slashes causing 404 errors
Automatic Fix (v1.1.0+): Leading and trailing slashes are now automatically trimmed.
Before:
// urlStrategy() returns: '/product-name/' or null . '/' . 'product' = '/product' // Result: 404 error ❌
After (v1.1.0+):
// urlStrategy() returns: '/product-name/' or '/product' // Automatically trimmed to: 'product-name' or 'product' ✅ // Warning logged for debugging
Solution:
Update to v1.1.0+ or fix your urlStrategy() method:
public function urlStrategy($language, $locale): string { // ✅ Good: Returns clean slug return Str::slug($this->name); // ❌ Avoid: Returns slug with slashes // return '/' . Str::slug($this->name) . '/'; // ✅ OK: Auto-trimmed (but will log warning) // return $this->parent?->getSlug() . '/' . Str::slug($this->name); }
Empty slug exceptions
Error message (v1.1.0+):
Cannot generate URL: empty slug for App\Models\Product (ID: 123).
Check the urlStrategy() method returns a non-empty string.
Common causes:
urlStrategy()returns empty string or null- Slug becomes empty after trimming (e.g., only slashes:
'/') - Model attribute used in slug is empty
Solution:
public function urlStrategy($language, $locale): string { // ✅ Ensure name is not empty if (empty($this->name)) { throw new \Exception('Cannot generate URL: product name is empty'); } return Str::slug($this->name); }
TypeError when using url() helper with model URLs
Error:
TypeError: Argument #2 ($url) must be of type ?string, Illuminate\Routing\UrlGenerator given
Cause: Passing null to url() helper (fixed in v1.1.0+)
Solution (v1.1.0+):
// ✅ Safe: getSlug() returns empty string instead of null $url = url($model->relative_url); // Works even if no URL exists // ✅ Safe: Use null coalescing for older versions $url = url($model->relative_url ?? '');
URLs not generating automatically
Check these:
- Does your model use the
HasUniqueUrlstrait? - Did you implement
urlStrategy()andurlHandler()methods? - Is
isAutoGenerateUrls()returningtrue(or not defined)? - Is the catch-all route registered in
routes/web.php?
Force generation:
# Generate URLs even if isAutoGenerateUrls() = false
php artisan urls:generate --model=Product --force
Reserved slug errors
Error (when validation enabled):
Invalid slug 'admin' for App\Models\Page (ID: 1): slug is reserved.
Reserved slugs are defined in config/unique-urls.php. Choose a different slug.
Solution:
Either change the slug or remove it from reserved list in config/unique-urls.php:
'reserved_slugs' => [ // 'admin', // Remove if you need to use this slug 'api', 'login', ],
Performance Tips
For large datasets (10,000+ models):
1. Use chunking:
# Process in smaller chunks to reduce memory usage
php artisan urls:generate --model=Product --chunk-size=100
2. Generate only missing URLs:
# Skip existing URLs to save time
php artisan urls:generate --only-missing
3. Disable during imports:
// Disable auto-generation during bulk imports public function isAutoGenerateUrls(): bool { return false; } // Generate URLs after import completes Product::all()->each(fn($p) => $p->generateUrl());
4. Use database indexing:
// Add indexes to urls table for better performance Schema::table('urls', function (Blueprint $table) { $table->index('slug'); $table->index(['related_type', 'related_id']); $table->index('language'); });
5. Cache URL queries:
// Cache frequently accessed URLs $url = Cache::remember( "product_url_{$product->id}", now()->addHour(), fn() => $product->relative_url );
Memory optimization:
Process large datasets in chunks:
// Instead of: Product::all()->each(fn($p) => $p->generateUrl()); // ❌ Loads all into memory // Use chunking: Product::chunk(500, function ($products) { // ✅ Processes 500 at a time foreach ($products as $product) { $product->generateUrl(); } });
Testing
Running Package Tests
composer test
Testing Helpers for Your Application
The package provides a testing trait with helpful assertions for testing URL generation in your application.
Setup
In your test class, use the AssertsUniqueUrls trait:
use Tests\TestCase; use Vlados\LaravelUniqueUrls\Testing\AssertsUniqueUrls; class ProductTest extends TestCase { use AssertsUniqueUrls; public function test_product_has_url() { $product = Product::factory()->create(['name' => 'Test Product']); $this->assertHasUrl($product); } }
Available Assertions
// Assert model has at least one URL $this->assertHasUrl($product); // Assert model has no URLs $this->assertHasNoUrl($product); // Assert URL doesn't start with slash $this->assertNoLeadingSlash($product); $this->assertNoLeadingSlash($product, language: 'en'); // Assert URL doesn't end with slash $this->assertNoTrailingSlash($product); // Assert URL is accessible (doesn't return 404) $this->assertUrlIsAccessible($product); // Assert model has URL for specific language $this->assertHasUrlForLanguage($product, 'en'); $this->assertHasUrlForLanguage($product, 'bg'); // Assert URL matches expected slug $this->assertUrlEquals($product, 'test-product'); $this->assertUrlEquals($product, 'test-product', language: 'en'); // Assert slug is unique in database $this->assertSlugIsUnique('test-product'); // Assert URL contains substring $this->assertUrlContains($product, 'test'); // Assert URL matches regex pattern $this->assertUrlMatchesPattern($product, '/^[a-z0-9\-]+$/'); // Assert model has URLs for all configured languages $this->assertHasUrlsForAllLanguages($product);
Example Test Cases
public function test_product_url_generation() { $product = Product::factory()->create(['name' => 'Test Product']); // Basic assertions $this->assertHasUrl($product); $this->assertUrlEquals($product, 'test-product'); // Format assertions $this->assertNoLeadingSlash($product); $this->assertNoTrailingSlash($product); // Multi-language support $this->assertHasUrlsForAllLanguages($product); $this->assertHasUrlForLanguage($product, 'en'); $this->assertHasUrlForLanguage($product, 'bg'); } public function test_product_url_is_accessible() { $product = Product::factory()->create(['name' => 'Accessible Product']); $this->assertUrlIsAccessible($product); } public function test_category_product_url_structure() { $category = Category::factory()->create(['name' => 'Electronics']); $product = Product::factory()->create([ 'name' => 'Laptop', 'category_id' => $category->id, ]); // Assert URL contains category name $this->assertUrlContains($product, 'electronics'); // Assert URL matches hierarchical pattern $this->assertUrlMatchesPattern($product, '/^electronics\/.+$/'); } public function test_url_slug_uniqueness() { $product1 = Product::factory()->create(['name' => 'Unique Product']); $product2 = Product::factory()->create(['name' => 'Unique Product']); // Second product should get a unique slug with suffix $this->assertUrlEquals($product1, 'unique-product'); $this->assertUrlEquals($product2, 'unique-product_1'); }
Custom Messages
All assertions support custom failure messages:
$this->assertHasUrl( $product, message: 'Product should have a URL after creation' ); $this->assertUrlEquals( $product, 'expected-slug', message: 'Product URL should match the expected format' );
TODO
Planned Features
- Pest 4.x Compatibility - Investigate and fix compatibility issues with Pest 4.1.6+
- URL Versioning - Track URL change history with timestamps and reasons
- Sitemap Generation - Automatic sitemap.xml generation command
- URL Analytics - Track URL hits and redirects for insights
- Soft Delete Support - Handle URLs for soft-deleted models
- Custom Redirect Rules - Support for regex-based redirect patterns
- URL Preview/Dry Run - Preview what URLs would be generated before committing
- Duplicate Detection - Better handling of potential slug conflicts
- URL Health Check - Command to verify all URLs are accessible
- Performance Dashboard - Artisan command showing URL generation statistics
- API Documentation - OpenAPI/Swagger documentation for API methods
- GraphQL Support - Integration with Laravel Lighthouse
- Queue Support - Queue URL generation for large batches
- Event System - Fire events on URL creation, update, and redirect
- URL Aliases - Support multiple URLs pointing to same resource
- URL Templates - Define URL patterns in config
- SEO Analyzer - Check URLs for SEO best practices
- Multi-Tenant Support - Tenant-specific URL handling
- URL Expiration - Support for temporary URLs
- Wildcard Routes - Support for pattern-based dynamic routes
Nice to Have
- Admin UI - Simple web interface for managing URLs
- Import/Export - Import URLs from CSV/JSON
- URL Shortener Integration - Generate short URLs automatically
- Custom Slug Transformers - Plugin system for custom slug generation
- URL Monitoring - Integration with monitoring tools (Sentry, etc.)
Documentation
- Video tutorials for common use cases
- Migration guide from other URL packages
- Performance benchmarks
- Integration examples with popular packages
- Troubleshooting guide with common errors
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Semantic Commit Messages
See how a minor change to your commit message style can make you a better programmer.
Format: <type>(<scope>): <subject>
<scope> is optional
feat: add hat wobble
^--^ ^------------^
| |
| +-> Summary in present tense.
|
+-------> Type: chore, docs, feat, fix, refactor, style, or test.
More Examples:
feat: (new feature for the user, not a new feature for build script)fix: (bug fix for the user, not a fix to a build script)docs: (changes to the documentation)style: (formatting, missing semi colons, etc; no production code change)refactor: (refactoring production code, eg. renaming a variable)test: (adding missing tests, refactoring tests; no production code change)chore: (updating grunt tasks etc; no production code change)
References:
- https://www.conventionalcommits.org/
- https://seesparkbox.com/foundry/semantic_commit_messages
- http://karma-runner.github.io/1.0/dev/git-commit-msg.html
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.