PHP Packagev8.3.0MIT License

Slugify

Fast, framework-agnostic slug generation for PHP. Turn any string into a URL-friendly slug. By default it keeps Unicode letters as they are. ASCII is opt-in, locales only change language-specific behavior, and symbols, emoji and numbers each follow an explicit policy. Same input, same slug, every time.

Unicode First

Arabic, Cyrillic, Greek, CJK and other scripts stay as they are, so slugs read naturally in their own language.

ASCII on Demand

Transliterate to Latin-only slugs when you need them, with optional locale rules for languages such as German or Ukrainian.

Explicit Policies

Symbols, emoji and Unicode numbers are handled on purpose, not by accident, with no invisible residue.

Quick Tip

Slugify::make() covers most needs. Reach for Slugify::of() when you want a fluent, immutable builder with maxLength(), per-slug rules, or a custom transliterator.

Installation

Install the package with Composer. There is nothing to register or publish.

Requirements

  • PHP 8.3.x (each 8.x release line targets the matching PHP version)
  • ext-mbstring
  • voku/portable-ascii ^2.0 and symfony/polyfill-intl-normalizer (installed automatically)
  • ext-intl (optional): Unicode normalization works without it, but the native extension is faster than the polyfill.

Composer Installation

Terminal
composer require pharaonic/php-slugify

Composer autoloads the Pharaonic\Slugify namespace and the global slug() helper.

Installation Complete

You're all set! Try Slugify::make('Hello World'), which returns hello-world.

Basic Usage

Import the Slugify class and call make():

1use Pharaonic\Slugify\Slugify;
2 
3Slugify::make('Hello World'); // "hello-world"
4Slugify::make(' Hello World!! '); // "hello-world"
5Slugify::make('hello - _ world'); // "hello-world"
6Slugify::make('Top 10 Tips for 2026'); // "top-10-tips-for-2026"

The full signature is make(string $value, string $separator = '-', bool $ascii = false, ?string $language = null). $language is the locale:

1Slugify::make('Hello World', '_'); // "hello_world"
2Slugify::make('Crème brûlée', '-', true); // "creme-brulee"
3Slugify::make('Äpfel und Öl', '-', true, 'de'); // "aepfel-und-oel"

The slug() Helper

The global helper takes the same arguments. It also accepts any scalar or Stringable value, and null gives an empty string.

1slug('Hello World'); // "hello-world"
2slug('Hello World', '_', true); // "hello_world"
3slug(2026); // "2026"
4slug(null); // ""

How a Slug Is Built

Each call runs the same deterministic steps in order. Every step can be inspected with explain().

  1. Unicode normalization: repair invalid UTF-8, turn control characters into word breaks, normalize to NFC and fold presentation forms (fi → fi).
  2. CamelCase splitting: helloWorld → hello World.
  3. Replacement rules: your rules, before anything else changes the text.
  4. Numbers: ١٢, ۱۲, ①② → 12.
  5. Emoji policy: removed by default, as whole sequences.
  6. Symbol policy: removed by default.
  7. Lowercasing, locale-aware (tr: I → ı).
  8. Transliteration (ASCII mode only): locale overrides, then portable-ascii. In ASCII mode, rules made of ASCII words (allh → allah) run right after this step.
  9. Filtering: invisible characters are dropped. Letters, numbers and combining marks are kept (only [A-Za-z0-9] in ASCII mode).
  10. Joining: words joined with the separator, within maxLength() if set.

Empty results are returned as "". "0" is treated as real input and gives "0".

Unicode & ASCII

Slugify is Unicode-first: by default a slug keeps the letters of every script. ASCII output is something you ask for explicitly.

Unicode Slugs (Default)

Unicode letters, numbers and combining marks are kept. Punctuation, symbols and whitespace become separators, and emoji are removed.

1Slugify::make('مرحبا بالعالم'); // "مرحبا-بالعالم"
2Slugify::make('سلام دنیا'); // "سلام-دنیا"
3Slugify::make('Привет, мир!'); // "привет-мир"
4Slugify::make('Γειά σου Κόσμε'); // "γειά-σου-κόσμε"
5Slugify::make('你好,世界'); // "你好-世界"
6Slugify::make('नमस्ते दुनिया'); // "नमस्ते-दुनिया"
7Slugify::make('Laravel مع PHP'); // "laravel-مع-php"
8Slugify::make('PHP 🚀 Rocks'); // "php-rocks"

