Completed
Pull Request — master (#16)
by
unknown
01:36
created

Inflector::ucfirst()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
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'))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
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;
0 ignored issues
show
Bug Best Practice introduced by
The expression $count of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
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;
0 ignored issues
show
Bug Best Practice introduced by
The expression $count of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
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