3 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15 * This software consists of voluntary contributions made by many individuals
16 * and is licensed under the MIT license. For more information, see
17 * <http://www.doctrine-project.org>.
20 namespace Doctrine\Common\Inflector;
23 * Doctrine inflector has static methods for inflecting text.
25 * The methods in these classes are from several different sources collected
26 * across several different php projects and several different authors. The
27 * original author names and emails are not known.
29 * Pluralize & Singularize implementation are borrowed from CakePHP with some modifications.
31 * @link www.doctrine-project.org
33 * @author Konsta Vesterinen <kvesteri@cc.hut.fi>
34 * @author Jonathan H. Wage <jonwage@gmail.com>
39 * Plural inflector rules.
43 private static $plural = array(
45 '/(s)tatus$/i' => '\1\2tatuses',
46 '/(quiz)$/i' => '\1zes',
47 '/^(ox)$/i' => '\1\2en',
48 '/([m|l])ouse$/i' => '\1ice',
49 '/(matr|vert|ind)(ix|ex)$/i' => '\1ices',
50 '/(x|ch|ss|sh)$/i' => '\1es',
51 '/([^aeiouy]|qu)y$/i' => '\1ies',
52 '/(hive|gulf)$/i' => '\1s',
53 '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves',
55 '/([ti])um$/i' => '\1a',
56 '/(c)riterion$/i' => '\1riteria',
57 '/(p)erson$/i' => '\1eople',
58 '/(m)an$/i' => '\1en',
59 '/(c)hild$/i' => '\1hildren',
60 '/(f)oot$/i' => '\1eet',
61 '/(buffal|her|potat|tomat|volcan)o$/i' => '\1\2oes',
62 '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$/i' => '\1i',
64 '/(alias)$/i' => '\1es',
65 '/(analys|ax|cris|test|thes)is$/i' => '\1es',
70 'uninflected' => array(
86 'brother' => 'brothers',
88 'chateau' => 'chateaux',
89 'niveau' => 'niveaux',
90 'child' => 'children',
91 'cookie' => 'cookies',
92 'corpus' => 'corpuses',
94 'criterion' => 'criteria',
95 'curriculum' => 'curricula',
97 'domino' => 'dominoes',
101 'ganglion' => 'ganglions',
105 'graffito' => 'graffiti',
106 'hippopotamus' => 'hippopotami',
115 'memorandum' => 'memoranda',
117 'mongoose' => 'mongooses',
118 'motto' => 'mottoes',
120 'mythos' => 'mythoi',
122 'nucleus' => 'nuclei',
124 'occiput' => 'occiputs',
125 'octopus' => 'octopuses',
128 'passerby' => 'passersby',
129 'penis' => 'penises',
130 'person' => 'people',
131 'plateau' => 'plateaux',
132 'runner-up' => 'runners-up',
134 'soliloquy' => 'soliloquies',
135 'son-in-law' => 'sons-in-law',
136 'syllabus' => 'syllabi',
137 'testis' => 'testes',
138 'thief' => 'thieves',
140 'tornado' => 'tornadoes',
141 'trilby' => 'trilbys',
144 'volcano' => 'volcanoes',
149 * Singular inflector rules.
153 private static $singular = array(
155 '/(s)tatuses$/i' => '\1\2tatus',
156 '/^(.*)(menu)s$/i' => '\1\2',
157 '/(quiz)zes$/i' => '\\1',
158 '/(matr)ices$/i' => '\1ix',
159 '/(vert|ind)ices$/i' => '\1ex',
160 '/^(ox)en/i' => '\1',
161 '/(alias)(es)*$/i' => '\1',
162 '/(buffal|her|potat|tomat|volcan)oes$/i' => '\1o',
163 '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us',
164 '/([ftw]ax)es/i' => '\1',
165 '/(analys|ax|cris|test|thes)es$/i' => '\1is',
166 '/(shoe|slave)s$/i' => '\1',
168 '/ouses$/' => 'ouse',
169 '/([^a])uses$/' => '\1us',
170 '/([m|l])ice$/i' => '\1ouse',
171 '/(x|ch|ss|sh)es$/i' => '\1',
172 '/(m)ovies$/i' => '\1\2ovie',
173 '/(s)eries$/i' => '\1\2eries',
174 '/([^aeiouy]|qu)ies$/i' => '\1y',
175 '/([lr])ves$/i' => '\1f',
176 '/(tive)s$/i' => '\1',
177 '/(hive)s$/i' => '\1',
178 '/(drive)s$/i' => '\1',
179 '/(dive)s$/i' => '\1',
180 '/(olive)s$/i' => '\1',
181 '/([^fo])ves$/i' => '\1fe',
182 '/(^analy)ses$/i' => '\1sis',
183 '/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis',
184 '/(c)riteria$/i' => '\1riterion',
185 '/([ti])a$/i' => '\1um',
186 '/(p)eople$/i' => '\1\2erson',
187 '/(m)en$/i' => '\1an',
188 '/(c)hildren$/i' => '\1\2hild',
189 '/(f)eet$/i' => '\1oot',
190 '/(n)ews$/i' => '\1\2ews',
192 '/^(.*us)$/' => '\\1',
195 'uninflected' => array(
209 'irregular' => array(
211 'avalanches' => 'avalanche',
213 'criteria' => 'criterion',
215 'emphases' => 'emphasis',
221 'neuroses' => 'neurosis',
229 * Words that should not be inflected.
233 private static $uninflected = array(
234 '.*?media', 'Amoyese', 'audio', 'bison', 'Borghese', 'bream', 'breeches',
235 'britches', 'buffalo', 'cantus', 'carp', 'chassis', 'clippers', 'cod', 'coitus', 'compensation', 'Congoese',
236 'contretemps', 'coreopsis', 'corps', 'data', 'debris', 'deer', 'diabetes', 'djinn', 'education', 'eland',
237 'elk', 'emoji', 'equipment', 'evidence', 'Faroese', 'feedback', 'fish', 'flounder', 'Foochowese',
238 'Furniture', 'furniture', 'gallows', 'Genevese', 'Genoese', 'Gilbertese', 'gold',
239 'headquarters', 'herpes', 'hijinks', 'Hottentotese', 'information', 'innings', 'jackanapes', 'jedi',
240 'Kiplingese', 'knowledge', 'Kongoese', 'love', 'Lucchese', 'Luggage', 'mackerel', 'Maltese', 'metadata',
241 'mews', 'moose', 'mumps', 'Nankingese', 'news', 'nexus', 'Niasese', 'nutrition', 'offspring',
242 'Pekingese', 'Piedmontese', 'pincers', 'Pistoiese', 'plankton', 'pliers', 'pokemon', 'police', 'Portuguese',
243 'proceedings', 'rabies', 'rain', 'rhinoceros', 'rice', 'salmon', 'Sarawakese', 'scissors', 'sea[- ]bass',
244 'series', 'Shavese', 'shears', 'sheep', 'siemens', 'species', 'staff', 'swine', 'traffic',
245 'trousers', 'trout', 'tuna', 'us', 'Vermontese', 'Wenchowese', 'wheat', 'whiting', 'wildebeest', 'Yengeese'
249 * Method cache array.
253 private static $cache = array();
256 * The initial state of Inflector so reset() works.
260 private static $initialState = array();
263 * Converts a word into the format for a Doctrine table name. Converts 'ModelName' to 'model_name'.
265 public static function tableize(string $word) : string
267 return strtolower(preg_replace('~(?<=\\w)([A-Z])~', '_$1', $word));
271 * Converts a word into the format for a Doctrine class name. Converts 'table_name' to 'TableName'.
273 public static function classify(string $word) : string
275 return str_replace([' ', '_', '-'], '', ucwords($word, ' _-'));
279 * Camelizes a word. This uses the classify() method and turns the first character to lowercase.
281 public static function camelize(string $word) : string
283 return lcfirst(self::classify($word));
287 * Uppercases words with configurable delimeters between words.
289 * Takes a string and capitalizes all of the words, like PHP's built-in
290 * ucwords function. This extends that behavior, however, by allowing the
291 * word delimeters to be configured, rather than only separating on
294 * Here is an example:
297 * $string = 'top-o-the-morning to all_of_you!';
298 * echo \Doctrine\Common\Inflector\Inflector::ucwords($string);
299 * // Top-O-The-Morning To All_of_you!
301 * echo \Doctrine\Common\Inflector\Inflector::ucwords($string, '-_ ');
302 * // Top-O-The-Morning To All_Of_You!
306 * @param string $string The string to operate on.
307 * @param string $delimiters A list of word separators.
309 * @return string The string with all delimeter-separated words capitalized.
311 public static function ucwords(string $string, string $delimiters = " \n\t\r\0\x0B-") : string
313 return ucwords($string, $delimiters);
317 * Clears Inflectors inflected value caches, and resets the inflection
318 * rules to the initial values.
320 public static function reset() : void
322 if (empty(self::$initialState)) {
323 self::$initialState = get_class_vars('Inflector');
328 foreach (self::$initialState as $key => $val) {
329 if ($key !== 'initialState') {
330 self::${$key} = $val;
336 * Adds custom inflection $rules, of either 'plural' or 'singular' $type.
341 * Inflector::rules('plural', array('/^(inflect)or$/i' => '\1ables'));
342 * Inflector::rules('plural', array(
343 * 'rules' => array('/^(inflect)ors$/i' => '\1ables'),
344 * 'uninflected' => array('dontinflectme'),
345 * 'irregular' => array('red' => 'redlings')
349 * @param string $type The type of inflection, either 'plural' or 'singular'
350 * @param array|iterable $rules An array of rules to be added.
351 * @param boolean $reset If true, will unset default inflections for all
352 * new rules that are being defined in $rules.
356 public static function rules(string $type, iterable $rules, bool $reset = false) : void
358 foreach ($rules as $rule => $pattern) {
359 if ( ! is_array($pattern)) {
364 self::${$type}[$rule] = $pattern;
366 self::${$type}[$rule] = ($rule === 'uninflected')
367 ? array_merge($pattern, self::${$type}[$rule])
368 : $pattern + self::${$type}[$rule];
371 unset($rules[$rule], self::${$type}['cache' . ucfirst($rule)]);
373 if (isset(self::${$type}['merged'][$rule])) {
374 unset(self::${$type}['merged'][$rule]);
377 if ($type === 'plural') {
378 self::$cache['pluralize'] = self::$cache['tableize'] = array();
379 } elseif ($type === 'singular') {
380 self::$cache['singularize'] = array();
384 self::${$type}['rules'] = $rules + self::${$type}['rules'];
388 * Returns a word in plural form.
390 * @param string $word The word in singular form.
392 * @return string The word in plural form.
394 public static function pluralize(string $word) : string
396 if (isset(self::$cache['pluralize'][$word])) {
397 return self::$cache['pluralize'][$word];
400 if (!isset(self::$plural['merged']['irregular'])) {
401 self::$plural['merged']['irregular'] = self::$plural['irregular'];
404 if (!isset(self::$plural['merged']['uninflected'])) {
405 self::$plural['merged']['uninflected'] = array_merge(self::$plural['uninflected'], self::$uninflected);
408 if (!isset(self::$plural['cacheUninflected']) || !isset(self::$plural['cacheIrregular'])) {
409 self::$plural['cacheUninflected'] = '(?:' . implode('|', self::$plural['merged']['uninflected']) . ')';
410 self::$plural['cacheIrregular'] = '(?:' . implode('|', array_keys(self::$plural['merged']['irregular'])) . ')';
413 if (preg_match('/(.*)\\b(' . self::$plural['cacheIrregular'] . ')$/i', $word, $regs)) {
414 self::$cache['pluralize'][$word] = $regs[1] . $word[0] . substr(self::$plural['merged']['irregular'][strtolower($regs[2])], 1);
416 return self::$cache['pluralize'][$word];
419 if (preg_match('/^(' . self::$plural['cacheUninflected'] . ')$/i', $word, $regs)) {
420 self::$cache['pluralize'][$word] = $word;
425 foreach (self::$plural['rules'] as $rule => $replacement) {
426 if (preg_match($rule, $word)) {
427 self::$cache['pluralize'][$word] = preg_replace($rule, $replacement, $word);
429 return self::$cache['pluralize'][$word];
435 * Returns a word in singular form.
437 * @param string $word The word in plural form.
439 * @return string The word in singular form.
441 public static function singularize(string $word) : string
443 if (isset(self::$cache['singularize'][$word])) {
444 return self::$cache['singularize'][$word];
447 if (!isset(self::$singular['merged']['uninflected'])) {
448 self::$singular['merged']['uninflected'] = array_merge(
449 self::$singular['uninflected'],
454 if (!isset(self::$singular['merged']['irregular'])) {
455 self::$singular['merged']['irregular'] = array_merge(
456 self::$singular['irregular'],
457 array_flip(self::$plural['irregular'])
461 if (!isset(self::$singular['cacheUninflected']) || !isset(self::$singular['cacheIrregular'])) {
462 self::$singular['cacheUninflected'] = '(?:' . implode('|', self::$singular['merged']['uninflected']) . ')';
463 self::$singular['cacheIrregular'] = '(?:' . implode('|', array_keys(self::$singular['merged']['irregular'])) . ')';
466 if (preg_match('/(.*)\\b(' . self::$singular['cacheIrregular'] . ')$/i', $word, $regs)) {
467 self::$cache['singularize'][$word] = $regs[1] . $word[0] . substr(self::$singular['merged']['irregular'][strtolower($regs[2])], 1);
469 return self::$cache['singularize'][$word];
472 if (preg_match('/^(' . self::$singular['cacheUninflected'] . ')$/i', $word, $regs)) {
473 self::$cache['singularize'][$word] = $word;
478 foreach (self::$singular['rules'] as $rule => $replacement) {
479 if (preg_match($rule, $word)) {
480 self::$cache['singularize'][$word] = preg_replace($rule, $replacement, $word);
482 return self::$cache['singularize'][$word];
486 self::$cache['singularize'][$word] = $word;