Nested (hierarchical) slugs
Compose ancestor slugs into the URL path — /electronics/phones/iphone_TOKEN. Override
polyslugParent() to point at the parent, and scope uniqueness on the parent key so the
same segment can repeat under different parents:
#[Polyslug(source: 'name', scope: 'parent_id')]
class Category extends Model implements Sluggable
{
use HasPolyslug;
public function polyslugParent(): ?Sluggable
{
return $this->parent_id === null ? null : self::find($this->parent_id);
}
}
Route it with a catch-all segment:
Route::polyslug('/{category}', [CategoryController::class, 'show'])->where('category', '.*');
Composed on read, never stored
The path is computed from the ancestors' current slugs each time it is built. Renaming
or reparenting an ancestor therefore changes the URL immediately, and the canonical
middleware 301s the stale one — with no cascade to run and no stored path column to keep
in sync.
That is the whole point of the design: a stored materialized path is a second source of truth, and the moment a background job fails, it disagrees with the slugs it was derived from. Here there is nothing to disagree with.
Read the path directly when you need it:
$category->polyslugPath(); // "electronics/phones/iphone"
$category->polyslugPath('de'); // the German path
$category->polyslugPath('de', 10); // with a tighter recursion bound
Recursion is depth-bounded — 20 levels by default — so an accidental parent cycle stops rather than looping forever.
Scope on the parent key
Without scope: 'parent_id', phones under Electronics and phones under Accessories
would collide and the second would become phones-2. Scoping on the parent is what makes
each level's names independent. See Uniqueness and scope.
Combining with id-less URLs
Nested composes with idLess, which gives a fully readable path with
no token at all — the natural shape for a documentation tree or a category hierarchy whose
segments are already unique per parent.
A slug-only path is resolved by its last segment, and with scope: 'parent_id' two parents
may each have a child of the same name. The rest of the path then decides which one is
meant: /electronics/phones answers with the phones under Electronics and
/accessories/phones with the ones under Accessories. An old path still finds its record
after an ancestor is renamed or moved, because the ancestor part resolves through the
parent's earlier slugs, and the canonical middleware redirects it to the new path.
The e-commerce and real-estate recipes both build on this.