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
|
|
|
* The Inflector transforms words from singular to plural, class names to table names, modularized |
16
|
|
|
* class names to ones without, and class names to foreign keys. Inflections can be localized, the |
17
|
|
|
* default english inflections for pluralization, singularization, and uncountable words are |
18
|
|
|
* kept in `lib/inflections/en.php`. |
19
|
|
|
* |
20
|
|
|
* @property-read Inflections $inflections Inflections used by the inflector. |
21
|
|
|
*/ |
22
|
|
|
class Inflector |
23
|
|
|
{ |
24
|
|
|
/** |
25
|
|
|
* Default inflector locale. |
26
|
|
|
* |
27
|
|
|
* Alias to {@link INFLECTOR_DEFAULT_LOCALE}. |
28
|
|
|
*/ |
29
|
|
|
const DEFAULT_LOCALE = INFLECTOR_DEFAULT_LOCALE; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* {@link camelize()} option to downcase the first letter. |
33
|
|
|
*/ |
34
|
|
|
const DOWNCASE_FIRST_LETTER = true; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* {@link camelize()} option to keep the first letter as is. |
38
|
|
|
*/ |
39
|
|
|
const UPCASE_FIRST_LETTER = false; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @var Inflector[] |
43
|
|
|
*/ |
44
|
|
|
static private $inflectors = array(); |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Returns an inflector for the specified locale. |
48
|
|
|
* |
49
|
|
|
* Note: Inflectors are shared for the same locale. If you need to alter an inflector you |
50
|
|
|
* MUST clone it first. |
51
|
|
|
* |
52
|
|
|
* @param string $locale |
53
|
|
|
* |
54
|
|
|
* @return \ICanBoogie\Inflector |
55
|
|
|
*/ |
56
|
|
|
static public function get($locale = self::DEFAULT_LOCALE) |
57
|
|
|
{ |
58
|
|
|
if (isset(self::$inflectors[$locale])) |
59
|
|
|
{ |
60
|
|
|
return self::$inflectors[$locale]; |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
return self::$inflectors[$locale] = new static(Inflections::get($locale)); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Inflections used by the inflector. |
68
|
|
|
* |
69
|
|
|
* @var Inflections |
70
|
|
|
*/ |
71
|
|
|
protected $inflections; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Initializes the {@link $inflections} property. |
75
|
|
|
* |
76
|
|
|
* @param Inflections $inflections |
77
|
|
|
*/ |
78
|
|
|
protected function __construct(Inflections $inflections = null) |
79
|
|
|
{ |
80
|
|
|
$this->inflections = $inflections ?: new Inflections; |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Returns the {@link $inflections} property. |
85
|
|
|
* |
86
|
|
|
* @param string $property |
87
|
|
|
* |
88
|
|
|
* @throws PropertyNotDefined in attempt to read an inaccessible property. If the {@link PropertyNotDefined} |
89
|
|
|
* class is not available a {@link \InvalidArgumentException} is thrown instead. |
90
|
|
|
*/ |
91
|
|
|
public function __get($property) |
92
|
|
|
{ |
93
|
|
|
if ($property === 'inflections') |
94
|
|
|
{ |
95
|
|
|
return $this->$property; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
View Code Duplication |
if (class_exists('ICanBoogie\PropertyNotDefined')) |
|
|
|
|
99
|
|
|
{ |
100
|
|
|
throw new PropertyNotDefined(array($property, $this)); |
101
|
|
|
} |
102
|
|
|
else |
103
|
|
|
{ |
104
|
|
|
throw new \InvalidArgumentException("Property not defined: $property"); |
105
|
|
|
} |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* Clone inflections. |
110
|
|
|
*/ |
111
|
|
|
public function __clone() |
112
|
|
|
{ |
113
|
|
|
$this->inflections = clone $this->inflections; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* Applies inflection rules for {@link singularize} and {@link pluralize}. |
118
|
|
|
* |
119
|
|
|
* <pre> |
120
|
|
|
* $this->apply_inflections('post', $this->plurals); // "posts" |
121
|
|
|
* $this->apply_inflections('posts', $this->singulars); // "post" |
122
|
|
|
* </pre> |
123
|
|
|
* |
124
|
|
|
* @param string $word |
125
|
|
|
* @param array $rules |
126
|
|
|
* |
127
|
|
|
* @return string |
128
|
|
|
*/ |
129
|
|
|
private function apply_inflections($word, array $rules) |
130
|
|
|
{ |
131
|
|
|
$rc = (string) $word; |
132
|
|
|
|
133
|
|
|
if (!$rc) |
134
|
|
|
{ |
135
|
|
|
return $rc; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
if (preg_match('/\b[[:word:]]+\Z/u', downcase($rc), $matches)) |
139
|
|
|
{ |
140
|
|
|
if (isset($this->inflections->uncountables[$matches[0]])) |
141
|
|
|
{ |
142
|
|
|
return $rc; |
143
|
|
|
} |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
foreach ($rules as $rule => $replacement) |
147
|
|
|
{ |
148
|
|
|
$rc = preg_replace($rule, $replacement, $rc, -1, $count); |
149
|
|
|
|
150
|
|
|
if ($count) break; |
|
|
|
|
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
return $rc; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* Returns the plural form of the word in the string. |
158
|
|
|
* |
159
|
|
|
* <pre> |
160
|
|
|
* $this->pluralize('post'); // "posts" |
161
|
|
|
* $this->pluralize('children'); // "child" |
162
|
|
|
* $this->pluralize('sheep'); // "sheep" |
163
|
|
|
* $this->pluralize('words'); // "words" |
164
|
|
|
* $this->pluralize('CamelChild'); // "CamelChild" |
165
|
|
|
* </pre> |
166
|
|
|
* |
167
|
|
|
* @param string $word |
168
|
|
|
* |
169
|
|
|
* @return string |
170
|
|
|
*/ |
171
|
|
|
public function pluralize($word) |
172
|
|
|
{ |
173
|
|
|
return $this->apply_inflections($word, $this->inflections->plurals); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* The reverse of {@link pluralize}, returns the singular form of a word in a string. |
178
|
|
|
* |
179
|
|
|
* <pre> |
180
|
|
|
* $this->singularize('posts'); // "post" |
181
|
|
|
* $this->singularize('childred'); // "child" |
182
|
|
|
* $this->singularize('sheep'); // "sheep" |
183
|
|
|
* $this->singularize('word'); // "word" |
184
|
|
|
* $this->singularize('CamelChildren'); // "CamelChild" |
185
|
|
|
* </pre> |
186
|
|
|
* |
187
|
|
|
* @param string $word |
188
|
|
|
* |
189
|
|
|
* @return string |
190
|
|
|
*/ |
191
|
|
|
public function singularize($word) |
192
|
|
|
{ |
193
|
|
|
return $this->apply_inflections($word, $this->inflections->singulars); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* By default, {@link camelize} converts strings to UpperCamelCase. |
198
|
|
|
* |
199
|
|
|
* {@link camelize} will also convert "/" to "\" which is useful for converting paths to |
200
|
|
|
* namespaces. |
201
|
|
|
* |
202
|
|
|
* <pre> |
203
|
|
|
* $this->camelize('active_model'); // 'ActiveModel' |
204
|
|
|
* $this->camelize('active_model', true); // 'activeModel' |
205
|
|
|
* $this->camelize('active_model/errors'); // 'ActiveModel\Errors' |
206
|
|
|
* $this->camelize('active_model/errors', true); // 'activeModel\Errors' |
207
|
|
|
* </pre> |
208
|
|
|
* |
209
|
|
|
* As a rule of thumb you can think of {@link camelize} as the inverse of {@link underscore}, |
210
|
|
|
* though there are cases where that does not hold: |
211
|
|
|
* |
212
|
|
|
* <pre> |
213
|
|
|
* $this->camelize($this->underscore('SSLError')); // "SslError" |
214
|
|
|
* </pre> |
215
|
|
|
* |
216
|
|
|
* @param string $term |
217
|
|
|
* @param bool $downcase_first_letter One of {@link UPCASE_FIRST_LETTER}, |
218
|
|
|
* {@link DOWNCASE_FIRST_LETTER}. |
219
|
|
|
* |
220
|
|
|
* @return string |
221
|
|
|
*/ |
222
|
|
|
public function camelize($term, $downcase_first_letter = self::UPCASE_FIRST_LETTER) |
223
|
|
|
{ |
224
|
|
|
$string = (string) $term; |
225
|
|
|
$acronyms = $this->inflections->acronyms; |
226
|
|
|
|
227
|
|
|
if ($downcase_first_letter) |
228
|
|
|
{ |
229
|
|
|
$string = preg_replace_callback('/^(?:' . trim($this->inflections->acronym_regex, '/') . '(?=\b|[[:upper:]_])|\w)/u', function($matches) { |
230
|
|
|
|
231
|
|
|
return downcase($matches[0]); |
232
|
|
|
|
233
|
|
|
}, $string, 1); |
234
|
|
|
} |
235
|
|
|
else |
236
|
|
|
{ |
237
|
|
View Code Duplication |
$string = preg_replace_callback('/^[[:lower:]\d]*/u', function($matches) use($acronyms) { |
|
|
|
|
238
|
|
|
|
239
|
|
|
$m = $matches[0]; |
240
|
|
|
|
241
|
|
|
return !empty($acronyms[$m]) ? $acronyms[$m] : self::ucfirst($m); |
242
|
|
|
|
243
|
|
|
}, $string, 1); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
$string = preg_replace_callback('/(?:_|-|(\/))([[:alnum:]]*)/u', function($matches) use($acronyms) { |
247
|
|
|
|
248
|
|
|
list(, $m1, $m2) = $matches; |
249
|
|
|
|
250
|
|
|
return $m1 . (isset($acronyms[$m2]) ? $acronyms[$m2] : self::ucfirst($m2)); |
251
|
|
|
|
252
|
|
|
}, $string); |
253
|
|
|
|
254
|
|
|
$string = str_replace('/', '\\', $string); |
255
|
|
|
|
256
|
|
|
return $string; |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* Makes an underscored, lowercase form from the expression in the string. |
261
|
|
|
* |
262
|
|
|
* Changes "\" to "/" to convert namespaces to paths. |
263
|
|
|
* |
264
|
|
|
* <pre> |
265
|
|
|
* $this->underscore('ActiveModel'); // 'active_model' |
266
|
|
|
* $this->underscore('ActiveModel\Errors'); // 'active_model/errors' |
267
|
|
|
* </pre> |
268
|
|
|
* |
269
|
|
|
* As a rule of thumb you can think of {@link underscore} as the inverse of {@link camelize()}, |
270
|
|
|
* though there are cases where that does not hold: |
271
|
|
|
* |
272
|
|
|
* <pre> |
273
|
|
|
* $this->camelize($this->underscore('SSLError')); // "SslError" |
274
|
|
|
* </pre> |
275
|
|
|
* |
276
|
|
|
* @param string $camel_cased_word |
277
|
|
|
* |
278
|
|
|
* @return string |
279
|
|
|
*/ |
280
|
|
|
public function underscore($camel_cased_word) |
281
|
|
|
{ |
282
|
|
|
$word = (string) $camel_cased_word; |
283
|
|
|
$word = str_replace('\\', '/', $word); |
284
|
|
|
$word = preg_replace_callback('/(?:([[:alpha:]\d])|^)(' . trim($this->inflections->acronym_regex, '/') . ')(?=\b|[^[:lower:]])/u', function($matches) { |
285
|
|
|
|
286
|
|
|
list(, $m1, $m2) = $matches; |
287
|
|
|
|
288
|
|
|
return $m1 . ($m1 ? '_' : '') . downcase($m2); |
289
|
|
|
|
290
|
|
|
}, $word); |
291
|
|
|
|
292
|
|
|
$word = preg_replace('/([[:upper:]\d]+)([[:upper:]][[:lower:]])/u', '\1_\2', $word); |
293
|
|
|
$word = preg_replace('/([[:lower:]\d])([[:upper:]])/u','\1_\2', $word); |
294
|
|
|
$word = strtr($word, "-", "_"); |
295
|
|
|
$word = downcase($word); |
296
|
|
|
|
297
|
|
|
return $word; |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
/** |
301
|
|
|
* Capitalizes the first word and turns underscores into spaces and strips a trailing "_id", |
302
|
|
|
* if any. Like {@link titleize()}, this is meant for creating pretty output. |
303
|
|
|
* |
304
|
|
|
* <pre> |
305
|
|
|
* $this->humanize('employee_salary'); // "Employee salary" |
306
|
|
|
* $this->humanize('author_id'); // "Author" |
307
|
|
|
* </pre> |
308
|
|
|
* |
309
|
|
|
* @param string $lower_case_and_underscored_word |
310
|
|
|
* |
311
|
|
|
* @return string |
312
|
|
|
*/ |
313
|
|
|
public function humanize($lower_case_and_underscored_word) |
314
|
|
|
{ |
315
|
|
|
$result = (string) $lower_case_and_underscored_word; |
316
|
|
|
|
317
|
|
|
foreach ($this->inflections->humans as $rule => $replacement) |
318
|
|
|
{ |
319
|
|
|
$result = preg_replace($rule, $replacement, $result, 1, $count); |
320
|
|
|
|
321
|
|
|
if ($count) break; |
|
|
|
|
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
$acronyms = $this->inflections->acronyms; |
325
|
|
|
|
326
|
|
|
$result = preg_replace('/_id$/', "", $result); |
327
|
|
|
$result = strtr($result, '_', ' '); |
328
|
|
View Code Duplication |
$result = preg_replace_callback('/([[:alnum:]]+)/u', function($matches) use($acronyms) { |
|
|
|
|
329
|
|
|
|
330
|
|
|
list($m) = $matches; |
331
|
|
|
|
332
|
|
|
return !empty($acronyms[$m]) ? $acronyms[$m] : downcase($m); |
333
|
|
|
|
334
|
|
|
}, $result); |
335
|
|
|
|
336
|
|
|
$result = preg_replace_callback('/^[[:lower:]]/u', function($matches) { |
337
|
|
|
|
338
|
|
|
return upcase($matches[0]); |
339
|
|
|
|
340
|
|
|
}, $result); |
341
|
|
|
|
342
|
|
|
return $result; |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
/** |
346
|
|
|
* Capitalizes all the words and replaces some characters in the string to create a nicer |
347
|
|
|
* looking title. {@link titleize()} is meant for creating pretty output. It is not used in |
348
|
|
|
* the Rails internals. |
349
|
|
|
* |
350
|
|
|
* <pre> |
351
|
|
|
* $this->titleize('man from the boondocks'); // "Man From The Boondocks" |
352
|
|
|
* $this->titleize('x-men: the last stand'); // "X Men: The Last Stand" |
353
|
|
|
* $this->titleize('TheManWithoutAPast'); // "The Man Without A Past" |
354
|
|
|
* $this->titleize('raiders_of_the_lost_ark'); // "Raiders Of The Lost Ark" |
355
|
|
|
* </pre> |
356
|
|
|
* |
357
|
|
|
* @param string $str |
358
|
|
|
* |
359
|
|
|
* @return string |
360
|
|
|
*/ |
361
|
|
|
public function titleize($str) |
362
|
|
|
{ |
363
|
|
|
$str = $this->underscore($str); |
364
|
|
|
$str = $this->humanize($str); |
365
|
|
|
|
366
|
|
|
$str = preg_replace_callback('/\b(?<![\'’`])[[:lower:]]/u', function($matches) { |
367
|
|
|
|
368
|
|
|
return upcase($matches[0]); |
369
|
|
|
|
370
|
|
|
}, $str); |
371
|
|
|
|
372
|
|
|
return $str; |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
/** |
376
|
|
|
* Replaces underscores with dashes in the string. |
377
|
|
|
* |
378
|
|
|
* <pre> |
379
|
|
|
* $this->dasherize('puni_puni'); // "puni-puni" |
380
|
|
|
* </pre> |
381
|
|
|
* |
382
|
|
|
* @param string $underscored_word |
383
|
|
|
* |
384
|
|
|
* @return string |
385
|
|
|
*/ |
386
|
|
|
public function dasherize($underscored_word) |
387
|
|
|
{ |
388
|
|
|
return strtr($underscored_word, '_', '-'); |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
/** |
392
|
|
|
* Makes an hyphenated, lowercase form from the expression in the string. |
393
|
|
|
* |
394
|
|
|
* This is a combination of {@link underscore} and {@link dasherize}. |
395
|
|
|
* |
396
|
|
|
* @param string $str |
397
|
|
|
* |
398
|
|
|
* @return string |
399
|
|
|
*/ |
400
|
|
|
public function hyphenate($str) |
401
|
|
|
{ |
402
|
|
|
return $this->dasherize($this->underscore($str)); |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* Returns the suffix that should be added to a number to denote the position in an ordered |
407
|
|
|
* sequence such as 1st, 2nd, 3rd, 4th. |
408
|
|
|
* |
409
|
|
|
* <pre> |
410
|
|
|
* $this->ordinal(1); // "st" |
411
|
|
|
* $this->ordinal(2); // "nd" |
412
|
|
|
* $this->ordinal(1002); // "nd" |
413
|
|
|
* $this->ordinal(1003); // "rd" |
414
|
|
|
* $this->ordinal(-11); // "th" |
415
|
|
|
* $this->ordinal(-1021); // "st" |
416
|
|
|
* </pre> |
417
|
|
|
* |
418
|
|
|
* @param int $number |
419
|
|
|
* |
420
|
|
|
* @return string |
421
|
|
|
*/ |
422
|
|
|
public function ordinal($number) |
423
|
|
|
{ |
424
|
|
|
$abs_number = abs($number); |
425
|
|
|
|
426
|
|
|
if (($abs_number % 100) > 10 && ($abs_number % 100) < 14) |
427
|
|
|
{ |
428
|
|
|
return 'th'; |
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
switch ($abs_number % 10) |
432
|
|
|
{ |
433
|
|
|
case 1; return "st"; |
434
|
|
|
case 2; return "nd"; |
435
|
|
|
case 3; return "rd"; |
436
|
|
|
default: return "th"; |
437
|
|
|
} |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
/** |
441
|
|
|
* Turns a number into an ordinal string used to denote the position in an ordered sequence |
442
|
|
|
* such as 1st, 2nd, 3rd, 4th. |
443
|
|
|
* |
444
|
|
|
* <pre> |
445
|
|
|
* $this->ordinalize(1); // "1st" |
446
|
|
|
* $this->ordinalize(2); // "2nd" |
447
|
|
|
* $this->ordinalize(1002); // "1002nd" |
448
|
|
|
* $this->ordinalize(1003); // "1003rd" |
449
|
|
|
* $this->ordinalize(-11); // "-11th" |
450
|
|
|
* $this->ordinalize(-1021); // "-1021st" |
451
|
|
|
* </pre> |
452
|
|
|
* |
453
|
|
|
* @param int $number |
454
|
|
|
* |
455
|
|
|
* @return string |
456
|
|
|
*/ |
457
|
|
|
public function ordinalize($number) |
458
|
|
|
{ |
459
|
|
|
return $number . $this->ordinal($number); |
460
|
|
|
} |
461
|
|
|
|
462
|
|
|
/** |
463
|
|
|
* Multi-byte safe ucfirst |
464
|
|
|
* |
465
|
|
|
* @param $str |
466
|
|
|
* @return string |
467
|
|
|
*/ |
468
|
|
|
private static function ucfirst($str) |
469
|
|
|
{ |
470
|
|
|
return upcase(mb_substr($str, 0, 1)) . mb_substr($str, 1); |
471
|
|
|
} |
472
|
|
|
} |
473
|
|
|
|
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.