From c3cce5c694b93d318231917d1d8c4469c52b2c1c Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 11 Sep 2014 18:57:52 +0200 Subject: [PATCH] [Intl] Improved bundle reader implementations --- .../Exception/MissingResourceException.php | 21 ++ .../ResourceBundleNotFoundException.php | 19 ++ src/Symfony/Component/Intl/Locale.php | 48 +++ .../Reader/AbstractBundleReader.php | 44 --- .../Reader/BinaryBundleReader.php | 25 +- .../ResourceBundle/Reader/PhpBundleReader.php | 22 +- .../Reader/StructuredBundleReader.php | 159 +++++++--- .../StructuredBundleReaderInterface.php | 7 +- .../Util/RecursiveArrayAccess.php | 22 +- .../Reader/AbstractBundleReaderTest.php | 64 ---- .../Reader/BinaryBundleReaderTest.php | 50 ++- .../ResourceBundle/Reader/Fixtures/build.sh | 17 ++ .../ResourceBundle/Reader/Fixtures/php/en.php | 14 + .../Reader/Fixtures/res/alias.res | Bin 0 -> 88 bytes .../ResourceBundle/Reader/Fixtures/res/mo.res | Bin 0 -> 92 bytes .../ResourceBundle/Reader/Fixtures/res/ro.res | Bin 0 -> 84 bytes .../Reader/Fixtures/res/ro_MD.res | Bin 0 -> 84 bytes .../Reader/Fixtures/res/root.res | Bin 0 -> 76 bytes .../Reader/Fixtures/txt/alias.txt | 3 + .../ResourceBundle/Reader/Fixtures/txt/mo.txt | 3 + .../ResourceBundle/Reader/Fixtures/txt/ro.txt | 3 + .../Reader/Fixtures/txt/ro_MD.txt | 3 + .../Reader/Fixtures/txt/root.txt | 6 + .../Reader/PhpBundleReaderTest.php | 12 +- .../Reader/StructuredBundleReaderTest.php | 284 +++++++++++++----- 25 files changed, 573 insertions(+), 253 deletions(-) create mode 100644 src/Symfony/Component/Intl/Exception/MissingResourceException.php create mode 100644 src/Symfony/Component/Intl/Exception/ResourceBundleNotFoundException.php create mode 100644 src/Symfony/Component/Intl/Locale.php delete mode 100644 src/Symfony/Component/Intl/ResourceBundle/Reader/AbstractBundleReader.php delete mode 100644 src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/AbstractBundleReaderTest.php create mode 100755 src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/build.sh create mode 100644 src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/php/en.php create mode 100644 src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/alias.res create mode 100644 src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/mo.res create mode 100644 src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/ro.res create mode 100644 src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/ro_MD.res create mode 100644 src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/root.res create mode 100644 src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/alias.txt create mode 100644 src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/mo.txt create mode 100644 src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/ro.txt create mode 100644 src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/ro_MD.txt create mode 100644 src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/root.txt diff --git a/src/Symfony/Component/Intl/Exception/MissingResourceException.php b/src/Symfony/Component/Intl/Exception/MissingResourceException.php new file mode 100644 index 0000000000000..e2eb3f210e751 --- /dev/null +++ b/src/Symfony/Component/Intl/Exception/MissingResourceException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Exception; + +/** + * Thrown when an invalid entry of a resource bundle was requested. + * + * @author Bernhard Schussek + */ +class MissingResourceException extends RuntimeException +{ +} diff --git a/src/Symfony/Component/Intl/Exception/ResourceBundleNotFoundException.php b/src/Symfony/Component/Intl/Exception/ResourceBundleNotFoundException.php new file mode 100644 index 0000000000000..59da5ec0d53d5 --- /dev/null +++ b/src/Symfony/Component/Intl/Exception/ResourceBundleNotFoundException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Exception; + +/** + * @author Bernhard Schussek + */ +class ResourceBundleNotFoundException extends RuntimeException +{ +} diff --git a/src/Symfony/Component/Intl/Locale.php b/src/Symfony/Component/Intl/Locale.php new file mode 100644 index 0000000000000..d7e0d33e84070 --- /dev/null +++ b/src/Symfony/Component/Intl/Locale.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl; + +/** + * Provides access to locale-related data. + * + * @author Bernhard Schussek + * + * @internal + */ +final class Locale extends \Locale +{ + /** + * Returns the fallback locale for a given locale, if any + * + * @param string $locale The ICU locale code to find the fallback for. + * + * @return string|null The ICU locale code of the fallback locale, or null + * if no fallback exists + */ + public static function getFallback($locale) + { + if (false === $pos = strrpos($locale, '_')) { + if ('root' === $locale) { + return; + } + + return 'root'; + } + + return substr($locale, 0, $pos); + } + + /** + * This class must not be instantiated. + */ + private function __construct() {} +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/AbstractBundleReader.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/AbstractBundleReader.php deleted file mode 100644 index 02db55b4072c6..0000000000000 --- a/src/Symfony/Component/Intl/ResourceBundle/Reader/AbstractBundleReader.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Intl\ResourceBundle\Reader; - -/** - * Base class for {@link BundleReaderInterface} implementations. - * - * @author Bernhard Schussek - * - * @internal - */ -abstract class AbstractBundleReader implements BundleReaderInterface -{ - /** - * {@inheritdoc} - */ - public function getLocales($path) - { - $extension = '.' . $this->getFileExtension(); - $locales = glob($path . '/*' . $extension); - - // Remove file extension and sort - array_walk($locales, function (&$locale) use ($extension) { $locale = basename($locale, $extension); }); - sort($locales); - - return $locales; - } - - /** - * Returns the extension of locale files in this bundle. - * - * @return string The file extension (without leading dot). - */ - abstract protected function getFileExtension(); -} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/BinaryBundleReader.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/BinaryBundleReader.php index 75738dd8c8829..77b86aee42598 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Reader/BinaryBundleReader.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/BinaryBundleReader.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Intl\ResourceBundle\Reader; -use Symfony\Component\Intl\Exception\RuntimeException; +use Symfony\Component\Intl\Exception\ResourceBundleNotFoundException; use Symfony\Component\Intl\ResourceBundle\Util\ArrayAccessibleResourceBundle; /** @@ -21,7 +21,7 @@ * * @internal */ -class BinaryBundleReader extends AbstractBundleReader implements BundleReaderInterface +class BinaryBundleReader implements BundleReaderInterface { /** * {@inheritdoc} @@ -31,28 +31,39 @@ public function read($path, $locale) // Point for future extension: Modify this class so that it works also // if the \ResourceBundle class is not available. try { - $bundle = new \ResourceBundle($locale, $path); + // Never enable fallback. We want to know if a bundle cannot be found + $bundle = new \ResourceBundle($locale, $path, false); } catch (\Exception $e) { // HHVM compatibility: constructor throws on invalid resource $bundle = null; } + // The bundle is NULL if the path does not look like a resource bundle + // (i.e. contain a bunch of *.res files) if (null === $bundle) { - throw new RuntimeException(sprintf( - 'Could not load the resource bundle "%s/%s.res".', + throw new ResourceBundleNotFoundException(sprintf( + 'The resource bundle "%s/%s.res" could not be found.', $path, $locale )); } + // Other possible errors are U_USING_FALLBACK_WARNING and U_ZERO_ERROR, + // which are OK for us. return new ArrayAccessibleResourceBundle($bundle); } /** * {@inheritdoc} */ - protected function getFileExtension() + public function getLocales($path) { - return 'res'; + $locales = glob($path.'/*.res'); + + // Remove file extension and sort + array_walk($locales, function (&$locale) { $locale = basename($locale, '.res'); }); + sort($locales); + + return $locales; } } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/PhpBundleReader.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/PhpBundleReader.php index 01dd00be51640..2082916827bd0 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Reader/PhpBundleReader.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/PhpBundleReader.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Intl\ResourceBundle\Reader; -use Symfony\Component\Intl\Exception\InvalidArgumentException; +use Symfony\Component\Intl\Exception\ResourceBundleNotFoundException; use Symfony\Component\Intl\Exception\RuntimeException; /** @@ -21,21 +21,17 @@ * * @internal */ -class PhpBundleReader extends AbstractBundleReader implements BundleReaderInterface +class PhpBundleReader implements BundleReaderInterface { /** * {@inheritdoc} */ public function read($path, $locale) { - if ('en' !== $locale) { - throw new InvalidArgumentException('Only the locale "en" is supported.'); - } - - $fileName = $path . '/' . $locale . '.php'; + $fileName = $path.'/'.$locale.'.php'; if (!file_exists($fileName)) { - throw new RuntimeException(sprintf( + throw new ResourceBundleNotFoundException(sprintf( 'The resource bundle "%s/%s.php" does not exist.', $path, $locale @@ -56,8 +52,14 @@ public function read($path, $locale) /** * {@inheritdoc} */ - protected function getFileExtension() + public function getLocales($path) { - return 'php'; + $locales = glob($path.'/*.php'); + + // Remove file extension and sort + array_walk($locales, function (&$locale) { $locale = basename($locale, '.php'); }); + sort($locales); + + return $locales; } } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReader.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReader.php index 689510bdd75f1..01ec26190a349 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReader.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReader.php @@ -11,10 +11,14 @@ namespace Symfony\Component\Intl\ResourceBundle\Reader; +use Symfony\Component\Intl\Exception\MissingResourceException; +use Symfony\Component\Intl\Exception\OutOfBoundsException; +use Symfony\Component\Intl\Exception\ResourceBundleNotFoundException; +use Symfony\Component\Intl\Locale; use Symfony\Component\Intl\ResourceBundle\Util\RecursiveArrayAccess; /** - * A structured reader wrapping an existing resource bundle reader. + * Default implementation of {@link StructuredBundleReaderInterface}. * * @author Bernhard Schussek * @@ -29,6 +33,13 @@ class StructuredBundleReader implements StructuredBundleReaderInterface */ private $reader; + /** + * A mapping of locale aliases to locales + * + * @var array + */ + private $localeAliases = array(); + /** * Creates an entry reader based on the given resource bundle reader. * @@ -40,19 +51,26 @@ public function __construct(BundleReaderInterface $reader) } /** - * {@inheritdoc} + * Stores a mapping of locale aliases to locales. + * + * This mapping is used when reading entries and merging them with their + * fallback locales. If an entry is read for a locale alias (e.g. "mo") + * that points to a locale with a fallback locale ("ro_MD"), the reader + * can continue at the correct fallback locale ("ro"). + * + * @param array $localeAliases A mapping of locale aliases to locales */ - public function read($path, $locale) + public function setLocaleAliases($localeAliases) { - return $this->reader->read($path, $locale); + $this->localeAliases = $localeAliases; } /** * {@inheritdoc} */ - public function getLocales($path) + public function read($path, $locale) { - return $this->reader->getLocales($path); + return $this->reader->read($path, $locale); } /** @@ -60,56 +78,115 @@ public function getLocales($path) */ public function readEntry($path, $locale, array $indices, $fallback = true) { - $data = $this->reader->read($path, $locale); + $entry = null; + $isMultiValued = false; + $readSucceeded = false; + $exception = null; + $currentLocale = $locale; + $testedLocales = array(); + + while (null !== $currentLocale) { + // Resolve any aliases to their target locales + if (isset($this->localeAliases[$currentLocale])) { + $currentLocale = $this->localeAliases[$currentLocale]; + } - $entry = RecursiveArrayAccess::get($data, $indices); - $multivalued = is_array($entry) || $entry instanceof \Traversable; + try { + $data = $this->reader->read($path, $currentLocale); + $currentEntry = RecursiveArrayAccess::get($data, $indices); + $readSucceeded = true; - if (!($fallback && (null === $entry || $multivalued))) { - return $entry; - } + $isCurrentTraversable = $currentEntry instanceof \Traversable; + $isCurrentMultiValued = $isCurrentTraversable || is_array($currentEntry); - if (null !== ($fallbackLocale = $this->getFallbackLocale($locale))) { - $parentEntry = $this->readEntry($path, $fallbackLocale, $indices, true); + // Return immediately if fallback is disabled or we are dealing + // with a scalar non-null entry + if (!$fallback || (!$isCurrentMultiValued && null !== $currentEntry)) { + return $currentEntry; + } - if ($entry || $parentEntry) { - $multivalued = $multivalued || is_array($parentEntry) || $parentEntry instanceof \Traversable; + // ========================================================= + // Fallback is enabled, entry is either multi-valued or NULL + // ========================================================= - if ($multivalued) { - if ($entry instanceof \Traversable) { - $entry = iterator_to_array($entry); - } + // If entry is multi-valued, convert to array + if ($isCurrentTraversable) { + $currentEntry = iterator_to_array($currentEntry); + } - if ($parentEntry instanceof \Traversable) { - $parentEntry = iterator_to_array($parentEntry); - } + // If previously read entry was multi-valued too, merge them + if ($isCurrentMultiValued && $isMultiValued) { + $currentEntry = array_merge($currentEntry, $entry); + } - $entry = array_merge( - $parentEntry ?: array(), - $entry ?: array() - ); - } else { - $entry = null === $entry ? $parentEntry : $entry; + // Keep the previous entry if the current entry is NULL + if (null !== $currentEntry) { + $entry = $currentEntry; } + + // If this or the previous entry was multi-valued, we are dealing + // with a merged, multi-valued entry now + $isMultiValued = $isMultiValued || $isCurrentMultiValued; + } catch (ResourceBundleNotFoundException $e) { + // Continue if there is a fallback locale for the current + // locale + $exception = $e; + } catch (OutOfBoundsException $e) { + // Remember exception and rethrow if we cannot find anything in + // the fallback locales either + $exception = $e; } + + // Remember which locales we tried + $testedLocales[] = $currentLocale.'.res'; + + // Check whether fallback is allowed + if (!$fallback) { + break; + } + + // Then determine fallback locale + $currentLocale = Locale::getFallback($currentLocale); + } + + // Multi-valued entry was merged + if ($isMultiValued) { + return $entry; + } + + // Entry is still NULL, but no read error occurred + if ($readSucceeded) { + return $entry; } - return $entry; + // Entry is still NULL, read error occurred. Throw an exception + // containing the detailed path and locale + $errorMessage = sprintf( + 'Couldn\'t read the indices [%s] from "%s/%s.res".', + implode('][', $indices), + $path, + $locale + ); + + // Append fallback locales, if any + if (count($testedLocales) > 1) { + // Remove original locale + array_shift($testedLocales); + + $errorMessage .= sprintf( + ' The indices also couldn\'t be found in the fallback locale(s) "%s".', + implode('", "', $testedLocales) + ); + } + + throw new MissingResourceException($errorMessage, 0, $exception); } /** - * Returns the fallback locale for a given locale, if any - * - * @param string $locale The locale to find the fallback for. - * - * @return string|null The fallback locale, or null if no parent exists + * {@inheritdoc} */ - private function getFallbackLocale($locale) + public function getLocales($path) { - if (false === $pos = strrpos($locale, '_')) { - return; - } - - return substr($locale, 0, $pos); + return $this->reader->getLocales($path); } } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReaderInterface.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReaderInterface.php index 408ee67ee03ef..19bb3fb49bdd2 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReaderInterface.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReaderInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Intl\ResourceBundle\Reader; +use Symfony\Component\Intl\Exception\MissingResourceException; + /** * Reads individual entries of a resource file. * @@ -45,8 +47,9 @@ interface StructuredBundleReaderInterface extends BundleReaderInterface * in the requested locale. * * @return mixed Returns an array or {@link \ArrayAccess} instance for - * complex data, a scalar value for simple data and NULL - * if the given path could not be accessed. + * complex data and a scalar value for simple data. + * + * @throws MissingResourceException If the indices cannot be accessed */ public function readEntry($path, $locale, array $indices, $fallback = true); } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Util/RecursiveArrayAccess.php b/src/Symfony/Component/Intl/ResourceBundle/Util/RecursiveArrayAccess.php index 6e46790c6f75d..0c22550401cb2 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Util/RecursiveArrayAccess.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Util/RecursiveArrayAccess.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Intl\ResourceBundle\Util; +use Symfony\Component\Intl\Exception\OutOfBoundsException; + /** * @author Bernhard Schussek * @@ -21,11 +23,23 @@ class RecursiveArrayAccess public static function get($array, array $indices) { foreach ($indices as $index) { - if (!$array instanceof \ArrayAccess && !is_array($array)) { - return; + // Use array_key_exists() for arrays, isset() otherwise + if (is_array($array)) { + if (array_key_exists($index, $array)) { + $array = $array[$index]; + continue; + } + } elseif ($array instanceof \ArrayAccess) { + if (isset($array[$index])) { + $array = $array[$index]; + continue; + } } - - $array = $array[$index]; + + throw new OutOfBoundsException(sprintf( + 'The index %s does not exist.', + $index + )); } return $array; diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/AbstractBundleReaderTest.php b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/AbstractBundleReaderTest.php deleted file mode 100644 index 2da7f90de49e3..0000000000000 --- a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/AbstractBundleReaderTest.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Intl\Tests\ResourceBundle\Reader; - -use Symfony\Component\Filesystem\Filesystem; - -/** - * @author Bernhard Schussek - */ -class AbstractBundleReaderTest extends \PHPUnit_Framework_TestCase -{ - private $directory; - - /** - * @var Filesystem - */ - private $filesystem; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $reader; - - protected function setUp() - { - $this->directory = sys_get_temp_dir() . '/AbstractBundleReaderTest/' . rand(1000, 9999); - $this->filesystem = new Filesystem(); - $this->reader = $this->getMockForAbstractClass('Symfony\Component\Intl\ResourceBundle\Reader\AbstractBundleReader'); - - $this->filesystem->mkdir($this->directory); - } - - protected function tearDown() - { - $this->filesystem->remove($this->directory); - } - - public function testGetLocales() - { - $this->filesystem->touch($this->directory . '/en.foo'); - $this->filesystem->touch($this->directory . '/de.foo'); - $this->filesystem->touch($this->directory . '/fr.foo'); - $this->filesystem->touch($this->directory . '/bo.txt'); - $this->filesystem->touch($this->directory . '/gu.bin'); - $this->filesystem->touch($this->directory . '/s.lol'); - - $this->reader->expects($this->any()) - ->method('getFileExtension') - ->will($this->returnValue('foo')); - - $sortedLocales = array('de', 'en', 'fr'); - - $this->assertSame($sortedLocales, $this->reader->getLocales($this->directory)); - } -} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/BinaryBundleReaderTest.php b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/BinaryBundleReaderTest.php index 3aefbae7fd911..526424d1a3d19 100644 --- a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/BinaryBundleReaderTest.php +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/BinaryBundleReaderTest.php @@ -33,19 +33,61 @@ protected function setUp() public function testReadReturnsArrayAccess() { - $data = $this->reader->read(__DIR__ . '/Fixtures', 'en'); + $data = $this->reader->read(__DIR__.'/Fixtures/res', 'ro'); $this->assertInstanceOf('\ArrayAccess', $data); $this->assertSame('Bar', $data['Foo']); $this->assertFalse(isset($data['ExistsNot'])); } + public function testReadFollowsAlias() + { + // "alias" = "ro" + $data = $this->reader->read(__DIR__.'/Fixtures/res', 'alias'); + + $this->assertInstanceOf('\ArrayAccess', $data); + $this->assertSame('Bar', $data['Foo']); + $this->assertFalse(isset($data['ExistsNot'])); + } + + public function testReadDoesNotFollowFallback() + { + // "ro_MD" -> "ro" + $data = $this->reader->read(__DIR__.'/Fixtures/res', 'ro_MD'); + + $this->assertInstanceOf('\ArrayAccess', $data); + $this->assertSame('Bam', $data['Baz']); + $this->assertFalse(isset($data['Foo'])); + $this->assertNull($data['Foo']); + $this->assertFalse(isset($data['ExistsNot'])); + } + + public function testReadDoesNotFollowFallbackAlias() + { + // "mo" = "ro_MD" -> "ro" + $data = $this->reader->read(__DIR__.'/Fixtures/res', 'mo'); + + $this->assertInstanceOf('\ArrayAccess', $data); + $this->assertSame('Bam', $data['Baz'], 'data from the aliased locale can be accessed'); + $this->assertFalse(isset($data['Foo'])); + $this->assertNull($data['Foo']); + $this->assertFalse(isset($data['ExistsNot'])); + } + /** - * @expectedException \Symfony\Component\Intl\Exception\RuntimeException + * @expectedException \Symfony\Component\Intl\Exception\ResourceBundleNotFoundException */ public function testReadFailsIfNonExistingLocale() { - $this->reader->read(__DIR__ . '/Fixtures', 'foo'); + $this->reader->read(__DIR__.'/Fixtures/res', 'foo'); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\ResourceBundleNotFoundException + */ + public function testReadFailsIfNonExistingFallbackLocale() + { + $this->reader->read(__DIR__.'/Fixtures/res', 'ro_AT'); } /** @@ -53,6 +95,6 @@ public function testReadFailsIfNonExistingLocale() */ public function testReadFailsIfNonExistingDirectory() { - $this->reader->read(__DIR__ . '/foo', 'en'); + $this->reader->read(__DIR__.'/foo', 'ro'); } } diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/build.sh b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/build.sh new file mode 100755 index 0000000000000..50513e7a946c3 --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/build.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +if [ -z "$ICU_BUILD_DIR" ]; then + echo "Please set the ICU_BUILD_DIR environment variable" + exit +fi + +if [ ! -d "$ICU_BUILD_DIR" ]; then + echo "The directory $ICU_BUILD_DIR pointed at by ICU_BUILD_DIR does not exist" + exit +fi + +DIR=`dirname $0` + +rm $DIR/res/*.res + +LD_LIBRARY_PATH=$ICU_BUILD_DIR/lib $ICU_BUILD_DIR/bin/genrb -d $DIR/res $DIR/txt/*.txt diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/php/en.php b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/php/en.php new file mode 100644 index 0000000000000..f2b06a91ad32e --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/php/en.php @@ -0,0 +1,14 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return array( + 'Foo' => 'Bar', +); diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/alias.res b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/alias.res new file mode 100644 index 0000000000000000000000000000000000000000..4f0ab7eaa316685f0bf153446f37aeb0221fdd10 GIT binary patch literal 88 zcmY#jxTP+_00K-5L8-+~Oh6VR3s?Y50GR>oKo%De^Fc8qSO&sZRdw|7bPNWH6fxuj MNk#?*AYQc!03g)|MgRZ+ literal 0 HcmV?d00001 diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/mo.res b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/mo.res new file mode 100644 index 0000000000000000000000000000000000000000..3f8911a7317ed2f6e19b291c131ca68bb7f7cad1 GIT binary patch literal 92 zcmY#jxTP+_00K-5L8-+~Oh6VR3s?Y5urn|O05Jm>5c5MZBUlE)S5reader->read(__DIR__ . '/Fixtures', 'en'); + $data = $this->reader->read(__DIR__.'/Fixtures/php', 'en'); $this->assertTrue(is_array($data)); $this->assertSame('Bar', $data['Foo']); @@ -38,11 +38,11 @@ public function testReadReturnsArray() } /** - * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException + * @expectedException \Symfony\Component\Intl\Exception\ResourceBundleNotFoundException */ - public function testReadFailsIfLocaleOtherThanEn() + public function testReadFailsIfNonExistingLocale() { - $this->reader->read(__DIR__ . '/Fixtures', 'foo'); + $this->reader->read(__DIR__.'/Fixtures/php', 'foo'); } /** @@ -50,7 +50,7 @@ public function testReadFailsIfLocaleOtherThanEn() */ public function testReadFailsIfNonExistingDirectory() { - $this->reader->read(__DIR__ . '/foo', 'en'); + $this->reader->read(__DIR__.'/foo', 'en'); } /** @@ -58,6 +58,6 @@ public function testReadFailsIfNonExistingDirectory() */ public function testReadFailsIfNotAFile() { - $this->reader->read(__DIR__ . '/Fixtures/NotAFile', 'en'); + $this->reader->read(__DIR__.'/Fixtures/NotAFile', 'en'); } } diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/StructuredBundleReaderTest.php b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/StructuredBundleReaderTest.php index 600236eb3ec56..e51e7ef2eeeeb 100644 --- a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/StructuredBundleReaderTest.php +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/StructuredBundleReaderTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Intl\Tests\ResourceBundle\Reader; +use Symfony\Component\Intl\Exception\ResourceBundleNotFoundException; use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReader; /** @@ -30,73 +31,146 @@ class StructuredBundleReaderTest extends \PHPUnit_Framework_TestCase */ private $readerImpl; + private static $data = array( + 'Entries' => array( + 'Foo' => 'Bar', + 'Bar' => 'Baz', + ), + 'Foo' => 'Bar', + 'Version' => '2.0', + ); + + private static $fallbackData = array( + 'Entries' => array( + 'Foo' => 'Foo', + 'Bam' => 'Lah', + ), + 'Baz' => 'Foo', + 'Version' => '1.0', + ); + + private static $mergedData = array( + // no recursive merging -> too complicated + 'Entries' => array( + 'Foo' => 'Bar', + 'Bar' => 'Baz', + ), + 'Baz' => 'Foo', + 'Version' => '2.0', + 'Foo' => 'Bar', + ); + protected function setUp() { $this->readerImpl = $this->getMock('Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface'); $this->reader = new StructuredBundleReader($this->readerImpl); } - public function testGetLocales() + public function testForwardCallToRead() { - $locales = array('en', 'de', 'fr'); - $this->readerImpl->expects($this->once()) - ->method('getLocales') - ->with(self::RES_DIR) - ->will($this->returnValue($locales)); + ->method('read') + ->with(self::RES_DIR, 'root') + ->will($this->returnValue(self::$data)); - $this->assertSame($locales, $this->reader->getLocales(self::RES_DIR)); + $this->assertSame(self::$data, $this->reader->read(self::RES_DIR, 'root')); } - public function testRead() + public function testReadEntireDataFileIfNoIndicesGiven() { - $data = array('foo', 'bar'); - - $this->readerImpl->expects($this->once()) + $this->readerImpl->expects($this->at(0)) ->method('read') ->with(self::RES_DIR, 'en') - ->will($this->returnValue($data)); + ->will($this->returnValue(self::$data)); - $this->assertSame($data, $this->reader->read(self::RES_DIR, 'en')); + $this->readerImpl->expects($this->at(1)) + ->method('read') + ->with(self::RES_DIR, 'root') + ->will($this->returnValue(self::$fallbackData)); + + $this->assertSame(self::$mergedData, $this->reader->readEntry(self::RES_DIR, 'en', array())); } - public function testReadEntryNoParams() + public function testReadExistingEntry() { - $data = array('foo', 'bar'); + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'root') + ->will($this->returnValue(self::$data)); + $this->assertSame('Bar', $this->reader->readEntry(self::RES_DIR, 'root', array('Entries', 'Foo'))); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\MissingResourceException + */ + public function testReadNonExistingEntry() + { $this->readerImpl->expects($this->once()) ->method('read') - ->with(self::RES_DIR, 'en') - ->will($this->returnValue($data)); + ->with(self::RES_DIR, 'root') + ->will($this->returnValue(self::$data)); - $this->assertSame($data, $this->reader->readEntry(self::RES_DIR, 'en', array())); + $this->reader->readEntry(self::RES_DIR, 'root', array('Entries', 'NonExisting')); } - public function testReadEntryWithParam() + public function testFallbackIfEntryDoesNotExist() { - $data = array('Foo' => array('Bar' => 'Baz')); + $this->readerImpl->expects($this->at(0)) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->returnValue(self::$data)); - $this->readerImpl->expects($this->once()) + $this->readerImpl->expects($this->at(1)) ->method('read') ->with(self::RES_DIR, 'en') - ->will($this->returnValue($data)); + ->will($this->returnValue(self::$fallbackData)); - $this->assertSame('Baz', $this->reader->readEntry(self::RES_DIR, 'en', array('Foo', 'Bar'))); + $this->assertSame('Lah', $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Entries', 'Bam'))); } - public function testReadEntryWithUnresolvablePath() + /** + * @expectedException \Symfony\Component\Intl\Exception\MissingResourceException + */ + public function testDontFallbackIfEntryDoesNotExistAndFallbackDisabled() { - $data = array('Foo' => 'Baz'); - $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->returnValue(self::$data)); + + $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Entries', 'Bam'), false); + } + + public function testFallbackIfLocaleDoesNotExist() + { + $this->readerImpl->expects($this->at(0)) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->throwException(new ResourceBundleNotFoundException())); + + $this->readerImpl->expects($this->at(1)) ->method('read') ->with(self::RES_DIR, 'en') - ->will($this->returnValue($data)); + ->will($this->returnValue(self::$fallbackData)); + + $this->assertSame('Lah', $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Entries', 'Bam'))); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\MissingResourceException + */ + public function testDontFallbackIfLocaleDoesNotExistAndFallbackDisabled() + { + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->throwException(new ResourceBundleNotFoundException())); - $this->assertNull($this->reader->readEntry(self::RES_DIR, 'en', array('Foo', 'Bar'))); + $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Entries', 'Bam'), false); } - public function readMergedEntryProvider() + public function provideMergeableValues() { return array( array('foo', null, 'foo'), @@ -110,46 +184,72 @@ public function readMergedEntryProvider() } /** - * @dataProvider readMergedEntryProvider + * @dataProvider provideMergeableValues */ - public function testReadMergedEntryNoParams($childData, $parentData, $result) + public function testMergeDataWithFallbackData($childData, $parentData, $result) { - $this->readerImpl->expects($this->at(0)) - ->method('read') - ->with(self::RES_DIR, 'en_GB') - ->will($this->returnValue($childData)); - if (null === $childData || is_array($childData)) { - $this->readerImpl->expects($this->at(1)) + $this->readerImpl->expects($this->at(0)) ->method('read') ->with(self::RES_DIR, 'en') + ->will($this->returnValue($childData)); + + $this->readerImpl->expects($this->at(1)) + ->method('read') + ->with(self::RES_DIR, 'root') ->will($this->returnValue($parentData)); + } else { + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'en') + ->will($this->returnValue($childData)); } - $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en_GB', array(), true)); + $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en', array(), true)); } /** - * @dataProvider readMergedEntryProvider + * @dataProvider provideMergeableValues */ - public function testReadMergedEntryWithParams($childData, $parentData, $result) + public function testDontMergeDataIfFallbackDisabled($childData, $parentData, $result) { - $this->readerImpl->expects($this->at(0)) + $this->readerImpl->expects($this->once()) ->method('read') ->with(self::RES_DIR, 'en_GB') - ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); + ->will($this->returnValue($childData)); + $this->assertSame($childData, $this->reader->readEntry(self::RES_DIR, 'en_GB', array(), false)); + } + + /** + * @dataProvider provideMergeableValues + */ + public function testMergeExistingEntryWithExistingFallbackEntry($childData, $parentData, $result) + { if (null === $childData || is_array($childData)) { - $this->readerImpl->expects($this->at(1)) + $this->readerImpl->expects($this->at(0)) ->method('read') ->with(self::RES_DIR, 'en') + ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); + + $this->readerImpl->expects($this->at(1)) + ->method('read') + ->with(self::RES_DIR, 'root') ->will($this->returnValue(array('Foo' => array('Bar' => $parentData)))); + } else { + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'en') + ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); } - $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true)); + $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en', array('Foo', 'Bar'), true)); } - public function testReadMergedEntryWithUnresolvablePath() + /** + * @dataProvider provideMergeableValues + */ + public function testMergeNonExistingEntryWithExistingFallbackEntry($childData, $parentData, $result) { $this->readerImpl->expects($this->at(0)) ->method('read') @@ -159,29 +259,40 @@ public function testReadMergedEntryWithUnresolvablePath() $this->readerImpl->expects($this->at(1)) ->method('read') ->with(self::RES_DIR, 'en') - ->will($this->returnValue(array('Foo' => 'Bar'))); + ->will($this->returnValue(array('Foo' => array('Bar' => $parentData)))); - $this->assertNull($this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true)); + $this->assertSame($parentData, $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true)); } - public function testReadMergedEntryWithUnresolvablePathInParent() + /** + * @dataProvider provideMergeableValues + */ + public function testMergeExistingEntryWithNonExistingFallbackEntry($childData, $parentData, $result) { - $this->readerImpl->expects($this->at(0)) - ->method('read') - ->with(self::RES_DIR, 'en_GB') - ->will($this->returnValue(array('Foo' => array('Bar' => array('three'))))); - - $this->readerImpl->expects($this->at(1)) - ->method('read') - ->with(self::RES_DIR, 'en') - ->will($this->returnValue(array('Foo' => 'Bar'))); + if (null === $childData || is_array($childData)) { + $this->readerImpl->expects($this->at(0)) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); - $result = array('three'); + $this->readerImpl->expects($this->at(1)) + ->method('read') + ->with(self::RES_DIR, 'en') + ->will($this->returnValue(array('Foo' => 'Bar'))); + } else { + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); + } - $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true)); + $this->assertSame($childData, $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true)); } - public function testReadMergedEntryWithUnresolvablePathInChild() + /** + * @expectedException \Symfony\Component\Intl\Exception\MissingResourceException + */ + public function testFailIfEntryFoundNeitherInParentNorChild() { $this->readerImpl->expects($this->at(0)) ->method('read') @@ -191,33 +302,64 @@ public function testReadMergedEntryWithUnresolvablePathInChild() $this->readerImpl->expects($this->at(1)) ->method('read') ->with(self::RES_DIR, 'en') - ->will($this->returnValue(array('Foo' => array('Bar' => array('one', 'two'))))); - - $result = array('one', 'two'); + ->will($this->returnValue(array('Foo' => 'Bar'))); - $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true)); + $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true); } /** - * @dataProvider readMergedEntryProvider + * @dataProvider provideMergeableValues */ - public function testReadMergedEntryWithTraversables($childData, $parentData, $result) + public function testMergeTraversables($childData, $parentData, $result) { $parentData = is_array($parentData) ? new \ArrayObject($parentData) : $parentData; $childData = is_array($childData) ? new \ArrayObject($childData) : $childData; - $this->readerImpl->expects($this->at(0)) - ->method('read') - ->with(self::RES_DIR, 'en_GB') - ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); - if (null === $childData || $childData instanceof \ArrayObject) { + $this->readerImpl->expects($this->at(0)) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); + $this->readerImpl->expects($this->at(1)) ->method('read') ->with(self::RES_DIR, 'en') ->will($this->returnValue(array('Foo' => array('Bar' => $parentData)))); + } else { + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); } $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true)); } + + /** + * @dataProvider provideMergeableValues + */ + public function testFollowLocaleAliases($childData, $parentData, $result) + { + $this->reader->setLocaleAliases(array('mo' => 'ro_MD')); + + if (null === $childData || is_array($childData)) { + $this->readerImpl->expects($this->at(0)) + ->method('read') + ->with(self::RES_DIR, 'ro_MD') + ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); + + // Read fallback locale of aliased locale ("ro_MD" -> "ro") + $this->readerImpl->expects($this->at(1)) + ->method('read') + ->with(self::RES_DIR, 'ro') + ->will($this->returnValue(array('Foo' => array('Bar' => $parentData)))); + } else { + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'ro_MD') + ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); + } + + $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'mo', array('Foo', 'Bar'), true)); + } }