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