Arabic diacritics (tashkeel), the tatweel and Hebrew points (niqqud) are removed in every mode, and the alef wasla (ٱ) becomes a plain alef, so vocalized and plain spellings give the same slug:

1Slugify::make('مُحَمَّد'); // "محمد"
2Slugify::make('مـحـمـد'); // "محمد"
3Slugify::make('ٱلْحَمْدُ'); // "الحمد"

ASCII Slugs

Pass true as the third argument, or call ->ascii() on the fluent builder. Transliteration is handled by voku/portable-ascii, plus a few curated locale overrides.

1Slugify::make('Crème brûlée', '-', true); // "creme-brulee"
2Slugify::make('Привет мир', '-', true); // "privet-mir"
3Slugify::make('مرحبا', '-', true); // "mrhba"
4Slugify::make('你好世界', '-', true); // "ni-hao-shi-jie"
5Slugify::make('한국어', '-', true); // "hangugeo"
6 
7Slugify::of('Crème brûlée')->ascii()->toString(); // "creme-brulee"

ASCII mode always returns [a-z0-9] words joined by your separator ([A-Za-z0-9] with lowercase(false)). Characters that can't be transliterated are dropped.

The text is lowercased before it is transliterated, so the result never depends on the input's case: Χαρά, χαρά and ΧΑΡΆ all give the same slug.

Transliteration is not romanization

ASCII mode makes readable, deterministic slugs. It is not a linguistic romanization engine: unvocalized scripts such as Arabic and Hebrew lose information, and Japanese kanji are read as Chinese. If you need a specific standard, add rules or a custom transliterator.

Unicode Normalization

Every slug starts from well-formed, canonical text. This works the same with or without ext-intl, because the package requires symfony/polyfill-intl-normalizer.

  • NFC: e + U+0301 and é give the same slug in both modes.
  • Presentation forms are folded to the letters they display: ligatures (fi → fi), full-width and half-width forms (Hello → hello, カタカナ → カタカナ), Arabic presentation forms, and mathematical alphanumerics (𝐇𝐞𝐥𝐥𝐨 → hello).
  • Symbols with a compatibility form, such as ™ or ½, are not folded. They follow the symbol policy.
  • Invalid UTF-8, control characters, null bytes and the zero-width space become word breaks.
  • Invisible characters never reach the slug: zero-width joiners, soft hyphens, bidi marks, the BOM and variation selectors are removed. A soft hyphen or a Persian ZWNJ inside a word doesn't split it: می‌خواهم → میخواهم.

Locales

A locale selects language-specific behavior. It never turns ASCII output on by itself:

  • locale: how language-specific behavior works.
  • ascii: whether the output must be ASCII.
1Slugify::of('Äpfel Straße')->locale('de')->toString(); // "äpfel-straße"
2Slugify::of('Äpfel Straße')->locale('de')->ascii()->toString(); // "aepfel-strasse"
3Slugify::of('Äpfel Straße')->ascii()->toString(); // "apfel-strasse"

ascii('de') is a shorthand for locale('de')->ascii(), and make() takes the locale as its fourth argument:

1Slugify::of('München')->ascii('de')->toString(); // "muenchen"
2Slugify::make('München', '-', true, 'de'); // "muenchen"

Locale codes can include a region or script: de-DE, de_AT, sr-Latn and uk-UA all work. If portable-ascii has a regional variant (de-AT writes ß as sz), it is used. Otherwise the base language applies. Unknown or malformed codes fall back to the generic behavior.

In Unicode Mode

Turkish and Azerbaijani have a dotless ı, so their capital I lowercases differently:

1Slugify::make('IŞIK'); // "işik"
2Slugify::of('IŞIK')->locale('tr')->toString(); // "ışık"
3Slugify::of('IŞIQ')->locale('az')->toString(); // "ışıq"

In ASCII Mode

The locale is passed to portable-ascii, which has maps for dozens of languages. Pharaonic adds an override only where the generic result is demonstrably wrong:

LocaleGeneric resultWith the localePharaonic override
de GermanapfelaepfelNo (portable-ascii)
sr Serbianshabats, dordesabac, djordjeNo (portable-ascii)
uk Ukrainiankiyiv, zukkyiv, zhukYes: the official 2010 national system
tr, az, pl, ro, vialready correct—No
ru, el, ar, fa, he, hy, ka, zh, ja, ko, mydeterministic and readable—No
1Slugify::of('Київ Запоріжжя Щастя')->ascii('uk')->toString();
2// "kyiv-zaporizhzhia-shchastia"

Your own rules always win over locale overrides, which win over the generic transliterator:

1Slugify::of('Київ')->ascii('uk')->rule('київ', 'kiev')->toString(); // "kiev"
Requesting a locale override

Overrides live in src/Resources/locales/ and each needs a sample in the test corpus (tests/Fixtures/Transliteration/) where the generic result fails. The test suite rejects an override that the corpus doesn't justify.

Symbols, Emoji & Numbers

Symbols, emoji and numbers each have their own explicit, independent policy.

Symbols

By default, symbols are removed and act as word breaks:

1Slugify::make('R&D'); // "r-d"
2Slugify::make('50% off'); // "50-off"
3Slugify::make('Acme™ Pro'); // "acme-pro"

The one exception is the historical @ → at default rule: user@host → user-at-host.

Choose another policy with symbols():

1use Pharaonic\Slugify\Policies\SymbolPolicy;
2 
3Slugify::of('R&D')->symbols(SymbolPolicy::words())->toString(); // "r-and-d"
4Slugify::of('50% off')->symbols(SymbolPolicy::words())->toString(); // "50-percent-off"
5Slugify::of('€100')->symbols(SymbolPolicy::words())->toString(); // "euro-100"
6 
7Slugify::of('R&D')->locale('de')->symbols(SymbolPolicy::words())->toString(); // "r-und-d"
8 
9Slugify::of('C# & C++')
10 ->symbols(SymbolPolicy::custom(['#' => 'sharp', '+' => 'plus']))
11 ->toString(); // "c-sharp-c-plus-plus"
PolicyBehavior
SymbolPolicy::remove()Default. Symbols become word breaks.
SymbolPolicy::words()Spells symbols out in the slug's locale (English fallback): & + = %, currencies, © ® ™ @. Words come from portable-ascii. # is not spelled out because its meaning varies (C#, #1).
SymbolPolicy::custom(array $map)Spells out only the symbols you list. Others are removed.

Replacement words are always separate words in the slug.

Emoji

Emoji are removed by default, always as whole sequences. Skin tones, ZWJ families, flags, keycaps and variation selectors never leave invisible characters behind:

1Slugify::make('PHP 🚀 Rocks'); // "php-rocks"
2Slugify::make('I ❤️ PHP'); // "i-php"
3Slugify::make('👨‍👩‍👧‍👦 family'); // "family"
4Slugify::make('Egypt 🇪🇬'); // "egypt"

To turn emoji into words, provide your own words. The package ships no emoji names:

1use Pharaonic\Slugify\Policies\EmojiPolicy;
2 
3$emoji = EmojiPolicy::custom(['🚀' => 'rocket', '👍' => 'thumbs up', '🇪🇬' => 'egypt']);
4 
5Slugify::of('PHP 🚀 Rocks')->emoji($emoji)->toString(); // "php-rocket-rocks"
6Slugify::of('👍🏽')->emoji($emoji)->toString(); // "thumbs-up"

A custom word matches an emoji with any skin tone or presentation selector. Emoji you don't list are removed.

"©" vs "©️"

© is a symbol and follows the symbol policy. ©️ (with the emoji variation selector) is an emoji and follows the emoji policy.

Numbers

Digits from any script, and unambiguous numeric forms, are converted to ASCII digits, in both Unicode and ASCII mode:

1Slugify::make('الإصدار ١٢'); // "الإصدار-12" (Arabic-Indic)
2Slugify::make('نسخه ۱۲'); // "نسخه-12" (Persian)
3Slugify::make('123'); // "123" (full-width)
4Slugify::make('①②③'); // "123" (circled)
5Slugify::make('H₂O'); // "h2o" (subscript)
6Slugify::make('10 m²'); // "10-m2" (superscript)
7Slugify::make('Top 1️⃣'); // "top-1" (keycap)

Arabic-Indic ٣ and Persian ۳ look alike but are different characters. Normalizing both gives one slug for the same title.

Forms whose meaning isn't a plain number are left alone: fractions (½), Roman numerals (Ⅻ) and numbers without a decomposition (❶). An exponent never merges into its base: 10² → 10-2, not 102.

Turn the conversion off with normalizeNumbers(false):

1Slugify::of('الفصل ٣')->normalizeNumbers(false)->toString(); // "الفصل-٣"

Separators

The second argument of make(), or separator() on the builder, sets the string that joins words. The default is -.

1Slugify::make('Hello World', '-'); // "hello-world"
2Slugify::make('Hello World', '_'); // "hello_world"
3Slugify::make('Hello World', '.'); // "hello.world"
4Slugify::make('Hello World', '~'); // "hello~world"
5Slugify::make('Hello World', ''); // "helloworld"

Dashes, underscores, dots and other punctuation already in the input are treated as word breaks. Repeats are collapsed, and the slug never starts or ends with a separator:

1Slugify::make('-hello__big.. ~world-', '_'); // "hello_big_world"
2Slugify::make('hello---world'); // "hello-world"
3Slugify::make('---'); // ""
Invalid Separators

A separator must not contain letters, numbers, combining marks or whitespace. For example, 'x', '1' and ' ' throw Pharaonic\Slugify\Exceptions\InvalidArgumentException.

CamelCase & Acronyms

Words written in camelCase or PascalCase are split before slugging. Runs of capitals are kept together as acronyms.

1Slugify::make('helloWorld'); // "hello-world"
2Slugify::make('HelloWorld'); // "hello-world"
3Slugify::make('XMLHttpRequest'); // "xml-http-request"
4Slugify::make('APIResponse'); // "api-response"
5Slugify::make('getUserID'); // "get-user-id"
6Slugify::make('PharaonicPHP'); // "pharaonic-php"
7Slugify::make('Version2Beta'); // "version2-beta"
8Slugify::make('3D Printing'); // "3d-printing"
9Slugify::make('ÉtéÀParis'); // "été-à-paris"

The splitting rules are:

  • A lowercase letter followed by an uppercase letter starts a new word (helloWorld).
  • An uppercase letter or digit followed by a capitalized word starts a new word (XMLHttp, 2Beta).
  • A digit followed by a lone capital doesn't split (3D, 5G).

Turn splitting off for a single slug with splitCamelCase(false):

1Slugify::of('helloWorld')->splitCamelCase(false)->toString(); // "helloworld"

Replacement Rules

Rules replace text before the slug is built. Replacements are literal, so add spaces when the replacement should be its own word.

Package-wide Rules

1Slugify::addRule('&', ' and ');
2Slugify::make('Tom & Jerry'); // "tom-and-jerry"
3 
4Slugify::addRules([
5 'ö' => 'oe',
6 'ü' => 'ue',
7 'ä' => 'ae',
8]);
9Slugify::make('Äpfel Öl'); // "aepfel-oel"

Package-wide rules are global static state. Register them once during your app's bootstrap, for example in a Laravel service provider's boot() method.

Per-slug Rules

Rules added on the fluent builder apply to that slug only:

1Slugify::of('500$ Bill')->rule('$', ' dollar ')->toString(); // "500-dollar-bill"
2Slugify::make('500$ Bill'); // "500-bill"
3 
4Slugify::of('C++ & C#')
5 ->rules(['c++' => 'cpp', '&' => ' and ', 'c#' => 'csharp'])
6 ->toString(); // "cpp-and-csharp"

Default Rule

The package ships with one default rule, kept from 2.x:

RulePurpose
@ → atuser@host → user-at-host

Override or remove it like any other rule:

