Failed Conditions
Pull Request — master (#90)
by Jonathan
02:09
created

Inflector::rules()   C

Complexity

Conditions 8
Paths 20

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 8

Importance

Changes 0
Metric Value
cc 8
eloc 17
nc 20
nop 3
dl 0
loc 29
ccs 17
cts 17
cp 1
crap 8
rs 5.3846
c 0
b 0
f 0
1
<?php
2
3
namespace Doctrine\Inflector;
4
5
use function array_flip;
6
use function array_keys;
7
use function array_merge;
8
use function get_class_vars;
9
use function implode;
10
use function is_array;
11
use function lcfirst;
12
use function preg_match;
13
use function preg_replace;
14
use function str_replace;
15
use function strtolower;
16
use function substr;
17
use function ucfirst;
18
use function ucwords;
19
20
/**
21
 * Doctrine inflector has static methods for inflecting text.
22
 *
23
 * The methods in these classes are from several different sources collected
24
 * across several different php projects and several different authors. The
25
 * original author names and emails are not known.
26
 *
27
 * Pluralize & Singularize implementation are borrowed from CakePHP with some modifications.
28
 */
29
class Inflector
30
{
31
    /**
32
     * Plural inflector rules.
33
     *
34
     * @var string|string[][]
35
     */
36
    private static $plural = [
37
        'rules' => [
38
            '/(s)tatus$/i' => '\1\2tatuses',
39
            '/(quiz)$/i' => '\1zes',
40
            '/^(ox)$/i' => '\1\2en',
41
            '/([m|l])ouse$/i' => '\1ice',
42
            '/(matr|vert|ind)(ix|ex)$/i' => '\1ices',
43
            '/(x|ch|ss|sh)$/i' => '\1es',
44
            '/([^aeiouy]|qu)y$/i' => '\1ies',
45
            '/(hive|gulf)$/i' => '\1s',
46
            '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves',
47
            '/sis$/i' => 'ses',
48
            '/([ti])um$/i' => '\1a',
49
            '/(tax)on$/i' => '\1a',
50
            '/(c)riterion$/i' => '\1riteria',
51
            '/(p)erson$/i' => '\1eople',
52
            '/(m)an$/i' => '\1en',
53
            '/(c)hild$/i' => '\1hildren',
54
            '/(f)oot$/i' => '\1eet',
55
            '/(buffal|her|potat|tomat|volcan)o$/i' => '\1\2oes',
56
            '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$/i' => '\1i',
57
            '/us$/i' => 'uses',
58
            '/(alias)$/i' => '\1es',
59
            '/(analys|ax|cris|test|thes)is$/i' => '\1es',
60
            '/s$/' => 's',
61
            '/^$/' => '',
62
            '/$/' => 's',
63
        ],
64
        'uninflected' => [
65
            '.*[nrlm]ese',
66
            '.*deer',
67
            '.*fish',
68
            '.*measles',
69
            '.*ois',
70
            '.*pox',
71
            '.*sheep',
72
            'people',
73
            'cookie',
74
            'police',
75
            'middleware',
76
        ],
77
        'irregular' => [
78
            'atlas' => 'atlases',
79
            'axe' => 'axes',
80
            'beef' => 'beefs',
81
            'brother' => 'brothers',
82
            'cafe' => 'cafes',
83
            'chateau' => 'chateaux',
84
            'niveau' => 'niveaux',
85
            'child' => 'children',
86
            'cookie' => 'cookies',
87
            'corpus' => 'corpuses',
88
            'cow' => 'cows',
89
            'criterion' => 'criteria',
90
            'curriculum' => 'curricula',
91
            'demo' => 'demos',
92
            'domino' => 'dominoes',
93
            'echo' => 'echoes',
94
            'foot' => 'feet',
95
            'fungus' => 'fungi',
96
            'ganglion' => 'ganglions',
97
            'genie' => 'genies',
98
            'genus' => 'genera',
99
            'goose' => 'geese',
100
            'graffito' => 'graffiti',
101
            'hippopotamus' => 'hippopotami',
102
            'hoof' => 'hoofs',
103
            'human' => 'humans',
104
            'iris' => 'irises',
105
            'larva' => 'larvae',
106
            'leaf' => 'leaves',
107
            'loaf' => 'loaves',
108
            'man' => 'men',
109
            'medium' => 'media',
110
            'memorandum' => 'memoranda',
111
            'money' => 'monies',
112
            'mongoose' => 'mongooses',
113
            'motto' => 'mottoes',
114
            'move' => 'moves',
115
            'mythos' => 'mythoi',
116
            'niche' => 'niches',
117
            'nucleus' => 'nuclei',
118
            'numen' => 'numina',
119
            'occiput' => 'occiputs',
120
            'octopus' => 'octopuses',
121
            'opus' => 'opuses',
122
            'ox' => 'oxen',
123
            'passerby' => 'passersby',
124
            'penis' => 'penises',
125
            'person' => 'people',
126
            'plateau' => 'plateaux',
127
            'runner-up' => 'runners-up',
128
            'safe' => 'safes',
129
            'sex' => 'sexes',
130
            'soliloquy' => 'soliloquies',
131
            'son-in-law' => 'sons-in-law',
132
            'syllabus' => 'syllabi',
133
            'testis' => 'testes',
134
            'thief' => 'thieves',
135
            'tooth' => 'teeth',
136
            'tornado' => 'tornadoes',
137
            'trilby' => 'trilbys',
138
            'turf' => 'turfs',
139
            'valve' => 'valves',
140
            'volcano' => 'volcanoes',
141
        ],
142
    ];
143
144
    /**
145
     * Singular inflector rules.
146
     *
147
     * @var string|string[][]
148
     */
149
    private static $singular = [
150
        'rules' => [
151
            '/(s)tatuses$/i' => '\1\2tatus',
152
            '/^(.*)(menu)s$/i' => '\1\2',
153
            '/(quiz)zes$/i' => '\\1',
154
            '/(matr)ices$/i' => '\1ix',
155
            '/(vert|ind)ices$/i' => '\1ex',
156
            '/^(ox)en/i' => '\1',
157
            '/(alias)(es)*$/i' => '\1',
158
            '/(buffal|her|potat|tomat|volcan)oes$/i' => '\1o',
159
            '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us',
160
            '/([ftw]ax)es/i' => '\1',
161
            '/(analys|ax|cris|test|thes)es$/i' => '\1is',
162
            '/(shoe|slave)s$/i' => '\1',
163
            '/(o)es$/i' => '\1',
164
            '/ouses$/' => 'ouse',
165
            '/([^a])uses$/' => '\1us',
166
            '/([m|l])ice$/i' => '\1ouse',
167
            '/(x|ch|ss|sh)es$/i' => '\1',
168
            '/(m)ovies$/i' => '\1\2ovie',
169
            '/(s)eries$/i' => '\1\2eries',
170
            '/([^aeiouy]|qu)ies$/i' => '\1y',
171
            '/([lr])ves$/i' => '\1f',
172
            '/(tive)s$/i' => '\1',
173
            '/(hive)s$/i' => '\1',
174
            '/(drive)s$/i' => '\1',
175
            '/(dive)s$/i' => '\1',
176
            '/(olive)s$/i' => '\1',
177
            '/([^fo])ves$/i' => '\1fe',
178
            '/(^analy)ses$/i' => '\1sis',
179
            '/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis',
180
            '/(tax)a$/i' => '\1on',
181
            '/(c)riteria$/i' => '\1riterion',
182
            '/([ti])a$/i' => '\1um',
183
            '/(p)eople$/i' => '\1\2erson',
184
            '/(m)en$/i' => '\1an',
185
            '/(c)hildren$/i' => '\1\2hild',
186
            '/(f)eet$/i' => '\1oot',
187
            '/(n)ews$/i' => '\1\2ews',
188
            '/eaus$/' => 'eau',
189
            '/^(.*us)$/' => '\\1',
190
            '/s$/i' => '',
191
        ],
192
        'uninflected' => [
193
            '.*[nrlm]ese',
194
            '.*deer',
195
            '.*fish',
196
            '.*measles',
197
            '.*ois',
198
            '.*pox',
199
            '.*sheep',
200
            '.*ss',
201
            'data',
202
            'police',
203
            'pants',
204
            'clothes',
205
        ],
206
        'irregular' => [
207
            'abuses'     => 'abuse',
208
            'avalanches' => 'avalanche',
209
            'caches'     => 'cache',
210
            'criteria'   => 'criterion',
211
            'curves'     => 'curve',
212
            'emphases'   => 'emphasis',
213
            'foes'       => 'foe',
214
            'geese'      => 'goose',
215
            'graves'     => 'grave',
216
            'hoaxes'     => 'hoax',
217
            'media'      => 'medium',
218
            'neuroses'   => 'neurosis',
219
            'saves'      => 'save',
220
            'waves'      => 'wave',
221
            'oases'      => 'oasis',
222
            'valves'     => 'valve',
223
        ],
224
    ];
225
226
    /**
227
     * Words that should not be inflected.
228
     *
229
     * @var string[]
230
     */
231
    private static $uninflected = [
232
        '.*?media',
233
        'Amoyese',
234
        'audio',
235
        'bison',
236
        'Borghese',
237
        'bream',
238
        'breeches',
239
        'britches',
240
        'buffalo',
241
        'cantus',
242
        'carp',
243
        'chassis',
244
        'clippers',
245
        'cod',
246
        'coitus',
247
        'compensation',
248
        'Congoese',
249
        'contretemps',
250
        'coreopsis',
251
        'corps',
252
        'data',
253
        'debris',
254
        'deer',
255
        'diabetes',
256
        'djinn',
257
        'education',
258
        'eland',
259
        'elk',
260
        'emoji',
261
        'equipment',
262
        'evidence',
263
        'Faroese',
264
        'feedback',
265
        'fish',
266
        'flounder',
267
        'Foochowese',
268
        'Furniture',
269
        'furniture',
270
        'gallows',
271
        'Genevese',
272
        'Genoese',
273
        'Gilbertese',
274
        'gold',
275
        'headquarters',
276
        'herpes',
277
        'hijinks',
278
        'Hottentotese',
279
        'information',
280
        'innings',
281
        'jackanapes',
282
        'jedi',
283
        'Kiplingese',
284
        'knowledge',
285
        'Kongoese',
286
        'love',
287
        'Lucchese',
288
        'Luggage',
289
        'mackerel',
290
        'Maltese',
291
        'metadata',
292
        'mews',
293
        'moose',
294
        'mumps',
295
        'Nankingese',
296
        'news',
297
        'nexus',
298
        'Niasese',
299
        'nutrition',
300
        'offspring',
301
        'Pekingese',
302
        'Piedmontese',
303
        'pincers',
304
        'Pistoiese',
305
        'plankton',
306
        'pliers',
307
        'pokemon',
308
        'police',
309
        'Portuguese',
310
        'proceedings',
311
        'rabies',
312
        'rain',
313
        'rhinoceros',
314
        'rice',
315
        'salmon',
316
        'Sarawakese',
317
        'scissors',
318
        'sea[- ]bass',
319
        'series',
320
        'Shavese',
321
        'shears',
322
        'sheep',
323
        'siemens',
324
        'species',
325
        'staff',
326
        'swine',
327
        'traffic',
328
        'trousers',
329
        'trout',
330
        'tuna',
331
        'us',
332
        'Vermontese',
333
        'Wenchowese',
334
        'wheat',
335
        'whiting',
336
        'wildebeest',
337
        'Yengeese',
338
    ];
339
340
    /**
341
     * Method cache array.
342
     *
343
     * @var string[][]
344
     */
345
    private static $cache = [];
346
347
    /**
348
     * The initial state of Inflector so reset() works.
349
     *
350
     * @var mixed[]
351
     */
352
    private static $initialState = [];
353
354
    /**
355
     * Converts a word into the format for a Doctrine table name. Converts 'ModelName' to 'model_name'.
356
     */
357 3
    public static function tableize(string $word) : string
358
    {
359 3
        return strtolower(preg_replace('~(?<=\\w)([A-Z])~', '_$1', $word));
360
    }
361
362
    /**
363
     * Converts a word into the format for a Doctrine class name. Converts 'table_name' to 'TableName'.
364
     */
365 11
    public static function classify(string $word) : string
366
    {
367 11
        return str_replace([' ', '_', '-'], '', ucwords($word, ' _-'));
368
    }
369
370
    /**
371
     * Camelizes a word. This uses the classify() method and turns the first character to lowercase.
372
     */
373 5
    public static function camelize(string $word) : string
374
    {
375 5
        return lcfirst(self::classify($word));
376
    }
377
378
    /**
379
     * Uppercases words with configurable delimiters between words.
380
     *
381
     * Takes a string and capitalizes all of the words, like PHP's built-in
382
     * ucwords function. This extends that behavior, however, by allowing the
383
     * word delimiters to be configured, rather than only separating on
384
     * whitespace.
385
     *
386
     * Here is an example:
387
     * <code>
388
     * <?php
389
     * $string = 'top-o-the-morning to all_of_you!';
390
     * echo \Doctrine\Common\Inflector\Inflector::ucwords($string);
391
     * // Top-O-The-Morning To All_of_you!
392
     *
393
     * echo \Doctrine\Common\Inflector\Inflector::ucwords($string, '-_ ');
394
     * // Top-O-The-Morning To All_Of_You!
395
     * ?>
396
     * </code>
397
     *
398
     * @param string $string     The string to operate on.
399
     * @param string $delimiters A list of word separators.
400
     *
401
     * @return string The string with all delimiter-separated words capitalized.
402
     */
403 2
    public static function ucwords(string $string, string $delimiters = " \n\t\r\0\x0B-") : string
404
    {
405 2
        return ucwords($string, $delimiters);
406
    }
407
408
    /**
409
     * Clears Inflectors inflected value caches, and resets the inflection
410
     * rules to the initial values.
411
     */
412 4
    public static function reset() : void
413
    {
414 4
        if (empty(self::$initialState)) {
415 4
            self::$initialState = get_class_vars('Inflector');
416
417 4
            return;
418
        }
419
420
        foreach (self::$initialState as $key => $val) {
421
            if ($key === 'initialState') {
422
                continue;
423
            }
424
425
            self::${$key} = $val;
426
        }
427
    }
428
429
    /**
430
     * Adds custom inflection $rules, of either 'plural' or 'singular' $type.
431
     *
432
     * ### Usage:
433
     *
434
     * {{{
435
     * Inflector::rules('plural', ['/^(inflect)or$/i' => '\1ables']);
436
     * Inflector::rules('plural', [
437
     *     'rules' => ['/^(inflect)ors$/i' => '\1ables'],
438
     *     'uninflected' => ['dontinflectme'],
439
     *     'irregular' => ['red' => 'redlings']
440
     * ]);
441
     * }}}
442
     *
443
     * @param string           $type  The type of inflection, either 'plural' or 'singular'
444
     * @param iterable|mixed[] $rules An array of rules to be added.
445
     *                                new rules that are being defined in $rules.
446
     * @param bool             $reset If true, will unset default inflections for all
447
     *                                new rules that are being defined in $rules.
448
     */
449 4
    public static function rules(string $type, iterable $rules, bool $reset = false) : void
450
    {
451 4
        foreach ($rules as $rule => $pattern) {
452 4
            if (! is_array($pattern)) {
453 2
                continue;
454
            }
455
456 4
            if ($reset) {
457 1
                self::${$type}[$rule] = $pattern;
458
            } else {
459 3
                self::${$type}[$rule] = ($rule === 'uninflected')
460 2
                    ? array_merge($pattern, self::${$type}[$rule])
461 3
                    : $pattern + self::${$type}[$rule];
462
            }
463
464 4
            unset($rules[$rule], self::${$type}['cache' . ucfirst($rule)]);
465
466 4
            if (isset(self::${$type}['merged'][$rule])) {
467 4
                unset(self::${$type}['merged'][$rule]);
468
            }
469
470 4
            if ($type === 'plural') {
471 3
                self::$cache['pluralize'] = self::$cache['tableize'] = [];
472 3
            } elseif ($type === 'singular') {
473 4
                self::$cache['singularize'] = [];
474
            }
475
        }
476
477 4
        self::${$type}['rules'] = $rules + self::${$type}['rules'];
478 4
    }
479
480
    /**
481
     * Returns a word in plural form.
482
     *
483
     * @param string $word The word in singular form.
484
     *
485
     * @return string The word in plural form.
486
     */
487 256
    public static function pluralize(string $word) : string
488
    {
489 256
        if (isset(self::$cache['pluralize'][$word])) {
490 12
            return self::$cache['pluralize'][$word];
491
        }
492
493 244
        if (! isset(self::$plural['merged']['irregular'])) {
494 4
            self::$plural['merged']['irregular'] = self::$plural['irregular'];
495
        }
496
497 244
        if (! isset(self::$plural['merged']['uninflected'])) {
498 3
            self::$plural['merged']['uninflected'] = array_merge(self::$plural['uninflected'], self::$uninflected);
499
        }
500
501 244
        if (! isset(self::$plural['cacheUninflected']) || ! isset(self::$plural['cacheIrregular'])) {
502 4
            self::$plural['cacheUninflected'] = '(?:' . implode('|', self::$plural['merged']['uninflected']) . ')';
503 4
            self::$plural['cacheIrregular']   = '(?:' . implode('|', array_keys(self::$plural['merged']['irregular'])) . ')';
504
        }
505
506 244
        if (preg_match('/(.*)\\b(' . self::$plural['cacheIrregular'] . ')$/i', $word, $regs)) {
507 41
            self::$cache['pluralize'][$word] = $regs[1] . $word[0] . substr(self::$plural['merged']['irregular'][strtolower($regs[2])], 1);
508
509 41
            return self::$cache['pluralize'][$word];
510
        }
511
512 205
        if (preg_match('/^(' . self::$plural['cacheUninflected'] . ')$/i', $word, $regs)) {
513 110
            self::$cache['pluralize'][$word] = $word;
514
515 110
            return $word;
516
        }
517
518 97
        foreach (self::$plural['rules'] as $rule => $replacement) {
519 97
            if (preg_match($rule, $word)) {
520 97
                self::$cache['pluralize'][$word] = preg_replace($rule, $replacement, $word);
521
522 97
                return self::$cache['pluralize'][$word];
523
            }
524
        }
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
525
    }
526
527
    /**
528
     * Returns a word in singular form.
529
     *
530
     * @param string $word The word in plural form.
531
     *
532
     * @return string The word in singular form.
533
     */
534 256
    public static function singularize(string $word) : string
535
    {
536 256
        if (isset(self::$cache['singularize'][$word])) {
537 12
            return self::$cache['singularize'][$word];
538
        }
539
540 244
        if (! isset(self::$singular['merged']['uninflected'])) {
541 3
            self::$singular['merged']['uninflected'] = array_merge(
542 3
                self::$singular['uninflected'],
543 3
                self::$uninflected
544
            );
545
        }
546
547 244
        if (! isset(self::$singular['merged']['irregular'])) {
548 2
            self::$singular['merged']['irregular'] = array_merge(
549 2
                self::$singular['irregular'],
550 2
                array_flip(self::$plural['irregular'])
551
            );
552
        }
553
554 244
        if (! isset(self::$singular['cacheUninflected']) || ! isset(self::$singular['cacheIrregular'])) {
555 3
            self::$singular['cacheUninflected'] = '(?:' . implode('|', self::$singular['merged']['uninflected']) . ')';
556 3
            self::$singular['cacheIrregular']   = '(?:' . implode('|', array_keys(self::$singular['merged']['irregular'])) . ')';
557
        }
558
559 244
        if (preg_match('/(.*)\\b(' . self::$singular['cacheIrregular'] . ')$/i', $word, $regs)) {
560 52
            self::$cache['singularize'][$word] = $regs[1] . $word[0] . substr(self::$singular['merged']['irregular'][strtolower($regs[2])], 1);
561
562 52
            return self::$cache['singularize'][$word];
563
        }
564
565 193
        if (preg_match('/^(' . self::$singular['cacheUninflected'] . ')$/i', $word, $regs)) {
566 111
            self::$cache['singularize'][$word] = $word;
567
568 111
            return $word;
569
        }
570
571 84
        foreach (self::$singular['rules'] as $rule => $replacement) {
572 84
            if (preg_match($rule, $word)) {
573 82
                self::$cache['singularize'][$word] = preg_replace($rule, $replacement, $word);
574
575 84
                return self::$cache['singularize'][$word];
576
            }
577
        }
578
579 2
        self::$cache['singularize'][$word] = $word;
580
581 2
        return $word;
582
    }
583
}
584