1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the ICanBoogie package. |
5
|
|
|
* |
6
|
|
|
* (c) Olivier Laviale <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace ICanBoogie; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* A representation of the inflections used by an inflector. |
16
|
|
|
* |
17
|
|
|
* @property-read array $plurals Rules for {@link pluralize()}. |
18
|
|
|
* @property-read array $singulars Rules for {@link singularize()}. |
19
|
|
|
* @property-read array $uncountables Uncountables. |
20
|
|
|
* @property-read array $humans Rules for {@link humanize()}. |
21
|
|
|
* @property-read array $acronyms Acronyms. |
22
|
|
|
* @property-read string $acronym_regex Acronyms regex. |
23
|
|
|
*/ |
24
|
|
|
class Inflections |
25
|
|
|
{ |
26
|
|
|
/** |
27
|
|
|
* @var array<string , Inflections> |
28
|
|
|
*/ |
29
|
|
|
static private $inflections = []; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Returns inflections for the specified locale. |
33
|
|
|
* |
34
|
|
|
* Note: Inflections are shared for the same locale. If you need to alter an instance you |
35
|
|
|
* MUST clone it first, otherwise your changes will affect others. |
36
|
|
|
*/ |
37
|
|
|
static public function get(string $locale = INFLECTOR_DEFAULT_LOCALE): Inflections |
38
|
|
|
{ |
39
|
|
|
if (isset(self::$inflections[$locale])) |
40
|
|
|
{ |
41
|
|
|
return self::$inflections[$locale]; |
42
|
|
|
} |
43
|
|
|
|
44
|
|
|
$instance = new static; |
45
|
|
|
|
46
|
|
|
/* @var $inflections callable */ |
47
|
|
|
|
48
|
|
|
$inflections = require __DIR__ . "/inflections/{$locale}.php"; |
49
|
|
|
$inflections($instance); |
50
|
|
|
|
51
|
|
|
return self::$inflections[$locale] = $instance; |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Rules for {@link pluralize()}. |
56
|
|
|
* |
57
|
|
|
* @var array |
58
|
|
|
*/ |
59
|
|
|
protected $plurals = []; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Rules for {@link singularize()}. |
63
|
|
|
* |
64
|
|
|
* @var array |
65
|
|
|
*/ |
66
|
|
|
protected $singulars = []; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Uncountables. |
70
|
|
|
* |
71
|
|
|
* @var array |
72
|
|
|
*/ |
73
|
|
|
protected $uncountables = []; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Rules for {@link humanize()}. |
77
|
|
|
* |
78
|
|
|
* @var array |
79
|
|
|
*/ |
80
|
|
|
protected $humans = []; |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* Acronyms. |
84
|
|
|
* |
85
|
|
|
* @var array |
86
|
|
|
*/ |
87
|
|
|
protected $acronyms = []; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* Acronyms regex. |
91
|
|
|
* |
92
|
|
|
* @var string |
93
|
|
|
*/ |
94
|
|
|
protected $acronym_regex = '/(?=a)b/'; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Returns the {@link $acronyms}, {@link $acronym_regex}, {@link $plurals}, {@link $singulars}, |
98
|
|
|
* {@link $uncountables} and {@link $humans} properties. |
99
|
|
|
* |
100
|
|
|
* @param string $property |
101
|
|
|
* |
102
|
|
|
* @return mixed |
103
|
|
|
* |
104
|
|
|
* @throws PropertyNotDefined in attempt to read an inaccessible property. If the {@link PropertyNotDefined} |
105
|
|
|
* class is not available a {@link \InvalidArgumentException} is thrown instead. |
106
|
|
|
*/ |
107
|
|
|
public function __get(string $property) |
108
|
|
|
{ |
109
|
|
|
static $readers = [ 'acronyms', 'acronym_regex', 'plurals', 'singulars', 'uncountables', 'humans' ]; |
110
|
|
|
|
111
|
|
|
if (in_array($property, $readers)) |
112
|
|
|
{ |
113
|
|
|
return $this->$property; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
if (class_exists('ICanBoogie\PropertyNotDefined')) |
117
|
|
|
{ |
118
|
|
|
throw new PropertyNotDefined([ $property, $this ]); |
119
|
|
|
} |
120
|
|
|
else |
121
|
|
|
{ |
122
|
|
|
throw new \InvalidArgumentException("Property not defined: $property"); |
123
|
|
|
} |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* Specifies a new acronym. An acronym must be specified as it will appear |
128
|
|
|
* in a camelized string. An underscore string that contains the acronym |
129
|
|
|
* will retain the acronym when passed to {@link camelize}, {@link humanize}, or |
130
|
|
|
* {@link titleize}. A camelized string that contains the acronym will maintain |
131
|
|
|
* the acronym when titleized or humanized, and will convert the acronym |
132
|
|
|
* into a non-delimited single lowercase word when passed to {@link underscore}. |
133
|
|
|
* |
134
|
|
|
* <pre> |
135
|
|
|
* $this->acronym('HTML'); |
136
|
|
|
* $this->titleize('html'); // 'HTML' |
137
|
|
|
* $this->camelize('html'); // 'HTML' |
138
|
|
|
* $this->underscore('MyHTML'); // 'my_html' |
139
|
|
|
* </pre> |
140
|
|
|
* |
141
|
|
|
* The acronym, however, must occur as a delimited unit and not be part of |
142
|
|
|
* another word for conversions to recognize it: |
143
|
|
|
* |
144
|
|
|
* <pre> |
145
|
|
|
* $this->acronym('HTTP'); |
146
|
|
|
* $this->camelize('my_http_delimited'); // 'MyHTTPDelimited' |
147
|
|
|
* $this->camelize('https'); // 'Https', not 'HTTPs' |
148
|
|
|
* $this->underscore('HTTPS'); // 'http_s', not 'https' |
149
|
|
|
* |
150
|
|
|
* $this->acronym('HTTPS'); |
151
|
|
|
* $this->camelize('https'); // 'HTTPS' |
152
|
|
|
* $this->underscore('HTTPS'); // 'https' |
153
|
|
|
* </pre> |
154
|
|
|
* |
155
|
|
|
* Note: Acronyms that are passed to {@link pluralize} will no longer be |
156
|
|
|
* recognized, since the acronym will not occur as a delimited unit in the |
157
|
|
|
* pluralized result. To work around this, you must specify the pluralized |
158
|
|
|
* form as an acronym as well: |
159
|
|
|
* |
160
|
|
|
* <pre> |
161
|
|
|
* $this->acronym('API'); |
162
|
|
|
* $this->camelize($this->pluralize('api')); // 'Apis' |
163
|
|
|
* |
164
|
|
|
* $this->acronym('APIs'); |
165
|
|
|
* $this->camelize($this->pluralize('api')); // 'APIs' |
166
|
|
|
* </pre> |
167
|
|
|
* |
168
|
|
|
* {@link acronym} may be used to specify any word that contains an acronym or |
169
|
|
|
* otherwise needs to maintain a non-standard capitalization. The only |
170
|
|
|
* restriction is that the word must begin with a capital letter. |
171
|
|
|
* |
172
|
|
|
* <pre> |
173
|
|
|
* $this->acronym('RESTful'); |
174
|
|
|
* $this->underscore('RESTful'); // 'restful' |
175
|
|
|
* $this->underscore('RESTfulController'); // 'restful_controller' |
176
|
|
|
* $this->titleize('RESTfulController'); // 'RESTful Controller' |
177
|
|
|
* $this->camelize('restful'); // 'RESTful' |
178
|
|
|
* $this->camelize('restful_controller'); // 'RESTfulController' |
179
|
|
|
* |
180
|
|
|
* $this->acronym('McHammer'); |
181
|
|
|
* $this->underscore('McHammer'); // 'mchammer' |
182
|
|
|
* $this->camelize('mchammer'); // 'McHammer' |
183
|
|
|
* </pre> |
184
|
|
|
* |
185
|
|
|
* @return $this |
186
|
|
|
*/ |
187
|
|
|
public function acronym(string $acronym): self |
188
|
|
|
{ |
189
|
|
|
$this->acronyms[downcase($acronym)] = $acronym; |
190
|
|
|
$this->acronym_regex = '/' . implode('|', $this->acronyms) . '/'; |
191
|
|
|
|
192
|
|
|
return $this; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* Specifies a new pluralization rule and its replacement. |
197
|
|
|
* |
198
|
|
|
* <pre> |
199
|
|
|
* $this->plural('/^(ax|test)is$/i', '\1es'); |
200
|
|
|
* $this->plural('/(buffal|tomat)o$/i', '\1oes'); |
201
|
|
|
* $this->plural('/^(m|l)ouse$/i', '\1ice'); |
202
|
|
|
* </pre> |
203
|
|
|
* |
204
|
|
|
* @param string $rule A regex string. |
205
|
|
|
* @param string $replacement The replacement should always be a string that may include |
206
|
|
|
* references to the matched data from the rule. |
207
|
|
|
* |
208
|
|
|
* @return $this |
209
|
|
|
*/ |
210
|
|
View Code Duplication |
public function plural(string $rule, string $replacement): self |
|
|
|
|
211
|
|
|
{ |
212
|
|
|
unset($this->uncountables[$rule]); |
213
|
|
|
unset($this->uncountables[$replacement]); |
214
|
|
|
|
215
|
|
|
$this->plurals = [ $rule => $replacement ] + $this->plurals; |
216
|
|
|
|
217
|
|
|
return $this; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Specifies a new singularization rule and its replacement. |
222
|
|
|
* |
223
|
|
|
* <pre> |
224
|
|
|
* $this->singular('/(n)ews$/i', '\1ews'); |
225
|
|
|
* $this->singular('/([^aeiouy]|qu)ies$/i', '\1y'); |
226
|
|
|
* $this->singular('/(quiz)zes$/i', '\1'); |
227
|
|
|
* </pre> |
228
|
|
|
* |
229
|
|
|
* @param string $rule A regex string. |
230
|
|
|
* @param string $replacement The replacement should always be a string that may include |
231
|
|
|
* references to the matched data from the rule. |
232
|
|
|
* |
233
|
|
|
* @return $this |
234
|
|
|
*/ |
235
|
|
View Code Duplication |
public function singular(string $rule, string $replacement): self |
|
|
|
|
236
|
|
|
{ |
237
|
|
|
unset($this->uncountables[$rule]); |
238
|
|
|
unset($this->uncountables[$replacement]); |
239
|
|
|
|
240
|
|
|
$this->singulars = [ $rule => $replacement ] + $this->singulars; |
241
|
|
|
|
242
|
|
|
return $this; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Specifies a new irregular that applies to both pluralization and singularization at the |
247
|
|
|
* same time. This can only be used for strings, not regular expressions. You simply pass |
248
|
|
|
* the irregular in singular and plural form. |
249
|
|
|
* |
250
|
|
|
* <pre> |
251
|
|
|
* $this->irregular('child', 'children'); |
252
|
|
|
* $this->irregular('person', 'people'); |
253
|
|
|
* </pre> |
254
|
|
|
* |
255
|
|
|
* @return $this |
256
|
|
|
*/ |
257
|
|
|
public function irregular(string $singular, string $plural): self |
258
|
|
|
{ |
259
|
|
|
unset($this->uncountables[$singular]); |
260
|
|
|
unset($this->uncountables[$plural]); |
261
|
|
|
|
262
|
|
|
$s0 = mb_substr($singular, 0, 1); |
263
|
|
|
$s0_upcase = upcase($s0); |
264
|
|
|
$srest = mb_substr($singular, 1); |
265
|
|
|
|
266
|
|
|
$p0 = mb_substr($plural, 0, 1); |
267
|
|
|
$p0_upcase = upcase($p0); |
268
|
|
|
$prest = mb_substr($plural, 1); |
269
|
|
|
|
270
|
|
|
if ($s0_upcase == $p0_upcase) |
271
|
|
|
{ |
272
|
|
|
$this->plural("/({$s0}){$srest}$/i", '\1' . $prest); |
273
|
|
|
$this->plural("/({$p0}){$prest}$/i", '\1' . $prest); |
274
|
|
|
|
275
|
|
|
$this->singular("/({$s0}){$srest}$/i", '\1' . $srest); |
276
|
|
|
$this->singular("/({$p0}){$prest}$/i", '\1' . $srest); |
277
|
|
|
} |
278
|
|
|
else |
279
|
|
|
{ |
280
|
|
|
$s0_downcase = downcase($s0); |
281
|
|
|
$p0_downcase = downcase($p0); |
282
|
|
|
|
283
|
|
|
$this->plural("/{$s0_upcase}(?i){$srest}$/", $p0_upcase . $prest); |
284
|
|
|
$this->plural("/{$s0_downcase}(?i){$srest}$/", $p0_downcase . $prest); |
285
|
|
|
$this->plural("/{$p0_upcase}(?i){$prest}$/", $p0_upcase . $prest); |
286
|
|
|
$this->plural("/{$p0_downcase}(?i){$prest}$/", $p0_downcase . $prest); |
287
|
|
|
|
288
|
|
|
$this->singular("/{$s0_upcase}(?i){$srest}$/", $s0_upcase . $srest); |
289
|
|
|
$this->singular("/{$s0_downcase}(?i){$srest}$/", $s0_downcase . $srest); |
290
|
|
|
$this->singular("/{$p0_upcase}(?i){$prest}$/", $s0_upcase . $srest); |
291
|
|
|
$this->singular("/{$p0_downcase}(?i){$prest}$/", $s0_downcase . $srest); |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
return $this; |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* Add uncountable words that shouldn't be attempted inflected. |
299
|
|
|
* |
300
|
|
|
* <pre> |
301
|
|
|
* $this->uncountable('money'); |
302
|
|
|
* $this->uncountable(explode(' ', 'money information rice')); |
303
|
|
|
* </pre> |
304
|
|
|
* |
305
|
|
|
* @param string|array $word |
306
|
|
|
* |
307
|
|
|
* @return $this |
308
|
|
|
*/ |
309
|
|
|
public function uncountable($word): self |
310
|
|
|
{ |
311
|
|
|
if (is_array($word)) |
312
|
|
|
{ |
313
|
|
|
$this->uncountables += array_combine($word, $word); |
314
|
|
|
|
315
|
|
|
return $this; |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
$this->uncountables[$word] = $word; |
319
|
|
|
|
320
|
|
|
return $this; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
/** |
324
|
|
|
* Specifies a humanized form of a string by a regular expression rule or by a string mapping. |
325
|
|
|
* When using a regular expression based replacement, the normal humanize formatting is |
326
|
|
|
* called after the replacement. When a string is used, the human form should be specified |
327
|
|
|
* as desired (example: 'The name', not 'the_name'). |
328
|
|
|
* |
329
|
|
|
* <pre> |
330
|
|
|
* $this->human('/_cnt$/i', '\1_count'); |
331
|
|
|
* $this->human('legacy_col_person_name', 'Name'); |
332
|
|
|
* </pre> |
333
|
|
|
* |
334
|
|
|
* @param string $rule A regular expression rule or a string mapping. Strings that starts with |
335
|
|
|
* "/", "#" or "~" are recognized as regular expressions. |
336
|
|
|
* |
337
|
|
|
* @return $this |
338
|
|
|
*/ |
339
|
|
|
public function human(string $rule, string $replacement): self |
340
|
|
|
{ |
341
|
|
|
$r0 = $rule[0]; |
342
|
|
|
|
343
|
|
|
if ($r0 != '/' && $r0 != '#' && $r0 != '~') |
344
|
|
|
{ |
345
|
|
|
$rule = '/' . preg_quote($rule, '/') . '/'; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
$this->humans = [ $rule => $replacement ] + $this->humans; |
349
|
|
|
|
350
|
|
|
return $this; |
351
|
|
|
} |
352
|
|
|
} |
353
|
|
|
|
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.