1Slugify::addRule('@', ' chez '); // override
2Slugify::removeRule('@'); // remove
3Slugify::resetRules(); // restore the package defaults
4Slugify::rules()->all(); // inspect: ['@' => ' at ']

Other symbols are handled by the symbol policy. Arabic tashkeel and Hebrew points are removed by Unicode normalization, not by rules.

To ignore every default and package-wide rule for one slug, call withoutRules():

1Slugify::of('user@host')->withoutRules()->toString(); // "user-host"

How Rules Match

  • Case: when the slug is lowercased (the default), rules match case-insensitively. With lowercase(false) they match exactly.
  • Single pass: all rules are applied at once and the longest search string wins. A replacement is never re-processed by another rule.
  • Overrides: adding a rule with an existing search string replaces it, and per-slug rules override package-wide ones.
  • Precedence: rules run before numbers, emoji, symbols, locale overrides and transliteration, so your rules always win.
  • ASCII mode: rules made only of ASCII letters, digits, spaces, _, . and - (allh → allah) run after transliteration, so they also match transliterated text. Every other rule (ö, $, c++) runs first.
1Slugify::addRule('allh', 'allah');
2Slugify::make('بسم الله', '-', true); // "bsm-allah"

Fluent API

Slugify::of() returns a Pharaonic\Slugify\Slugger. The builder is immutable: every method returns a new instance, so you can configure one base builder and reuse it.

1use Pharaonic\Slugify\Slugify;
2 
3Slugify::of('Hello World')
4 ->separator('_')
5 ->lowercase()
6 ->toString(); // "hello_world"
7 
8(string) Slugify::of('Hello World'); // "hello-world"

Reusing a Builder

1$base = Slugify::of('Hello World');
2 
3$base->separator('_')->toString(); // "hello_world"
4$base->ascii()->toString(); // "hello-world"
5$base->toString(); // "hello-world" (unchanged)

Locale and Policies

1use Pharaonic\Slugify\Policies\EmojiPolicy;
2use Pharaonic\Slugify\Policies\SymbolPolicy;
3 
4Slugify::of('R&D 🚀 Straße')
5 ->locale('de')
6 ->ascii()
7 ->symbols(SymbolPolicy::words())
8 ->emoji(EmojiPolicy::custom(['🚀' => 'rakete']))
9 ->toString(); // "r-und-d-rakete-strasse"

See Locales and Symbols, Emoji & Numbers.

Limiting the Length

maxLength() limits the slug to a number of characters, cutting on a word boundary, so it never ends with a separator. A single word longer than the limit is truncated between whole characters, so a letter never loses its accents or vowel signs.

1Slugify::of('The quick brown fox jumps')->maxLength(15)->toString(); // "the-quick-brown"
2Slugify::of('The quick brown fox jumps')->maxLength(14)->toString(); // "the-quick"
3Slugify::of('Supercalifragilistic')->maxLength(5)->toString(); // "super"

Keeping Case

1Slugify::of('Hello World')->lowercase(false)->toString(); // "Hello-World"
2Slugify::of('Crème Brûlée')->lowercase(false)->ascii()->toString(); // "Creme-Brulee"

Options Object

All options can be passed at once with SlugOptions. Named arguments keep it readable:

1use Pharaonic\Slugify\Policies\SymbolPolicy;
2use Pharaonic\Slugify\SlugOptions;
3 
4$options = new SlugOptions(separator: '_', ascii: true, language: 'de', maxLength: 60);
5 
6Slugify::of('München ist schön', $options)->toString(); // "muenchen_ist_schoen"
7 
8$options = new SlugOptions(symbols: SymbolPolicy::words(), normalizeNumbers: false);

Explaining a Slug

explain() returns the text after every step of the pipeline, as structured data. Use it to debug a surprising slug. toString() runs the same steps without recording them.

