GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Inflector   C
last analyzed

Complexity

Total Complexity 56

Size/Duplication

Total Lines 480
Duplicated Lines 9.58 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 48.28%

Importance

Changes 0
Metric Value
dl 46
loc 480
ccs 56
cts 116
cp 0.4828
rs 6.5957
c 0
b 0
f 0
wmc 56
lcom 1
cbo 1

26 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A setCachePath() 0 5 1
A setCacheFile() 0 4 1
B readCache() 0 18 6
A isCached() 0 10 3
A hasCacheFile() 0 4 2
A getCacheTTL() 0 11 3
A __destruct() 0 6 2
A writeCache() 0 8 3
A unclassify() 0 4 1
A doInflection() 0 10 2
A singularize() 0 4 1
A camelize() 0 4 1
A classify() 0 4 1
A __call() 0 6 1
C doSingularize() 23 23 7
A doCamelize() 0 4 1
A doHyphenize() 0 6 1
A doUnderscore() 0 5 1
A doTableize() 0 4 1
A pluralize() 0 4 1
A underscore() 0 4 1
A doClassify() 0 7 1
A doUnclassify() 0 8 1
B doOrdinalize() 0 20 5
C doPluralize() 23 24 7

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Inflector often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Inflector, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Nip\Inflector;
4
5
/**
6
 * Class Inflector
7
 * @package Nip\Inflector
8
 * @based on https://github.com/cakephp/cakephp/blob/master/src/Utility/Inflector.php
9
 */
