Download the PHP package jobmetric/laravel-url without Composer
On this page you can find all versions of the php package jobmetric/laravel-url. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Informations about the package laravel-url
Url And Slug for laravel Model
It is a package for url and slug storage management in each model that you can use in your Laravel projects.
Install via composer
Run the following command to pull in the latest version:
Documentation
This package gives each Eloquent model:
One canonical slug stored in a dedicated slugs
table (polymorphic one-to-one).
A versioned full URL history stored in the urls
table (polymorphic one-to-many with soft deletes).
Automatic syncing of URLs when the slug or any parent path segment changes.
Global uniqueness for active full URLs, enforced at the application layer.
Soft-delete/restore support for slugs and URLs, with conflict checks on restore.
Run migrations once after installing the package:
HasUrl Trait
1) Quick Start
1.1 Add the trait and implement UrlContract
HasUrl
relies on your model implementing UrlContract
(i.e. it must expose how to compute its full URL).
Why
UrlContract?
The package must know the current full path for the model.
getFullUrl()
is your canonical builder for that path.
1.2 Create a record and assign a slug
You don’t set columns on the urls
table yourself. You only provide a slug
(optional slug_collection
) and the package will do the rest.
- The package slugifies and length-limits your input (100 chars).
- It stores one row in slugs (per model).
- It creates version=1 in urls.
- It guarantees no active conflict with another model’s active full URL.
1.3 Read back the slug and current full URL
2) What the package does for you
-
Keeps one slug per model. (Polymorphic
slugs
table) -
Tracks full URL history. (
urls
rows are versioned; active row = highestversion
withdeleted_at NULL
) -
Auto-version on changes. If the computed full URL changes, the previous URL is soft-deleted and a new version is inserted.
-
Conflict safety.
-
Throws
SlugConflictException
if another record uses the same slug in the same collection. - Throws
UrlConflictException
if another active record uses the same full URL.
-
-
Cascade refresh. If your model’s URL affects children, you can cascade (see §5).
- Delete/restore aware. Soft delete removes active slug/URL from public view; restore validates conflicts and re-syncs.
3) Slugs & Collections
3.1 One slug, optional collection
Each model gets exactly one slug row. You may tag it with a collection (e.g. to group slugs by context).
If you omit the collection:
-
If your model defines
getSlugCollectionDefault(): ?string
, it will be used. -
Else, if your model has an attribute
type
, that value becomes the collection. - Else, collection is
NULL
.
3.2 Reading by collection
3.3 Finding models by slug
3.4 Removing the slug
Note: A model without a slug can still compute a full URL if your
getFullUrl()
does not depend on it, but most setups will.
4) Versioned full URLs
4.1 Automatic syncing
You do not call any URL method on normal saves; syncing happens transparently in the trait:
-
On
saving
: caches the pre-save full URL. - On
saved
:- upserts the slug (if provided this request),
- computes the new full URL via
getFullUrl()
, - if first time → insert version
1
, - if changed → soft-delete previous active row and insert
version+1
, - fires
UrlChanged
event with(model, oldFullUrl|null, newFullUrl, newVersion)
.
4.2 Read the current URL or full history
4.3 Resolving owners and redirects
Example redirect middleware (simplified):
5) Cascading URL updates to descendants
If a parent’s path segment changes (e.g., a Category slug), you may need to refresh child URLs (e.g., Products). The trait supports this via an optional method on your model:
When the parent’s slug changes, HasUrl
will:
- Re-compute each child’s
getFullUrl()
, - Version and insert the new active URL if changed,
- Throw
UrlConflictException
if a child’s new full URL conflicts with another model.
Temporarily disable cascade:
6) Soft delete, restore, and force delete
-
Soft delete the parent model
- The single
slugs
row is soft-deleted. -
All active
urls
rows are soft-deleted.(No model will claim the path anymore.)
- The single
-
Restore the parent model
- Before restoring, it checks slug conflicts (same type & collection).
-
On restored, it restores the
slugs
row and re-syncs the URL.If the full URL is already taken by another active record, it throws
UrlConflictException
.
- Force delete the parent model
- Permanently removes its slug and all URL history.
Examples
7) Rebuilding URLs in bulk
Useful after changing your getFullUrl()
logic or migrating data.
- Processes in chunks.
- Calls the same versioning logic as normal saves.
- Does not trigger your model’s
saved()
hooks or cascades (it directly re-syncs).
8) Exceptions you should know
-
ModelUrlContractNotFoundException
Your model must implement
UrlContract
. The trait checks this at boot. -
SlugConflictException
Another model of the same type already uses this slug (in the same collection). Handle this when calling
dispatchSlug()
or when restoring. -
UrlConflictException
Another active model already owns the computed full URL. Can be thrown during saves, cascades, rebuilds, or restore.
Example handling
9) API Reference (trait helpers)
Methods below are provided by
HasUrl
unless otherwise noted.
Slug methods
URL methods
Finders
Bulk operations
Cascade control
10) Real-World Examples
10.1 Category → Product path dependency with cascade
10.2 Changing only the collection
10.3 Handling a user-entered duplicate slug
10.4 301 redirects from old URLs
Combine resolveRedirectTarget()
with a middleware (see §4.3). This protects SEO when URLs change over time.
11) Recommended database indexes
These are already baked into the package’s migrations. If you maintain your own schema, consider the following:
-
slugs
table- Unique composite:
(slugable_type, slugable_id, deleted_at)
- Optional unique composite for multi-collection setups:
(slugable_type, collection, slug, deleted_at)
- Unique composite:
urls
table- Unique composite:
(urlable_type, urlable_id, version)
- Index on
full_url
with a filter ondeleted_at
can speed up conflict checks.
- Unique composite:
12) Events
UrlChanged
- Fired after a new active URL row is created.
- Signature:
new UrlChanged(Model&UrlContract $model, ?string $old, string $new, int $version)
Use it to update caches, ping search engines, or trigger webhooks.
13) Testing tips
- When asserting URL changes, check both:
- The active URL row (
deleted_at NULL
, highestversion
). - The soft-deleted previous row for redirection logic.
- The active URL row (
- If you test cascades, ensure child models also implement
UrlContract
.
Fallback Route (Smart URL Resolver)
This package can register a single Laravel fallback route that resolves any unmatched path using the versioned urls
table:
- If there’s an active URL row → it fires an
UrlMatched
event so your app can decide what to return (product page, category page, CMS page, JSON, etc.). - If there’s only a legacy (soft-deleted) match → it issues a 301 redirect to the current canonical URL (SEO-friendly).
- Otherwise → returns a translated 404.
Enabling / Disabling
The fallback is on by default. Control it in config/url.php
:
The provider wires it for you:
How it resolves paths
For a request like GET /shop/laptops/mbp-14?color=silver
, the controller builds these candidates and looks them up (most recent first):
shop/laptops/mbp-14
shop/laptops/mbp-14/
/shop/laptops/mbp-14
/shop/laptops/mbp-14/
/
(root special-case for empty paths)
Then it:
- Tries to find an active URL (
deleted_at NULL
, latestversion
). - If not found, checks legacy URLs (soft-deleted) and redirects (301) to the canonical URL of the same model (preserving query string).
- If still nothing, returns 404 (translated:
trans('url::base.exceptions.not_found')
).
The UrlMatched
Event
When an active Url
row is found, the controller emits:
Useful properties:
$event->request
— the incomingIlluminate\Http\Request
$event->url
— the matchedUrl
row (active)$event->urlable
— the polymorphic model instance (e.g., Product, Category)$event->collection
— optional URL collection string$event->response
— initiallynull
. Your listener must set it via$event->respond($response)
to short-circuit and return the response.
If no listener sets a response, the controller returns 404.
Writing Listeners (Many Ways)
You can wire listeners in the EventServiceProvider, or use Event::listen
at boot time, or register a dedicated invokable class. Below are several patterns with realistic content.
1) Quick inline listener (closure) — Product page
app/Providers/EventServiceProvider.php
app/Http/Controllers/ProductController.php
2) Another closure — Category page with pagination
app/Http/Controllers/CategoryController.php
3) Invokable listener class — clean separation
app/Listeners/HandleMatchedUrl.php
app/Providers/EventServiceProvider.php
4) Listener that does a custom redirect
Use this when you want to override the canonical route entirely.
5) Listener returning an API response (JSON)
6) Using route model binding after match (optional pattern)
You can also “bridge” into a named route:
Security, Middleware & Guards
You can add auth, localization, throttling, etc., by stacking middleware in config/url.php
:
If a listener must be protected:
Redirects from Legacy URLs (Built-in)
When a previously active URL becomes obsolete (soft-deleted), the fallback will automatically 301 to the model’s current canonical URL. Query strings are preserved:
- Request:
/old/path?ref=fb
- Redirect:
301 → /new/path?ref=fb
You don’t need to configure anything for this behavior; it’s baked into the controller.
Testing Recipes
1) Exactly one event dispatched per request
If you see a
404
in the test, it usually means no listener set a response. Make sure your listener calls$event->respond(...)
.
2) Legacy redirect
3) JSON response when Accept: application/json
Troubleshooting
-
I get 404 on a known URL
- Ensure a listener sets a response via
$event->respond(...)
. - Confirm the URL row is active (not soft-deleted) and the model is present.
- Check that
register_fallback
istrue
and the middleware group includesweb
(or your session/localization needs).
- Ensure a listener sets a response via
-
Infinite redirects
- Don’t redirect the same matched path to itself.
- If you redirect into another path that also resolves to the same model, consider returning the view instead of chaining redirects.
- Wrong page returned
- Check your “router” logic in the listener (e.g.,
instanceof
checks). - Verify your model implements
UrlContract
and thatgetFullUrl()
computes the intended canonical path.
- Check your “router” logic in the listener (e.g.,
Example: Full Setup Summary
-
Models implement
UrlContract
and useHasUrl
. -
Assign slugs (versioned URL is created automatically).
-
Register listeners to render pages.
- Enjoy free 301 redirects for old paths (no extra code).
With this fallback + event pattern, you get one URL entry point that can render anything (products, categories, blogs, CMS pages) based on the database — while preserving SEO via automatic legacy redirects, and keeping your controllers and routes tidy.
Validation: SlugExistRule
SlugExistRule
validates that a slug is unique for a given model class and optional collection, ignoring soft-deleted rows and optionally excluding the current record (useful for update forms).
Despite the name, it enforces
uniqueness
(it fails if a matching active slug already exists). It alsonormalizes
the incoming value exactly like the trait does:Str::slug(trim($value))
and limits it to 100 chars before checking.
Constructor
- $className: your Eloquent model FQCN (e.g.,
App\Models\Product::class
). - $collection: pass
null
for the default collection; pass a non-empty string to scope by collection. - $objectId: exclude the current record when updating so the user can keep the same slug.
The rule queries the slugs
table with:
slugable_type = $className
collection = $collection
(orNULL
if omitted)deleted_at IS NULL
(only active rows)slug = normalized($value)
slugable_id != $objectId
(when provided)
If a row exists, validation fails with trans('url::base.rule.exist')
.
Why use this rule?
- Same normalization as
HasUrl
→ your validation view matches what will be stored. - Active-only uniqueness → allows reusing slugs from soft-deleted records.
- Update-safe → exclude the current record by ID.
- Prevents late exceptions → catch conflicts before calling
dispatchSlug()
.
Common Recipes
1) Create request: simple uniqueness in a fixed collection
Tip: Set
max:100
to align with internal normalization (Str::limit(..., 100)
).
2) Update request: exclude current record
If the slug did not change, the rule allows it (because it excludes $productId
).
3) Dynamic collection from request (or model type)
If you omit the collection entirely when you later call dispatchSlug()
, the trait will fall back to your model’s getSlugCollectionDefault()
or its type
attribute (see the HasUrl docs).
4) Programmatic validation (no FormRequest)
5) Nested payloads or admin panels
Error Messages
By default, failures use trans('url::base.rule.exist')
. You can override per-field:
Or customize the translation key in resources/lang/{locale}/url/base.php
:
End-to-End Example
Controller (simplified):
- The request ensures pre-flight uniqueness with
SlugExistRule
. dispatchSlug()
will upsert theslugs
row and sync the versioned URL.
Testing the Rule
1) It fails when another active slug exists
2) It allows reusing a soft-deleted slug
3) It allows keeping the same slug on update
Pitfalls & Tips
- Do not rely on
unique
: database rules for slugs: they won’t match this package’s normalization and soft-delete semantics. - Match the 100-char limit in your form rules. The rule internally truncates to 100; adding
max:100
gives clear UX. - Empty values are ignored by the rule (let
required|string
handle presence). - Collection
''
is treated asnull
by the rule for convenience. - This rule validates pre-flight; conflicts can still be thrown later by
dispatchSlug()
if something changes between validation and save (rare, but possible under race conditions). Handle exceptions likeSlugConflictException
defensively in your save flow if needed.
With SlugExistRule
in place, your forms catch slug collisions before calling dispatchSlug()
, keeping user feedback fast and precise while staying perfectly aligned with how the package stores and normalizes slugs.
Contributing
Thank you for considering contributing to the Laravel Url! The contribution guide can be found in the CONTRIBUTING.md.
License
The MIT License (MIT). Please see License File for more information.
All versions of laravel-url with dependencies
ext-json Version *
laravel/framework Version >=9.19
jobmetric/laravel-package-core Version ^1.7