1Slugify::of('Äpfel & Straße 🚀')->locale('de')->ascii()->explain();
2 
3// [
4// 'original' => 'Äpfel & Straße 🚀',
5// 'unicode_normalized' => 'Äpfel & Straße 🚀',
6// 'camel_case_split' => 'Äpfel & Straße 🚀',
7// 'custom_replacements' => 'Äpfel & Straße 🚀',
8// 'numbers_normalized' => 'Äpfel & Straße 🚀',
9// 'emoji_processed' => 'Äpfel & Straße ',
10// 'symbols_processed' => 'Äpfel & Straße ',
11// 'lowercased' => 'äpfel & straße ',
12// 'transliterated' => 'aepfel & strasse ',
13// 'ascii_replacements' => 'aepfel & strasse ',
14// 'filtered' => 'aepfel strasse',
15// 'final' => 'aepfel-strasse',
16// ]
Global Rules Are Captured

Slugify::of() captures the package-wide rules when it is called. Rules added later with Slugify::addRule() don't affect builders that already exist.

Extending

Custom Transliterator

ASCII transliteration goes through the Pharaonic\Slugify\Contracts\Transliterator interface. The default is LocaleAwareTransliterator, which applies the curated locale overrides and passes everything else to PortableAsciiTransliterator. Implement the interface to use another engine, such as ICU:

app/Support/IntlTransliterator.php
1namespace App\Support;
2 
3use Pharaonic\Slugify\Contracts\Transliterator;
4 
5final class IntlTransliterator implements Transliterator
6{
7 public function transliterate(string $value, ?string $language = null): string
8 {
9 return \Transliterator::create('Any-Latin; Latin-ASCII')->transliterate($value);
10 }
11}

Use it for every slug, or for a single one:

1use App\Support\IntlTransliterator;
2use Pharaonic\Slugify\Slugify;
3 
4Slugify::useTransliterator(new IntlTransliterator()); // package-wide
5 
6Slugify::of('Привет')
7 ->ascii()
8 ->transliterator(new IntlTransliterator()) // this slug only
9 ->toString();
10 
11Slugify::useTransliterator(null); // restore the default

The transliterator only runs in ASCII mode, and receives the locale as $language. Anything it returns outside [A-Za-z0-9] is treated as a word break.

A custom transliterator replaces both the locale overrides and portable-ascii. To keep the overrides around your own engine, wrap it:

1use Pharaonic\Slugify\Transliteration\LocaleAwareTransliterator;
2 
3Slugify::useTransliterator(new LocaleAwareTransliterator(new IntlTransliterator()));

Using Slugger Directly

Slugger can be built without the static entry point. In that case no default or package-wide rules are applied:

1use Pharaonic\Slugify\Rules\RuleSet;
2use Pharaonic\Slugify\Slugger;
3 
4(new Slugger('user@host'))->toString(); // "user-host"
5(new Slugger('user@host', null, RuleSet::defaults()))->toString(); // "user-at-host"

API Reference

Pharaonic\Slugify\Slugify

MethodDescriptionReturns
make(string $value, string $separator = '-', bool $ascii = false, ?string $language = null)Generate a slug.string
of(string $value, ?SlugOptions $options = null)Start a fluent builder that uses the current package-wide rules.Slugger
get(mixed $value, string $separator = '-', bool $ascii_only = false, ?string $ascii_lang = 'en')2.x alias of make(). Accepts scalars, Stringable and null.string
addRule(string $search, string $replace)Add or override a package-wide rule.void
addRules(array $rules)Add or override several package-wide rules.void
rule(string $key, string $value)2.x alias of addRule().void
removeRule(string $search)Remove a package-wide rule, including a default.void
rules()The current package-wide rules.RuleSet
resetRules()Restore the package default rules.void
useTransliterator(?Transliterator $transliterator)Replace the package-wide transliterator; null restores the default.void

Pharaonic\Slugify\Slugger

Every configuration method returns a new instance.