10
class Inflector
11
{
12
13
    protected $plural = [
14
        '/(s)tatus$/i' => '\1tatuses',
15
        '/(quiz)$/i' => '\1zes',
16
        '/^(ox)$/i' => '\1\2en',
17
        '/([m|l])ouse$/i' => '\1ice',
18
        '/(matr|vert|ind)(ix|ex)$/i' => '\1ices',
19
        '/(x|ch|ss|sh)$/i' => '\1es',
20
        '/([^aeiouy]|qu)y$/i' => '\1ies',
21
        '/(hive)$/i' => '\1s',
22
        '/(chef)$/i' => '\1s',
23
        '/(?:([^f])fe|([lre])f)$/i' => '\1\2ves',
24
        '/sis$/i' => 'ses',
25
        '/([ti])um$/i' => '\1a',
26
        '/(p)erson$/i' => '\1eople',
27
        '/(?<!u)(m)an$/i' => '\1en',
28
        '/(c)hild$/i' => '\1hildren',
29
        '/(buffal|tomat)o$/i' => '\1\2oes',
30
        '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin)us$/i' => '\1i',
31
        '/us$/i' => 'uses',
32
        '/(alias)$/i' => '\1es',
33
        '/(ax|cris|test)is$/i' => '\1es',
34
        '/s$/' => 's',
35
        '/^$/' => '',
36
        '/$/' => 's',
37
    ];
38
    protected $singular = [
39
        '/(s)tatuses$/i' => '\1\2tatus',
40
        '/^(.*)(menu)s$/i' => '\1\2',
41
        '/(quiz)zes$/i' => '\\1',
42
        '/(matr)ices$/i' => '\1ix',
43
        '/(vert|ind)ices$/i' => '\1ex',
44
        '/^(ox)en/i' => '\1',
45
        '/(alias)(es)*$/i' => '\1',
46
        '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us',
47
        '/([ftw]ax)es/i' => '\1',
48
        '/(cris|ax|test)es$/i' => '\1is',
49
        '/(shoe)s$/i' => '\1',
50
        '/(o)es$/i' => '\1',
51
        '/ouses$/' => 'ouse',
52
        '/([^a])uses$/' => '\1us',
53
        '/([m|l])ice$/i' => '\1ouse',
54
        '/(x|ch|ss|sh)es$/i' => '\1',
55
        '/(m)ovies$/i' => '\1\2ovie',
56
        '/(s)eries$/i' => '\1\2eries',
57
        '/([^aeiouy]|qu)ies$/i' => '\1y',
58
        '/(tive)s$/i' => '\1',
59
        '/(hive)s$/i' => '\1',
60
        '/(drive)s$/i' => '\1',
61
        '/([le])ves$/i' => '\1f',
62
        '/([^rfoa])ves$/i' => '\1fe',
63
        '/(^analy)ses$/i' => '\1sis',
64
        '/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis',
65
        '/([ti])a$/i' => '\1um',
66
        '/(p)eople$/i' => '\1\2erson',
67
        '/(m)en$/i' => '\1an',
68
        '/(c)hildren$/i' => '\1\2hild',
69
        '/(n)ews$/i' => '\1\2ews',
70
        '/eaus$/' => 'eau',
71
        '/^(.*us)$/' => '\\1',
72
        '/s$/i' => ''
73
    ];
74
    protected $uncountable = [
75
        '.*[nrlm]ese',
76
        '.*data',
77
        '.*deer',
78
        '.*fish',
79
        '.*measles',
80
        '.*ois',
81
        '.*pox',
82
        '.*sheep',
83
        'people',
84
        'feedback',
85
        'stadia',
86
        '.*?media',
87
        'chassis',
88
        'clippers',
89
        'debris',
90
        'diabetes',
91
        'equipment',
92
        'gallows',
93
        'graffiti',
94
        'headquarters',
95
        'information',
96
        'innings',
97
        'news',
98
        'nexus',
99
        'pokemon',
100
        'proceedings',
101
        'research',
102
        'sea[- ]bass',
103
        'series',
104
        'species',
105
        'weather'
106
    ];
107
    protected $irregular = [
108
        'atlas' => 'atlases',
109
        'beef' => 'beefs',
110
        'brief' => 'briefs',
111
        'brother' => 'brothers',
112
        'cafe' => 'cafes',
113
        'child' => 'children',
114
        'cookie' => 'cookies',
115
        'corpus' => 'corpuses',
116
        'cow' => 'cows',
117
        'criterion' => 'criteria',
118
        'ganglion' => 'ganglions',
119
        'genie' => 'genies',
120
        'genus' => 'genera',
121
        'graffito' => 'graffiti',
122
        'hoof' => 'hoofs',
123
        'loaf' => 'loaves',
124
        'man' => 'men',
125
        'money' => 'monies',
126
        'mongoose' => 'mongooses',
127
        'move' => 'moves',
128
        'mythos' => 'mythoi',
129
        'niche' => 'niches',
130
        'numen' => 'numina',
131
        'occiput' => 'occiputs',
132
        'octopus' => 'octopuses',
133
        'opus' => 'opuses',
134
        'ox' => 'oxen',
135
        'penis' => 'penises',
136
        'person' => 'people',
137
        'sex' => 'sexes',
138
        'soliloquy' => 'soliloquies',
139
        'testis' => 'testes',
140
        'trilby' => 'trilbys',
141
        'turf' => 'turfs',
142
        'potato' => 'potatoes',
143
        'hero' => 'heroes',
144
        'tooth' => 'teeth',
145
        'goose' => 'geese',
146
        'foot' => 'feet',
147
        'foe' => 'foes',
148
        'sieve' => 'sieves'
149
    ];
150
151
    protected $dictionary;
152
153
    protected $cacheFile = null;
154
155
    protected $toCache = false;
156
157
    /**
158
     * Inflector constructor.
159
     */
160 19
    public function __construct()
161
    {
162 19
    }
163
164
    /**
165
     * @param $directory
166
     */
167
    public function setCachePath($directory)
168
    {
169
        $file = $directory . DIRECTORY_SEPARATOR . 'inflector.php';
170
        $this->setCacheFile($file);
171
    }
172
173
    /**
174
     * @param null|string $cacheFile
175
     */
176
    public function setCacheFile($cacheFile)
177
    {
178
        $this->cacheFile = $cacheFile;
179
    }
180
181
    public function readCache()
182
    {
183
        if ($this->isCached()) {
184
            /** @noinspection PhpIncludeInspection */
185
            include($this->cacheFile);
186
187
            /** @noinspection PhpUndefinedVariableInspection */
188
            if ($inflector) {
189
                foreach ($inflector as $type => $words) {
0 ignored issues
show
Bug introduced by
The variable $inflector does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
190
                    if ($words) {
191
                        foreach ($words as $word => $inflection) {
192
                            $this->dictionary[$type][$word] = $inflection;
193
                        }
194
                    }
195
                }
196
            }
197
        }
198
    }
199
200
    /**
201
     * @return bool
202
     */
203
    public function isCached()
204
    {
205
        if ($this->hasCacheFile()) {
206
            if (filemtime($this->cacheFile) + $this->getCacheTTL() > time()) {
207
                return true;
208
            }
209
        }
210
211
        return false;
212
    }
213
214
    /**
215
     * @return bool
216
     */
217
    public function hasCacheFile()
218
    {
219
        return ($this->cacheFile && file_exists($this->cacheFile));
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->cacheFile of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
220
    }
221
222
    /**
223
     * @return int
224
     */
225
    public function getCacheTTL()
226
    {
227
        if (app()->has('config')) {
228
            $config = app()->get('config');
229
            if ($config->has('MISC.inflector_cache')) {
230
                return $config->get('MISC.inflector_cache');
231
            }
232
        }
233
234
        return 86400;
235
    }
236
237
    public function __destruct()
238
    {
239
        if ($this->toCache) {
240
            $this->writeCache();
241
        }
242
    }
243
244
    public function writeCache()
245
    {
246
        if ($this->dictionary && $this->cacheFile) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->cacheFile of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
247
            $file = new \Nip_File_Handler(["path" => $this->cacheFile]);
0 ignored issues
show
Documentation introduced by
array('path' => $this->cacheFile) is of type array<string,string,{"path":"string"}>, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
248
            $data = '<?php $inflector = ' . var_export($this->dictionary, true) . ";";
249
            $file->rewrite($data);
0 ignored issues
show
Documentation introduced by
$data is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
250
        }
251
    }
252
253
    /**
254
     * @param $word
255
     * @return mixed
256
     */
257 21
    public function unclassify($word)
258
    {
259 21
        return $this->doInflection('unclassify', $word);
260
    }
261
262
    /**
263
     * @param $name
264
     * @param $word
265
     * @return mixed
266
     */
267 47
    public function doInflection($name, $word)
268
    {
269 47
        if (!isset($this->dictionary[$name][$word])) {
270 35
            $this->toCache = true;
271 35
            $method = "do" . ucfirst($name);
272 35
            $this->dictionary[$name][$word] = $this->$method($word);
273
        }
274
275 47
        return $this->dictionary[$name][$word];
276
    }
277
278
    /**
279
     * @param $word
280
     * @return mixed
281
     */
282 6
    public function singularize($word)
283
    {
284 6
        return $this->doInflection('singularize', $word);
285
    }
286
287
    /**
288
     * @param $word
289
     * @return mixed
290
     */
291 22
    public function camelize($word)
292
    {
293 22
        return $this->doInflection('camelize', $word);
294
    }
295
296
    /**
297
     * @param $word
298
     * @return mixed
299
     */
300 9
    public function classify($word)
301
    {
302 9
        return $this->doInflection('classify', $word);
303
    }
304
305
    /**
306
     * @param $name
307
     * @param $arguments
308
     * @return mixed
309
     */
310
    public function __call($name, $arguments)
311
    {
312
        $word = $arguments[0];
313
314
        return $this->doInflection($name, $word);
315
    }
316
317
    /**
318
     * @param $word
319
     * @return bool|mixed
320
     */
321 4 View Code Duplication
    protected function doPluralize($word)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
322
    {
323 4
        $lowerCased_word = strtolower($word);
324
325 4
        foreach ($this->uncountable as $_uncountable) {
326 4
            if (substr($lowerCased_word, (-1 * strlen($_uncountable))) == $_uncountable) {
327 4
                return $word;
328
            }
329
        }
330
331 4
        foreach ($this->irregular as $_plural => $_singular) {
332 4
            if (preg_match('/(' . $_plural . ')$/i', $word, $arr)) {
333 4
                return preg_replace('/(' . $_plural . ')$/i', substr($arr[0], 0, 1) . substr($_singular, 1), $word);
334
            }
335
        }
336
337 4
        foreach ($this->plural as $rule => $replacement) {
338 4
            if (preg_match($rule, $word)) {
339 4
                return preg_replace($rule, $replacement, $word);
340
            }
341
        }
342
343
        return false;
344
    }
