Test Failed
Push — intl ( e65f29...e84382 )
by Fabio
05:21
created

CultureInfo::dataDir()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * CultureInfo class file.
5
 *
6
 * This program is free software; you can redistribute it and/or modify
7
 * it under the terms of the BSD License.
8
 *
9
 * Copyright(c) 2004 by Qiang Xue. All rights reserved.
10
 *
11
 * To contact the author write to {@link mailto:[email protected] Qiang Xue}
12
 * The latest version of PRADO can be obtained from:
13
 * {@link http://prado.sourceforge.net/}
14
 *
15
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
16
 * @author Fabio Bas <ctrlaltca[at]gmail[dot]com>
17
 * @package Prado\I18N\core
18
 */
19
20
namespace Prado\I18N\core;
21
22
use Exception;
23
24
/**
25
 * CultureInfo class.
26
 *
27
 * Represents information about a specific culture including the
28
 * names of the culture, the calendar used, as well as access to
29
 * culture-specific objects that provide methods for common operations,
30
 * such as formatting dates, numbers, and currency.
31
 *
32
 * The CultureInfo class holds culture-specific information, such as the
33
 * associated language, sublanguage, country/region, calendar, and cultural
34
 * conventions. This class also provides access to culture-specific
35
 * instances. These objects
36
 * contain the information required for culture-specific operations,
37
 * such as formatting dates, numbers and currency.
38
 *
39
 * The culture names follow the format "<languagecode>_<country/regioncode>",
40
 * where <languagecode> is a lowercase two-letter code derived from ISO 639
41
 * codes. You can find a full list of the ISO-639 codes at
42
 * http://www.ics.uci.edu/pub/ietf/http/related/iso639.txt
43
 *
44
 * The <country/regioncode2> is an uppercase two-letter code derived from
45
 * ISO 3166. A copy of ISO-3166 can be found at
46
 * http://www.chemie.fu-berlin.de/diverse/doc/ISO_3166.html
47
 *
48
 * For example, Australian English is "en_AU".
49
 *
50
 * @author Xiang Wei Zhuo <weizhuo[at]gmail[dot]com>
51
 * @author Fabio Bas <ctrlaltca[at]gmail[dot]com>
52
 * @package Prado\I18N\core
53
 */
54
class CultureInfo
55
{
56
	/**
57
	 * The ICU data array.
58
	 * @var array
59
	 */
60
	private $data = [];
61
62
	/**
63
	 * The current culture.
64
	 * @var string
65
	 */
66
	private $culture;
67
68
	/**
69
	 * A list of CLDR resource bundles loaded
70
	 * @var array
71
	 */
72
	private $resourceBundles = array();
73
74
	/**
75
	 * A list of resource bundles keys
76
	 * @var array
77
	 */
78
	protected static $bundleNames = [
79
		'Core' => null,
80
		'Currencies' => 'ICUDATA-curr',
81
		'Languages' => 'ICUDATA-lang',
82
		'Countries' => 'ICUDATA-region',
83
		'zoneStrings' => 'ICUDATA-zone',
84
	];
85
86
	/**
87
	 * A list of properties that are accessable/writable.
88
	 * @var array
89
	 */
90
	protected $properties = [];
91
92
	/**
93
	 * Culture type, all.
94
	 * @see getCultures()
95
	 * @var int
96
	 */
97
	const ALL = 0;
98
99
	/**
100
	 * Culture type, neutral.
101
	 * @see getCultures()
102
	 * @var int
103
	 */
104
	const NEUTRAL = 1;
105
106
	/**
107
	 * Culture type, specific.
108
	 * @see getCultures()
109
	 * @var int
110
	 */
111
	const SPECIFIC = 2;
112
113
	/**
114
	 * Display the culture name.
115
	 * @return string the culture name.
116
	 * @see getName()
117
	 */
118
	public function __toString()
119
	{
120
		return $this->getName();
121
	}
122
123
124
	/**
125
	 * Allow functions that begins with 'set' to be called directly
126
	 * as an attribute/property to retrieve the value.
127
	 * @param mixed $name
128
	 * @return mixed
129
	 */
130 View Code Duplication
	public function __get($name)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
131
	{
132
		$getProperty = 'get' . $name;
133
		if (in_array($getProperty, $this->properties)) {
134
			return $this->$getProperty();
135
		} else {
136
			throw new Exception('Property ' . $name . ' does not exists.');
137
		}
138
	}
139
140
	/**
141
	 * Allow functions that begins with 'set' to be called directly
142
	 * as an attribute/property to set the value.
143
	 * @param mixed $name
144
	 * @param mixed $value
145
	 */
146 View Code Duplication
	public function __set($name, $value)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
147
	{
148
		$setProperty = 'set' . $name;
149
		if (in_array($setProperty, $this->properties)) {
150
			$this->$setProperty($value);
151
		} else {
152
			throw new Exception('Property ' . $name . ' can not be set.');
153
		}
154
	}
155
156
157
	/**
158
	 * Initializes a new instance of the CultureInfo class based on the
159
	 * culture specified by name. E.g. <code>new CultureInfo('en_AU');</cdoe>
160
	 * The culture indentifier must be of the form
161
	 * "language_(country/region/variant)".
162
	 * @param string $culture a culture name, e.g. "en_AU".
163
	 * @return return new CultureInfo.
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
164
	 */
165
	public function __construct($culture = 'en')
166
	{
167
		$this->properties = get_class_methods($this);
168
169
		if (empty($culture)) {
170
			$culture = 'en';
171
		}
172
173
		$this->setCulture($culture);
174
175
		$this->loadCultureData('root');
176
		$this->loadCultureData($culture);
177
	}
178
179
	/**
180
	 * Gets the CultureInfo that for this culture string
181
	 * @param mixed $culture
182
	 * @return CultureInfo invariant culture info is "en".
183
	 */
184
	public static function getInstance($culture)
185
	{
186
		static $instances = [];
187
		if (!isset($instances[$culture])) {
188
			$instances[$culture] = new CultureInfo($culture);
189
		}
190
		return $instances[$culture];
191
	}
192
193
	/**
194
	 * Determine if a given culture is valid. Simply checks that the
195
	 * culture data exists.
196
	 * @param string $culture a culture
197
	 * @return bool true if valid, false otherwise.
198
	 */
199
	public static function validCulture($culture)
200
	{
201
		return in_array($culture, self::getCultures());
202
	}
203
204
	/**
205
	 * Set the culture for the current instance. The culture indentifier
206
	 * must be of the form "<language>_(country/region)".
207
	 * @param string $culture culture identifier, e.g. "fr_FR_EURO".
208
	 */
209
	protected function setCulture($culture)
210
	{
211
		if (!empty($culture)) {
212
			if (!preg_match('/^[_\\w]+$/', $culture)) {
213
				throw new Exception('Invalid culture supplied: ' . $culture);
214
			}
215
		}
216
217
		$this->culture = $culture;
218
	}
219
220
	/**
221
	 * Load the ICU culture data for the specific culture identifier.
222
	 * @param string $culture the culture identifier.
0 ignored issues
show
Documentation introduced by
There is no parameter named $culture. Did you maybe mean $cultureName?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
223
	 */
224
	protected function loadCultureData($cultureName)
225
	{
226
		$culture_parts = explode('_', $cultureName);
227
		$current_part = $culture_parts[0];
228
229
		$culturesToLoad = [$current_part];
230
		for($i = 1, $k = count($culture_parts); $i < $k; ++$i)
231
		{
232
			$current_part .= '_'.$culture_parts[$i];
233
			$culturesToLoad[] = $current_part;
234
		}
235
236
		foreach(self::$bundleNames as $key => $bundleName)
237
		{
238
			if(!array_key_exists($key, $this->data))
239
				$this->data[$key] = [];
240
		}
241
		foreach($culturesToLoad as $culture)
242
		{
243
			if(in_array($culture, $this->resourceBundles))
244
				continue;
245
246
			array_unshift($this->resourceBundles, $culture);
247
			foreach(self::$bundleNames as $key => $bundleName)
248
			{
249
				$this->data[$key][$culture] = \ResourceBundle::create($culture, $bundleName, false);
250
			}
251
		}
252
	}
253
254
	/**
255
	 * Find the specific ICU data information from the data.
256
	 * The path to the specific ICU data is separated with a slash "/".
257
	 * E.g. To find the default calendar used by the culture, the path
258
	 * "calendar/default" will return the corresponding default calendar.
259
	 * Use merge=true to return the ICU including the parent culture.
260
	 * E.g. The currency data for a variant, say "en_AU" contains one
261
	 * entry, the currency for AUD, the other currency data are stored
262
	 * in the "en" data file. Thus to retrieve all the data regarding
263
	 * currency for "en_AU", you need to use findInfo("Currencies,true);.
264
	 * @param string $path the data you want to find.
265
	 * @param bool $merge merge the data from its parents.
266
	 * @return mixed the specific ICU data.
267
	 */
268
	public function findInfo($path='/', $merge=false, $key = null)
269
	{
270
		$result = [];
271
272
		if($key === null)
273
		{
274
			// try to guess the bundle from the path. Always defaults to "Core".
275
			$key = 'Core';
276
			foreach(self::$bundleNames as $bundleName => $icuBundleName)
277
			{
278
				if(strpos($path, $bundleName) === 0)
279
				{
280
					$key = $bundleName;
281
					break;
282
				}
283
			}
284
		}
285
286
		if(!array_key_exists($key, $this->data))
287
			return $result;
288
		foreach($this->resourceBundles as $culture)
289
		{
290
			$res = $this->data[$key][$culture];
291
			if($res === null)
292
				continue;
293
			$info = $this->searchResources($res, $path);
294
			if($info)
295
			{
296
				if($merge)
297
					$result = array_merge($result, $info);
298
				else
299
					return $info;
300
			}
301
		}
302
303
		return $result;
304
	}
305
306
	/**
307
	 * Search the array for a specific value using a path separated using
308
	 * slash "/" separated path. e.g to find $info['hello']['world'],
309
	 * the path "hello/world" will return the corresponding value.
310
	 * @param array $info the array for search
311
	 * @param string $path slash "/" separated array path.
312
	 * @return mixed the value array using the path
313
	 */
314
	private function searchResources($info, $path='/')
315
	{
316
		$index = explode('/', $path);
317
318
		$resource = $info;
319
		for($i = 0, $k = count($index); $i < $k; ++$i)
320
		{
321
322
			$resource = $resource->get($index[$i], false);
323
			if($resource === null)
324
				return null;
325
		}
326
327
		return $this->simplify($resource);
328
	}
329
330
	/**
331
	 * Gets the culture name in the format
332
	 * "<languagecode2>_(country/regioncode2)".
333
	 * @return string culture name.
334
	 */
335
	public function getName()
336
	{
337
		return $this->culture;
338
	}
339
340
	/**
341
	 * Gets the default calendar used by the culture, e.g. "gregorian".
342
	 * @return string the default calendar.
343
	 */
344
	public function getCalendar()
345
	{
346
		return $this->findInfo('calendar/default');
347
	}
348
349
	/**
350
	 * Gets the culture name in the language that the culture is set
351
	 * to display. Returns <code>array('Language','Country');</code>
352
	 * 'Country' is omitted if the culture is neutral.
353
	 * @return array array with language and country as elements, localized.
354
	 */
355
	public function getNativeName()
356
	{
357
		$lang = substr($this->culture, 0, 2);
358
		$reg = substr($this->culture, 3, 2);
359
		$language = $this->findInfo("Languages/{$lang}");
360
		$region = $this->findInfo("Countries/{$reg}");
361
		if($region)
0 ignored issues
show
Bug Best Practice introduced by
The expression $region of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
362
			return $language.' ('.$region.')';
363
		else
364
			return $language;
365
	}
366
367
	/**
368
	 * Gets the culture name in English.
369
	 * Returns <code>array('Language','Country');</code>
370
	 * 'Country' is omitted if the culture is neutral.
371
	 * @return string language (country), it may locale code string if english name does not exist.
372
	 */
373
	public function getEnglishName()
374
	{
375
		$lang = substr($this->culture, 0, 2);
376
		$reg = substr($this->culture, 3, 2);
377
		$culture = $this->getInvariantCulture();
378
379
		$language = $culture->findInfo("Languages/{$lang}");
380
		if (count($language) == 0) {
381
			return $this->culture;
382
		}
383
384
		$region = $culture->findInfo("Countries/{$reg}");
385
		if($region)
0 ignored issues
show
Bug Best Practice introduced by
The expression $region of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
386
			return $language.' ('.$region.')';
387
		else
388
			return $language;
389
	}
390
391
	/**
392
	 * Gets the CultureInfo that is culture-independent (invariant).
393
	 * Any changes to the invariant culture affects all other
394
	 * instances of the invariant culture.
395
	 * The invariant culture is assumed to be "en";
396
	 * @return CultureInfo invariant culture info is "en".
397
	 */
398
	public static function getInvariantCulture()
399
	{
400
		static $invariant;
401
		if ($invariant === null) {
402
			$invariant = new CultureInfo();
403
		}
404
		return $invariant;
405
	}
406
407
	/**
408
	 * Gets a value indicating whether the current CultureInfo
409
	 * represents a neutral culture. Returns true if the culture
410
	 * only contains two characters.
411
	 * @return bool true if culture is neutral, false otherwise.
412
	 */
413
	public function getIsNeutralCulture()
414
	{
415
		return strlen($this->culture) == 2;
416
	}
417
418
	/**
419
	 * Gets the CultureInfo that represents the parent culture of the
420
	 * current CultureInfo
421
	 * @return CultureInfo parent culture information.
422
	 */
423
	public function getParent()
424
	{
425
		if (strlen($this->culture) == 2) {
426
			return $this->getInvariantCulture();
427
		}
428
429
		$lang = substr($this->culture, 0, 2);
430
		return new CultureInfo($lang);
431
	}
432
433
	/**
434
	 * Gets the list of supported cultures filtered by the specified
435
	 * culture type. This is an EXPENSIVE function, it needs to traverse
436
	 * a list of ICU files in the data directory.
437
	 * This function can be called statically.
438
	 * @param int $type culture type, CultureInfo::ALL, CultureInfo::NEUTRAL
439
	 * or CultureInfo::SPECIFIC.
440
	 * @return array list of culture information available.
441
	 */
442
	public static function getCultures($type=CultureInfo::ALL)
443
	{
444
		$all = \ResourceBundle::getLocales('');
445
446
		switch($type)
447
		{
448
			case CultureInfo::ALL :
449
				return $all;
450 View Code Duplication
			case CultureInfo::NEUTRAL :
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...
451
				foreach($all as $key => $culture)
452
				{
453
					if(strlen($culture) != 2)
454
						unset($all[$key]);
455
				}
456
				return $all;
457 View Code Duplication
			case CultureInfo::SPECIFIC :
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...
458
				foreach($all as $key => $culture)
459
				{
460
					if(strlen($culture) == 2)
461
						unset($all[$key]);
462
				}
463
				return $all;
464
		}
465
	}
466
467
	/**
468
	 * Simplify a single element array into its own value.
469
	 * E.g. <code>array(0 => array('hello'), 1 => 'world');</code>
470
	 * becomes <code>array(0 => 'hello', 1 => 'world');</code>
471
	 * @param array $array with single elements arrays
0 ignored issues
show
Bug introduced by
There is no parameter named $array. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
472
	 * @return array simplified array.
473
	 */
474
	protected function simplify($obj)
475
	{
476
		if(is_scalar($obj)) {
477
			return $obj;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $obj; (integer|double|string|boolean) is incompatible with the return type documented by Prado\I18N\core\CultureInfo::simplify of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
478
		} elseif($obj instanceof \ResourceBundle) {
479
			$array = array();
480
			foreach($obj as $k => $v)
0 ignored issues
show
Bug introduced by
The expression $obj of type object<ResourceBundle> is not traversable.
Loading history...
481
				$array[$k] = $v;
482
		} else {
483
			$array = $obj;
484
		}
485
486
		for($i = 0, $k = count($array); $i<$k; ++$i)
487
		{
488
			$key = key($array);
489
			if ($key !== null
490
				&& is_array($array[$key])
491
				&& count($array[$key]) == 1) {
492
				$array[$key] = $array[$key][0];
493
			}
494
			next($array);
495
		}
496
		return $array;
497
	}
498
499
	/**
500
	 * Get a list of countries in the language of the localized version.
501
	 * @return array a list of localized country names.
502
	 */
503
	public function getCountries()
504
	{
505
		return $this->simplify($this->findInfo('Countries', true, 'Countries'));
506
	}
507
508
	/**
509
	 * Get a list of currencies in the language of the localized version.
510
	 * @return array a list of localized currencies.
511
	 */
512
	public function getCurrencies()
513
	{
514
		static $arr;
515
		if($arr === null)
516
		{
517
			$arr = $this->findInfo('Currencies', false, 'Currencies');
518
			foreach($arr as $k => $v)
519
				$arr[$k] = $this->simplify($v);
520
		}
521
		return $arr;
522
	}
523
524
	/**
525
	 * Get a list of languages in the language of the localized version.
526
	 * @return array list of localized language names.
527
	 */
528
	public function getLanguages()
529
	{
530
		return $this->simplify($this->findInfo('Languages', true, 'Languages'));
531
	}
532
533
	/**
534
	 * Get a list of scripts in the language of the localized version.
535
	 * @return array list of localized script names.
536
	 */
537
	public function getScripts()
538
	{
539
		return $this->simplify($this->findInfo('Scripts', true, 'Languages'));
540
	}
541
542
	/**
543
	 * Get a list of timezones in the language of the localized version.
544
	 * @return array list of localized timezones.
545
	 */
546
	public function getTimeZones()
547
	{
548
		static $arr;
549
		if($arr === null)
550
		{
551
			$validPrefixes = array('Africa', 'America', 'Antarctica', 'Arctic', 'Asia', 'Atlantic', 'Australia', 'Etc', 'Europe', 'Indian', 'Pacific');
552
			$tmp = $this->findInfo('zoneStrings', false, 'zoneStrings');
553
			foreach($tmp as $k => $v)
554
			{
555
				foreach($validPrefixes as $prefix)
556
				{
557
					if(strpos($k, $prefix) === 0)
558
					{
559
						$arr[] = str_replace(':', '/', $k);
560
						break;
561
					}
562
				}
563
			}
564
		}
565
		return $arr;
566
	}
567
}
568