├── LICENSE ├── README.md ├── composer.json ├── docs └── en │ └── index.rst └── src ├── CachedWordInflector.php ├── GenericLanguageInflectorFactory.php ├── Inflector.php ├── InflectorFactory.php ├── Language.php ├── LanguageInflectorFactory.php ├── NoopWordInflector.php ├── Rules ├── English │ ├── Inflectible.php │ ├── InflectorFactory.php │ ├── Rules.php │ └── Uninflected.php ├── French │ ├── Inflectible.php │ ├── InflectorFactory.php │ ├── Rules.php │ └── Uninflected.php ├── NorwegianBokmal │ ├── Inflectible.php │ ├── InflectorFactory.php │ ├── Rules.php │ └── Uninflected.php ├── Pattern.php ├── Patterns.php ├── Portuguese │ ├── Inflectible.php │ ├── InflectorFactory.php │ ├── Rules.php │ └── Uninflected.php ├── Ruleset.php ├── Spanish │ ├── Inflectible.php │ ├── InflectorFactory.php │ ├── Rules.php │ └── Uninflected.php ├── Substitution.php ├── Substitutions.php ├── Transformation.php ├── Transformations.php ├── Turkish │ ├── Inflectible.php │ ├── InflectorFactory.php │ ├── Rules.php │ └── Uninflected.php └── Word.php ├── RulesetInflector.php └── WordInflector.php /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2015 Doctrine Project 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doctrine Inflector 2 | 3 | Doctrine Inflector is a small library that can perform string manipulations 4 | with regard to uppercase/lowercase and singular/plural forms of words. 5 | 6 | [![Build Status](https://github.com/doctrine/inflector/workflows/Continuous%20Integration/badge.svg)](https://github.com/doctrine/inflector/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A4.0.x) 7 | [![Code Coverage](https://codecov.io/gh/doctrine/inflector/branch/2.0.x/graph/badge.svg)](https://codecov.io/gh/doctrine/inflector/branch/2.0.x) 8 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doctrine/inflector", 3 | "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "php", 8 | "strings", 9 | "words", 10 | "manipulation", 11 | "inflector", 12 | "inflection", 13 | "uppercase", 14 | "lowercase", 15 | "singular", 16 | "plural" 17 | ], 18 | "authors": [ 19 | { 20 | "name": "Guilherme Blanco", 21 | "email": "guilhermeblanco@gmail.com" 22 | }, 23 | { 24 | "name": "Roman Borschel", 25 | "email": "roman@code-factory.org" 26 | }, 27 | { 28 | "name": "Benjamin Eberlei", 29 | "email": "kontakt@beberlei.de" 30 | }, 31 | { 32 | "name": "Jonathan Wage", 33 | "email": "jonwage@gmail.com" 34 | }, 35 | { 36 | "name": "Johannes Schmitt", 37 | "email": "schmittjoh@gmail.com" 38 | } 39 | ], 40 | "homepage": "https://www.doctrine-project.org/projects/inflector.html", 41 | "require": { 42 | "php": "^7.2 || ^8.0" 43 | }, 44 | "require-dev": { 45 | "doctrine/coding-standard": "^11.0", 46 | "phpstan/phpstan": "^1.8", 47 | "phpstan/phpstan-phpunit": "^1.1", 48 | "phpstan/phpstan-strict-rules": "^1.3", 49 | "phpunit/phpunit": "^8.5 || ^9.5" 50 | }, 51 | "autoload": { 52 | "psr-4": { 53 | "Doctrine\\Inflector\\": "src" 54 | } 55 | }, 56 | "autoload-dev": { 57 | "psr-4": { 58 | "Doctrine\\Tests\\Inflector\\": "tests" 59 | } 60 | }, 61 | "config": { 62 | "allow-plugins": { 63 | "dealerdirect/phpcodesniffer-composer-installer": true 64 | }, 65 | "sort-packages": true 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /docs/en/index.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | The Doctrine Inflector has methods for inflecting text. The features include pluralization, 5 | singularization, converting between camelCase and under_score and capitalizing 6 | words. 7 | 8 | Installation 9 | ============ 10 | 11 | You can install the Inflector with composer: 12 | 13 | .. code-block:: console 14 | 15 | $ composer require doctrine/inflector 16 | 17 | Usage 18 | ===== 19 | 20 | Using the inflector is easy, you can create a new ``Doctrine\Inflector\Inflector`` instance by using 21 | the ``Doctrine\Inflector\InflectorFactory`` class: 22 | 23 | .. code-block:: php 24 | 25 | use Doctrine\Inflector\InflectorFactory; 26 | 27 | $inflector = InflectorFactory::create()->build(); 28 | 29 | By default it will create an English inflector. If you want to use another language, just pass the language 30 | you want to create an inflector for to the ``createForLanguage()`` method: 31 | 32 | .. code-block:: php 33 | 34 | use Doctrine\Inflector\InflectorFactory; 35 | use Doctrine\Inflector\Language; 36 | 37 | $inflector = InflectorFactory::createForLanguage(Language::SPANISH)->build(); 38 | 39 | The supported languages are as follows: 40 | 41 | - ``Language::ENGLISH`` 42 | - ``Language::FRENCH`` 43 | - ``Language::NORWEGIAN_BOKMAL`` 44 | - ``Language::PORTUGUESE`` 45 | - ``Language::SPANISH`` 46 | - ``Language::TURKISH`` 47 | 48 | If you want to manually construct the inflector instead of using a factory, you can do so like this: 49 | 50 | .. code-block:: php 51 | 52 | use Doctrine\Inflector\CachedWordInflector; 53 | use Doctrine\Inflector\RulesetInflector; 54 | use Doctrine\Inflector\Rules\English; 55 | 56 | $inflector = new Inflector( 57 | new CachedWordInflector(new RulesetInflector( 58 | English\Rules::getSingularRuleset() 59 | )), 60 | new CachedWordInflector(new RulesetInflector( 61 | English\Rules::getPluralRuleset() 62 | )) 63 | ); 64 | 65 | Adding Languages 66 | ---------------- 67 | 68 | If you are interested in adding support for your language, take a look at the other languages defined in the 69 | ``Doctrine\Inflector\Rules`` namespace and the tests located in ``Doctrine\Tests\Inflector\Rules``. You can copy 70 | one of the languages and update the rules for your language. 71 | 72 | Once you have done this, send a pull request to the ``doctrine/inflector`` repository with the additions. 73 | 74 | Custom Setup 75 | ============ 76 | 77 | If you want to setup custom singular and plural rules, you can configure these in the factory: 78 | 79 | .. code-block:: php 80 | 81 | use Doctrine\Inflector\InflectorFactory; 82 | use Doctrine\Inflector\Rules\Pattern; 83 | use Doctrine\Inflector\Rules\Patterns; 84 | use Doctrine\Inflector\Rules\Ruleset; 85 | use Doctrine\Inflector\Rules\Substitution; 86 | use Doctrine\Inflector\Rules\Substitutions; 87 | use Doctrine\Inflector\Rules\Transformation; 88 | use Doctrine\Inflector\Rules\Transformations; 89 | use Doctrine\Inflector\Rules\Word; 90 | 91 | $inflector = InflectorFactory::create() 92 | ->withSingularRules( 93 | new Ruleset( 94 | new Transformations( 95 | new Transformation(new Pattern('/^(bil)er$/i'), '\1'), 96 | new Transformation(new Pattern('/^(inflec|contribu)tors$/i'), '\1ta') 97 | ), 98 | new Patterns(new Pattern('singulars')), 99 | new Substitutions(new Substitution(new Word('spins'), new Word('spinor'))) 100 | ) 101 | ) 102 | ->withPluralRules( 103 | new Ruleset( 104 | new Transformations( 105 | new Transformation(new Pattern('^(bil)er$'), '\1'), 106 | new Transformation(new Pattern('^(inflec|contribu)tors$'), '\1ta') 107 | ), 108 | new Patterns(new Pattern('noflect'), new Pattern('abtuse')), 109 | new Substitutions( 110 | new Substitution(new Word('amaze'), new Word('amazable')), 111 | new Substitution(new Word('phone'), new Word('phonezes')) 112 | ) 113 | ) 114 | ) 115 | ->build(); 116 | 117 | No operation inflector 118 | ---------------------- 119 | 120 | The ``Doctrine\Inflector\NoopWordInflector`` may be used to configure an inflector that doesn't perform any operation for 121 | pluralization and/or singularization. If will simply return the input as output. 122 | 123 | This is an implementation of the `Null Object design pattern `_. 124 | 125 | .. code-block:: php 126 | 127 | use Doctrine\Inflector\Inflector; 128 | use Doctrine\Inflector\NoopWordInflector; 129 | 130 | $inflector = new Inflector(new NoopWordInflector(), new NoopWordInflector()); 131 | 132 | Tableize 133 | ======== 134 | 135 | Converts ``ModelName`` to ``model_name``: 136 | 137 | .. code-block:: php 138 | 139 | echo $inflector->tableize('ModelName'); // model_name 140 | 141 | Classify 142 | ======== 143 | 144 | Converts ``model_name`` to ``ModelName``: 145 | 146 | .. code-block:: php 147 | 148 | echo $inflector->classify('model_name'); // ModelName 149 | 150 | Camelize 151 | ======== 152 | 153 | This method uses `Classify`_ and then converts the first character to lowercase: 154 | 155 | .. code-block:: php 156 | 157 | echo $inflector->camelize('model_name'); // modelName 158 | 159 | Capitalize 160 | ========== 161 | 162 | Takes a string and capitalizes all of the words, like PHP's built-in 163 | ``ucwords`` function. This extends that behavior, however, by allowing the 164 | word delimiters to be configured, rather than only separating on 165 | whitespace. 166 | 167 | Here is an example: 168 | 169 | .. code-block:: php 170 | 171 | $string = 'top-o-the-morning to all_of_you!'; 172 | 173 | echo $inflector->capitalize($string); // Top-O-The-Morning To All_of_you! 174 | 175 | echo $inflector->capitalize($string, '-_ '); // Top-O-The-Morning To All_Of_You! 176 | 177 | Pluralize 178 | ========= 179 | 180 | Returns a word in plural form. 181 | 182 | .. code-block:: php 183 | 184 | echo $inflector->pluralize('browser'); // browsers 185 | 186 | Singularize 187 | =========== 188 | 189 | Returns a word in singular form. 190 | 191 | .. code-block:: php 192 | 193 | echo $inflector->singularize('browsers'); // browser 194 | 195 | Urlize 196 | ====== 197 | 198 | Generate a URL friendly string from a string of text: 199 | 200 | .. code-block:: php 201 | 202 | echo $inflector->urlize('My first blog post'); // my-first-blog-post 203 | 204 | Unaccent 205 | ======== 206 | 207 | You can unaccent a string of text using the ``unaccent()`` method: 208 | 209 | .. code-block:: php 210 | 211 | echo $inflector->unaccent('año'); // ano 212 | 213 | Legacy API 214 | ========== 215 | 216 | The API present in Inflector 1.x is still available, but will be deprecated in a future release and dropped for 3.0. 217 | Support for languages other than English is available in the 2.0 API only. 218 | 219 | Acknowledgements 220 | ================ 221 | 222 | The language rules in this library have been adapted from several different sources, including but not limited to: 223 | 224 | - `Ruby On Rails Inflector `_ 225 | - `ICanBoogie Inflector `_ 226 | - `CakePHP Inflector `_ 227 | -------------------------------------------------------------------------------- /src/CachedWordInflector.php: -------------------------------------------------------------------------------- 1 | wordInflector = $wordInflector; 18 | } 19 | 20 | public function inflect(string $word): string 21 | { 22 | return $this->cache[$word] ?? $this->cache[$word] = $this->wordInflector->inflect($word); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/GenericLanguageInflectorFactory.php: -------------------------------------------------------------------------------- 1 | singularRulesets[] = $this->getSingularRuleset(); 22 | $this->pluralRulesets[] = $this->getPluralRuleset(); 23 | } 24 | 25 | final public function build(): Inflector 26 | { 27 | return new Inflector( 28 | new CachedWordInflector(new RulesetInflector( 29 | ...$this->singularRulesets 30 | )), 31 | new CachedWordInflector(new RulesetInflector( 32 | ...$this->pluralRulesets 33 | )) 34 | ); 35 | } 36 | 37 | final public function withSingularRules(?Ruleset $singularRules, bool $reset = false): LanguageInflectorFactory 38 | { 39 | if ($reset) { 40 | $this->singularRulesets = []; 41 | } 42 | 43 | if ($singularRules instanceof Ruleset) { 44 | array_unshift($this->singularRulesets, $singularRules); 45 | } 46 | 47 | return $this; 48 | } 49 | 50 | final public function withPluralRules(?Ruleset $pluralRules, bool $reset = false): LanguageInflectorFactory 51 | { 52 | if ($reset) { 53 | $this->pluralRulesets = []; 54 | } 55 | 56 | if ($pluralRules instanceof Ruleset) { 57 | array_unshift($this->pluralRulesets, $pluralRules); 58 | } 59 | 60 | return $this; 61 | } 62 | 63 | abstract protected function getSingularRuleset(): Ruleset; 64 | 65 | abstract protected function getPluralRuleset(): Ruleset; 66 | } 67 | -------------------------------------------------------------------------------- /src/Inflector.php: -------------------------------------------------------------------------------- 1 | 'A', 28 | 'Á' => 'A', 29 | 'Â' => 'A', 30 | 'Ã' => 'A', 31 | 'Ä' => 'Ae', 32 | 'Æ' => 'Ae', 33 | 'Å' => 'Aa', 34 | 'æ' => 'a', 35 | 'Ç' => 'C', 36 | 'È' => 'E', 37 | 'É' => 'E', 38 | 'Ê' => 'E', 39 | 'Ë' => 'E', 40 | 'Ì' => 'I', 41 | 'Í' => 'I', 42 | 'Î' => 'I', 43 | 'Ï' => 'I', 44 | 'Ñ' => 'N', 45 | 'Ò' => 'O', 46 | 'Ó' => 'O', 47 | 'Ô' => 'O', 48 | 'Õ' => 'O', 49 | 'Ö' => 'Oe', 50 | 'Ù' => 'U', 51 | 'Ú' => 'U', 52 | 'Û' => 'U', 53 | 'Ü' => 'Ue', 54 | 'Ý' => 'Y', 55 | 'ß' => 'ss', 56 | 'à' => 'a', 57 | 'á' => 'a', 58 | 'â' => 'a', 59 | 'ã' => 'a', 60 | 'ä' => 'ae', 61 | 'å' => 'aa', 62 | 'ç' => 'c', 63 | 'è' => 'e', 64 | 'é' => 'e', 65 | 'ê' => 'e', 66 | 'ë' => 'e', 67 | 'ì' => 'i', 68 | 'í' => 'i', 69 | 'î' => 'i', 70 | 'ï' => 'i', 71 | 'ñ' => 'n', 72 | 'ò' => 'o', 73 | 'ó' => 'o', 74 | 'ô' => 'o', 75 | 'õ' => 'o', 76 | 'ö' => 'oe', 77 | 'ù' => 'u', 78 | 'ú' => 'u', 79 | 'û' => 'u', 80 | 'ü' => 'ue', 81 | 'ý' => 'y', 82 | 'ÿ' => 'y', 83 | 'Ā' => 'A', 84 | 'ā' => 'a', 85 | 'Ă' => 'A', 86 | 'ă' => 'a', 87 | 'Ą' => 'A', 88 | 'ą' => 'a', 89 | 'Ć' => 'C', 90 | 'ć' => 'c', 91 | 'Ĉ' => 'C', 92 | 'ĉ' => 'c', 93 | 'Ċ' => 'C', 94 | 'ċ' => 'c', 95 | 'Č' => 'C', 96 | 'č' => 'c', 97 | 'Ď' => 'D', 98 | 'ď' => 'd', 99 | 'Đ' => 'D', 100 | 'đ' => 'd', 101 | 'Ē' => 'E', 102 | 'ē' => 'e', 103 | 'Ĕ' => 'E', 104 | 'ĕ' => 'e', 105 | 'Ė' => 'E', 106 | 'ė' => 'e', 107 | 'Ę' => 'E', 108 | 'ę' => 'e', 109 | 'Ě' => 'E', 110 | 'ě' => 'e', 111 | 'Ĝ' => 'G', 112 | 'ĝ' => 'g', 113 | 'Ğ' => 'G', 114 | 'ğ' => 'g', 115 | 'Ġ' => 'G', 116 | 'ġ' => 'g', 117 | 'Ģ' => 'G', 118 | 'ģ' => 'g', 119 | 'Ĥ' => 'H', 120 | 'ĥ' => 'h', 121 | 'Ħ' => 'H', 122 | 'ħ' => 'h', 123 | 'Ĩ' => 'I', 124 | 'ĩ' => 'i', 125 | 'Ī' => 'I', 126 | 'ī' => 'i', 127 | 'Ĭ' => 'I', 128 | 'ĭ' => 'i', 129 | 'Į' => 'I', 130 | 'į' => 'i', 131 | 'İ' => 'I', 132 | 'ı' => 'i', 133 | 'IJ' => 'IJ', 134 | 'ij' => 'ij', 135 | 'Ĵ' => 'J', 136 | 'ĵ' => 'j', 137 | 'Ķ' => 'K', 138 | 'ķ' => 'k', 139 | 'ĸ' => 'k', 140 | 'Ĺ' => 'L', 141 | 'ĺ' => 'l', 142 | 'Ļ' => 'L', 143 | 'ļ' => 'l', 144 | 'Ľ' => 'L', 145 | 'ľ' => 'l', 146 | 'Ŀ' => 'L', 147 | 'ŀ' => 'l', 148 | 'Ł' => 'L', 149 | 'ł' => 'l', 150 | 'Ń' => 'N', 151 | 'ń' => 'n', 152 | 'Ņ' => 'N', 153 | 'ņ' => 'n', 154 | 'Ň' => 'N', 155 | 'ň' => 'n', 156 | 'ʼn' => 'N', 157 | 'Ŋ' => 'n', 158 | 'ŋ' => 'N', 159 | 'Ō' => 'O', 160 | 'ō' => 'o', 161 | 'Ŏ' => 'O', 162 | 'ŏ' => 'o', 163 | 'Ő' => 'O', 164 | 'ő' => 'o', 165 | 'Œ' => 'OE', 166 | 'œ' => 'oe', 167 | 'Ø' => 'O', 168 | 'ø' => 'o', 169 | 'Ŕ' => 'R', 170 | 'ŕ' => 'r', 171 | 'Ŗ' => 'R', 172 | 'ŗ' => 'r', 173 | 'Ř' => 'R', 174 | 'ř' => 'r', 175 | 'Ś' => 'S', 176 | 'ś' => 's', 177 | 'Ŝ' => 'S', 178 | 'ŝ' => 's', 179 | 'Ş' => 'S', 180 | 'ş' => 's', 181 | 'Š' => 'S', 182 | 'š' => 's', 183 | 'Ţ' => 'T', 184 | 'ţ' => 't', 185 | 'Ť' => 'T', 186 | 'ť' => 't', 187 | 'Ŧ' => 'T', 188 | 'ŧ' => 't', 189 | 'Ũ' => 'U', 190 | 'ũ' => 'u', 191 | 'Ū' => 'U', 192 | 'ū' => 'u', 193 | 'Ŭ' => 'U', 194 | 'ŭ' => 'u', 195 | 'Ů' => 'U', 196 | 'ů' => 'u', 197 | 'Ű' => 'U', 198 | 'ű' => 'u', 199 | 'Ų' => 'U', 200 | 'ų' => 'u', 201 | 'Ŵ' => 'W', 202 | 'ŵ' => 'w', 203 | 'Ŷ' => 'Y', 204 | 'ŷ' => 'y', 205 | 'Ÿ' => 'Y', 206 | 'Ź' => 'Z', 207 | 'ź' => 'z', 208 | 'Ż' => 'Z', 209 | 'ż' => 'z', 210 | 'Ž' => 'Z', 211 | 'ž' => 'z', 212 | 'ſ' => 's', 213 | '€' => 'E', 214 | '£' => '', 215 | ]; 216 | 217 | /** @var WordInflector */ 218 | private $singularizer; 219 | 220 | /** @var WordInflector */ 221 | private $pluralizer; 222 | 223 | public function __construct(WordInflector $singularizer, WordInflector $pluralizer) 224 | { 225 | $this->singularizer = $singularizer; 226 | $this->pluralizer = $pluralizer; 227 | } 228 | 229 | /** 230 | * Converts a word into the format for a Doctrine table name. Converts 'ModelName' to 'model_name'. 231 | */ 232 | public function tableize(string $word): string 233 | { 234 | $tableized = preg_replace('~(?<=\\w)([A-Z])~u', '_$1', $word); 235 | 236 | if ($tableized === null) { 237 | throw new RuntimeException(sprintf( 238 | 'preg_replace returned null for value "%s"', 239 | $word 240 | )); 241 | } 242 | 243 | return mb_strtolower($tableized); 244 | } 245 | 246 | /** 247 | * Converts a word into the format for a Doctrine class name. Converts 'table_name' to 'TableName'. 248 | */ 249 | public function classify(string $word): string 250 | { 251 | return str_replace([' ', '_', '-'], '', ucwords($word, ' _-')); 252 | } 253 | 254 | /** 255 | * Camelizes a word. This uses the classify() method and turns the first character to lowercase. 256 | */ 257 | public function camelize(string $word): string 258 | { 259 | return lcfirst($this->classify($word)); 260 | } 261 | 262 | /** 263 | * Uppercases words with configurable delimiters between words. 264 | * 265 | * Takes a string and capitalizes all of the words, like PHP's built-in 266 | * ucwords function. This extends that behavior, however, by allowing the 267 | * word delimiters to be configured, rather than only separating on 268 | * whitespace. 269 | * 270 | * Here is an example: 271 | * 272 | * capitalize($string); 275 | * // Top-O-The-Morning To All_of_you! 276 | * 277 | * echo $inflector->capitalize($string, '-_ '); 278 | * // Top-O-The-Morning To All_Of_You! 279 | * ?> 280 | * 281 | * 282 | * @param string $string The string to operate on. 283 | * @param string $delimiters A list of word separators. 284 | * 285 | * @return string The string with all delimiter-separated words capitalized. 286 | */ 287 | public function capitalize(string $string, string $delimiters = " \n\t\r\0\x0B-"): string 288 | { 289 | return ucwords($string, $delimiters); 290 | } 291 | 292 | /** 293 | * Checks if the given string seems like it has utf8 characters in it. 294 | * 295 | * @param string $string The string to check for utf8 characters in. 296 | */ 297 | public function seemsUtf8(string $string): bool 298 | { 299 | for ($i = 0; $i < strlen($string); $i++) { 300 | if (ord($string[$i]) < 0x80) { 301 | continue; // 0bbbbbbb 302 | } 303 | 304 | if ((ord($string[$i]) & 0xE0) === 0xC0) { 305 | $n = 1; // 110bbbbb 306 | } elseif ((ord($string[$i]) & 0xF0) === 0xE0) { 307 | $n = 2; // 1110bbbb 308 | } elseif ((ord($string[$i]) & 0xF8) === 0xF0) { 309 | $n = 3; // 11110bbb 310 | } elseif ((ord($string[$i]) & 0xFC) === 0xF8) { 311 | $n = 4; // 111110bb 312 | } elseif ((ord($string[$i]) & 0xFE) === 0xFC) { 313 | $n = 5; // 1111110b 314 | } else { 315 | return false; // Does not match any model 316 | } 317 | 318 | for ($j = 0; $j < $n; $j++) { // n bytes matching 10bbbbbb follow ? 319 | if (++$i === strlen($string) || ((ord($string[$i]) & 0xC0) !== 0x80)) { 320 | return false; 321 | } 322 | } 323 | } 324 | 325 | return true; 326 | } 327 | 328 | /** 329 | * Remove any illegal characters, accents, etc. 330 | * 331 | * @param string $string String to unaccent 332 | * 333 | * @return string Unaccented string 334 | */ 335 | public function unaccent(string $string): string 336 | { 337 | if (preg_match('/[\x80-\xff]/', $string) === false) { 338 | return $string; 339 | } 340 | 341 | if ($this->seemsUtf8($string)) { 342 | $string = strtr($string, self::ACCENTED_CHARACTERS); 343 | } else { 344 | $characters = []; 345 | 346 | // Assume ISO-8859-1 if not UTF-8 347 | $characters['in'] = 348 | chr(128) 349 | . chr(131) 350 | . chr(138) 351 | . chr(142) 352 | . chr(154) 353 | . chr(158) 354 | . chr(159) 355 | . chr(162) 356 | . chr(165) 357 | . chr(181) 358 | . chr(192) 359 | . chr(193) 360 | . chr(194) 361 | . chr(195) 362 | . chr(196) 363 | . chr(197) 364 | . chr(199) 365 | . chr(200) 366 | . chr(201) 367 | . chr(202) 368 | . chr(203) 369 | . chr(204) 370 | . chr(205) 371 | . chr(206) 372 | . chr(207) 373 | . chr(209) 374 | . chr(210) 375 | . chr(211) 376 | . chr(212) 377 | . chr(213) 378 | . chr(214) 379 | . chr(216) 380 | . chr(217) 381 | . chr(218) 382 | . chr(219) 383 | . chr(220) 384 | . chr(221) 385 | . chr(224) 386 | . chr(225) 387 | . chr(226) 388 | . chr(227) 389 | . chr(228) 390 | . chr(229) 391 | . chr(231) 392 | . chr(232) 393 | . chr(233) 394 | . chr(234) 395 | . chr(235) 396 | . chr(236) 397 | . chr(237) 398 | . chr(238) 399 | . chr(239) 400 | . chr(241) 401 | . chr(242) 402 | . chr(243) 403 | . chr(244) 404 | . chr(245) 405 | . chr(246) 406 | . chr(248) 407 | . chr(249) 408 | . chr(250) 409 | . chr(251) 410 | . chr(252) 411 | . chr(253) 412 | . chr(255); 413 | 414 | $characters['out'] = 'EfSZszYcYuAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy'; 415 | 416 | $string = strtr($string, $characters['in'], $characters['out']); 417 | 418 | $doubleChars = []; 419 | 420 | $doubleChars['in'] = [ 421 | chr(140), 422 | chr(156), 423 | chr(198), 424 | chr(208), 425 | chr(222), 426 | chr(223), 427 | chr(230), 428 | chr(240), 429 | chr(254), 430 | ]; 431 | 432 | $doubleChars['out'] = ['OE', 'oe', 'AE', 'DH', 'TH', 'ss', 'ae', 'dh', 'th']; 433 | 434 | $string = str_replace($doubleChars['in'], $doubleChars['out'], $string); 435 | } 436 | 437 | return $string; 438 | } 439 | 440 | /** 441 | * Convert any passed string to a url friendly string. 442 | * Converts 'My first blog post' to 'my-first-blog-post' 443 | * 444 | * @param string $string String to urlize. 445 | * 446 | * @return string Urlized string. 447 | */ 448 | public function urlize(string $string): string 449 | { 450 | // Remove all non url friendly characters with the unaccent function 451 | $unaccented = $this->unaccent($string); 452 | 453 | if (function_exists('mb_strtolower')) { 454 | $lowered = mb_strtolower($unaccented); 455 | } else { 456 | $lowered = strtolower($unaccented); 457 | } 458 | 459 | $replacements = [ 460 | '/\W/' => ' ', 461 | '/([A-Z]+)([A-Z][a-z])/' => '\1_\2', 462 | '/([a-z\d])([A-Z])/' => '\1_\2', 463 | '/[^A-Z^a-z^0-9^\/]+/' => '-', 464 | ]; 465 | 466 | $urlized = $lowered; 467 | 468 | foreach ($replacements as $pattern => $replacement) { 469 | $replaced = preg_replace($pattern, $replacement, $urlized); 470 | 471 | if ($replaced === null) { 472 | throw new RuntimeException(sprintf( 473 | 'preg_replace returned null for value "%s"', 474 | $urlized 475 | )); 476 | } 477 | 478 | $urlized = $replaced; 479 | } 480 | 481 | return trim($urlized, '-'); 482 | } 483 | 484 | /** 485 | * Returns a word in singular form. 486 | * 487 | * @param string $word The word in plural form. 488 | * 489 | * @return string The word in singular form. 490 | */ 491 | public function singularize(string $word): string 492 | { 493 | return $this->singularizer->inflect($word); 494 | } 495 | 496 | /** 497 | * Returns a word in plural form. 498 | * 499 | * @param string $word The word in singular form. 500 | * 501 | * @return string The word in plural form. 502 | */ 503 | public function pluralize(string $word): string 504 | { 505 | return $this->pluralizer->inflect($word); 506 | } 507 | } 508 | -------------------------------------------------------------------------------- /src/InflectorFactory.php: -------------------------------------------------------------------------------- 1 | getFlippedSubstitutions() 20 | ); 21 | } 22 | 23 | public static function getPluralRuleset(): Ruleset 24 | { 25 | return new Ruleset( 26 | new Transformations(...Inflectible::getPlural()), 27 | new Patterns(...Uninflected::getPlural()), 28 | new Substitutions(...Inflectible::getIrregular()) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Rules/English/Uninflected.php: -------------------------------------------------------------------------------- 1 | getFlippedSubstitutions() 20 | ); 21 | } 22 | 23 | public static function getPluralRuleset(): Ruleset 24 | { 25 | return new Ruleset( 26 | new Transformations(...Inflectible::getPlural()), 27 | new Patterns(...Uninflected::getPlural()), 28 | new Substitutions(...Inflectible::getIrregular()) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Rules/French/Uninflected.php: -------------------------------------------------------------------------------- 1 | getFlippedSubstitutions() 20 | ); 21 | } 22 | 23 | public static function getPluralRuleset(): Ruleset 24 | { 25 | return new Ruleset( 26 | new Transformations(...Inflectible::getPlural()), 27 | new Patterns(...Uninflected::getPlural()), 28 | new Substitutions(...Inflectible::getIrregular()) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Rules/NorwegianBokmal/Uninflected.php: -------------------------------------------------------------------------------- 1 | pattern = $pattern; 20 | 21 | if (isset($this->pattern[0]) && $this->pattern[0] === '/') { 22 | $this->regex = $this->pattern; 23 | } else { 24 | $this->regex = '/' . $this->pattern . '/i'; 25 | } 26 | } 27 | 28 | public function getPattern(): string 29 | { 30 | return $this->pattern; 31 | } 32 | 33 | public function getRegex(): string 34 | { 35 | return $this->regex; 36 | } 37 | 38 | public function matches(string $word): bool 39 | { 40 | return preg_match($this->getRegex(), $word) === 1; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Rules/Patterns.php: -------------------------------------------------------------------------------- 1 | getPattern(); 20 | }, $patterns); 21 | 22 | $this->regex = '/^(?:' . implode('|', $patterns) . ')$/i'; 23 | } 24 | 25 | public function matches(string $word): bool 26 | { 27 | return preg_match($this->regex, $word, $regs) === 1; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Rules/Portuguese/Inflectible.php: -------------------------------------------------------------------------------- 1 | getFlippedSubstitutions() 20 | ); 21 | } 22 | 23 | public static function getPluralRuleset(): Ruleset 24 | { 25 | return new Ruleset( 26 | new Transformations(...Inflectible::getPlural()), 27 | new Patterns(...Uninflected::getPlural()), 28 | new Substitutions(...Inflectible::getIrregular()) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Rules/Portuguese/Uninflected.php: -------------------------------------------------------------------------------- 1 | regular = $regular; 21 | $this->uninflected = $uninflected; 22 | $this->irregular = $irregular; 23 | } 24 | 25 | public function getRegular(): Transformations 26 | { 27 | return $this->regular; 28 | } 29 | 30 | public function getUninflected(): Patterns 31 | { 32 | return $this->uninflected; 33 | } 34 | 35 | public function getIrregular(): Substitutions 36 | { 37 | return $this->irregular; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Rules/Spanish/Inflectible.php: -------------------------------------------------------------------------------- 1 | getFlippedSubstitutions() 20 | ); 21 | } 22 | 23 | public static function getPluralRuleset(): Ruleset 24 | { 25 | return new Ruleset( 26 | new Transformations(...Inflectible::getPlural()), 27 | new Patterns(...Uninflected::getPlural()), 28 | new Substitutions(...Inflectible::getIrregular()) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Rules/Spanish/Uninflected.php: -------------------------------------------------------------------------------- 1 | from = $from; 18 | $this->to = $to; 19 | } 20 | 21 | public function getFrom(): Word 22 | { 23 | return $this->from; 24 | } 25 | 26 | public function getTo(): Word 27 | { 28 | return $this->to; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Rules/Substitutions.php: -------------------------------------------------------------------------------- 1 | substitutions[$substitution->getFrom()->getWord()] = $substitution; 22 | } 23 | } 24 | 25 | public function getFlippedSubstitutions(): Substitutions 26 | { 27 | $substitutions = []; 28 | 29 | foreach ($this->substitutions as $substitution) { 30 | $substitutions[] = new Substitution( 31 | $substitution->getTo(), 32 | $substitution->getFrom() 33 | ); 34 | } 35 | 36 | return new Substitutions(...$substitutions); 37 | } 38 | 39 | public function inflect(string $word): string 40 | { 41 | $lowerWord = strtolower($word); 42 | 43 | if (isset($this->substitutions[$lowerWord])) { 44 | $firstLetterUppercase = $lowerWord[0] !== $word[0]; 45 | 46 | $toWord = $this->substitutions[$lowerWord]->getTo()->getWord(); 47 | 48 | if ($firstLetterUppercase) { 49 | return strtoupper($toWord[0]) . substr($toWord, 1); 50 | } 51 | 52 | return $toWord; 53 | } 54 | 55 | return $word; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Rules/Transformation.php: -------------------------------------------------------------------------------- 1 | pattern = $pattern; 22 | $this->replacement = $replacement; 23 | } 24 | 25 | public function getPattern(): Pattern 26 | { 27 | return $this->pattern; 28 | } 29 | 30 | public function getReplacement(): string 31 | { 32 | return $this->replacement; 33 | } 34 | 35 | public function inflect(string $word): string 36 | { 37 | return (string) preg_replace($this->pattern->getRegex(), $this->replacement, $word); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Rules/Transformations.php: -------------------------------------------------------------------------------- 1 | transformations = $transformations; 17 | } 18 | 19 | public function inflect(string $word): string 20 | { 21 | foreach ($this->transformations as $transformation) { 22 | if ($transformation->getPattern()->matches($word)) { 23 | return $transformation->inflect($word); 24 | } 25 | } 26 | 27 | return $word; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Rules/Turkish/Inflectible.php: -------------------------------------------------------------------------------- 1 | getFlippedSubstitutions() 20 | ); 21 | } 22 | 23 | public static function getPluralRuleset(): Ruleset 24 | { 25 | return new Ruleset( 26 | new Transformations(...Inflectible::getPlural()), 27 | new Patterns(...Uninflected::getPlural()), 28 | new Substitutions(...Inflectible::getIrregular()) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Rules/Turkish/Uninflected.php: -------------------------------------------------------------------------------- 1 | word = $word; 15 | } 16 | 17 | public function getWord(): string 18 | { 19 | return $this->word; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/RulesetInflector.php: -------------------------------------------------------------------------------- 1 | rulesets = array_merge([$ruleset], $rulesets); 28 | } 29 | 30 | public function inflect(string $word): string 31 | { 32 | if ($word === '') { 33 | return ''; 34 | } 35 | 36 | foreach ($this->rulesets as $ruleset) { 37 | if ($ruleset->getUninflected()->matches($word)) { 38 | return $word; 39 | } 40 | 41 | $inflected = $ruleset->getIrregular()->inflect($word); 42 | 43 | if ($inflected !== $word) { 44 | return $inflected; 45 | } 46 | 47 | $inflected = $ruleset->getRegular()->inflect($word); 48 | 49 | if ($inflected !== $word) { 50 | return $inflected; 51 | } 52 | } 53 | 54 | return $word; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/WordInflector.php: -------------------------------------------------------------------------------- 1 |