345
346
    /**
347
     * @param $word
348
     * @return mixed
349
     */
350 4 View Code Duplication
    protected function doSingularize($word)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
351
    {
352 4
        $lowercased_word = strtolower($word);
353 4
        foreach ($this->uncountable as $_uncountable) {
354 4
            if (substr($lowercased_word, (-1 * strlen($_uncountable))) == $_uncountable) {
355 4
                return $word;
356
            }
357
        }
358
359 4
        foreach ($this->irregular as $_plural => $_singular) {
360 4
            if (preg_match('/(' . $_singular . ')$/i', $word, $arr)) {
361 4
                return preg_replace('/(' . $_singular . ')$/i', substr($arr[0], 0, 1) . substr($_plural, 1), $word);
362
            }
363
        }
364
365 4
        foreach ($this->singular as $rule => $replacement) {
366 4
            if (preg_match($rule, $word)) {
367 4
                return preg_replace($rule, $replacement, $word);
368
            }
369
        }
370
371
        return $word;
372
    }
373
374
    /**
375
     * @param $word
376
     * @return mixed
377
     */
378 13
    protected function doCamelize($word)
379
    {
380 13
        return str_replace(' ', '', ucwords(preg_replace('/[^A-Z^a-z^0-9]+/', ' ', $word)));
381
    }
382
383
    /**
384
     * @param $word
385
     * @return mixed
386
     */
387
    protected function doHyphenize($word)
388
    {
389
        $word = $this->doUnderscore($word);
390
391
        return str_replace('_', '-', $word);
392
    }
393
394
    /**
395
     * @param $word
396
     * @return string
397
     */
398 16
    protected function doUnderscore($word)
399
    {
400 16
        return strtolower(preg_replace('/[^A-Z^a-z^0-9]+/', '_',
401 16
            preg_replace('/([a-zd])([A-Z])/', '\1_\2', preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1_\2', $word))));
402
    }
403
404
    /**
405
     * Converts a class name to its table name according to rails
406
     * naming conventions.
407
     *
408
     * Converts "Person" to "people"
409
     *
410
     * @param string $class_name Class name for getting related table_name.
411
     * @return string plural_table_name
412
     */
413
    protected function doTableize($class_name)
414
    {
415
        return $this->pluralize($this->underscore($class_name));
416
    }
417
418
    /**
419
     * @param $word
420
     * @return mixed
421
     */
422 4
    public function pluralize($word)
423
    {
424 4
        return $this->doInflection('pluralize', $word);
425
    }
426
427
    /**
428
     * @param $word
429
     * @return mixed
430
     */
431 19
    public function underscore($word)
432
    {
433 19
        return $this->doInflection('underscore', $word);
434
    }
435
436
    /**
437
     * Converts lowercase string to underscored camelize class format
438
     *
439
     * @param string $string
440
     * @return string
441
     */
442 9
    protected function doClassify($string)
443
    {
444 9
        $parts = explode("-", $string);
445 9
        $parts = array_map([$this, "camelize"], $parts);
446
447 9
        return implode("_", $parts);
448
    }
449
450
    /**
451
     * Reverses classify()
452
     *
453
     * @param string $string
454
     * @return string
455
     */
456 16
    protected function doUnclassify($string)
457
    {
458 16
        $string = str_replace('\\', '_', $string);
459 16
        $parts = explode("_", $string);
460 16
        $parts = array_map([$this, "underscore"], $parts);
461
462 16
        return implode("-", $parts);
463
    }
464
465
    /**
466
     * @param $number
467
     * @return string
468
     */
469
    protected function doOrdinalize($number)
470
    {
471
        if (in_array(($number % 100), range(11, 13))) {
472
            return $number . 'th';
473
        } else {
474
            switch (($number % 10)) {
475
                case 1:
476
                    return $number . 'st';
477
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
478
                case 2:
479
                    return $number . 'nd';
480
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
481
                case 3:
482
                    return $number . 'rd';
483
                default:
484
                    return $number . 'th';
485
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
486
            }
487
        }
488
    }
489
}
490