diff --git a/CHANGELOG-2.2.md b/CHANGELOG-2.2.md index 2c7a93f004..de5c5a51f4 100644 --- a/CHANGELOG-2.2.md +++ b/CHANGELOG-2.2.md @@ -7,6 +7,41 @@ in 2.2 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v2.2.0...v2.2.1 +* 2.2.6 (2013-08-26) + + * f936b41: clearToken exception is thrown at wrong place. + * d0faf55: [Locale] Fixed: StubLocale::setDefault() throws no exception when "en" is passed + * 566d79c: [Yaml] fixed embedded folded string parsing + * 0951b8d: [Translation] Fixed regression: When only one rule is passed to transChoice(), this rule should be used + * 4563f1b: [Yaml] Fix comment containing a colon on a scalar line being parsed as a hash. + * 7e87eb1: fixed request format when forwarding a request + * ccaaedf: [Form] PropertyPathMapper::mapDataToForms() *always* calls setData() on every child to ensure that all *_DATA events were fired when the initialization phase is over (except for virtual forms) + * 00bc270: [Form] Fixed: submit() reacts to dynamic modifications of the form children + * 05fdb12: Fixed issue #6932 - Inconsistent locale handling in subrequests + * b3c3159: fixed locale of sub-requests when explicitely set by the developer (refs #8821) + * b72bc0b: [Locale] fixed build-data exit code in case of an error + * 9bb7a3d: fixed request format of sub-requests when explicitely set by the developer (closes #8787) + * fa35597: Sets _format attribute only if it wasn't set previously by the user. + * f946108: fixed the format of the request used to render an exception + * 51022c3: Fix typo in the check_path validator + * 5f7219e: added a missing use statement (closes #8808) + * 262879d: fix for Process:isSuccessful() + * 0723c10: [Process] Use a consistent way to reset data of the process latest run + * 85a9c9d: [HttpFoundation] Fixed removing a nonexisting namespaced attribute. + * 191d320: [Validation] Fixed IdentityTranslator to pass correct Locale to MessageSelector + * c6ecd83: SwiftMailerHandler in Monolog bridge now able to react to kernel.terminate event + * 99adcf1: {HttpFoundation] [Session] fixed session compatibility with memcached/redis session storage + * ab9a96b: Fixes for hasParameterOption and getParameterOption methods of ArgvInput + * dbd0855: Added sleep() workaround for windows php rename bug + * fa769a2: [Process] Add more precision to Process::stop timeout + * 3ef517b: [Process] Fix #8739 + * 18896d5a: [Validator] fixed the wrong isAbstract() check against the class (fixed #8589) + * e8e76ec: [TwigBridge] Prevent code extension to display warning + * 1a73b44: added missing support for the new output API in PHP 5.4+ + * e0c7d3d: Fixed bug introduced in #8675 + * 0b965fb: made the filesystem loader compatible with Twig 2.0 + * 322f880: replaced deprecated Twig features + * 2.2.5 (2013-08-07) * c35cc5b: added trusted hosts check diff --git a/CHANGELOG-2.3.md b/CHANGELOG-2.3.md index db170b4f9a..6916ef76ee 100644 --- a/CHANGELOG-2.3.md +++ b/CHANGELOG-2.3.md @@ -7,6 +7,55 @@ in 2.3 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v2.3.0...v2.3.1 +* 2.3.4 (2013-08-27) + + * f936b41: clearToken exception is thrown at wrong place. + * ea480bd: [Form] Fixed Form::all() signature for PHP 5.3.3 + * e1f40f2: [Locale] Fixed: Locale::setDefault() throws no exception when "en" is passed + * d0faf55: [Locale] Fixed: StubLocale::setDefault() throws no exception when "en" is passed + * 566d79c: [Yaml] fixed embedded folded string parsing + * 33b0a17: [Validator] fixed Boolean handling in XML constraint mappings (closes #5603) + * 0951b8d: [Translation] Fixed regression: When only one rule is passed to transChoice(), this rule should be used + * 4563f1b: [Yaml] Fix comment containing a colon on a scalar line being parsed as a hash. + * 7e87eb1: fixed request format when forwarding a request + * 07d14e5: [Form] Removed exception in Button::setData(): setData() is now always called for all elements in the form tree during the initialization of the tree + * ccaaedf: [Form] PropertyPathMapper::mapDataToForms() *always* calls setData() on every child to ensure that all *_DATA events were fired when the initialization phase is over (except for virtual forms) + * 00bc270: [Form] Fixed: submit() reacts to dynamic modifications of the form children + * c4636e1: added a functional test for locale handling in sub-requests + * 05fdb12: Fixed issue #6932 - Inconsistent locale handling in subrequests + * b3c3159: fixed locale of sub-requests when explicitely set by the developer (refs #8821) + * 9bb7a3d: fixed request format of sub-requests when explicitely set by the developer (closes #8787) + * fa35597: Sets _format attribute only if it wasn't set previously by the user. + * f946108: fixed the format of the request used to render an exception + * 51022c3: Fix typo in the check_path validator + * 5f7219e: added a missing use statement (closes #8808) + * 262879d: fix for Process:isSuccessful() + * 0723c10: [Process] Use a consistent way to reset data of the process latest run + * 85a9c9d: [HttpFoundation] Fixed removing a nonexisting namespaced attribute. + * 191d320: [Validation] Fixed IdentityTranslator to pass correct Locale to MessageSelector + * c6ecd83: SwiftMailerHandler in Monolog bridge now able to react to kernel.terminate event + * 99adcf1: {HttpFoundation] [Session] fixed session compatibility with memcached/redis session storage + * ab9a96b: Fixes for hasParameterOption and getParameterOption methods of ArgvInput + * dbd0855: Added sleep() workaround for windows php rename bug + * c342715: [Form] Fixed: Added "validation_groups" option to submit button + * fa01e6b: [Process] Fix for #8754 (Timed-out processes are successful) + * 909fab6: [Process] Fix #8742 : Signal-terminated processes are not successful + * fa769a2: [Process] Add more precision to Process::stop timeout + * 3ef517b: [Process] Fix #8739 + * 572ba68: [TwigBridge] removed superflous ; when rendering form_enctype() (closes #8660) + * 18896d5a: [Validator] fixed the wrong isAbstract() check against the class (fixed #8589) + * e8e76ec: [TwigBridge] Prevent code extension to display warning + * 96aec0f: Fix internal sub-request creation + * 6ed0fdf: [Form] Moved auto_initialize option to the BaseType + * e47657d: Make sure ContextErrorException is loaded during compile time errors + * 98f6969: Fix empty process argument escaping on Windows + * 1a73b44: added missing support for the new output API in PHP 5.4+ + * e0c7d3d: Fixed bug introduced in #8675 + * 0b965fb: made the filesystem loader compatible with Twig 2.0 + * 8fa0453: [Intl] Updated stubs to reflect ICU 51.2 + * 322f880: replaced deprecated Twig features + * 48338fc: Ignore null value in comparison validators + * 2.3.3 (2013-08-07) * c35cc5b: added trusted hosts check diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5633e1af40..b5189836fe 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -23,10 +23,11 @@ Symfony2 is the result of the work of many people who made the code better - Benjamin Eberlei (beberlei) - Jakub Zalas (jakubzalas) - Hugo Hamon (hhamon) - - Eriksen Costa (eriksencosta) - Martin Hasoň (hason) - - Jonathan Wage (jwage) + - Eriksen Costa (eriksencosta) - William Durand (couac) + - Jonathan Wage (jwage) + - Romain Neutron (romain) - Alexandre Salomé (alexandresalome) - ornicar - stealth35 ‏ (stealth35) @@ -36,7 +37,6 @@ Symfony2 is the result of the work of many people who made the code better - Henrik Bjørnskov (henrikbjorn) - Miha Vrhovnik - Bilal Amarni (bamarni) - - Romain Neutron (romain) - Florin Patan (florinpatan) - Konstantin Kudryashov (everzet) - Saša Stamenković (umpirsky) @@ -44,13 +44,13 @@ Symfony2 is the result of the work of many people who made the code better - Eric Clemmons (ericclemmons) - Dariusz Górecki (canni) - Henrik Westphal (snc) + - Grégoire Pineau (lyrixx) - Deni - Marc Weistroff (futurecat) - Jordan Alliot (jalliot) - Arnout Boks (aboks) - Hidenori Goto (hidenorigoto) - Fran Moreno (franmomu) - - Grégoire Pineau (lyrixx) - Andrej Hudec (pulzarraider) - Lee McDermott - Brandon Turner @@ -69,8 +69,8 @@ Symfony2 is the result of the work of many people who made the code better - lenar - Fabien Pennequin (fabienpennequin) - excelwebzone - - woodspire - Douglas Greenshields (shieldo) + - woodspire - Mario A. Alvarez Garcia (nomack84) - Kevin Bond (kbond) - Richard Miller (mr_r_miller) @@ -91,27 +91,27 @@ Symfony2 is the result of the work of many people who made the code better - Larry Garfield (crell) - Arnaud Kleinpeter (nanocom) - Jonathan Ingram (jonathaningram) + - Wouter De Jong (wouterj) - Sebastiaan Stok (sstok) + - Helmer Aaviksoo - Javier Eguiluz (javier.eguiluz) - Matthieu Ouellette-Vachon (maoueh) - Amal Raghav (kertz) - Artur Kotyrba - Pablo Godel (pgodel) - - Helmer Aaviksoo + - Dmitrii Chekaliuk (lazyhammer) - Clément JOBEILI (dator) - Hiromi Hishida (77web) - Julien Brochet (mewt) - Rafael Dohms (rdohms) - - Wouter De Jong (wouterj) + - Dennis Benkert (denderello) - Benjamin Dulau (dbenjamin) - Andreas Hucks (meandmymonkey) - Noel Guilbert (noel) - - Dmitrii Chekaliuk (lazyhammer) - Dorian Villet (gnutix) - Guilherme Blanco (guilhermeblanco) - Martin Schuhfuß (usefulthink) - Thomas Rabaix (rande) - - Dennis Benkert (denderello) - Marcel Beerta (mazen) - Albert Casademont (acasademont) - Matthieu Bontemps (mbontemps) @@ -194,9 +194,12 @@ Symfony2 is the result of the work of many people who made the code better - Peter Kruithof (pkruithof) - Kristen Gilden (kgilden) - hossein zolfi (ocean) + - Philipp Kräutli (pkraeutli) - Christian Flothmann (xabbuh) - Greg Thornton (xdissent) + - Atsuhiro KUBO (iteman) - Lars Strojny + - Loïc Chardonnet (gnusat) - Costin Bereveanu (schniper) - Markus Lanthaler (lanthaler) - Jérôme Vieilledent (lolautruche) @@ -262,6 +265,7 @@ Symfony2 is the result of the work of many people who made the code better - mcben - Maks Slesarenko - Vicent Soria Durá (vicentgodella) + - Andrew Udvare - alexpods - Erik Trapman (eriktrapman) - De Cock Xavier (xdecock) @@ -274,7 +278,6 @@ Symfony2 is the result of the work of many people who made the code better - Luis Cordova (cordoval) - Michaël Perrin (michael.perrin) - sasezaki - - Loïc Chardonnet (gnusat) - Xavier HAUSHERR - Steven Surowiec - Marek Kalnik (marekkalnik) @@ -339,13 +342,14 @@ Symfony2 is the result of the work of many people who made the code better - Johannes Klauss (cloppy) - fzerorubigd - develop - - Atsuhiro KUBO (iteman) + - Tomasz Kowalczyk (thunderer) - Samy Dindane (dinduks) - yclian - Pascal Helfenstein - Matt Daum (daum) - Baldur Rensch - Alex Xandra Albert Sim + - Gabor Toth (tgabi333) - Yuen-Chi Lian - Joshua Nye - avorobiev @@ -363,8 +367,8 @@ Symfony2 is the result of the work of many people who made the code better - Christian Soronellas Vallespí (theunic) - Benjamin Grandfond (benjamin) - Degory Valentine + - hacfi (hifi) - Krzysiek Łabuś - - Andrew Udvare - Xavier Lacot (xavier) - Olivier Maisonneuve - Iwan van Staveren (istaveren) @@ -381,7 +385,6 @@ Symfony2 is the result of the work of many people who made the code better - Reen Lokum - Pierre Vanliefland (pvanliefland) - Martin Parsiegla (spea) - - Philipp Kräutli (pkraeutli) - Stefano Sala (stefano.sala) - frost-nzcr4 - Abhoryo @@ -398,6 +401,7 @@ Symfony2 is the result of the work of many people who made the code better - Gustavo Falco (gfalco) - Matt Robinson (inanimatt) - Bob den Otter (bopp) + - David Marín Carreño (davefx) - Jörn Lang (j.lang) - julien pauli (jpauli) - mwsaz @@ -408,6 +412,7 @@ Symfony2 is the result of the work of many people who made the code better - Derek ROTH - Shin Ohno (ganchiku) - Drew Butler (nodrew) + - Christian Morgan - Alexander Miehe (engerim) - Titouan Galopin (tgalopin) - Don Pinkster @@ -473,6 +478,7 @@ Symfony2 is the result of the work of many people who made the code better - Dan Finnie - Martijn Evers - Benjamin Paap (benjaminpaap) + - Christian - Sergii Smertin (nfx) - Eddie Jaoude - Nerijus Arlauskas @@ -485,7 +491,6 @@ Symfony2 is the result of the work of many people who made the code better - matteo giachino - Daniel Mecke (daniel_mecke) - Alex Demchenko (pilot) - - Tomasz Kowalczyk (thunderer) - Vincent AUBERT (vincent) - Benoit Garret - DerManoMann @@ -501,6 +506,7 @@ Symfony2 is the result of the work of many people who made the code better - Neil Katin - peter - Gustavo Adrian + - Nikita Konstantinov - Brooks Boyd - Roger Webb - Nicolas Fabre (nfabre) @@ -525,15 +531,18 @@ Symfony2 is the result of the work of many people who made the code better - Sebastian Göttschkes (sgoettschkes) - erikaheidi - Pierre Tachoire + - marcj - Ludek Stepan - Balázs Benyó (duplabe) - Marc Morera (mmoreram) + - Daniel Wehner - Saem Ghani - Sebastian Utz - Keri Henare (kerihenare) - Cédric Lahouste (rapotor) - Anthony Ferrara - Janusz Jablonski + - ShiraNai7 - George Giannoulopoulos - Chris Wilkinson (thewilkybarkid) - Ilya Biryukov @@ -541,13 +550,13 @@ Symfony2 is the result of the work of many people who made the code better - m.chwedziak - Lance McNearney - Alberto Pirovano (geezmo) - - Philipp W (hifi) - - Gabor Toth (tgabi333) - Martin Pärtel - Xavier Briand (xavierbriand) - Evan Kaufman - Romain Geissler + - Charles Sarrazin (csarrazi) - Marcus Stöhr (dafish) + - Emmanuel Vella (emmanuel.vella) - Carsten Nielsen (phreaknerd) - Jay Severson - René Kerner @@ -580,6 +589,7 @@ Symfony2 is the result of the work of many people who made the code better - jskvara - Mephistofeles - Hoffmann András + - Olivier - pscheit - Ramon Kleiss (akathos) - Nicolas Badey (nico-b) @@ -602,6 +612,7 @@ Symfony2 is the result of the work of many people who made the code better - Andrey Esaulov (andremaha) - hicham ELGUAROUANI (hiiimoo) - Paul Seiffert (seiffert) + - Vasily (sirian) - Stefan Koopmanschap (skoop) - Ivan Kurnosov - stloyd @@ -672,7 +683,6 @@ Symfony2 is the result of the work of many people who made the code better - cyrillej - Alex Pods - timaschew - - Christian Morgan - Ian Phillips - Haritz - Grummfy @@ -696,7 +706,6 @@ Symfony2 is the result of the work of many people who made the code better - Rafał - Masao Maeda (brtriver) - Dave Marshall (davedevelopment) - - David Marín Carreño (davefx) - Denis Klementjev (dklementjev) - Kévin Dunglas (dunglas) - Vincent Composieux (eko) @@ -737,11 +746,14 @@ Symfony2 is the result of the work of many people who made the code better - Christian Eikermann - Antonio Angelino - Vladimir Sazhin + - Jáchym Toušek - Vyacheslav Slinko - Johannes - Jörg Rühl - patrick-mcdougle + - Daniel Basten - Giacomo Gallico + - Steve Müller - andreabreu98 - Michael Schneider - Jerome Tamarelle @@ -787,6 +799,7 @@ Symfony2 is the result of the work of many people who made the code better - Besnik Br - sualko - Nicolas Roudaire + - Lee Rowlands - Alex Olmos (alexolmos) - Juan Ases García (ases) - Bernd Matzner (bmatzner) @@ -802,10 +815,12 @@ Symfony2 is the result of the work of many people who made the code better - Yohan Giarelli (frequence-web) - Massimiliano Arione (garak) - Vladislav Krupenkin (ideea) + - Jaik Dean (jaikdean) - joris de wit (jdewit) - Jérémy CROMBEZ (jeremy) - Jorge Maiden (jorgemaiden) - Justin Rainbow (jrainbow) + - JuntaTom (juntatom) - Sébastien Armand (khepin) - Krzysztof Menżyk (krymen) - Martin Ledgard (le6o) @@ -819,8 +834,10 @@ Symfony2 is the result of the work of many people who made the code better - Rich Sage (richsage) - Ruud Kamphuis (ruudk) - Sarah Khalil (saro0h) + - scourgen hung (scourgen) - Sebastian Busch (sebu) - Andrea Giuliano (shark) + - Julien Sanchez (sumbobyboys) - Markus Tacker (tacker) - Tyler Stroud (tystr) - Víctor Mateo (victormateo) diff --git a/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/RuntimeInstantiator.php b/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/RuntimeInstantiator.php index 4364019345..789255554a 100644 --- a/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/RuntimeInstantiator.php +++ b/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/RuntimeInstantiator.php @@ -51,10 +51,10 @@ class RuntimeInstantiator implements InstantiatorInterface return $this->factory->createProxy( $definition->getClass(), function (&$wrappedInstance, LazyLoadingInterface $proxy) use ($realInstantiator) { - $proxy->setProxyInitializer(null); - $wrappedInstance = call_user_func($realInstantiator); + $proxy->setProxyInitializer(null); + return true; } ); diff --git a/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/ProxyDumper.php b/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/ProxyDumper.php index 05f3ae1bff..fd64f97e6f 100644 --- a/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/ProxyDumper.php +++ b/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/ProxyDumper.php @@ -75,10 +75,10 @@ class ProxyDumper implements DumperInterface $instantiation new $proxyClass( function (&\$wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface \$proxy) use (\$container) { - \$proxy->setProxyInitializer(null); - \$wrappedInstance = \$container->$methodName(false); + \$proxy->setProxyInitializer(null); + return true; } ); diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service.php b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service.php index e2158b8f9b..aba65e85f1 100644 --- a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service.php +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service.php @@ -50,10 +50,10 @@ class LazyServiceProjectServiceContainer extends Container return $this->services['foo'] = new stdClass_c1d194250ee2e2b7d2eab8b8212368a8( function (& $wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface $proxy) use ($container) { - $proxy->setProxyInitializer(null); - $wrappedInstance = $container->getFooService(false); + $proxy->setProxyInitializer(null); + return true; } ); diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service_structure.txt b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service_structure.txt index 4205e71086..332370c87e 100644 --- a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service_structure.txt +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service_structure.txt @@ -10,10 +10,10 @@ class ProjectServiceContainer extends Container return $this->services['foo'] = new stdClass_%s( function (&$wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface $proxy) use ($container) { - $proxy->setProxyInitializer(null); - $wrappedInstance = $container->getFooService(false); + $proxy->setProxyInitializer(null); + return true; } ); diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/ProxyDumperTest.php b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/ProxyDumperTest.php index 5fefbfac8c..7823c63be6 100644 --- a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/ProxyDumperTest.php +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/ProxyDumperTest.php @@ -74,7 +74,7 @@ class ProxyDumperTest extends \PHPUnit_Framework_TestCase '%wif ($lazyLoad) {%w$container = $this;%wreturn $this->services[\'foo\'] = new ' . 'SymfonyBridgeProxyManagerLazyProxyTestsInstantiatorProxyDumperTest_%s(%wfunction ' . '(&$wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface $proxy) use ($container) {' - . '%w$proxy->setProxyInitializer(null);%w$wrappedInstance = $container->getFooService(false);' + . '%w$wrappedInstance = $container->getFooService(false);%w$proxy->setProxyInitializer(null);' . '%wreturn true;%w}%w);%w}%w', $code ); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/views/translation.html.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/views/translation.html.php index 48ea9fdb08..23631b9943 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/views/translation.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/views/translation.html.php @@ -1,2 +1,3 @@ This template is used for translation message extraction tests -trans('new key') ?> +trans('single-quoted key') ?> +trans("double-quoted key") ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/PhpExtractorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/PhpExtractorTest.php index 4305f0e69c..d639f01806 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/PhpExtractorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/PhpExtractorTest.php @@ -28,8 +28,9 @@ class PhpExtractorTest extends TestCase $extractor->extract(__DIR__.'/../Fixtures/Resources/views/', $catalogue); // Assert - $this->assertCount(1, $catalogue->all('messages'), '->extract() should find 1 translation'); - $this->assertTrue($catalogue->has('new key'), '->extract() should find at leat "new key" message'); - $this->assertEquals('prefixnew key', $catalogue->get('new key'), '->extract() should apply "prefix" as prefix'); + $this->assertCount(2, $catalogue->all('messages'), '->extract() should find 1 translation'); + $this->assertTrue($catalogue->has('single-quoted key'), '->extract() should find the "single-quoted key" message'); + $this->assertTrue($catalogue->has('double-quoted key'), '->extract() should find the "double-quoted key" message'); + $this->assertEquals('prefixsingle-quoted key', $catalogue->get('single-quoted key'), '->extract() should apply "prefix" as prefix'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Translation/PhpExtractor.php b/src/Symfony/Bundle/FrameworkBundle/Translation/PhpExtractor.php index 1eb36052b3..1b8bfe44a7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Translation/PhpExtractor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Translation/PhpExtractor.php @@ -111,7 +111,7 @@ class PhpExtractor implements ExtractorInterface } } - $message = trim($message, '\''); + $message = trim($message, '\'"'); if ($message) { $catalog->set($message, $this->prefix.$message); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index abf1b80efc..ad75ac14a3 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -13,6 +13,7 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\DefinitionDecorator; use Symfony\Component\DependencyInjection\Alias; @@ -413,7 +414,7 @@ class SecurityExtension extends Extension } if (false === $hasListeners) { - throw new \LogicException(sprintf('No authentication listener registered for firewall "%s".', $id)); + throw new InvalidConfigurationException(sprintf('No authentication listener registered for firewall "%s".', $id)); } return array($listeners, $defaultEntryPoint); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php new file mode 100644 index 0000000000..2e55643c70 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php @@ -0,0 +1,200 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection; + +use Symfony\Component\DependencyInjection\Reference; + +use Symfony\Component\DependencyInjection\Parameter; +use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +abstract class CompleteConfigurationTest extends \PHPUnit_Framework_TestCase +{ + abstract protected function loadFromFile(ContainerBuilder $container, $file); + + public function testRolesHierarchy() + { + $container = $this->getContainer('container1'); + $this->assertEquals(array( + 'ROLE_ADMIN' => array('ROLE_USER'), + 'ROLE_SUPER_ADMIN' => array('ROLE_USER', 'ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH'), + 'ROLE_REMOTE' => array('ROLE_USER', 'ROLE_ADMIN'), + ), $container->getParameter('security.role_hierarchy.roles')); + } + + public function testUserProviders() + { + $container = $this->getContainer('container1'); + + $providers = array_values(array_filter($container->getServiceIds(), function ($key) { return 0 === strpos($key, 'security.user.provider.concrete'); })); + + $expectedProviders = array( + 'security.user.provider.concrete.default', + 'security.user.provider.concrete.default_foo', + 'security.user.provider.concrete.digest', + 'security.user.provider.concrete.digest_foo', + 'security.user.provider.concrete.basic', + 'security.user.provider.concrete.basic_foo', + 'security.user.provider.concrete.basic_bar', + 'security.user.provider.concrete.service', + 'security.user.provider.concrete.chain', + ); + + $this->assertEquals(array(), array_diff($expectedProviders, $providers)); + $this->assertEquals(array(), array_diff($providers, $expectedProviders)); + + // chain provider + $this->assertEquals(array(array( + new Reference('security.user.provider.concrete.service'), + new Reference('security.user.provider.concrete.basic'), + )), $container->getDefinition('security.user.provider.concrete.chain')->getArguments()); + } + + public function testFirewalls() + { + $container = $this->getContainer('container1'); + + $arguments = $container->getDefinition('security.firewall.map')->getArguments(); + $listeners = array(); + foreach (array_keys($arguments[1]) as $contextId) { + $contextDef = $container->getDefinition($contextId); + $arguments = $contextDef->getArguments(); + $listeners[] = array_map(function ($ref) { return (string) $ref; }, $arguments['index_0']); + } + + $this->assertEquals(array( + array(), + array( + 'security.channel_listener', + 'security.logout_listener.secure', + 'security.authentication.listener.x509.secure', + 'security.authentication.listener.form.secure', + 'security.authentication.listener.basic.secure', + 'security.authentication.listener.digest.secure', + 'security.authentication.listener.anonymous.secure', + 'security.access_listener', + 'security.authentication.switchuser_listener.secure', + ), + ), $listeners); + } + + public function testAccess() + { + $container = $this->getContainer('container1'); + + $rules = array(); + foreach ($container->getDefinition('security.access_map')->getMethodCalls() as $call) { + if ($call[0] == 'add') { + $rules[] = array((string) $call[1][0], $call[1][1], $call[1][2]); + } + } + + $matcherIds = array(); + foreach ($rules as $rule) { + list($matcherId, $roles, $channel) = $rule; + $requestMatcher = $container->getDefinition($matcherId); + + $this->assertFalse(isset($matcherIds[$matcherId])); + $matcherIds[$matcherId] = true; + + $i = count($matcherIds); + if (1 === $i) { + $this->assertEquals(array('ROLE_USER'), $roles); + $this->assertEquals('https', $channel); + $this->assertEquals( + array('/blog/524', null, array('GET', 'POST')), + $requestMatcher->getArguments() + ); + } elseif (2 === $i) { + $this->assertEquals(array('IS_AUTHENTICATED_ANONYMOUSLY'), $roles); + $this->assertNull($channel); + $this->assertEquals( + array('/blog/.*'), + $requestMatcher->getArguments() + ); + } + } + } + + public function testMerge() + { + $container = $this->getContainer('merge'); + + $this->assertEquals(array( + 'FOO' => array('MOO'), + 'ADMIN' => array('USER'), + ), $container->getParameter('security.role_hierarchy.roles')); + } + + public function testEncoders() + { + $container = $this->getContainer('container1'); + + $this->assertEquals(array(array( + 'JMS\FooBundle\Entity\User1' => array( + 'class' => new Parameter('security.encoder.plain.class'), + 'arguments' => array(false), + ), + 'JMS\FooBundle\Entity\User2' => array( + 'class' => new Parameter('security.encoder.digest.class'), + 'arguments' => array('sha1', false, 5), + ), + 'JMS\FooBundle\Entity\User3' => array( + 'class' => new Parameter('security.encoder.digest.class'), + 'arguments' => array('md5', true, 5000), + ), + 'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'), + 'JMS\FooBundle\Entity\User5' => array( + 'class' => new Parameter('security.encoder.pbkdf2.class'), + 'arguments' => array('sha1', false, 5, 30), + ), + 'JMS\FooBundle\Entity\User6' => array( + 'class' => new Parameter('security.encoder.bcrypt.class'), + 'arguments' => array(15), + ), + )), $container->getDefinition('security.encoder_factory.generic')->getArguments()); + } + + public function testAcl() + { + $container = $this->getContainer('container1'); + + $this->assertTrue($container->hasDefinition('security.acl.dbal.provider')); + $this->assertEquals('security.acl.dbal.provider', (string) $container->getAlias('security.acl.provider')); + } + + public function testCustomAclProvider() + { + $container = $this->getContainer('custom_acl_provider'); + + $this->assertFalse($container->hasDefinition('security.acl.dbal.provider')); + $this->assertEquals('foo', (string) $container->getAlias('security.acl.provider')); + } + + protected function getContainer($file) + { + $container = new ContainerBuilder(); + $security = new SecurityExtension(); + $container->registerExtension($security); + + $bundle = new SecurityBundle(); + $bundle->build($container); // Attach all default factories + $this->loadFromFile($container, $file); + + $container->getCompilerPassConfig()->setOptimizationPasses(array()); + $container->getCompilerPassConfig()->setRemovingPasses(array()); + $container->compile(); + + return $container; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/invalid_check_path.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/invalid_check_path.php deleted file mode 100644 index 62e1acbc6c..0000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/invalid_check_path.php +++ /dev/null @@ -1,16 +0,0 @@ -loadFromExtension('security', array( - 'providers' => array( - 'default' => array('id' => 'foo'), - ), - - 'firewalls' => array( - 'some_firewall' => array( - 'pattern' => '/secured_area/.*', - 'form_login' => array( - 'check_path' => '/some_area/login_check', - ) - ) - ) -)); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/invalid_check_path.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/invalid_check_path.xml deleted file mode 100644 index f507d7037a..0000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/invalid_check_path.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/invalid_check_path.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/invalid_check_path.yml deleted file mode 100644 index dcdef71202..0000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/invalid_check_path.yml +++ /dev/null @@ -1,9 +0,0 @@ -security: - providers: - default: { id: foo } - - firewalls: - some_firewall: - pattern: /secured_area/.* - form_login: - check_path: /some_area/login_check \ No newline at end of file diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php similarity index 96% rename from src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/ConfigurationTest.php rename to src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php index 0925e8b759..047821cfdb 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php @@ -14,7 +14,7 @@ namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection; use Symfony\Bundle\SecurityBundle\DependencyInjection\MainConfiguration; use Symfony\Component\Config\Definition\Processor; -class ConfigurationTest extends \PHPUnit_Framework_TestCase +class MainConfigurationTest extends \PHPUnit_Framework_TestCase { /** * The minimal, required config needed to not have any required validation diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/PhpSecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/PhpCompleteConfigurationTest.php similarity index 90% rename from src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/PhpSecurityExtensionTest.php rename to src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/PhpCompleteConfigurationTest.php index fc54797fc3..131c38d5e5 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/PhpSecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/PhpCompleteConfigurationTest.php @@ -15,7 +15,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\Config\FileLocator; -class PhpSecurityExtensionTest extends SecurityExtensionTest +class PhpCompleteConfigurationTest extends CompleteConfigurationTest { protected function loadFromFile(ContainerBuilder $container, $file) { diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index 7060b05c04..0605a1619e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -11,199 +11,81 @@ namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection; -use Symfony\Component\DependencyInjection\Reference; - -use Symfony\Component\DependencyInjection\Parameter; -use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension; +use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Component\DependencyInjection\ContainerBuilder; -abstract class SecurityExtensionTest extends \PHPUnit_Framework_TestCase +class SecurityExtensionTest extends \PHPUnit_Framework_TestCase { - abstract protected function loadFromFile(ContainerBuilder $container, $file); - - public function testRolesHierarchy() + /** + * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException + * @expectedExceptionMessage The check_path "/some_area/login_check" for login method "form_login" is not matched by the firewall pattern "/secured_area/.*". + */ + public function testInvalidCheckPath() { - $container = $this->getContainer('container1'); - $this->assertEquals(array( - 'ROLE_ADMIN' => array('ROLE_USER'), - 'ROLE_SUPER_ADMIN' => array('ROLE_USER', 'ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH'), - 'ROLE_REMOTE' => array('ROLE_USER', 'ROLE_ADMIN'), - ), $container->getParameter('security.role_hierarchy.roles')); - } + $container = $this->getRawContainer(); - public function testUserProviders() - { - $container = $this->getContainer('container1'); - - $providers = array_values(array_filter($container->getServiceIds(), function ($key) { return 0 === strpos($key, 'security.user.provider.concrete'); })); - - $expectedProviders = array( - 'security.user.provider.concrete.default', - 'security.user.provider.concrete.default_foo', - 'security.user.provider.concrete.digest', - 'security.user.provider.concrete.digest_foo', - 'security.user.provider.concrete.basic', - 'security.user.provider.concrete.basic_foo', - 'security.user.provider.concrete.basic_bar', - 'security.user.provider.concrete.service', - 'security.user.provider.concrete.chain', - ); - - $this->assertEquals(array(), array_diff($expectedProviders, $providers)); - $this->assertEquals(array(), array_diff($providers, $expectedProviders)); - - // chain provider - $this->assertEquals(array(array( - new Reference('security.user.provider.concrete.service'), - new Reference('security.user.provider.concrete.basic'), - )), $container->getDefinition('security.user.provider.concrete.chain')->getArguments()); - } - - public function testFirewalls() - { - $container = $this->getContainer('container1'); - - $arguments = $container->getDefinition('security.firewall.map')->getArguments(); - $listeners = array(); - foreach (array_keys($arguments[1]) as $contextId) { - $contextDef = $container->getDefinition($contextId); - $arguments = $contextDef->getArguments(); - $listeners[] = array_map(function ($ref) { return (string) $ref; }, $arguments['index_0']); - } - - $this->assertEquals(array( - array(), - array( - 'security.channel_listener', - 'security.logout_listener.secure', - 'security.authentication.listener.x509.secure', - 'security.authentication.listener.form.secure', - 'security.authentication.listener.basic.secure', - 'security.authentication.listener.digest.secure', - 'security.authentication.listener.anonymous.secure', - 'security.access_listener', - 'security.authentication.switchuser_listener.secure', + $container->loadFromExtension('security', array( + 'providers' => array( + 'default' => array('id' => 'foo'), ), - ), $listeners); - } - public function testAccess() - { - $container = $this->getContainer('container1'); + 'firewalls' => array( + 'some_firewall' => array( + 'pattern' => '/secured_area/.*', + 'form_login' => array( + 'check_path' => '/some_area/login_check', + ) + ) + ) + )); - $rules = array(); - foreach ($container->getDefinition('security.access_map')->getMethodCalls() as $call) { - if ($call[0] == 'add') { - $rules[] = array((string) $call[1][0], $call[1][1], $call[1][2]); - } - } - - $matcherIds = array(); - foreach ($rules as $rule) { - list($matcherId, $roles, $channel) = $rule; - $requestMatcher = $container->getDefinition($matcherId); - - $this->assertFalse(isset($matcherIds[$matcherId])); - $matcherIds[$matcherId] = true; - - $i = count($matcherIds); - if (1 === $i) { - $this->assertEquals(array('ROLE_USER'), $roles); - $this->assertEquals('https', $channel); - $this->assertEquals( - array('/blog/524', null, array('GET', 'POST')), - $requestMatcher->getArguments() - ); - } elseif (2 === $i) { - $this->assertEquals(array('IS_AUTHENTICATED_ANONYMOUSLY'), $roles); - $this->assertNull($channel); - $this->assertEquals( - array('/blog/.*'), - $requestMatcher->getArguments() - ); - } - } - } - - public function testMerge() - { - $container = $this->getContainer('merge'); - - $this->assertEquals(array( - 'FOO' => array('MOO'), - 'ADMIN' => array('USER'), - ), $container->getParameter('security.role_hierarchy.roles')); - } - - public function testEncoders() - { - $container = $this->getContainer('container1'); - - $this->assertEquals(array(array( - 'JMS\FooBundle\Entity\User1' => array( - 'class' => new Parameter('security.encoder.plain.class'), - 'arguments' => array(false), - ), - 'JMS\FooBundle\Entity\User2' => array( - 'class' => new Parameter('security.encoder.digest.class'), - 'arguments' => array('sha1', false, 5), - ), - 'JMS\FooBundle\Entity\User3' => array( - 'class' => new Parameter('security.encoder.digest.class'), - 'arguments' => array('md5', true, 5000), - ), - 'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'), - 'JMS\FooBundle\Entity\User5' => array( - 'class' => new Parameter('security.encoder.pbkdf2.class'), - 'arguments' => array('sha1', false, 5, 30), - ), - 'JMS\FooBundle\Entity\User6' => array( - 'class' => new Parameter('security.encoder.bcrypt.class'), - 'arguments' => array(15), - ), - )), $container->getDefinition('security.encoder_factory.generic')->getArguments()); - } - - public function testAcl() - { - $container = $this->getContainer('container1'); - - $this->assertTrue($container->hasDefinition('security.acl.dbal.provider')); - $this->assertEquals('security.acl.dbal.provider', (string) $container->getAlias('security.acl.provider')); - } - - public function testCustomAclProvider() - { - $container = $this->getContainer('custom_acl_provider'); - - $this->assertFalse($container->hasDefinition('security.acl.dbal.provider')); - $this->assertEquals('foo', (string) $container->getAlias('security.acl.provider')); + $container->compile(); } /** * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - * @expectedExceptionMessage not matched by the firewall pattern + * @expectedExceptionMessage No authentication listener registered for firewall "some_firewall" */ - public function testInvalidCheckPath() + public function testFirewallWithoutAuthenticationListener() { - $container = $this->getContainer('invalid_check_path'); + $container = $this->getRawContainer(); + + $container->loadFromExtension('security', array( + 'providers' => array( + 'default' => array('id' => 'foo'), + ), + + 'firewalls' => array( + 'some_firewall' => array( + 'pattern' => '/.*', + ) + ) + )); + + $container->compile(); } - protected function getContainer($file) + protected function getRawContainer() { $container = new ContainerBuilder(); $security = new SecurityExtension(); $container->registerExtension($security); $bundle = new SecurityBundle(); - $bundle->build($container); // Attach all default factories - $this->loadFromFile($container, $file); + $bundle->build($container); $container->getCompilerPassConfig()->setOptimizationPasses(array()); $container->getCompilerPassConfig()->setRemovingPasses(array()); - $container->compile(); return $container; } + + protected function getContainer() + { + $containter = $this->getRawContainer(); + $containter->compile(); + + return $containter; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/XmlSecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/XmlCompleteConfigurationTest.php similarity index 90% rename from src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/XmlSecurityExtensionTest.php rename to src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/XmlCompleteConfigurationTest.php index 6ce0489f2e..cf6833a22a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/XmlSecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/XmlCompleteConfigurationTest.php @@ -15,7 +15,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\Config\FileLocator; -class XmlSecurityExtensionTest extends SecurityExtensionTest +class XmlCompleteConfigurationTest extends CompleteConfigurationTest { protected function loadFromFile(ContainerBuilder $container, $file) { diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/YamlSecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/YamlCompleteConfigurationTest.php similarity index 90% rename from src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/YamlSecurityExtensionTest.php rename to src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/YamlCompleteConfigurationTest.php index f5fabe0257..568b8623ea 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/YamlSecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/YamlCompleteConfigurationTest.php @@ -15,7 +15,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\Config\FileLocator; -class YamlSecurityExtensionTest extends SecurityExtensionTest +class YamlCompleteConfigurationTest extends CompleteConfigurationTest { protected function loadFromFile(ContainerBuilder $container, $file) { diff --git a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php index 8a7636c7e8..3be55e64c1 100644 --- a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php +++ b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php @@ -167,7 +167,7 @@ class ViolationMapper implements ViolationMapperInterface // Skip forms inheriting their parent data when iterating the children $childIterator = new \RecursiveIteratorIterator( - new InheritDataAwareIterator($form->all()) + new InheritDataAwareIterator($form) ); // Make the path longer until we find a matching child diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index 556c190d8e..f7fa834b35 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -19,6 +19,7 @@ use Symfony\Component\Form\Exception\LogicException; use Symfony\Component\Form\Exception\OutOfBoundsException; use Symfony\Component\Form\Util\FormUtil; use Symfony\Component\Form\Util\InheritDataAwareIterator; +use Symfony\Component\Form\Util\OrderedHashMap; use Symfony\Component\PropertyAccess\PropertyPath; /** @@ -73,9 +74,9 @@ class Form implements \IteratorAggregate, FormInterface /** * The children of this form - * @var FormInterface[] An array of FormInterface instances + * @var FormInterface[] A map of FormInterface instances */ - private $children = array(); + private $children; /** * The errors of this form @@ -164,6 +165,7 @@ class Form implements \IteratorAggregate, FormInterface } $this->config = $config; + $this->children = new OrderedHashMap(); } public function __clone() @@ -370,9 +372,9 @@ class Form implements \IteratorAggregate, FormInterface // even if the form is compound. if (count($this->children) > 0) { // Update child forms from the data - $childrenIterator = new InheritDataAwareIterator($this->children); - $childrenIterator = new \RecursiveIteratorIterator($childrenIterator); - $this->config->getDataMapper()->mapDataToForms($viewData, $childrenIterator); + $iterator = new InheritDataAwareIterator($this->children); + $iterator = new \RecursiveIteratorIterator($iterator); + $this->config->getDataMapper()->mapDataToForms($viewData, $iterator); } if ($dispatcher->hasListeners(FormEvents::POST_SET_DATA)) { @@ -536,10 +538,7 @@ class Form implements \IteratorAggregate, FormInterface $submittedData = array(); } - for (reset($this->children); false !== current($this->children); next($this->children)) { - $child = current($this->children); - $name = key($this->children); - + foreach ($this->children as $name => $child) { if (array_key_exists($name, $submittedData) || $clearMissing) { $child->submit(isset($submittedData[$name]) ? $submittedData[$name] : null, $clearMissing); unset($submittedData[$name]); @@ -587,9 +586,9 @@ class Form implements \IteratorAggregate, FormInterface // descendants that inherit this form's data. // These descendants will not be submitted normally (see the check // for $this->config->getInheritData() above) - $childrenIterator = new InheritDataAwareIterator($this->children); - $childrenIterator = new \RecursiveIteratorIterator($childrenIterator); - $this->config->getDataMapper()->mapFormsToData($childrenIterator, $viewData); + $iterator = new InheritDataAwareIterator($this->children); + $iterator = new \RecursiveIteratorIterator($iterator); + $this->config->getDataMapper()->mapFormsToData($iterator, $viewData); } $modelData = null; @@ -765,9 +764,9 @@ class Form implements \IteratorAggregate, FormInterface /** * {@inheritdoc} */ - public function &all() + public function all() { - return $this->children; + return iterator_to_array($this->children); } /** @@ -836,10 +835,9 @@ class Form implements \IteratorAggregate, FormInterface $child->setParent($this); if (!$this->lockSetData && $this->defaultDataSet && !$this->config->getInheritData()) { - $children = array($child); - $childrenIterator = new InheritDataAwareIterator($children); - $childrenIterator = new \RecursiveIteratorIterator($childrenIterator); - $this->config->getDataMapper()->mapDataToForms($viewData, $childrenIterator); + $iterator = new InheritDataAwareIterator(new \ArrayIterator(array($child))); + $iterator = new \RecursiveIteratorIterator($iterator); + $this->config->getDataMapper()->mapDataToForms($viewData, $iterator); } return $this; @@ -940,11 +938,11 @@ class Form implements \IteratorAggregate, FormInterface /** * Returns the iterator for this group. * - * @return \ArrayIterator + * @return \Iterator */ public function getIterator() { - return new \ArrayIterator($this->children); + return $this->children; } /** diff --git a/src/Symfony/Component/Form/Tests/Util/InheritDataAwareIteratorTest.php b/src/Symfony/Component/Form/Tests/Util/InheritDataAwareIteratorTest.php deleted file mode 100644 index 19a0940bc0..0000000000 --- a/src/Symfony/Component/Form/Tests/Util/InheritDataAwareIteratorTest.php +++ /dev/null @@ -1,122 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Tests\Util; - -use Symfony\Component\Form\Util\InheritDataAwareIterator; - -/** - * @author Bernhard Schussek - */ -class InheritDataAwareIteratorTest extends \PHPUnit_Framework_TestCase -{ - public function testSupportDynamicModification() - { - $form = $this->getMockForm('form'); - $formToBeAdded = $this->getMockForm('added'); - $formToBeRemoved = $this->getMockForm('removed'); - - $forms = array('form' => $form, 'removed' => $formToBeRemoved); - $iterator = new InheritDataAwareIterator($forms); - - $iterator->rewind(); - $this->assertTrue($iterator->valid()); - $this->assertSame('form', $iterator->key()); - $this->assertSame($form, $iterator->current()); - - // dynamic modification - unset($forms['removed']); - $forms['added'] = $formToBeAdded; - - // continue iteration - $iterator->next(); - $this->assertTrue($iterator->valid()); - $this->assertSame('added', $iterator->key()); - $this->assertSame($formToBeAdded, $iterator->current()); - - // end of array - $iterator->next(); - $this->assertFalse($iterator->valid()); - } - - public function testSupportDynamicModificationInRecursiveCall() - { - $inheritingForm = $this->getMockForm('inheriting'); - $form = $this->getMockForm('form'); - $formToBeAdded = $this->getMockForm('added'); - $formToBeRemoved = $this->getMockForm('removed'); - - $inheritingForm->getConfig()->expects($this->any()) - ->method('getInheritData') - ->will($this->returnValue(true)); - - $inheritingForm->add($form); - $inheritingForm->add($formToBeRemoved); - - $forms = array('inheriting' => $inheritingForm); - $iterator = new InheritDataAwareIterator($forms); - - $iterator->rewind(); - $this->assertTrue($iterator->valid()); - $this->assertSame('inheriting', $iterator->key()); - $this->assertSame($inheritingForm, $iterator->current()); - $this->assertTrue($iterator->hasChildren()); - - // enter nested iterator - $nestedIterator = $iterator->getChildren(); - $this->assertSame('form', $nestedIterator->key()); - $this->assertSame($form, $nestedIterator->current()); - $this->assertFalse($nestedIterator->hasChildren()); - - // dynamic modification - $inheritingForm->remove('removed'); - $inheritingForm->add($formToBeAdded); - - // continue iteration - nested iterator discovers change in the form - $nestedIterator->next(); - $this->assertTrue($nestedIterator->valid()); - $this->assertSame('added', $nestedIterator->key()); - $this->assertSame($formToBeAdded, $nestedIterator->current()); - - // end of array - $nestedIterator->next(); - $this->assertFalse($nestedIterator->valid()); - } - - /** - * @param string $name - * - * @return \PHPUnit_Framework_MockObject_MockObject - */ - protected function getMockForm($name = 'name') - { - $config = $this->getMock('Symfony\Component\Form\FormConfigInterface'); - - $config->expects($this->any()) - ->method('getName') - ->will($this->returnValue($name)); - $config->expects($this->any()) - ->method('getCompound') - ->will($this->returnValue(true)); - $config->expects($this->any()) - ->method('getDataMapper') - ->will($this->returnValue($this->getMock('Symfony\Component\Form\DataMapperInterface'))); - $config->expects($this->any()) - ->method('getEventDispatcher') - ->will($this->returnValue($this->getMock('Symfony\Component\EventDispatcher\EventDispatcher'))); - - return $this->getMockBuilder('Symfony\Component\Form\Form') - ->setConstructorArgs(array($config)) - ->disableArgumentCloning() - ->setMethods(array('getViewData')) - ->getMock(); - } -} diff --git a/src/Symfony/Component/Form/Tests/Util/OrderedHashMapTest.php b/src/Symfony/Component/Form/Tests/Util/OrderedHashMapTest.php new file mode 100644 index 0000000000..60703c5b3b --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Util/OrderedHashMapTest.php @@ -0,0 +1,487 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Util; + +use Symfony\Component\Form\Util\OrderedHashMap; + +/** + * @author Bernhard Schussek + */ +class OrderedHashMapTest extends \PHPUnit_Framework_TestCase +{ + public function testGet() + { + $map = new OrderedHashMap(); + $map['first'] = 1; + + $this->assertSame(1, $map['first']); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testGetNonExistingFails() + { + $map = new OrderedHashMap(); + + $map['first']; + } + + public function testInsertStringKeys() + { + $map = new OrderedHashMap(); + $map['first'] = 1; + $map['second'] = 2; + + $this->assertSame(array('first' => 1, 'second' => 2), iterator_to_array($map)); + } + + public function testInsertNullKeys() + { + $map = new OrderedHashMap(); + $map[] = 1; + $map['foo'] = 2; + $map[] = 3; + + $this->assertSame(array(0 => 1, 'foo' => 2, 1 => 3), iterator_to_array($map)); + } + + /** + * Updates should not change the position of an element, otherwise we could + * turn foreach loops into endless loops if they change the current + * element: + * + * foreach ($map as $index => $value) { + * $map[$index] = $value + 1; + * } + * + * And we don't want this, right? :) + */ + public function testUpdateDoesNotChangeElementPosition() + { + $map = new OrderedHashMap(); + $map['first'] = 1; + $map['second'] = 2; + $map['first'] = 1; + + $this->assertSame(array('first' => 1, 'second' => 2), iterator_to_array($map)); + } + + public function testIsset() + { + $map = new OrderedHashMap(); + $map['first'] = 1; + + $this->assertTrue(isset($map['first'])); + } + + public function testIssetReturnsFalseForNonExisting() + { + $map = new OrderedHashMap(); + + $this->assertFalse(isset($map['first'])); + } + + public function testIssetReturnsFalseForNull() + { + $map = new OrderedHashMap(); + $map['first'] = null; + + $this->assertFalse(isset($map['first'])); + } + + public function testUnset() + { + $map = new OrderedHashMap(); + $map['first'] = 1; + $map['second'] = 2; + + unset($map['first']); + + $this->assertSame(array('second' => 2), iterator_to_array($map)); + } + + public function testUnsetNonExistingSucceeds() + { + $map = new OrderedHashMap(); + + unset($map['first']); + } + + public function testEmptyIteration() + { + $map = new OrderedHashMap(); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationSupportsInsertion() + { + $map = new OrderedHashMap(array('first' => 1)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('first', $it->key()); + $this->assertSame(1, $it->current()); + + // dynamic modification + $map['added'] = 2; + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('first', $it->key()); + $this->assertSame(1, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('added', $it->key()); + $this->assertSame(2, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationSupportsDeletionAndInsertion() + { + $map = new OrderedHashMap(array('first' => 1, 'removed' => 2)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('first', $it->key()); + $this->assertSame(1, $it->current()); + + // dynamic modification + unset($map['removed']); + $map['added'] = 3; + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('first', $it->key()); + $this->assertSame(1, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('added', $it->key()); + $this->assertSame(3, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationSupportsDeletionOfCurrentElement() + { + $map = new OrderedHashMap(array('removed' => 1, 'next' => 2)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('removed', $it->key()); + $this->assertSame(1, $it->current()); + + unset($map['removed']); + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('removed', $it->key()); + $this->assertSame(1, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('next', $it->key()); + $this->assertSame(2, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationIgnoresReplacementOfCurrentElement() + { + $map = new OrderedHashMap(array('replaced' => 1, 'next' => 2)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('replaced', $it->key()); + $this->assertSame(1, $it->current()); + + $map['replaced'] = 3; + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('replaced', $it->key()); + $this->assertSame(1, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('next', $it->key()); + $this->assertSame(2, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationSupportsDeletionOfCurrentAndLastElement() + { + $map = new OrderedHashMap(array('removed' => 1)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('removed', $it->key()); + $this->assertSame(1, $it->current()); + + unset($map['removed']); + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('removed', $it->key()); + $this->assertSame(1, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationIgnoresReplacementOfCurrentAndLastElement() + { + $map = new OrderedHashMap(array('replaced' => 1)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('replaced', $it->key()); + $this->assertSame(1, $it->current()); + + $map['replaced'] = 2; + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('replaced', $it->key()); + $this->assertSame(1, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationSupportsDeletionOfPreviousElement() + { + $map = new OrderedHashMap(array('removed' => 1, 'next' => 2, 'onemore' => 3)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('removed', $it->key()); + $this->assertSame(1, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('next', $it->key()); + $this->assertSame(2, $it->current()); + + unset($map['removed']); + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('next', $it->key()); + $this->assertSame(2, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('onemore', $it->key()); + $this->assertSame(3, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationIgnoresReplacementOfPreviousElement() + { + $map = new OrderedHashMap(array('replaced' => 1, 'next' => 2, 'onemore' => 3)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('replaced', $it->key()); + $this->assertSame(1, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('next', $it->key()); + $this->assertSame(2, $it->current()); + + $map['replaced'] = 4; + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('next', $it->key()); + $this->assertSame(2, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('onemore', $it->key()); + $this->assertSame(3, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testIterationSupportsDeletionOfMultiplePreviousElements() + { + $map = new OrderedHashMap(array('removed' => 1, 'alsoremoved' => 2, 'next' => 3, 'onemore' => 4)); + $it = $map->getIterator(); + + $it->rewind(); + $this->assertTrue($it->valid()); + $this->assertSame('removed', $it->key()); + $this->assertSame(1, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('alsoremoved', $it->key()); + $this->assertSame(2, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('next', $it->key()); + $this->assertSame(3, $it->current()); + + unset($map['removed'], $map['alsoremoved']); + + // iterator is unchanged + $this->assertTrue($it->valid()); + $this->assertSame('next', $it->key()); + $this->assertSame(3, $it->current()); + + // continue iteration + $it->next(); + $this->assertTrue($it->valid()); + $this->assertSame('onemore', $it->key()); + $this->assertSame(4, $it->current()); + + // end of map + $it->next(); + $this->assertFalse($it->valid()); + $this->assertNull($it->key()); + $this->assertNull($it->current()); + } + + public function testParallelIteration() + { + $map = new OrderedHashMap(array('first' => 1, 'second' => 2)); + $it1 = $map->getIterator(); + $it2 = $map->getIterator(); + + $it1->rewind(); + $this->assertTrue($it1->valid()); + $this->assertSame('first', $it1->key()); + $this->assertSame(1, $it1->current()); + + $it2->rewind(); + $this->assertTrue($it2->valid()); + $this->assertSame('first', $it2->key()); + $this->assertSame(1, $it2->current()); + + // 1: continue iteration + $it1->next(); + $this->assertTrue($it1->valid()); + $this->assertSame('second', $it1->key()); + $this->assertSame(2, $it1->current()); + + // 2: remains unchanged + $this->assertTrue($it2->valid()); + $this->assertSame('first', $it2->key()); + $this->assertSame(1, $it2->current()); + + // 1: advance to end of map + $it1->next(); + $this->assertFalse($it1->valid()); + $this->assertNull($it1->key()); + $this->assertNull($it1->current()); + + // 2: remains unchanged + $this->assertTrue($it2->valid()); + $this->assertSame('first', $it2->key()); + $this->assertSame(1, $it2->current()); + + // 2: continue iteration + $it2->next(); + $this->assertTrue($it2->valid()); + $this->assertSame('second', $it2->key()); + $this->assertSame(2, $it2->current()); + + // 1: remains unchanged + $this->assertFalse($it1->valid()); + $this->assertNull($it1->key()); + $this->assertNull($it1->current()); + + // 2: advance to end of map + $it2->next(); + $this->assertFalse($it2->valid()); + $this->assertNull($it2->key()); + $this->assertNull($it2->current()); + + // 1: remains unchanged + $this->assertFalse($it1->valid()); + $this->assertNull($it1->key()); + $this->assertNull($it1->current()); + } + + public function testCount() + { + $map = new OrderedHashMap(); + $map[] = 1; + $map['foo'] = 2; + unset($map[0]); + $map[] = 3; + + $this->assertSame(2, count($map)); + } +} diff --git a/src/Symfony/Component/Form/Util/OrderedHashMap.php b/src/Symfony/Component/Form/Util/OrderedHashMap.php new file mode 100644 index 0000000000..bf5b08cda6 --- /dev/null +++ b/src/Symfony/Component/Form/Util/OrderedHashMap.php @@ -0,0 +1,190 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Util; + +/** + * A hash map which keeps track of deletions and additions. + * + * Like in associative arrays, elements can be mapped to integer or string keys. + * Unlike associative arrays, the map keeps track of the order in which keys + * were added and removed. This order is reflected during iteration. + * + * The map supports concurrent modification during iteration. That means that + * you can insert and remove elements from within a foreach loop and the + * iterator will reflect those changes accordingly. + * + * While elements that are added during the loop are recognized by the iterator, + * changed elements are not. Otherwise the loop could be infinite if each loop + * changes the current element: + * + * $map = new OrderedHashMap(); + * $map[1] = 1; + * $map[2] = 2; + * $map[3] = 3; + * + * foreach ($map as $index => $value) { + * echo "$index: $value\n" + * if (1 === $index) { + * $map[1] = 4; + * $map[] = 5; + * } + * } + * + * print_r(iterator_to_array($map)); + * + * // => 1: 1 + * // 2: 2 + * // 3: 3 + * // 4: 5 + * // Array + * // ( + * // [1] => 4 + * // [2] => 2 + * // [3] => 3 + * // [4] => 5 + * // ) + * + * The map also supports multiple parallel iterators. That means that you can + * nest foreach loops without affecting each other's iteration: + * + * foreach ($map as $index => $value) { + * foreach ($map as $index2 => $value2) { + * // ... + * } + * } + * + * @author Bernhard Schussek + * + * @since 2.2.6 + */ +class OrderedHashMap implements \ArrayAccess, \IteratorAggregate, \Countable +{ + /** + * The elements of the map, indexed by their keys. + * + * @var array + */ + private $elements = array(); + + /** + * The keys of the map in the order in which they were inserted or changed. + * + * @var array + */ + private $orderedKeys = array(); + + /** + * References to the cursors of all open iterators. + * + * @var array + */ + private $managedCursors = array(); + + /** + * Creates a new map. + * + * @param array $elements The elements to insert initially. + * + * @since 2.2.6 + */ + public function __construct(array $elements = array()) + { + $this->elements = $elements; + $this->orderedKeys = array_keys($elements); + } + + /** + * {@inheritdoc} + * + * @since 2.2.6 + */ + public function offsetExists($key) + { + return isset($this->elements[$key]); + } + + /** + * {@inheritdoc} + * + * @since 2.2.6 + */ + public function offsetGet($key) + { + if (!isset($this->elements[$key])) { + throw new \OutOfBoundsException('The offset "' . $key . '" does not exist.'); + } + + return $this->elements[$key]; + } + + /** + * {@inheritdoc} + * + * @since 2.2.6 + */ + public function offsetSet($key, $value) + { + if (null === $key || !isset($this->elements[$key])) { + if (null === $key) { + $key = array() === $this->orderedKeys + // If the array is empty, use 0 as key + ? 0 + // Imitate PHP's behavior of generating a key that equals + // the highest existing integer key + 1 + : max($this->orderedKeys) + 1; + } + + $this->orderedKeys[] = $key; + } + + $this->elements[$key] = $value; + } + + /** + * {@inheritdoc} + * + * @since 2.2.6 + */ + public function offsetUnset($key) + { + if (false !== ($position = array_search($key, $this->orderedKeys))) { + array_splice($this->orderedKeys, $position, 1); + unset($this->elements[$key]); + + foreach ($this->managedCursors as $i => $cursor) { + if ($cursor >= $position) { + --$this->managedCursors[$i]; + } + } + } + } + + /** + * {@inheritdoc} + * + * @since 2.2.6 + */ + public function getIterator() + { + return new OrderedHashMapIterator($this->elements, $this->orderedKeys, $this->managedCursors); + } + + /** + * {@inheritdoc} + * + * @since 2.2.6 + */ + public function count() + { + return count($this->elements); + } +} diff --git a/src/Symfony/Component/Form/Util/OrderedHashMapIterator.php b/src/Symfony/Component/Form/Util/OrderedHashMapIterator.php new file mode 100644 index 0000000000..87cf6a9d39 --- /dev/null +++ b/src/Symfony/Component/Form/Util/OrderedHashMapIterator.php @@ -0,0 +1,163 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Util; + +/** + * Iterator for {@link OrderedHashMap} objects. + * + * This class is internal and should not be used. + * + * @author Bernhard Schussek + * + * @since 2.2.6 + */ +class OrderedHashMapIterator implements \Iterator +{ + /** + * @var array + */ + private $elements; + + /** + * @var array + */ + private $orderedKeys; + + /** + * @var integer + */ + private $cursor; + + /** + * @var integer + */ + private $cursorId; + + /** + * @var array + */ + private $managedCursors; + + /** + * @var string|integer|null + */ + private $key; + + /** + * @var mixed + */ + private $current; + + /** + * Creates a new iterator. + * + * @param array $elements The elements of the map, indexed by their + * keys. + * @param array $orderedKeys The keys of the map in the order in which + * they should be iterated. + * @param array $managedCursors An array from which to reference the + * iterator's cursor as long as it is alive. + * This array is managed by the corresponding + * {@link OrderedHashMap} instance to support + * recognizing the deletion of elements. + * + * @since 2.2.6 + */ + public function __construct(array &$elements, array &$orderedKeys, array &$managedCursors) + { + $this->elements = &$elements; + $this->orderedKeys = &$orderedKeys; + $this->managedCursors = &$managedCursors; + $this->cursorId = count($managedCursors); + + $this->managedCursors[$this->cursorId] = &$this->cursor; + } + + /** + * Removes the iterator's cursors from the managed cursors of the + * corresponding {@link OrderedHashMap} instance. + * + * @since 2.2.6 + */ + public function __destruct() + { + // Use array_splice() instead of isset() to prevent holes in the + // array indices, which would break the initialization of $cursorId + array_splice($this->managedCursors, $this->cursorId, 1); + } + + /** + *{@inheritdoc} + * + * @since 2.2.6 + */ + public function current() + { + return $this->current; + } + + /** + * {@inheritdoc} + * + * @since 2.2.6 + */ + public function next() + { + ++$this->cursor; + + if (isset($this->orderedKeys[$this->cursor])) { + $this->key = $this->orderedKeys[$this->cursor]; + $this->current = $this->elements[$this->key]; + } else { + $this->key = null; + $this->current = null; + } + } + + /** + *{@inheritdoc} + * + * @since 2.2.6 + */ + public function key() + { + return $this->key; + } + + /** + *{@inheritdoc} + * + * @since 2.2.6 + */ + public function valid() + { + return null !== $this->key; + } + + /** + *{@inheritdoc} + * + * @since 2.2.6 + */ + public function rewind() + { + $this->cursor = 0; + + if (isset($this->orderedKeys[0])) { + $this->key = $this->orderedKeys[0]; + $this->current = $this->elements[$this->key]; + } else { + $this->key = null; + $this->current = null; + } + } +} diff --git a/src/Symfony/Component/Form/Util/ReferencingArrayIterator.php b/src/Symfony/Component/Form/Util/ReferencingArrayIterator.php deleted file mode 100644 index 9bb64d79d2..0000000000 --- a/src/Symfony/Component/Form/Util/ReferencingArrayIterator.php +++ /dev/null @@ -1,78 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Util; - -/** - * Iterator that traverses an array. - * - * Contrary to {@link \ArrayIterator}, this iterator recognizes changes in the - * original array during iteration. - * - * @author Bernhard Schussek - */ -class ReferencingArrayIterator implements \Iterator -{ - /** - * @var array - */ - private $array; - - /** - * Creates a new iterator. - * - * @param array $array An array - */ - public function __construct(array &$array) - { - $this->array = &$array; - } - - /** - *{@inheritdoc} - */ - public function current() - { - return current($this->array); - } - - /** - *{@inheritdoc} - */ - public function next() - { - next($this->array); - } - - /** - *{@inheritdoc} - */ - public function key() - { - return key($this->array); - } - - /** - *{@inheritdoc} - */ - public function valid() - { - return null !== key($this->array); - } - - /** - *{@inheritdoc} - */ - public function rewind() - { - reset($this->array); - } -} diff --git a/src/Symfony/Component/Form/Util/VirtualFormAwareIterator.php b/src/Symfony/Component/Form/Util/VirtualFormAwareIterator.php index 708726dedf..581e3540e4 100644 --- a/src/Symfony/Component/Form/Util/VirtualFormAwareIterator.php +++ b/src/Symfony/Component/Form/Util/VirtualFormAwareIterator.php @@ -14,9 +14,6 @@ namespace Symfony\Component\Form\Util; /** * Iterator that traverses an array of forms. * - * Contrary to {@link \ArrayIterator}, this iterator recognizes changes in the - * original array during iteration. - * * You can wrap the iterator into a {@link \RecursiveIterator} in order to * enter any child form that inherits its parent's data and iterate the children * of that form as well. @@ -26,14 +23,14 @@ namespace Symfony\Component\Form\Util; * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use * {@link InheritDataAwareIterator} instead. */ -class VirtualFormAwareIterator extends ReferencingArrayIterator implements \RecursiveIterator +class VirtualFormAwareIterator extends \IteratorIterator implements \RecursiveIterator { /** * {@inheritdoc} */ public function getChildren() { - return new static($this->current()->all()); + return new static($this->current()); } /** diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php index 4d32094b41..2a8753064f 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php @@ -28,7 +28,7 @@ class MemcachedSessionHandlerTest extends \PHPUnit_Framework_TestCase protected function setUp() { if (!class_exists('Memcached')) { - $this->markTestSkipped('Skipped tests Memcache class is not present'); + $this->markTestSkipped('Skipped tests Memcached class is not present'); } $this->memcached = $this->getMock('Memcached'); diff --git a/src/Symfony/Component/Intl/Locale/Locale.php b/src/Symfony/Component/Intl/Locale/Locale.php index cca4e9e4f1..f16d937b02 100644 --- a/src/Symfony/Component/Intl/Locale/Locale.php +++ b/src/Symfony/Component/Intl/Locale/Locale.php @@ -312,6 +312,8 @@ class Locale */ public static function setDefault($locale) { - throw new MethodNotImplementedException(__METHOD__); + if ('en' !== $locale) { + throw new MethodNotImplementedException(__METHOD__); + } } } diff --git a/src/Symfony/Component/Intl/Tests/Locale/LocaleTest.php b/src/Symfony/Component/Intl/Tests/Locale/LocaleTest.php index 7e9571874f..2a5d5d2db7 100644 --- a/src/Symfony/Component/Intl/Tests/Locale/LocaleTest.php +++ b/src/Symfony/Component/Intl/Tests/Locale/LocaleTest.php @@ -150,6 +150,11 @@ class LocaleTest extends AbstractLocaleTest $this->call('setDefault', 'pt_BR'); } + public function testSetDefaultAcceptsEn() + { + $this->call('setDefault', 'en'); + } + protected function call($methodName) { $args = array_slice(func_get_args(), 1); diff --git a/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php b/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php index e248c6de5e..fdc2e8c75c 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php @@ -63,7 +63,7 @@ abstract class AbstractPreAuthenticatedListener implements ListenerInterface try { list($user, $credentials) = $this->getPreAuthenticatedData($request); } catch (BadCredentialsException $exception) { - $this->clearToken(); + $this->clearToken($exception); return; } @@ -91,21 +91,23 @@ abstract class AbstractPreAuthenticatedListener implements ListenerInterface $this->dispatcher->dispatch(SecurityEvents::INTERACTIVE_LOGIN, $loginEvent); } } catch (AuthenticationException $failed) { - $this->clearToken(); + $this->clearToken($failed); } } /** * Clears a PreAuthenticatedToken for this provider (if present) + * + * @param AuthenticationException $exception */ - protected function clearToken() + private function clearToken(AuthenticationException $exception) { $token = $this->securityContext->getToken(); if ($token instanceof PreAuthenticatedToken && $this->providerKey === $token->getProviderKey()) { $this->securityContext->setToken(null); if (null !== $this->logger) { - $this->logger->info(sprintf("Cleared security context due to exception: %s", $failed->getMessage())); + $this->logger->info(sprintf("Cleared security context due to exception: %s", $exception->getMessage())); } } } diff --git a/src/Symfony/Component/Translation/MessageSelector.php b/src/Symfony/Component/Translation/MessageSelector.php index 387c964d0d..8b416e947c 100644 --- a/src/Symfony/Component/Translation/MessageSelector.php +++ b/src/Symfony/Component/Translation/MessageSelector.php @@ -15,6 +15,7 @@ namespace Symfony\Component\Translation; * MessageSelector. * * @author Fabien Potencier + * @author Bernhard Schussek * * @api */ @@ -73,7 +74,14 @@ class MessageSelector } $position = PluralizationRules::get($number, $locale); + if (!isset($standardRules[$position])) { + // when there's exactly one rule given, and that rule is a standard + // rule, use this rule + if (1 === count($parts) && isset($standardRules[0])) { + return $standardRules[0]; + } + throw new \InvalidArgumentException(sprintf('Unable to choose a translation for "%s" with locale "%s". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples").', $message, $locale)); } diff --git a/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php b/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php index ce60995426..cec702a1b9 100644 --- a/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php @@ -79,12 +79,14 @@ class IdentityTranslatorTest extends \PHPUnit_Framework_TestCase public function getTransChoiceTests() { return array( - array('There is no apple', '{0} There is no apple|{1} There is one apple|]1,Inf] There are %count% apples', 0, array('%count%' => 0)), - array('There is one apple', '{0} There is no apple|{1} There is one apple|]1,Inf] There are %count% apples', 1, array('%count%' => 1)), - array('There are 10 apples', '{0} There is no apple|{1} There is one apple|]1,Inf] There are %count% apples', 10, array('%count%' => 10)), + array('There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0, array('%count%' => 0)), + array('There is one apple', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 1, array('%count%' => 1)), + array('There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10, array('%count%' => 10)), array('There are 0 apples', 'There is 1 apple|There are %count% apples', 0, array('%count%' => 0)), array('There is 1 apple', 'There is 1 apple|There are %count% apples', 1, array('%count%' => 1)), array('There are 10 apples', 'There is 1 apple|There are %count% apples', 10, array('%count%' => 10)), + // custom validation messages may be coded with a fixed value + array('There are 2 apples', 'There are 2 apples', 2, array('%count%' => 2)), ); } } diff --git a/src/Symfony/Component/Translation/Tests/MessageSelectorTest.php b/src/Symfony/Component/Translation/Tests/MessageSelectorTest.php index 74956294a1..d5a4f3e3d5 100644 --- a/src/Symfony/Component/Translation/Tests/MessageSelectorTest.php +++ b/src/Symfony/Component/Translation/Tests/MessageSelectorTest.php @@ -25,43 +25,61 @@ class MessageSelectorTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, $selector->choose($id, $number, 'en')); } - /** - * @expectedException InvalidArgumentException - */ - public function testChooseWhenNoEnoughChoices() + public function testReturnMessageIfExactlyOneStandardRuleIsGiven() { $selector = new MessageSelector(); - $selector->choose('foo', 10, 'en'); + $this->assertEquals('There are two apples', $selector->choose('There are two apples', 2, 'en')); + } + + /** + * @dataProvider getNonMatchingMessages + * @expectedException \InvalidArgumentException + */ + public function testThrowExceptionIfMatchingMessageCannotBeFound($id, $number) + { + $selector = new MessageSelector(); + + $selector->choose($id, $number, 'en'); + } + + public function getNonMatchingMessages() + { + return array( + array('{0} There are no apples|{1} There is one apple', 2), + array('{1} There is one apple|]1,Inf] There are %count% apples', 0), + array('{1} There is one apple|]2,Inf] There are %count% apples', 2), + array('{0} There are no apples|There is one apple', 2), + ); } public function getChooseTests() { return array( - array('There is no apples', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', 0), - array('There is no apples', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', 0), - array('There is no apples', '{0}There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', 0), + array('There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0), + array('There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0), + array('There are no apples', '{0}There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0), - array('There is one apple', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', 1), + array('There is one apple', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 1), - array('There is %count% apples', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', 10), - array('There is %count% apples', '{0} There is no apples|{1} There is one apple|]1,Inf]There is %count% apples', 10), - array('There is %count% apples', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', 10), + array('There are %count% apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10), + array('There are %count% apples', '{0} There are no apples|{1} There is one apple|]1,Inf]There are %count% apples', 10), + array('There are %count% apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10), - array('There is %count% apples', 'There is one apple|There is %count% apples', 0), - array('There is one apple', 'There is one apple|There is %count% apples', 1), - array('There is %count% apples', 'There is one apple|There is %count% apples', 10), + array('There are %count% apples', 'There is one apple|There are %count% apples', 0), + array('There is one apple', 'There is one apple|There are %count% apples', 1), + array('There are %count% apples', 'There is one apple|There are %count% apples', 10), - array('There is %count% apples', 'one: There is one apple|more: There is %count% apples', 0), - array('There is one apple', 'one: There is one apple|more: There is %count% apples', 1), - array('There is %count% apples', 'one: There is one apple|more: There is %count% apples', 10), + array('There are %count% apples', 'one: There is one apple|more: There are %count% apples', 0), + array('There is one apple', 'one: There is one apple|more: There are %count% apples', 1), + array('There are %count% apples', 'one: There is one apple|more: There are %count% apples', 10), - array('There is no apples', '{0} There is no apples|one: There is one apple|more: There is %count% apples', 0), - array('There is one apple', '{0} There is no apples|one: There is one apple|more: There is %count% apples', 1), - array('There is %count% apples', '{0} There is no apples|one: There is one apple|more: There is %count% apples', 10), + array('There are no apples', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 0), + array('There is one apple', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 1), + array('There are %count% apples', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 10), - array('', '{0}|{1} There is one apple|]1,Inf] There is %count% apples', 0), - array('', '{0} There is no apples|{1}|]1,Inf] There is %count% apples', 1), + array('', '{0}|{1} There is one apple|]1,Inf] There are %count% apples', 0), + array('', '{0} There are no apples|{1}|]1,Inf] There are %count% apples', 1), // Indexed only tests which are Gettext PoFile* compatible strings. array('There are %count% apples', 'There is one apple|There are %count% apples', 0), @@ -69,12 +87,12 @@ class MessageSelectorTest extends \PHPUnit_Framework_TestCase array('There are %count% apples', 'There is one apple|There are %count% apples', 2), // Tests for float numbers - array('There is almost one apple', '{0} There is no apples|]0,1[ There is almost one apple|{1} There is one apple|[1,Inf] There is more than one apple', 0.7), - array('There is one apple', '{0} There is no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1), - array('There is more than one apple', '{0} There is no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1.7), - array('There is no apples', '{0} There is no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0), - array('There is no apples', '{0} There is no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0.0), - array('There is no apples', '{0.0} There is no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0), + array('There is almost one apple', '{0} There are no apples|]0,1[ There is almost one apple|{1} There is one apple|[1,Inf] There is more than one apple', 0.7), + array('There is one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1), + array('There is more than one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1.7), + array('There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0), + array('There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0.0), + array('There are no apples', '{0.0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0), ); } } diff --git a/src/Symfony/Component/Translation/Tests/TranslatorTest.php b/src/Symfony/Component/Translation/Tests/TranslatorTest.php index fb843d727f..12837516f3 100644 --- a/src/Symfony/Component/Translation/Tests/TranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/TranslatorTest.php @@ -237,9 +237,9 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase public function getTransChoiceTests() { return array( - array('Il y a 0 pomme', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', '[0,1] Il y a %count% pomme|]1,Inf] Il y a %count% pommes', 0, array('%count%' => 0), 'fr', ''), - array('Il y a 1 pomme', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', '[0,1] Il y a %count% pomme|]1,Inf] Il y a %count% pommes', 1, array('%count%' => 1), 'fr', ''), - array('Il y a 10 pommes', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', '[0,1] Il y a %count% pomme|]1,Inf] Il y a %count% pommes', 10, array('%count%' => 10), 'fr', ''), + array('Il y a 0 pomme', '{0} There are no appless|{1} There is one apple|]1,Inf] There is %count% apples', '[0,1] Il y a %count% pomme|]1,Inf] Il y a %count% pommes', 0, array('%count%' => 0), 'fr', ''), + array('Il y a 1 pomme', '{0} There are no appless|{1} There is one apple|]1,Inf] There is %count% apples', '[0,1] Il y a %count% pomme|]1,Inf] Il y a %count% pommes', 1, array('%count%' => 1), 'fr', ''), + array('Il y a 10 pommes', '{0} There are no appless|{1} There is one apple|]1,Inf] There is %count% apples', '[0,1] Il y a %count% pomme|]1,Inf] Il y a %count% pommes', 10, array('%count%' => 10), 'fr', ''), array('Il y a 0 pomme', 'There is one apple|There is %count% apples', 'Il y a %count% pomme|Il y a %count% pommes', 0, array('%count%' => 0), 'fr', ''), array('Il y a 1 pomme', 'There is one apple|There is %count% apples', 'Il y a %count% pomme|Il y a %count% pommes', 1, array('%count%' => 1), 'fr', ''), @@ -249,11 +249,11 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase array('Il y a 1 pomme', 'one: There is one apple|more: There is %count% apples', 'one: Il y a %count% pomme|more: Il y a %count% pommes', 1, array('%count%' => 1), 'fr', ''), array('Il y a 10 pommes', 'one: There is one apple|more: There is %count% apples', 'one: Il y a %count% pomme|more: Il y a %count% pommes', 10, array('%count%' => 10), 'fr', ''), - array('Il n\'y a aucune pomme', '{0} There is no apple|one: There is one apple|more: There is %count% apples', '{0} Il n\'y a aucune pomme|one: Il y a %count% pomme|more: Il y a %count% pommes', 0, array('%count%' => 0), 'fr', ''), - array('Il y a 1 pomme', '{0} There is no apple|one: There is one apple|more: There is %count% apples', '{0} Il n\'y a aucune pomme|one: Il y a %count% pomme|more: Il y a %count% pommes', 1, array('%count%' => 1), 'fr', ''), - array('Il y a 10 pommes', '{0} There is no apple|one: There is one apple|more: There is %count% apples', '{0} Il n\'y a aucune pomme|one: Il y a %count% pomme|more: Il y a %count% pommes', 10, array('%count%' => 10), 'fr', ''), + array('Il n\'y a aucune pomme', '{0} There are no apples|one: There is one apple|more: There is %count% apples', '{0} Il n\'y a aucune pomme|one: Il y a %count% pomme|more: Il y a %count% pommes', 0, array('%count%' => 0), 'fr', ''), + array('Il y a 1 pomme', '{0} There are no apples|one: There is one apple|more: There is %count% apples', '{0} Il n\'y a aucune pomme|one: Il y a %count% pomme|more: Il y a %count% pommes', 1, array('%count%' => 1), 'fr', ''), + array('Il y a 10 pommes', '{0} There are no apples|one: There is one apple|more: There is %count% apples', '{0} Il n\'y a aucune pomme|one: Il y a %count% pomme|more: Il y a %count% pommes', 10, array('%count%' => 10), 'fr', ''), - array('Il y a 0 pomme', new String('{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples'), '[0,1] Il y a %count% pomme|]1,Inf] Il y a %count% pommes', 0, array('%count%' => 0), 'fr', ''), + array('Il y a 0 pomme', new String('{0} There are no appless|{1} There is one apple|]1,Inf] There is %count% apples'), '[0,1] Il y a %count% pomme|]1,Inf] Il y a %count% pommes', 0, array('%count%' => 0), 'fr', ''), ); } @@ -277,16 +277,15 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase $this->assertEquals('10 things', $translator->transChoice('some_message2', 10, array('%count%' => 10))); } - /** - * @expectedException \InvalidArgumentException - */ public function testTransChoiceFallbackWithNoTranslation() { $translator = new Translator('ru', new MessageSelector()); $translator->setFallbackLocales(array('en')); $translator->addLoader('array', new ArrayLoader()); - $this->assertEquals('10 things', $translator->transChoice('some_message2', 10, array('%count%' => 10))); + // consistent behavior with Translator::trans(), which returns the string + // unchanged if it can't be found + $this->assertEquals('some_message2', $translator->transChoice('some_message2', 10, array('%count%' => 10))); } } diff --git a/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php index 9b0958829e..cad247e883 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php @@ -166,7 +166,10 @@ class XmlFileLoader extends FileLoader $value = array(); } } else { - $value = trim($node); + $value = XmlUtils::phpize($node); + if (is_string($value)) { + $value = trim($value); + } } $options[(string) $node['name']] = $value; diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php index 1677b81b1f..7c6e355bd1 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -16,6 +16,7 @@ use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Range; use Symfony\Component\Validator\Constraints\Choice; +use Symfony\Component\Validator\Constraints\Regex; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; @@ -68,6 +69,22 @@ class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, $metadata); } + public function testLoadClassMetadataWithNonStrings() + { + $loader = new XmlFileLoader(__DIR__.'/constraint-mapping-non-strings.xml'); + $metadata = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\Entity'); + + $loader->loadClassMetadata($metadata); + + $expected = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\Entity'); + $expected->addPropertyConstraint('firstName', new Regex(array('pattern' => '/^1/', 'match' => false))); + + $properties = $metadata->getPropertyMetadata('firstName'); + $constraints = $properties[0]->getConstraints(); + + $this->assertFalse($constraints[0]->match); + } + public function testLoadGroupSequenceProvider() { $loader = new XmlFileLoader(__DIR__.'/constraint-mapping.xml'); diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping-non-strings.xml b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping-non-strings.xml new file mode 100644 index 0000000000..dfd5edddc5 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping-non-strings.xml @@ -0,0 +1,19 @@ + + + + + Symfony\Component\Validator\Tests\Fixtures\ + + + + + + + + + + + + diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml index 946200ba11..dfac70d9cf 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml @@ -78,7 +78,7 @@ - +