NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Sluggable for Laravel

The Slugs facade

The Slugs facade is the one entry point for everything that is not a model hook: flat verbs for text and a single model, and Slugs::model(Product::class) — a class name or its morph alias — for class-wide backfills, audits, indexes and history lookups:

use RoundlyConsulting\Sluggable\Enums\RegenerationMode;
use RoundlyConsulting\Sluggable\Facades\Slugs;

// Text and one model
Slugs::slugify('Žltý kôň @ home', language: 'sk');     // 'zlty-kon-at-home'
Slugs::generate($product, column: 'slug');              // the value it would get; sets nothing
Slugs::apply($product);                                 // run the save-time pass now (before saveQuietly/imports)
Slugs::recompute($product, columns: ['slug']);          // recompute from the sources; you save
Slugs::regenerate($product, columns: ['slug']);         // recompute + save
Slugs::withoutGeneration(fn () => $importer->run());    // nestable, exception-safe toggles
Slugs::unlocked(fn () => $product->update(['slug' => 'new']));
Slugs::locales();                                       // SlugLocales

// A whole model class (a class name or its morph alias)
$products = Slugs::model(Product::class);
$products->regenerate(mode: RegenerationMode::Stale, dryRun: true);   // RegenerationReport
$products->regenerate(mode: RegenerationMode::Missing, withHistory: true, columns: ['slug']);
$products->queueRegeneration(mode: RegenerationMode::All, chunk: 1000); // jobs dispatched (int)
$products->duplicates(column: 'slug', locale: 'sk');   // list<SlugDuplicate> a unique index would reject
$products->indexes(dryRun: true);                      // SlugIndexReport: the DDL, nothing executed
$products->indexes();                                  // create the missing engine-native indexes
$products->findInHistory('old-name', locale: 'sk');    // the row that retired a slug, or null
$products->options();                                  // ResolvedSlugOptions

Flat methods

MethodPurpose
slugify(string $text, ?string $language = null, ?string $separator = null): stringThe pipeline with the configured defaults.
generate(Model $model, ?string $column = null, ?string $locale = null): stringThe unique value the model would get; sets nothing.
apply(Model $model, ?array $columns = null, ?array $locales = null): SlugGenerationResultRun the save-time pass now (before saveQuietly() or imports).
recompute(Model $model, ?array $columns = null, ?array $locales = null): SlugGenerationResultRecompute from the sources on the instance; you save.
regenerate(Model $model, ?array $columns = null, ?array $locales = null): Modelrecompute() + save().
model(string $model): ModelSlugsClass-wide work for a class name or morph alias (below).
withoutGeneration(Closure $callback): mixedHooks off (nestable, exception-safe).
unlocked(Closure $callback): mixedBypass locks.
isGenerationEnabled(): bool, isUnlocked(): boolToggle state.
locales(): SlugLocalesThe bound locale source.

Class-wide: Slugs::model()

MethodPurpose
regenerate(mode, dryRun, withHistory, columns, locales, chunk, withoutEvents, force): RegenerationReportBackfill or recompute the whole class — the same options as sluggable:regenerate.
queueRegeneration(mode, withHistory, columns, locales, chunk, withoutEvents, force): intDispatch one RegenerateSlugsJob per chunk; returns the job count.
duplicates(?string $column = null, ?string $locale = null): arraylist<SlugDuplicate> a unique index would reject.
indexes(bool $dryRun = false, ?array $columns = null): SlugIndexReportCreate the missing engine-native indexes, or only plan them.
findInHistory(string $slug, ?string $column = null, ?string $locale = null, Builder|Relation|null $within = null): ?ModelThe row that retired a slug, through the default or the given query.
options(): ResolvedSlugOptionsThe resolved definitions.
  • regenerate() and queueRegeneration() scan rows without global scopes; each changed row is saved normally, so history and events apply. mode takes a RegenerationMode — Missing (default), Stale or All.
  • findInHistory() resolves through the model’s default query (global scopes applied) or the query you pass as within — a retired slug never reveals a row that query would not return.
  • Each Artisan command is the same call: sluggable:regenerate, :indexes and :duplicates run model()->regenerate() / queueRegeneration(), indexes() and duplicates().

The toggles are depth counters released in finally blocks — nestable, exception-safe and never left set between Octane requests. On a model, $model->regenerateSlugs() is Slugs::recompute($model) from the model side: it recomputes on the instance and leaves saving to you; Slugs::regenerate() does both.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.