• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ICanBoogie / Inflector / 11893385119

18 Nov 2024 01:23PM UTC coverage: 94.475% (+4.4%) from 90.058%
11893385119

push

github

olvlvl
Add StaticInflector

20 of 23 new or added lines in 2 files covered. (86.96%)

6 existing lines in 2 files now uncovered.

171 of 181 relevant lines covered (94.48%)

53.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.22
/lib/Inflections.php
1
<?php
2

3
namespace ICanBoogie;
4

5
use InvalidArgumentException;
6

7
use function class_exists;
8

9
/**
10
 * A representation of the inflections used by an inflector.
11
 *
12
 * @property-read array $plurals Rules for {@see pluralize()}.
13
 * @property-read array $singulars Rules for {@see singularize()}.
14
 * @property-read array $uncountables Uncountables.
15
 * @property-read array $humans Rules for {@see humanize()}.
16
 * @property-read array $acronyms Acronyms.
17
 * @property-read string $acronym_regex Acronyms regex.
18
 */
19
final class Inflections
20
{
21
    /**
22
     * @var array<string , Inflections>
23
     */
24
    private static $inflections = [];
25

26
    /**
27
     * Returns inflections for the specified locale.
28
     *
29
     * Note: Inflections are shared for the same locale. If you need to alter an instance you
30
     * MUST clone it first, otherwise your changes will affect others.
31
     */
32
    public static function get(string $locale = Inflector::DEFAULT_LOCALE): Inflections
33
    {
34
        if (isset(self::$inflections[$locale])) {
7✔
UNCOV
35
            return self::$inflections[$locale];
×
36
        }
37

38
        $inflections = new self();
7✔
39
        $configurator = __NAMESPACE__ . "\\Inflections\\$locale";
7✔
40

41
        if (!class_exists($configurator)) {
7✔
42
            throw new InflectionsNotFound("Unable to load inflections for `$locale`, tried `$configurator`.");
1✔
43
        }
44

45
        $configurator::configure($inflections);
6✔
46

47
        return self::$inflections[$locale] = $inflections;
6✔
48
    }
49

50
    /**
51
     * Rules for {@see pluralize()}.
52
     *
53
     * @var array<string, string> Where _key_ is a rule and _value_ a replacement.
54
     */
55
    protected $plurals = [];
56

57
    /**
58
     * Rules for {@see singularize()}.
59
     *
60
     * @var array<string, string> Where _key_ is a rule and _value_ a replacement.
61
     */
62
    protected $singulars = [];
63

64
    /**
65
     * Uncountables.
66
     *
67
     * @var array<string, string> Where _key_ is a word and _value_ the same word.
68
     */
69
    protected $uncountables = [];
70

71
    /**
72
     * Rules for {@see humanize()}.
73
     *
74
     * @var array<string, string> Where _key_ is a rule and _value_ a replacement.
75
     */
76
    protected $humans = [];
77

78
    /**
79
     * Acronyms.
80
     *
81
     * @var array<string, string> Where _key_ is a lower case version of _value_.
82
     */
83
    protected $acronyms = [];
84

85
    /**
86
     * Acronyms regex.
87
     *
88
     * @var string
89
     */
90
    protected $acronym_regex = '/(?=a)b/';
91

92
    /**
93
     * Returns the {@see $acronyms}, {@see $acronym_regex}, {@see $plurals}, {@see $singulars},
94
     * {@see $uncountables} and {@see $humans} properties.
95
     *
96
     * @param string $property
97
     *
98
     * @return mixed
99
     *
100
     * @throws PropertyNotDefined in an attempt to read an inaccessible property. If the {@see PropertyNotDefined}
101
     * class is not available a {@see \InvalidArgumentException} is thrown instead.
102
     */
103
    public function __get(string $property)
104
    {
105
        static $readers = [ 'acronyms', 'acronym_regex', 'plurals', 'singulars', 'uncountables', 'humans' ];
575✔
106

107
        if (in_array($property, $readers)) {
575✔
108
            return $this->$property;
575✔
109
        }
110

UNCOV
111
        if (class_exists(PropertyNotDefined::class)) {
×
UNCOV
112
            throw new PropertyNotDefined([ $property, $this ]);
×
113
        } else {
UNCOV
114
            throw new InvalidArgumentException("Property not defined: $property");
×
115
        }
116
    }
117

118
    /**
119
     * Specifies a new acronym. An acronym must be specified as it will appear
120
     * in a camelized string. An underscore string that contains the acronym
121
     * will retain the acronym when passed to {@see camelize}, {@see humanize}, or
122
     * {@see titleize}. A camelized string that contains the acronym will maintain
123
     * the acronym when titleized or humanized, and will convert the acronym
124
     * into a non-delimited single lowercase word when passed to {@see underscore}.
125
     *
126
     * <pre>
127
     * $this->acronym('HTML');
128
     * $this->titleize('html');                 // 'HTML'
129
     * $this->camelize('html');                 // 'HTML'
130
     * $this->underscore('MyHTML');             // 'my_html'
131
     * </pre>
132
     *
133
     * The acronym, however, must occur as a delimited unit and not be part of
134
     * another word for conversions to recognize it:
135
     *
136
     * <pre>
137
     * $this->acronym('HTTP');
138
     * $this->camelize('my_http_delimited');    // 'MyHTTPDelimited'
139
     * $this->camelize('https');                // 'Https', not 'HTTPs'
140
     * $this->underscore('HTTPS');              // 'http_s', not 'https'
141
     *
142
     * $this->acronym('HTTPS');
143
     * $this->camelize('https');                // 'HTTPS'
144
     * $this->underscore('HTTPS');              // 'https'
145
     * </pre>
146
     *
147
     * Note: Acronyms that are passed to {@see pluralize} will no longer be
148
     * recognized, since the acronym will not occur as a delimited unit in the
149
     * pluralized result. To work around this, you must specify the pluralized
150
     * form as an acronym as well:
151
     *
152
     * <pre>
153
     * $this->acronym('API');
154
     * $this->camelize($this->pluralize('api')); // 'Apis'
155
     *
156
     * $this->acronym('APIs');
157
     * $this->camelize($this->pluralize('api')); // 'APIs'
158
     * </pre>
159
     *
160
     * {@see acronym} may be used to specify any word that contains an acronym or
161
     * otherwise needs to maintain a non-standard capitalization. The only
162
     * restriction is that the word must begin with a capital letter.
163
     *
164
     * <pre>
165
     * $this->acronym('RESTful');
166
     * $this->underscore('RESTful');             // 'restful'
167
     * $this->underscore('RESTfulController');   // 'restful_controller'
168
     * $this->titleize('RESTfulController');     // 'RESTful Controller'
169
     * $this->camelize('restful');               // 'RESTful'
170
     * $this->camelize('restful_controller');    // 'RESTfulController'
171
     *
172
     * $this->acronym('McHammer');
173
     * $this->underscore('McHammer');            // 'mchammer'
174
     * $this->camelize('mchammer');              // 'McHammer'
175
     * </pre>
176
     *
177
     * @return $this
178
     */
179
    public function acronym(string $acronym): self
180
    {
181
        $this->acronyms[downcase($acronym)] = $acronym;
4✔
182
        $this->acronym_regex = '/' . implode('|', $this->acronyms) . '/';
4✔
183

184
        return $this;
4✔
185
    }
186

187
    /**
188
     * Specifies a new pluralization rule and its replacement.
189
     *
190
     * <pre>
191
     * $this->plural('/^(ax|test)is$/i', '\1es');
192
     * $this->plural('/(buffal|tomat)o$/i', '\1oes');
193
     * $this->plural('/^(m|l)ouse$/i', '\1ice');
194
     * </pre>
195
     *
196
     * @param string $rule A regex string.
197
     * @param string $replacement The replacement should always be a string that may include
198
     * references to the matched data from the rule.
199
     *
200
     * @return $this
201
     */
202
    public function plural(string $rule, string $replacement): self
203
    {
204
        unset($this->uncountables[$rule]);
6✔
205
        unset($this->uncountables[$replacement]);
6✔
206

207
        $this->plurals = [ $rule => $replacement ] + $this->plurals;
6✔
208

209
        return $this;
6✔
210
    }
211

212
    /**
213
     * Specifies a new singularization rule and its replacement.
214
     *
215
     * <pre>
216
     * $this->singular('/(n)ews$/i', '\1ews');
217
     * $this->singular('/([^aeiouy]|qu)ies$/i', '\1y');
218
     * $this->singular('/(quiz)zes$/i', '\1');
219
     * </pre>
220
     *
221
     * @param string $rule A regex string.
222
     * @param string $replacement The replacement should always be a string that may include
223
     * references to the matched data from the rule.
224
     *
225
     * @return $this
226
     */
227
    public function singular(string $rule, string $replacement): self
228
    {
229
        unset($this->uncountables[$rule]);
6✔
230
        unset($this->uncountables[$replacement]);
6✔
231

232
        $this->singulars = [ $rule => $replacement ] + $this->singulars;
6✔
233

234
        return $this;
6✔
235
    }
236

237
    /**
238
     * Specifies a new irregular that applies to both pluralization and singularization at the
239
     * same time. This can only be used for strings, not regular expressions. You simply pass
240
     * the irregular in singular and plural form.
241
     *
242
     * <pre>
243
     * $this->irregular('child', 'children');
244
     * $this->irregular('person', 'people');
245
     * </pre>
246
     *
247
     * @return $this
248
     */
249
    public function irregular(string $singular, string $plural): self
250
    {
251
        unset($this->uncountables[$singular]);
6✔
252
        unset($this->uncountables[$plural]);
6✔
253

254
        $s0 = mb_substr($singular, 0, 1);
6✔
255
        $s0_upcase = upcase($s0);
6✔
256
        $srest = mb_substr($singular, 1);
6✔
257

258
        $p0 = mb_substr($plural, 0, 1);
6✔
259
        $p0_upcase = upcase($p0);
6✔
260
        $prest = mb_substr($plural, 1);
6✔
261

262
        if ($s0_upcase == $p0_upcase) {
6✔
263
            $this->plural("/({$s0}){$srest}$/i", '\1' . $prest);
6✔
264
            $this->plural("/({$p0}){$prest}$/i", '\1' . $prest);
6✔
265

266
            $this->singular("/({$s0}){$srest}$/i", '\1' . $srest);
6✔
267
            $this->singular("/({$p0}){$prest}$/i", '\1' . $srest);
6✔
268
        } else {
269
            $s0_downcase = downcase($s0);
1✔
270
            $p0_downcase = downcase($p0);
1✔
271

272
            $this->plural("/{$s0_upcase}(?i){$srest}$/", $p0_upcase . $prest);
1✔
273
            $this->plural("/{$s0_downcase}(?i){$srest}$/", $p0_downcase . $prest);
1✔
274
            $this->plural("/{$p0_upcase}(?i){$prest}$/", $p0_upcase . $prest);
1✔
275
            $this->plural("/{$p0_downcase}(?i){$prest}$/", $p0_downcase . $prest);
1✔
276

277
            $this->singular("/{$s0_upcase}(?i){$srest}$/", $s0_upcase . $srest);
1✔
278
            $this->singular("/{$s0_downcase}(?i){$srest}$/", $s0_downcase . $srest);
1✔
279
            $this->singular("/{$p0_upcase}(?i){$prest}$/", $s0_upcase . $srest);
1✔
280
            $this->singular("/{$p0_downcase}(?i){$prest}$/", $s0_downcase . $srest);
1✔
281
        }
282

283
        return $this;
6✔
284
    }
285

286
    /**
287
     * Add uncountable words that shouldn't be attempted inflected.
288
     *
289
     * <pre>
290
     * $this->uncountable('money');
291
     * $this->uncountable(explode(' ', 'money information rice'));
292
     * </pre>
293
     *
294
     * @param string|string[] $word
295
     *
296
     * @return $this
297
     */
298
    public function uncountable($word): self
299
    {
300
        if (is_array($word)) {
5✔
301
            $this->uncountables += array_combine($word, $word);
3✔
302

303
            return $this;
3✔
304
        }
305

306
        $this->uncountables[$word] = $word;
2✔
307

308
        return $this;
2✔
309
    }
310

311
    /**
312
     * Specifies a humanized form of a string by a regular expression rule or by a string mapping.
313
     * When using a regular expression based replacement, the normal humanize formatting is
314
     * called after the replacement. When a string is used, the human form should be specified
315
     * as desired (example: 'The name', not 'the_name').
316
     *
317
     * <pre>
318
     * $this->human('/_cnt$/i', '\1_count');
319
     * $this->human('legacy_col_person_name', 'Name');
320
     * </pre>
321
     *
322
     * @param string $rule A regular expression rule or a string mapping. Strings that starts with
323
     * "/", "#" or "~" are recognized as regular expressions.
324
     *
325
     * @return $this
326
     */
327
    public function human(string $rule, string $replacement): self
328
    {
329
        $r0 = $rule[0];
2✔
330

331
        if ($r0 != '/' && $r0 != '#' && $r0 != '~') {
2✔
332
            $rule = '/' . preg_quote($rule, '/') . '/';
1✔
333
        }
334

335
        $this->humans = [ $rule => $replacement ] + $this->humans;
2✔
336

337
        return $this;
2✔
338
    }
339
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc