* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ICanBoogie; /** * The Inflector transforms words from singular to plural, class names to table names, modularized * class names to ones without, and class names to foreign keys. Inflections can be localized, the * default english inflections for pluralization, singularization, and uncountable words are * kept in `lib/inflections/en.php`. * * @property-read Inflections $inflections Inflections used by the inflector. */ class Inflector { static private $inflectors = array(); /** * Returns an inflector for the specified locale. * * Note: Inflectors are shared for the same locale. If you need to alter an inflector you * MUST clone it first. * * @param string $locale * * @return \ICanBoogie\Inflector */ static public function get($locale='en') { if (isset(self::$inflectors[$locale])) { return self::$inflectors[$locale]; } return self::$inflectors[$locale] = new static(Inflections::get($locale)); } /** * Inflections used by the inflector. * * @var Inflections */ protected $inflections; /** * Initializes the {@link $inflections} property. * * @param Inflections $inflections */ protected function __construct(Inflections $inflections=null) { $this->inflections = $inflections ?: new Inflections; } /** * Returns the {@link $inflections} property. * * @param string $property * * @throws PropertyNotDefined in attempt to read an unaccessible property. If the {@link PropertyNotDefined} * class is not available a {@link \InvalidArgumentException} is thrown instead. */ public function __get($property) { static $readers = array('inflections'); if (in_array($property, $readers)) { return $this->$property; } if (class_exists('ICanBoogie\PropertyNotDefined')) { throw new PropertyNotDefined(array($property, $this)); } else { throw new \InvalidArgumentException("Property not defined: $property"); } } /** * Clone inflections. */ public function __clone() { $this->inflections = clone $this->inflections; } /** * Applies inflection rules for {@link singularize} and {@link pluralize}. * *
* $this->apply_inflections('post', $this->plurals); // "posts"
* $this->apply_inflections('posts', $this->singulars); // "post"
*
*
* @param string $word
* @param array $rules
*
* @return string
*/
private function apply_inflections($word, array $rules)
{
$rc = (string) $word;
if (!$rc)
{
return $rc;
}
if (preg_match('/\b\w+\Z/', downcase($rc), $matches))
{
if (isset($this->inflections->uncountables[$matches[0]]))
{
return $rc;
}
}
foreach ($rules as $rule => $replacement)
{
$rc = preg_replace($rule, $replacement, $rc, -1, $count);
if ($count) break;
}
return $rc;
}
/**
* Returns the plural form of the word in the string.
*
*
* $this->pluralize('post'); // "posts"
* $this->pluralize('children'); // "child"
* $this->pluralize('sheep'); // "sheep"
* $this->pluralize('words'); // "words"
* $this->pluralize('CamelChild'); // "CamelChild"
*
*
* @param string $word
*
* @return string
*/
public function pluralize($word)
{
return $this->apply_inflections($word, $this->inflections->plurals);
}
/**
* The reverse of {@link pluralize}, returns the singular form of a word in a string.
*
*
* $this->singularize('posts'); // "post"
* $this->singularize('childred'); // "child"
* $this->singularize('sheep'); // "sheep"
* $this->singularize('word'); // "word"
* $this->singularize('CamelChildren'); // "CamelChild"
*
*
* @param string $word
*
* @return string
*/
public function singularize($word)
{
return $this->apply_inflections($word, $this->inflections->singulars);
}
/**
* By default, {@link camelize} converts strings to UpperCamelCase.
*
* {@link camelize} will also convert "/" to "\" which is useful for converting paths to
* namespaces.
*
*
* $this->camelize('active_model'); // 'ActiveModel'
* $this->camelize('active_model', true); // 'activeModel'
* $this->camelize('active_model/errors'); // 'ActiveModel\Errors'
* $this->camelize('active_model/errors', true); // 'activeModel\Errors'
*
*
* As a rule of thumb you can think of {@link camelize} as the inverse of {@link underscore},
* though there are cases where that does not hold:
*
*
* $this->camelize($this->underscore('SSLError')); // "SslError"
*
*
* @param string $term
* @param bool $downcase_first_letter If `false` then {@link camelize} produces
* lowerCamelCase.
*
* @return string
*/
public function camelize($term, $downcase_first_letter=false)
{
$string = (string) $term;
$acronyms = $this->inflections->acronyms;
if ($downcase_first_letter)
{
$string = preg_replace_callback('/^(?:' . trim($this->inflections->acronym_regex, '/') . '(?=\b|[A-Z_])|\w)/', function($matches) {
return downcase($matches[0]);
}, $string, 1);
}
else
{
$string = preg_replace_callback('/^[a-z\d]*/', function($matches) use($acronyms) {
$m = $matches[0];
return !empty($acronyms[$m]) ? $acronyms[$m] : capitalize($m);
}, $string, 1);
}
$string = preg_replace_callback('/(?:_|(\/))([a-z\d]*)/i', function($matches) use($acronyms) {
list(, $m1, $m2) = $matches;
return $m1 . (isset($acronyms[$m2]) ? $acronyms[$m2] : capitalize($m2));
}, $string);
$string = str_replace('/', '\\', $string);
return $string;
}
/**
* Makes an underscored, lowercase form from the expression in the string.
*
* Changes "\" to "/" to convert namespaces to paths.
*
*
* $this->underscore('ActiveModel'); // 'active_model'
* $this->underscore('ActiveModel\Errors'); // 'active_model/errors'
*
*
* As a rule of thumb you can think of {@link underscore} as the inverse of {@link camelize()},
* though there are cases where that does not hold:
*
*
* $this->camelize($this->underscore('SSLError')); // "SslError"
*
*
* @param string $camel_cased_word
*
* @return string
*/
public function underscore($camel_cased_word)
{
$word = (string) $camel_cased_word;
$word = str_replace('\\', '/', $word);
$word = preg_replace_callback('/(?:([A-Za-z\d])|^)(' . trim($this->inflections->acronym_regex, '/') . ')(?=\b|[^a-z])/', function($matches) {
list(, $m1, $m2) = $matches;
return $m1 . ($m1 ? '_' : '') . downcase($m2);
}, $word);
$word = preg_replace('/([A-Z\d]+)([A-Z][a-z])/', '\1_\2', $word);
$word = preg_replace('/([a-z\d])([A-Z])/','\1_\2', $word);
$word = strtr($word, "-", "_");
$word = downcase($word);
return $word;
}
/**
* Capitalizes the first word and turns underscores into spaces and strips a trailing "_id",
* if any. Like {@link titleize()}, this is meant for creating pretty output.
*
*
* $this->humanize('employee_salary'); // "Employee salary"
* $this->humanize('author_id'); // "Author"
*
*
* @param string $lower_case_and_underscored_word
*
* @return string
*/
public function humanize($lower_case_and_underscored_word)
{
$result = (string) $lower_case_and_underscored_word;
foreach ($this->inflections->humans as $rule => $replacement)
{
$result = preg_replace($rule, $replacement, $result, 1, $count);
if ($count) break;
}
$acronyms = $this->inflections->acronyms;
$result = preg_replace('/_id$/', "", $result);
$result = strtr($result, '_', ' ');
$result = preg_replace_callback('/([a-z\d]*)/i', function($matches) use($acronyms) {
list($m) = $matches;
return !empty($acronyms[$m]) ? $acronyms[$m] : downcase($m);
}, $result);
$result = preg_replace_callback('/^\w/', function($matches) {
return upcase($matches[0]);
}, $result);
return $result;
}
/**
* Capitalizes all the words and replaces some characters in the string to create a nicer
* looking title. {@link titleize()} is meant for creating pretty output. It is not used in
* the Rails internals.
*
*
* $this->titleize('man from the boondocks'); // "Man From The Boondocks"
* $this->titleize('x-men: the last stand'); // "X Men: The Last Stand"
* $this->titleize('TheManWithoutAPast'); // "The Man Without A Past"
* $this->titleize('raiders_of_the_lost_ark'); // "Raiders Of The Lost Ark"
*
*
* @param string $str
*
* @return string
*/
public function titleize($str)
{
$str = $this->underscore($str);
$str = $this->humanize($str);
$str = preg_replace_callback('/\b(?
* $this->dasherize('puni_puni'); // "puni-puni"
*
*
* @param string $underscored_word
*
* @return string
*/
public function dasherize($underscored_word)
{
return strtr($underscored_word, '_', '-');
}
/**
* Makes an hyphenated, lowercase form from the expression in the string.
*
* This is a combination of {@link underscore} and {@link dasherize}.
*
* @param string $str
*
* @return string
*/
public function hyphenate($str)
{
return $this->dasherize($this->underscore($str));
}
/**
* Returns the suffix that should be added to a number to denote the position in an ordered
* sequence such as 1st, 2nd, 3rd, 4th.
*
* * $this->ordinal(1); // "st" * $this->ordinal(2); // "nd" * $this->ordinal(1002); // "nd" * $this->ordinal(1003); // "rd" * $this->ordinal(-11); // "th" * $this->ordinal(-1021); // "st" **/ public function ordinal($number) { $abs_number = abs($number); if (($abs_number % 100) > 10 && ($abs_number % 100) < 14) { return 'th'; } switch ($abs_number % 10) { case 1; return "st"; case 2; return "nd"; case 3; return "rd"; default: return "th"; } } /** * Turns a number into an ordinal string used to denote the position in an ordered sequence * such as 1st, 2nd, 3rd, 4th. * *
* $this->ordinalize(1); // "1st" * $this->ordinalize(2); // "2nd" * $this->ordinalize(1002); // "1002nd" * $this->ordinalize(1003); // "1003rd" * $this->ordinalize(-11); // "-11th" * $this->ordinalize(-1021); // "-1021st" **/ public function ordinalize($number) { return $number . $this->ordinal($number); } }