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.
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.xrelease line targets the matching PHP version) ext-mbstringvoku/portable-ascii^2.0 andsymfony/polyfill-intl-normalizer(installed automatically)ext-intl(optional): Unicode normalization works without it, but the native extension is faster than the polyfill.
Composer Installation
composer require pharaonic/php-slugifyComposer autoloads the Pharaonic\Slugify namespace and the global slug() helper.
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().
- Unicode normalization: repair invalid UTF-8, turn control characters into word breaks, normalize to NFC and fold presentation forms (
fi→fi). - CamelCase splitting:
helloWorld→hello World. - Replacement rules: your rules, before anything else changes the text.
- Numbers:
١٢,۱۲,①②→12. - Emoji policy: removed by default, as whole sequences.
- Symbol policy: removed by default.
- Lowercasing, locale-aware (
tr:I→ı). - Transliteration (ASCII mode only): locale overrides, then portable-ascii. In ASCII mode, rules made of ASCII words (
allh→allah) run right after this step. - Filtering: invisible characters are dropped. Letters, numbers and combining marks are kept (only
[A-Za-z0-9]in ASCII mode). - 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.
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:
| Locale | Generic result | With the locale | Pharaonic override |
|---|---|---|---|
de German | apfel | aepfel | No (portable-ascii) |
sr Serbian | shabats, dorde | sabac, djordje | No (portable-ascii) |
uk Ukrainian | kiyiv, zuk | kyiv, zhuk | Yes: the official 2010 national system |
tr, az, pl, ro, vi | already correct | — | No |
ru, el, ar, fa, he, hy, ka, zh, ja, ko, my | deterministic 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"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"| Policy | Behavior |
|---|---|
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.
© 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('---'); // ""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:
| Rule | Purpose |
|---|---|
@ → at | user@host → user-at-host |
Override or remove it like any other rule:
1Slugify::addRule('@', ' chez '); // override2Slugify::removeRule('@'); // remove3Slugify::resetRules(); // restore the package defaults4Slugify::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// ]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:
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 defaultThe 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
| Method | Description | Returns |
|---|---|---|
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.
| Method | Description | Returns |
|---|---|---|
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
| Property | Type | Default | Description |
|---|---|---|---|
separator | string | '-' | Word separator. |
lowercase | bool | true | Lowercase the slug. |
ascii | bool | false | Transliterate to ASCII. |
language | ?string | null | Locale (de, tr, uk-UA). Never enables ASCII. |
splitCamelCase | bool | true | Split camelCase words and acronyms. |
maxLength | ?int | null | Maximum length in characters. |
symbols | SymbolPolicy | SymbolPolicy::remove() | How symbols are handled. |
emoji | EmojiPolicy | EmojiPolicy::remove() | How emoji are handled. |
normalizeNumbers | bool | true | Convert Unicode digits and numerals to ASCII digits. |
Pharaonic\Slugify\Policies\SymbolPolicy
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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().
| Method | Description | Returns |
|---|---|---|
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
| Name | Description |
|---|---|
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): string | Transliteration contract. |
Transliteration\LocaleAwareTransliterator | Default transliterator: curated locale overrides, then a wrapped generic transliterator. |
Transliteration\PortableAsciiTransliterator | Generic transliterator (voku/portable-ascii). |
Exceptions\InvalidArgumentException | Thrown 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:
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:
1use Pharaonic\Slugify\Slugify; 2 3public function boot(): void 4{ 5 Slugify::addRules([ 6 'c++' => 'cpp', 7 'c#' => 'csharp', 8 '&' => ' and ', 9 ]);10}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!
Want to Contribute?
We welcome contributions from everyone! Whether it's bug fixes, new features, or documentation improvements - every contribution counts.