MethodDescriptionReturns
separator(string $separator)Word separator.Slugger
lowercase(bool $lowercase = true)Lowercase the slug.Slugger
ascii(?string $language = null)Transliterate to ASCII. A language is a shorthand for locale($language)->ascii().Slugger
unicode()Turn ASCII mode off (the default). Keeps the locale.Slugger
locale(?string $locale)Language-specific behavior; never enables ASCII. null removes it.Slugger
symbols(SymbolPolicy $policy)How symbols are handled.Slugger
emoji(EmojiPolicy $policy)How emoji are handled.Slugger
normalizeNumbers(bool $normalize = true)Convert Unicode digits and numerals to ASCII digits.Slugger
splitCamelCase(bool $split = true)Split camelCase words and acronyms.Slugger
maxLength(?int $maxLength)Limit the length in characters; null removes the limit.Slugger
rule(string $search, string $replace)Add a rule for this slug.Slugger
rules(array $rules)Add several rules for this slug.Slugger
withoutRules()Drop the default and package-wide rules.Slugger
transliterator(Transliterator $transliterator)Use a custom transliterator for this slug.Slugger
options()A copy of the current options.SlugOptions
toString() / __toString()Build the slug.string
explain()The text after every pipeline step, keyed by step name.array<string, string>

Pharaonic\Slugify\SlugOptions

PropertyTypeDefaultDescription
separatorstring'-'Word separator.
lowercasebooltrueLowercase the slug.
asciiboolfalseTransliterate to ASCII.
language?stringnullLocale (de, tr, uk-UA). Never enables ASCII.
splitCamelCasebooltrueSplit camelCase words and acronyms.
maxLength?intnullMaximum length in characters.
symbolsSymbolPolicySymbolPolicy::remove()How symbols are handled.
emojiEmojiPolicyEmojiPolicy::remove()How emoji are handled.
normalizeNumbersbooltrueConvert Unicode digits and numerals to ASCII digits.

Pharaonic\Slugify\Policies\SymbolPolicy

MethodDescription
SymbolPolicy::remove()Default. Symbols become word breaks.
SymbolPolicy::words()Spell symbols out in the slug's locale, falling back to English.
SymbolPolicy::custom(array $map)Spell out only the given symbol => word pairs.

Pharaonic\Slugify\Policies\EmojiPolicy

MethodDescription
EmojiPolicy::remove()Default. Emoji sequences are removed.
EmojiPolicy::custom(array $map)Replace the given emoji => word pairs, regardless of skin tone or presentation selector; other emoji are removed.

Pharaonic\Slugify\Rules\RuleSet

An immutable, ordered rule collection, returned by Slugify::rules().

MethodDescriptionReturns
RuleSet::defaults()The package default rules.RuleSet
with(string $search, string $replace)Copy with one rule added.RuleSet
merge(RuleSet|array $rules)Copy with more rules merged in.RuleSet
without(string $search)Copy without a rule.RuleSet
has(string $search)Whether a rule exists.bool
all()Every rule as search => replacement.array
isEmpty() / count()Size checks.bool / int
apply(string $value, bool $caseInsensitive = false)Apply the rules to a string.string
splitForTransliteration()[before, after]: the rules applied before and after transliteration in ASCII mode.array

Other

NameDescription
slug(mixed $value, string $separator = '-', bool $ascii_only = false, ?string $ascii_lang = 'en')Global helper; same as Slugify::get().
Contracts\Transliterator::transliterate(string $value, ?string $language = null): stringTransliteration contract.
Transliteration\LocaleAwareTransliteratorDefault transliterator: curated locale overrides, then a wrapped generic transliterator.
Transliteration\PortableAsciiTransliteratorGeneric transliterator (voku/portable-ascii).
Exceptions\InvalidArgumentExceptionThrown for invalid separators, a maxLength below 1, empty rule, symbol or emoji search strings, and unsupported value types.

Examples

1. Blog Post Permalinks

Generate an SEO-friendly, length-limited permalink from a post title:

app/Posts/PermalinkGenerator.php
1use Pharaonic\Slugify\Slugify;
2 
3final class PermalinkGenerator
4{
5 public function forTitle(string $title): string
6 {
7 return Slugify::of($title)->maxLength(60)->toString();
8 }
9}
10 
11(new PermalinkGenerator())->forTitle('10 Tips for Writing Clean PHP Code in 2026');
12// "10-tips-for-writing-clean-php-code-in-2026"

2. Multilingual Site

Keep native-script slugs for Arabic content, and use ASCII slugs with the locale for German:

