Passed
Pull Request — master (#35)
by
unknown
02:28
created

Inflector::tableize()   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
namespace Doctrine\Common\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 517
    public static function tableize(string $word) : string
358
    {
359 517
        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 18
    public static function classify(string $word) : string
366
    {
367 18
        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 12
    public static function camelize(string $word) : string
374
    {
375 12
        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 258
    public static function pluralize(string $word) : string
488
    {
489 258
        $tablized_word = self::tableize($word);
490
491 258
        if (isset(self::$cache['pluralize'][$word])) {
492 12
            return self::$cache['pluralize'][$word];
493
        }
494
495 246
        if (! isset(self::$plural['merged']['irregular'])) {
496 4
            self::$plural['merged']['irregular'] = self::$plural['irregular'];
497
        }
498
499 246
        if (! isset(self::$plural['merged']['uninflected'])) {
500 3
            self::$plural['merged']['uninflected'] = array_merge(self::$plural['uninflected'], self::$uninflected);
501
        }
502
503 246
        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 246
        if (preg_match('/(.*(?:\\b|_))(' . self::$plural['cacheIrregular'] . ')$/i', $tablized_word, $regs)) {
509 45
            $result = $regs[1] . substr($regs[2], 0, 1) . substr(self::$plural['merged']['irregular'][strtolower($regs[2])], 1);
510 45
            if ($tablized_word !== $word) {
511 3
                $camelized_result = self::camelize($result);
512
513 3
                $result = substr($word, 0, 1) . substr($camelized_result, 1);
514
            }
515
516 45
            return self::$cache['pluralize'][$word] = $result;
517
        }
518
519 203
        if (preg_match('/^(' . self::$plural['cacheUninflected'] . ')$/i', $word, $regs)) {
520 109
            self::$cache['pluralize'][$word] = $word;
521
522 109
            return $word;
523
        }
524
525 96
        foreach (self::$plural['rules'] as $rule => $replacement) {
526 96
            if (preg_match($rule, $word)) {
527 96
                self::$cache['pluralize'][$word] = preg_replace($rule, $replacement, $word);
528
529 96
                return self::$cache['pluralize'][$word];
530
            }
531
        }
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...
532
    }
533
534
    /**
535
     * Returns a word in singular form.
536
     *
537
     * @param string $word The word in plural form.
538
     *
539
     * @return string The word in singular form.
540
     */
541 258
    public static function singularize(string $word) : string
542
    {
543 258
        $tablized_word = self::tableize($word);
544
545 258
        if (isset(self::$cache['singularize'][$word])) {
546 12
            return self::$cache['singularize'][$word];
547
        }
548
549 246
        if (! isset(self::$singular['merged']['uninflected'])) {
550 3
            self::$singular['merged']['uninflected'] = array_merge(
551 3
                self::$singular['uninflected'],
552 3
                self::$uninflected
553
            );
554
        }
555
556 246
        if (! isset(self::$singular['merged']['irregular'])) {
557 2
            self::$singular['merged']['irregular'] = array_merge(
558 2
                self::$singular['irregular'],
559 2
                array_flip(self::$plural['irregular'])
560
            );
561
        }
562
563 246
        if (! isset(self::$singular['cacheUninflected']) || ! isset(self::$singular['cacheIrregular'])) {
564 3
            self::$singular['cacheUninflected'] = '(?:' . implode('|', self::$singular['merged']['uninflected']) . ')';
565 3
            self::$singular['cacheIrregular']   = '(?:' . implode('|', array_keys(self::$singular['merged']['irregular'])) . ')';
566
        }
567
568 246
        if (preg_match('/(.*(?:\\b|_))(' . self::$singular['cacheIrregular'] . ')$/i', $tablized_word, $regs)) {
569 56
            $result = $regs[1] . substr($regs[2], 0, 1) . substr(self::$singular['merged']['irregular'][strtolower($regs[2])], 1);
570
571 56
            if ($tablized_word !== $word) {
572 4
                $camelized_result = self::camelize($result);
573
574 4
                $result = substr($word, 0, 1) . substr($camelized_result, 1);
575
            }
576 56
            return self::$cache['singularize'][$word] = $result;
577
        }
578
579 191
        if (preg_match('/^(' . self::$singular['cacheUninflected'] . ')$/i', $word, $regs)) {
580 110
            self::$cache['singularize'][$word] = $word;
581
582 110
            return $word;
583
        }
584
585 83
        foreach (self::$singular['rules'] as $rule => $replacement) {
586 83
            if (preg_match($rule, $word)) {
587 81
                self::$cache['singularize'][$word] = preg_replace($rule, $replacement, $word);
588
589 83
                return self::$cache['singularize'][$word];
590
            }
591
        }
592
593 2
        self::$cache['singularize'][$word] = $word;
594
595 2
        return $word;
596
    }
597
}
598