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

Inflector::camelize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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