1use Pharaonic\Slugify\Slugify;
2 
3$slug = match ($locale) {
4 'ar', 'fa' => Slugify::make($title),
5 'de' => Slugify::make($title, '-', true, 'de'),
6 default => Slugify::make($title, '-', true),
7};
8 
9// ar: "دليل-البرمجة" from "دليل البرمجة"
10// de: "schoene-gruesse" from "Schöne Grüße"

3. Domain-specific Rules

Register package-wide rules once at boot, then use per-slug rules for one-off cases:

app/Providers/AppServiceProvider.php
1use Pharaonic\Slugify\Slugify;
2 
3public function boot(): void
4{
5 Slugify::addRules([
6 'c++' => 'cpp',
7 'c#' => 'csharp',
8 '&' => ' and ',
9 ]);
10}
app/Http/Controllers/CourseController.php
1use Pharaonic\Slugify\Slugify;
2 
3Slugify::make('C++ & C# Fundamentals'); // "cpp-and-csharp-fundamentals"
4 
5Slugify::of('Price: 100%')
6 ->rule('%', ' percent ')
7 ->toString(); // "price-100-percent"

4. File Names and Keys

Use another separator for cache keys or file names:

1use Pharaonic\Slugify\Slugify;
2 
3Slugify::make('Quarterly Report Q3', '_') . '.pdf'; // "quarterly_report_q3.pdf"
4Slugify::make('UserProfileSettings', '.'); // "user.profile.settings"

5. Reusable Configuration

Build one configured builder and reuse it for every value:

1use Pharaonic\Slugify\SlugOptions;
2use Pharaonic\Slugify\Slugify;
3 
4$options = new SlugOptions(separator: '_', ascii: true, maxLength: 32);
5 
6$keys = array_map(
7 fn (string $name) => Slugify::of($name, $options)->toString(),
8 ['Café Latte', 'Crème Brûlée', 'Pain au Chocolat']
9);
10// ["cafe_latte", "creme_brulee", "pain_au_chocolat"]

Troubleshooting

Cyrillic, Greek or Chinese titles are no longer romanized

Since the 8.x line, Unicode mode keeps every script. For Latin output, enable ASCII mode:

1Slugify::make('Привет мир', '-', true); // "privet-mir"

German umlauts become a/o/u instead of ae/oe/ue

Without a locale, transliteration uses the generic tables. Pass de:

1Slugify::make('Äpfel', '-', true, 'de'); // "aepfel"
2Slugify::of('Äpfel')->locale('de')->ascii()->toString(); // "aepfel"

locale('de') doesn't give an ASCII slug

That's by design: a locale never enables ASCII output. Add ->ascii().

Or add rules: Slugify::addRules(['ä' => 'ae', 'ö' => 'oe', 'ü' => 'ue']).

My rule's replacement sticks to the next word

Replacements are literal. Add spaces around the replacement to make it a separate word:

1Slugify::of('500$')->rule('$', 'dollar')->toString(); // "500dollar"
2Slugify::of('500$')->rule('$', ' dollar ')->toString(); // "500-dollar"

A rule added at runtime doesn't apply

Slugify::of() captures the package-wide rules when it is called. Register rules before you create the builder, or add the rule to the builder with ->rule().

Rules leak between tests

Package-wide rules are static. Call Slugify::resetRules() and Slugify::useTransliterator(null) in your test's setUp()/tearDown().

InvalidArgumentException about the separator

The separator contains a letter, number, combining mark or whitespace. Use punctuation such as -, _, . or ~, or an empty string.

Arabic or Persian digits became 0-9

Unicode digits are normalized to ASCII by default. Keep them with ->normalizeNumbers(false).

& disappears from my slug

Symbols are removed by default. Spell them out with ->symbols(SymbolPolicy::words()), or add a rule such as ->rule('&', ' and ').

A slug looks wrong and I can't tell why

Call ->explain() on the builder to see the text after every step of the pipeline.

Contributors

Amazing people who made this package possible

This package wouldn't exist without the contributions of our amazing community. A huge thank you to everyone who has helped improve Slugify!

Moamen Eltouny

@MoamenEltouny

31 contributions

Chun-Sheng, Li

@peter279k

4 contributions

Want to Contribute?

We welcome contributions from everyone! Whether it's bug fixes, new features, or documentation improvements - every contribution counts.