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