From 5d96f4dbd73efcc15205472f2871c903a6b37e5a Mon Sep 17 00:00:00 2001 From: Hugo Hamon Date: Wed, 25 Mar 2015 11:05:20 +0100 Subject: [PATCH 01/60] =?UTF-8?q?[FrameworkBundle]=C2=A0added=20a=20protec?= =?UTF-8?q?ted=20shortcut=20getParameter()=20method=20in=20the=20base=20Co?= =?UTF-8?q?ntroller=20class.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FrameworkBundle/Controller/Controller.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php index 0df7823675..b78d475927 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php @@ -334,7 +334,7 @@ class Controller extends ContainerAware } /** - * Gets a service by id. + * Gets a container service by its id. * * @param string $id The service id * @@ -345,10 +345,22 @@ class Controller extends ContainerAware if ('request' === $id) { trigger_error('The "request" service is deprecated and will be removed in 3.0. Add a typehint for Symfony\\Component\\HttpFoundation\\Request to your controller parameters to retrieve the request instead.', E_USER_DEPRECATED); } - + return $this->container->get($id); } + /** + * Gets a container configuration parameter by its name. + * + * @param string $name The parameter name + * + * @return mixed + */ + protected function getParameter($name) + { + return $this->container->getParameter($name); + } + /** * Checks the validity of a CSRF token * From 94d3876c4c0d27e0c845266fb0aaa7c96e927eb3 Mon Sep 17 00:00:00 2001 From: Xavier Leune Date: Tue, 24 Mar 2015 14:45:14 +0100 Subject: [PATCH 02/60] FIX #13919 added TranslationsCacheWarmer to generate catalogues at warmup --- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../CacheWarmer/TranslationsCacheWarmer.php | 49 +++++++++++++++++++ .../Compiler/LoggingTranslatorPass.php | 2 + .../FrameworkExtension.php | 14 +++++- .../Resources/config/translation.xml | 5 ++ .../Compiler/LoggingTranslatorPassTest.php | 2 +- .../FrameworkExtensionTest.php | 4 +- .../Tests/Translation/TranslatorTest.php | 39 +++++++++++---- .../Translation/Translator.php | 46 +++++++++++------ 9 files changed, 133 insertions(+), 29 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TranslationsCacheWarmer.php diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index f4d69e73db..ad8cb2e6e9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ----- * Added possibility to extract translation messages from a file or files besides extracting from a directory + * Added `TranslationsCacheWarmer` to create catalogues at warmup 2.6.0 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TranslationsCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TranslationsCacheWarmer.php new file mode 100644 index 0000000000..223f0216ba --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TranslationsCacheWarmer.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; + +use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; +use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; +use Symfony\Component\Translation\TranslatorInterface; + +/** + * Generates the catalogues for translations. + * + * @author Xavier Leune + */ +class TranslationsCacheWarmer implements CacheWarmerInterface +{ + private $translator; + + public function __construct(TranslatorInterface $translator) + { + $this->translator = $translator; + } + + /** + * {@inheritdoc} + */ + public function warmUp($cacheDir) + { + if ($this->translator instanceof WarmableInterface) { + $this->translator->warmUp($cacheDir); + } + } + + /** + * {@inheritdoc} + */ + public function isOptional() + { + return true; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/LoggingTranslatorPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/LoggingTranslatorPass.php index c99dadf11e..3b9fd96659 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/LoggingTranslatorPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/LoggingTranslatorPass.php @@ -13,6 +13,7 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Reference; /** * @author Abdellatif Ait boudad @@ -38,6 +39,7 @@ class LoggingTranslatorPass implements CompilerPassInterface $refClass = new \ReflectionClass($class); if ($refClass->implementsInterface('Symfony\Component\Translation\TranslatorInterface') && $refClass->implementsInterface('Symfony\Component\Translation\TranslatorBagInterface')) { $container->getDefinition('translator.logging')->setDecoratedService('translator'); + $container->getDefinition('translation.warmer')->replaceArgument(0, new Reference('translator.logging.inner')); } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index c1d6e30b6d..2204c3ddeb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -705,12 +705,22 @@ class FrameworkExtension extends Extension ->in($dirs) ; + $locales = array(); foreach ($finder as $file) { list($domain, $locale, $format) = explode('.', $file->getBasename(), 3); - $files[] = (string) $file; + if (!isset($files[$locale])) { + $files[$locale] = array(); + } + + $files[$locale][] = (string) $file; } - $translator->replaceArgument(4, $files); + $options = array_merge( + $translator->getArgument(3), + array('resource_files' => $files) + ); + + $translator->replaceArgument(3, $options); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml index 1c6eb323b0..0007a360c6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml @@ -152,5 +152,10 @@ + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LoggingTranslatorPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LoggingTranslatorPassTest.php index e71237a7fc..62561612bb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LoggingTranslatorPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LoggingTranslatorPassTest.php @@ -35,7 +35,7 @@ class LoggingTranslatorPassTest extends \PHPUnit_Framework_TestCase ->method('getAlias') ->will($this->returnValue('translation.default')); - $container->expects($this->exactly(2)) + $container->expects($this->exactly(3)) ->method('getDefinition') ->will($this->returnValue($definition)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index ba3d6bdefe..a56bbff97a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -223,9 +223,9 @@ abstract class FrameworkExtensionTest extends TestCase $container = $this->createContainerFromFile('full'); $this->assertTrue($container->hasDefinition('translator.default'), '->registerTranslatorConfiguration() loads translation.xml'); $this->assertEquals('translator.default', (string) $container->getAlias('translator'), '->registerTranslatorConfiguration() redefines translator service from identity to real translator'); - $resources = $container->getDefinition('translator.default')->getArgument(4); + $options = $container->getDefinition('translator.default')->getArgument(3); - $files = array_map(function ($resource) { return realpath($resource); }, $resources); + $files = array_map(function ($resource) { return realpath($resource); }, $options['resource_files']['en']); $ref = new \ReflectionClass('Symfony\Component\Validator\Validation'); $this->assertContains( strtr(dirname($ref->getFileName()).'/Resources/translations/validators.en.xlf', '/', DIRECTORY_SEPARATOR), diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php index e2a3dafab2..ed3f0e0fda 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php @@ -95,7 +95,7 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase public function testTransWithCachingWithInvalidLocale() { $loader = $this->getMock('Symfony\Component\Translation\Loader\LoaderInterface'); - $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir), array(), 'loader', '\Symfony\Bundle\FrameworkBundle\Tests\Translation\TranslatorWithInvalidLocale'); + $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir), 'loader', '\Symfony\Bundle\FrameworkBundle\Tests\Translation\TranslatorWithInvalidLocale'); $translator->setLocale('invalid locale'); $this->setExpectedException('\InvalidArgumentException'); @@ -106,23 +106,25 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase { $loader = new \Symfony\Component\Translation\Loader\YamlFileLoader(); $resourceFiles = array( - __DIR__.'/../Fixtures/Resources/translations/messages.fr.yml', + 'fr' => array( + __DIR__.'/../Fixtures/Resources/translations/messages.fr.yml', + ), ); // prime the cache - $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir), $resourceFiles, 'yml'); + $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir, 'resource_files' => $resourceFiles), 'yml'); $translator->setLocale('fr'); $this->assertEquals('répertoire', $translator->trans('folder')); // do it another time as the cache is primed now - $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir), array(), 'yml'); + $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir), 'yml'); $translator->setLocale('fr'); $this->assertEquals('répertoire', $translator->trans('folder')); // refresh cache when resources is changed in debug mode. - $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir, 'debug' => true), array(), 'yml'); + $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir, 'debug' => true), 'yml'); $translator->setLocale('fr'); $this->assertEquals('folder', $translator->trans('folder')); @@ -132,10 +134,12 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase { $loader = new \Symfony\Component\Translation\Loader\YamlFileLoader(); $resourceFiles = array( - __DIR__.'/../Fixtures/Resources/translations/messages.fr.yml', + 'fr' => array( + __DIR__.'/../Fixtures/Resources/translations/messages.fr.yml', + ), ); - $translator = $this->getTranslator($loader, array(), $resourceFiles, 'yml'); + $translator = $this->getTranslator($loader, array('resource_files' => $resourceFiles), 'yml'); $translator->setLocale('fr'); $this->assertEquals('répertoire', $translator->trans('folder')); @@ -221,14 +225,13 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase return $container; } - public function getTranslator($loader, $options = array(), $resources = array(), $loaderFomat = 'loader', $translatorClass = '\Symfony\Bundle\FrameworkBundle\Translation\Translator') + public function getTranslator($loader, $options = array(), $loaderFomat = 'loader', $translatorClass = '\Symfony\Bundle\FrameworkBundle\Translation\Translator') { $translator = new $translatorClass( $this->getContainer($loader), new MessageSelector(), array($loaderFomat => array($loaderFomat)), - $options, - $resources + $options ); if ('loader' === $loaderFomat) { @@ -243,6 +246,22 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase return $translator; } + + public function testWarmup() + { + $loader = new \Symfony\Component\Translation\Loader\YamlFileLoader(); + $resourceFiles = array( + 'fr' => array( + __DIR__.'/../Fixtures/Resources/translations/messages.fr.yml', + ), + ); + + // prime the cache + $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir, 'resource_files' => $resourceFiles), 'yml'); + $this->assertFalse(file_exists($this->tmpDir.'/catalogue.fr.php')); + $translator->warmup($this->tmpDir); + $this->assertTrue(file_exists($this->tmpDir.'/catalogue.fr.php')); + } } class TranslatorWithInvalidLocale extends Translator diff --git a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php index cd5a9b5cc5..1583c20119 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Translation; +use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\Translation\Translator as BaseTranslator; use Symfony\Component\Translation\MessageSelector; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -20,17 +21,22 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * * @author Fabien Potencier */ -class Translator extends BaseTranslator +class Translator extends BaseTranslator implements WarmableInterface { protected $container; protected $loaderIds; - protected $resourceFiles; protected $options = array( 'cache_dir' => null, 'debug' => false, + 'resource_files' => array(), ); + /** + * @var array + */ + private $resourceLocales; + /** * Constructor. * @@ -38,20 +44,19 @@ class Translator extends BaseTranslator * * * cache_dir: The cache directory (or null to disable caching) * * debug: Whether to enable debugging or not (false by default) + * * resource_files: List of translation resources available grouped by locale. * - * @param ContainerInterface $container A ContainerInterface instance - * @param MessageSelector $selector The message selector for pluralization - * @param array $loaderIds An array of loader Ids - * @param array $options An array of options - * @param array $resourceFiles An array of resource directories + * @param ContainerInterface $container A ContainerInterface instance + * @param MessageSelector $selector The message selector for pluralization + * @param array $loaderIds An array of loader Ids + * @param array $options An array of options * * @throws \InvalidArgumentException */ - public function __construct(ContainerInterface $container, MessageSelector $selector, $loaderIds = array(), array $options = array(), $resourceFiles = array()) + public function __construct(ContainerInterface $container, MessageSelector $selector, $loaderIds = array(), array $options = array()) { $this->container = $container; $this->loaderIds = $loaderIds; - $this->resourceFiles = $resourceFiles; // check option names if ($diff = array_diff(array_keys($options), array_keys($this->options))) { @@ -59,6 +64,7 @@ class Translator extends BaseTranslator } $this->options = array_merge($this->options, $options); + $this->resourceLocales = array_keys($this->options['resource_files']); if (null !== $this->options['cache_dir'] && $this->options['debug']) { $this->loadResources(); } @@ -66,6 +72,16 @@ class Translator extends BaseTranslator parent::__construct(null, $selector, $this->options['cache_dir'], $this->options['debug']); } + /** + * {@inheritdoc} + */ + public function warmUp($cacheDir) + { + foreach ($this->resourceLocales as $locale) { + $this->loadCatalogue($locale); + } + } + /** * {@inheritdoc} */ @@ -87,11 +103,13 @@ class Translator extends BaseTranslator private function loadResources() { - foreach ($this->resourceFiles as $key => $file) { - // filename is domain.locale.format - list($domain, $locale, $format) = explode('.', basename($file), 3); - $this->addResource($format, $file, $locale, $domain); - unset($this->resourceFiles[$key]); + foreach ($this->options['resource_files'] as $locale => $files) { + foreach ($files as $key => $file) { + // filename is domain.locale.format + list($domain, $locale, $format) = explode('.', basename($file), 3); + $this->addResource($format, $file, $locale, $domain); + unset($this->options['resource_files'][$locale][$key]); + } } } } From 03efce1b568379eac21d880e427090e43035f505 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 26 Sep 2014 23:28:34 +0200 Subject: [PATCH 03/60] [Form] Refactored choice lists to support dynamic label, value, index and attribute generation --- .../Form/ChoiceList/EntityChoiceList.php | 7 +- .../Form/ChoiceList/EntityChoiceLoader.php | 267 +++++ .../Form/ChoiceList/ORMQueryBuilderLoader.php | 5 +- .../Doctrine/Form/DoctrineOrmExtension.php | 21 +- .../Doctrine/Form/Type/DoctrineType.php | 201 ++-- .../Tests/Form/Type/EntityTypeTest.php | 77 +- .../Bridge/Twig/Extension/FormExtension.php | 2 +- .../views/Form/form_div_layout.html.twig | 16 +- .../views/Form/choice_widget_options.html.php | 8 +- .../Form/ChoiceList/ArrayChoiceList.php | 136 +++ .../Form/ChoiceList/ArrayKeyChoiceList.php | 173 ++++ .../Form/ChoiceList/ChoiceListInterface.php | 76 ++ .../Factory/CachingFactoryDecorator.php | 189 ++++ .../Factory/ChoiceListFactoryInterface.php | 124 +++ .../Factory/DefaultChoiceListFactory.php | 414 ++++++++ .../Factory/PropertyAccessDecorator.php | 226 ++++ .../Form/ChoiceList/LazyChoiceList.php | 115 +++ .../Loader/ChoiceLoaderInterface.php | 76 ++ .../Form/ChoiceList/View/ChoiceGroupView.php | 55 + .../Form/ChoiceList/View/ChoiceListView.php | 51 + .../Form/ChoiceList/View/ChoiceView.php | 64 ++ .../Extension/Core/ChoiceList/ChoiceList.php | 5 +- .../Core/ChoiceList/ChoiceListInterface.php | 51 +- .../Core/ChoiceList/LazyChoiceList.php | 4 + .../Core/ChoiceList/ObjectChoiceList.php | 4 + .../Core/ChoiceList/SimpleChoiceList.php | 4 + .../Form/Extension/Core/CoreExtension.php | 25 +- .../Core/DataMapper/CheckboxListMapper.php | 93 ++ .../Core/DataMapper/PropertyPathMapper.php | 4 +- .../Core/DataMapper/RadioListMapper.php | 73 ++ .../ChoiceToBooleanArrayTransformer.php | 6 +- .../ChoiceToValueTransformer.php | 4 +- .../ChoicesToBooleanArrayTransformer.php | 6 +- .../ChoicesToValuesTransformer.php | 2 +- .../FixCheckboxInputListener.php | 8 +- .../EventListener/FixRadioInputListener.php | 6 +- .../Form/Extension/Core/Type/ChoiceType.php | 286 ++++-- .../Form/Extension/Core/View/ChoiceView.php | 27 +- .../Form/Tests/AbstractLayoutTest.php | 95 ++ .../ChoiceList/AbstractChoiceListTest.php | 173 ++++ .../Tests/ChoiceList/ArrayChoiceListTest.php | 52 + .../ChoiceList/ArrayKeyChoiceListTest.php | 187 ++++ .../Factory/CachingFactoryDecoratorTest.php | 668 ++++++++++++ .../Factory/DefaultChoiceListFactoryTest.php | 970 ++++++++++++++++++ .../Factory/PropertyAccessDecoratorTest.php | 338 ++++++ .../Tests/ChoiceList/LazyChoiceListTest.php | 141 +++ .../Extension/Core/Type/ChoiceTypeTest.php | 547 +++++++--- .../Extension/Core/Type/CountryTypeTest.php | 12 +- .../Extension/Core/Type/CurrencyTypeTest.php | 8 +- .../Extension/Core/Type/DateTypeTest.php | 22 +- .../Extension/Core/Type/LanguageTypeTest.php | 14 +- .../Extension/Core/Type/LocaleTypeTest.php | 8 +- .../Extension/Core/Type/TimeTypeTest.php | 14 +- .../Extension/Core/Type/TimezoneTypeTest.php | 6 +- 54 files changed, 5716 insertions(+), 450 deletions(-) create mode 100644 src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceLoader.php create mode 100644 src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php create mode 100644 src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php create mode 100644 src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php create mode 100644 src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.php create mode 100644 src/Symfony/Component/Form/ChoiceList/View/ChoiceGroupView.php create mode 100644 src/Symfony/Component/Form/ChoiceList/View/ChoiceListView.php create mode 100644 src/Symfony/Component/Form/ChoiceList/View/ChoiceView.php create mode 100644 src/Symfony/Component/Form/Extension/Core/DataMapper/CheckboxListMapper.php create mode 100644 src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/AbstractChoiceListTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/Factory/PropertyAccessDecoratorTest.php create mode 100644 src/Symfony/Component/Form/Tests/ChoiceList/LazyChoiceListTest.php diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php index 57e57e6e25..3566a33d7d 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php @@ -11,17 +11,20 @@ namespace Symfony\Bridge\Doctrine\Form\ChoiceList; +use Doctrine\Common\Persistence\Mapping\ClassMetadata; +use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\Form\Exception\RuntimeException; use Symfony\Component\Form\Exception\StringCastException; use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; -use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; /** * A choice list presenting a list of Doctrine entities as choices. * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link EntityChoiceLoader} instead. */ class EntityChoiceList extends ObjectChoiceList { diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceLoader.php new file mode 100644 index 0000000000..d1f7971e63 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceLoader.php @@ -0,0 +1,267 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Form\ChoiceList; + +use Doctrine\Common\Persistence\Mapping\ClassMetadata; +use Doctrine\Common\Persistence\ObjectManager; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\Exception\RuntimeException; + +/** + * Loads choices using a Doctrine object manager. + * + * @author Bernhard Schussek + */ +class EntityChoiceLoader implements ChoiceLoaderInterface +{ + /** + * @var ChoiceListFactoryInterface + */ + private $factory; + + /** + * @var ObjectManager + */ + private $manager; + + /** + * @var string + */ + private $class; + + /** + * @var ClassMetadata + */ + private $classMetadata; + + /** + * @var null|EntityLoaderInterface + */ + private $entityLoader; + + /** + * The identifier field, unless the identifier is composite + * + * @var null|string + */ + private $idField = null; + + /** + * Whether to use the identifier for value generation + * + * @var bool + */ + private $compositeId = true; + + /** + * @var ChoiceListInterface + */ + private $choiceList; + + /** + * Returns the value of the identifier field of an entity. + * + * Doctrine must know about this entity, that is, the entity must already + * be persisted or added to the identity map before. Otherwise an + * exception is thrown. + * + * This method assumes that the entity has a single-column identifier and + * will return a single value instead of an array. + * + * @param object $object The entity for which to get the identifier + * + * @return int|string The identifier value + * + * @throws RuntimeException If the entity does not exist in Doctrine's identity map + * + * @internal Should not be accessed by user-land code. This method is public + * only to be usable as callback. + */ + public static function getIdValue(ObjectManager $om, ClassMetadata $classMetadata, $object) + { + if (!$om->contains($object)) { + throw new RuntimeException( + 'Entities passed to the choice field must be managed. Maybe '. + 'persist them in the entity manager?' + ); + } + + $om->initializeObject($object); + + return current($classMetadata->getIdentifierValues($object)); + } + + /** + * Creates a new choice loader. + * + * Optionally, an implementation of {@link EntityLoaderInterface} can be + * passed which optimizes the entity loading for one of the Doctrine + * mapper implementations. + * + * @param ChoiceListFactoryInterface $factory The factory for creating + * the loaded choice list + * @param ObjectManager $manager The object manager + * @param string $class The entity class name + * @param null|EntityLoaderInterface $entityLoader The entity loader + */ + public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, EntityLoaderInterface $entityLoader = null) + { + $this->factory = $factory; + $this->manager = $manager; + $this->classMetadata = $manager->getClassMetadata($class); + $this->class = $this->classMetadata->getName(); + $this->entityLoader = $entityLoader; + + $identifier = $this->classMetadata->getIdentifierFieldNames(); + + if (1 === count($identifier)) { + $this->idField = $identifier[0]; + $this->compositeId = false; + } + } + + /** + * {@inheritdoc} + */ + public function loadChoiceList($value = null) + { + if ($this->choiceList) { + return $this->choiceList; + } + + $entities = $this->entityLoader + ? $this->entityLoader->getEntities() + : $this->manager->getRepository($this->class)->findAll(); + + // If the class has a multi-column identifier, we cannot index the + // entities by their IDs + if ($this->compositeId) { + $this->choiceList = $this->factory->createListFromChoices($entities, $value); + + return $this->choiceList; + } + + // Index the entities by ID + $entitiesById = array(); + + foreach ($entities as $entity) { + $id = self::getIdValue($this->manager, $this->classMetadata, $entity); + $entitiesById[$id] = $entity; + } + + $this->choiceList = $this->factory->createListFromChoices($entitiesById, $value); + + return $this->choiceList; + } + + /** + * Loads the values corresponding to the given entities. + * + * The values are returned with the same keys and in the same order as the + * corresponding entities in the given array. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the entity as first and the array key as the second + * argument. + * + * @param array $entities An array of entities. Non-existing entities + * in this array are ignored + * @param null|callable $value The callable generating the choice values + * + * @return string[] An array of choice values + */ + public function loadValuesForChoices(array $entities, $value = null) + { + // Performance optimization + if (empty($entities)) { + return array(); + } + + // Optimize performance for single-field identifiers. We already + // know that the IDs are used as values + + // Attention: This optimization does not check choices for existence + if (!$this->choiceList && !$this->compositeId) { + $values = array(); + + // Maintain order and indices of the given entities + foreach ($entities as $i => $entity) { + if ($entity instanceof $this->class) { + // Make sure to convert to the right format + $values[$i] = (string) self::getIdValue($this->manager, $this->classMetadata, $entity); + } + } + + return $values; + } + + return $this->loadChoiceList($value)->getValuesForChoices($entities); + } + + /** + * Loads the entities corresponding to the given values. + * + * The entities are returned with the same keys and in the same order as the + * corresponding values in the given array. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the entity as first and the array key as the second + * argument. + * + * @param string[] $values An array of choice values. Non-existing + * values in this array are ignored + * @param null|callable $value The callable generating the choice values + * + * @return array An array of entities + */ + public function loadChoicesForValues(array $values, $value = null) + { + // Performance optimization + // Also prevents the generation of "WHERE id IN ()" queries through the + // entity loader. At least with MySQL and on the development machine + // this was tested on, no exception was thrown for such invalid + // statements, consequently no test fails when this code is removed. + // https://github.com/symfony/symfony/pull/8981#issuecomment-24230557 + if (empty($values)) { + return array(); + } + + // Optimize performance in case we have an entity loader and + // a single-field identifier + if (!$this->choiceList && !$this->compositeId && $this->entityLoader) { + $unorderedEntities = $this->entityLoader->getEntitiesByIds($this->idField, $values); + $entitiesById = array(); + $entities = array(); + + // Maintain order and indices from the given $values + // An alternative approach to the following loop is to add the + // "INDEX BY" clause to the Doctrine query in the loader, + // but I'm not sure whether that's doable in a generic fashion. + foreach ($unorderedEntities as $entity) { + $id = self::getIdValue($this->manager, $this->classMetadata, $entity); + $entitiesById[$id] = $entity; + } + + foreach ($values as $i => $id) { + if (isset($entitiesById[$id])) { + $entities[$i] = $entitiesById[$id]; + } + } + + return $entities; + } + + return $this->loadChoiceList($value)->getChoicesForValues($values); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php index 872e77affe..9cfdd1fe48 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php @@ -17,7 +17,10 @@ use Doctrine\DBAL\Connection; use Doctrine\ORM\EntityManager; /** - * Getting Entities through the ORM QueryBuilder. + * Loads entities using a {@link QueryBuilder} instance. + * + * @author Benjamin Eberlei + * @author Bernhard Schussek */ class ORMQueryBuilderLoader implements EntityLoaderInterface { diff --git a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php index 570cc8f189..ed8e0a7934 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php @@ -14,21 +14,38 @@ namespace Symfony\Bridge\Doctrine\Form; use Doctrine\Common\Persistence\ManagerRegistry; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; class DoctrineOrmExtension extends AbstractExtension { protected $registry; - public function __construct(ManagerRegistry $registry) + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + /** + * @var ChoiceListFactoryInterface + */ + private $choiceListFactory; + + public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null) { $this->registry = $registry; + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + $this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor)); } protected function loadTypes() { return array( - new EntityType($this->registry, PropertyAccess::createPropertyAccessor()), + new EntityType($this->registry, $this->propertyAccessor, $this->choiceListFactory), ); } diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index ccc9bfc485..6c90a2eeb0 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -12,17 +12,20 @@ namespace Symfony\Bridge\Doctrine\Form\Type; use Doctrine\Common\Persistence\ManagerRegistry; -use Symfony\Component\Form\Exception\RuntimeException; use Doctrine\Common\Persistence\ObjectManager; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList; +use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceLoader; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; -use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener; use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; +use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; +use Symfony\Component\Form\Exception\RuntimeException; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; abstract class DoctrineType extends AbstractType @@ -33,19 +36,19 @@ abstract class DoctrineType extends AbstractType protected $registry; /** - * @var array + * @var ChoiceListFactoryInterface */ - private $choiceListCache = array(); + private $choiceListFactory; /** - * @var PropertyAccessorInterface + * @var EntityChoiceLoader[] */ - private $propertyAccessor; + private $choiceLoaders = array(); - public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null) + public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null) { $this->registry = $registry; - $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + $this->choiceListFactory = $choiceListFactory ?: new PropertyAccessDecorator(new DefaultChoiceListFactory(), $propertyAccessor); } public function buildForm(FormBuilderInterface $builder, array $options) @@ -60,86 +63,79 @@ abstract class DoctrineType extends AbstractType public function configureOptions(OptionsResolver $resolver) { - $choiceListCache = &$this->choiceListCache; $registry = $this->registry; - $propertyAccessor = $this->propertyAccessor; + $choiceListFactory = $this->choiceListFactory; + $choiceLoaders = &$this->choiceLoaders; $type = $this; - $loader = function (Options $options) use ($type) { - $queryBuilder = (null !== $options['query_builder']) - ? $options['query_builder'] - : $options['em']->getRepository($options['class'])->createQueryBuilder('e'); - - return $type->getLoader($options['em'], $queryBuilder, $options['class']); - }; - - $choiceList = function (Options $options) use (&$choiceListCache, $propertyAccessor) { - // Support for closures - $propertyHash = is_object($options['property']) - ? spl_object_hash($options['property']) - : $options['property']; - - $choiceHashes = $options['choices']; - - // Support for recursive arrays - if (is_array($choiceHashes)) { - // A second parameter ($key) is passed, so we cannot use - // spl_object_hash() directly (which strictly requires - // one parameter) - array_walk_recursive($choiceHashes, function (&$value) { - $value = spl_object_hash($value); - }); - } elseif ($choiceHashes instanceof \Traversable) { - $hashes = array(); - foreach ($choiceHashes as $value) { - $hashes[] = spl_object_hash($value); - } - - $choiceHashes = $hashes; - } - - $preferredChoiceHashes = $options['preferred_choices']; - - if (is_array($preferredChoiceHashes)) { - array_walk_recursive($preferredChoiceHashes, function (&$value) { - $value = spl_object_hash($value); - }); - } - - // Support for custom loaders (with query builders) - $loaderHash = is_object($options['loader']) - ? spl_object_hash($options['loader']) - : $options['loader']; - - // Support for closures - $groupByHash = is_object($options['group_by']) - ? spl_object_hash($options['group_by']) - : $options['group_by']; - - $hash = hash('sha256', json_encode(array( - spl_object_hash($options['em']), - $options['class'], - $propertyHash, - $loaderHash, - $choiceHashes, - $preferredChoiceHashes, - $groupByHash, - ))); - - if (!isset($choiceListCache[$hash])) { - $choiceListCache[$hash] = new EntityChoiceList( + $choiceLoader = function (Options $options) use ($choiceListFactory, &$choiceLoaders, $type) { + // Unless the choices are given explicitly, load them on demand + if (null === $options['choices']) { + $hash = CachingFactoryDecorator::generateHash(array( $options['em'], $options['class'], - $options['property'], + $options['query_builder'], $options['loader'], - $options['choices'], - $options['preferred_choices'], - $options['group_by'], - $propertyAccessor - ); + )); + + if (!isset($choiceLoaders[$hash])) { + if ($options['loader']) { + $loader = $options['loader']; + } elseif (null !== $options['query_builder']) { + $loader = $type->getLoader($options['em'], $options['query_builder'], $options['class']); + } else { + $queryBuilder = $options['em']->getRepository($options['class'])->createQueryBuilder('e'); + $loader = $type->getLoader($options['em'], $queryBuilder, $options['class']); + } + + $choiceLoaders[$hash] = new EntityChoiceLoader( + $choiceListFactory, + $options['em'], + $options['class'], + $loader + ); + } + + return $choiceLoaders[$hash]; + } + }; + + $choiceLabel = function (Options $options) { + // BC with the "property" option + if ($options['property']) { + return $options['property']; } - return $choiceListCache[$hash]; + // BC: use __toString() by default + return function ($entity) { + return (string) $entity; + }; + }; + + $choiceName = function (Options $options) { + /** @var ObjectManager $om */ + $om = $options['em']; + $classMetadata = $om->getClassMetadata($options['class']); + $ids = $classMetadata->getIdentifierFieldNames(); + $idType = $classMetadata->getTypeOfField(current($ids)); + + // If the entity has a single-column, numeric ID, use that ID as + // field name + if (1 === count($ids) && in_array($idType, array('integer', 'smallint', 'bigint'))) { + return function ($entity, $id) { + return $id; + }; + } + + // Otherwise, an incrementing integer is used as name automatically + }; + + // The choices are always indexed by ID (see "choices" normalizer + // and EntityChoiceLoader), unless the ID is composite. Then they + // are indexed by an incrementing integer. + // Use the ID/incrementing integer as choice value. + $choiceValue = function ($entity, $key) { + return $key; }; $emNormalizer = function (Options $options, $em) use ($registry) { @@ -165,19 +161,50 @@ abstract class DoctrineType extends AbstractType return $em; }; + $choicesNormalizer = function (Options $options, $entities) { + if (null === $entities || 0 === count($entities)) { + return $entities; + } + + // Make sure that the entities are indexed by their ID + /** @var ObjectManager $om */ + $om = $options['em']; + $classMetadata = $om->getClassMetadata($options['class']); + $ids = $classMetadata->getIdentifierFieldNames(); + + // We cannot use composite IDs as indices. In that case, keep the + // given indices + if (count($ids) > 1) { + return $entities; + } + + $entitiesById = array(); + + foreach ($entities as $entity) { + $id = EntityChoiceLoader::getIdValue($om, $classMetadata, $entity); + $entitiesById[$id] = $entity; + } + + return $entitiesById; + }; + $resolver->setDefaults(array( 'em' => null, - 'property' => null, + 'property' => null, // deprecated, use "choice_label" 'query_builder' => null, - 'loader' => $loader, + 'loader' => null, // deprecated, use "choice_loader" 'choices' => null, - 'choice_list' => $choiceList, - 'group_by' => null, + 'choices_as_values' => true, + 'choice_loader' => $choiceLoader, + 'choice_label' => $choiceLabel, + 'choice_name' => $choiceName, + 'choice_value' => $choiceValue, )); $resolver->setRequired(array('class')); $resolver->setNormalizer('em', $emNormalizer); + $resolver->setNormalizer('choices', $choicesNormalizer); $resolver->setAllowedTypes('em', array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager')); $resolver->setAllowedTypes('loader', array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface')); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index 25afbed492..9f1591f308 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -11,21 +11,23 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\Type; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Tools\SchemaTool; +use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; use Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; -use Symfony\Component\Form\FormBuilder; -use Symfony\Component\Form\Forms; -use Symfony\Component\Form\Test\TypeTestCase; +use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity; +use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\GroupableEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity; -use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity; -use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity; -use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; -use Doctrine\ORM\Tools\SchemaTool; -use Doctrine\Common\Collections\ArrayCollection; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\Forms; +use Symfony\Component\Form\Test\TypeTestCase; use Symfony\Component\PropertyAccess\PropertyAccess; class EntityTypeTest extends TypeTestCase @@ -37,12 +39,12 @@ class EntityTypeTest extends TypeTestCase const COMPOSITE_STRING_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity'; /** - * @var \Doctrine\ORM\EntityManager + * @var EntityManager */ private $em; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \PHPUnit_Framework_MockObject_MockObject|ManagerRegistry */ private $emRegistry; @@ -131,7 +133,7 @@ class EntityTypeTest extends TypeTestCase 'property' => 'name', )); - $this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); } public function testSetDataToUninitializedEntityWithNonRequiredToString() @@ -147,7 +149,7 @@ class EntityTypeTest extends TypeTestCase 'required' => false, )); - $this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); } public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder() @@ -166,7 +168,7 @@ class EntityTypeTest extends TypeTestCase 'query_builder' => $qb, )); - $this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); } /** @@ -249,7 +251,7 @@ class EntityTypeTest extends TypeTestCase $field->submit(null); $this->assertNull($field->getData()); - $this->assertSame(array(), $field->getViewData()); + $this->assertNull($field->getViewData()); } public function testSubmitSingleNonExpandedNull() @@ -510,7 +512,7 @@ class EntityTypeTest extends TypeTestCase $field->submit('2'); - $this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); $this->assertTrue($field->isSynchronized()); $this->assertSame($entity2, $field->getData()); $this->assertSame('2', $field->getViewData()); @@ -537,9 +539,14 @@ class EntityTypeTest extends TypeTestCase $this->assertSame('2', $field->getViewData()); $this->assertEquals(array( - 'Group1' => array(1 => new ChoiceView($item1, '1', 'Foo'), 2 => new ChoiceView($item2, '2', 'Bar')), - 'Group2' => array(3 => new ChoiceView($item3, '3', 'Baz')), - '4' => new ChoiceView($item4, '4', 'Boo!'), + 'Group1' => new ChoiceGroupView('Group1', array( + 1 => new ChoiceView('Foo', '1', $item1), + 2 => new ChoiceView('Bar', '2', $item2), + )), + 'Group2' => new ChoiceGroupView('Group2', array( + 3 => new ChoiceView('Baz', '3', $item3), + )), + 4 => new ChoiceView('Boo!', '4', $item4), ), $field->createView()->vars['choices']); } @@ -558,8 +565,8 @@ class EntityTypeTest extends TypeTestCase 'property' => 'name', )); - $this->assertEquals(array(3 => new ChoiceView($entity3, '3', 'Baz'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['preferred_choices']); - $this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo')), $field->createView()->vars['choices']); + $this->assertEquals(array(3 => new ChoiceView('Baz', '3', $entity3), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['preferred_choices']); + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1)), $field->createView()->vars['choices']); } public function testOverrideChoicesWithPreferredChoices() @@ -578,8 +585,8 @@ class EntityTypeTest extends TypeTestCase 'property' => 'name', )); - $this->assertEquals(array(3 => new ChoiceView($entity3, '3', 'Baz')), $field->createView()->vars['preferred_choices']); - $this->assertEquals(array(2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); + $this->assertEquals(array(3 => new ChoiceView('Baz', '3', $entity3)), $field->createView()->vars['preferred_choices']); + $this->assertEquals(array(2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); } public function testDisallowChoicesThatAreNotIncludedChoicesSingleIdentifier() @@ -833,6 +840,30 @@ class EntityTypeTest extends TypeTestCase $this->assertCount(1, $loaders); } + public function testCacheChoiceLists() + { + $entity1 = new SingleIntIdEntity(1, 'Foo'); + + $this->persist(array($entity1)); + + $field1 = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'required' => false, + 'property' => 'name', + )); + + $field2 = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'required' => false, + 'property' => 'name', + )); + + $this->assertInstanceOf('Symfony\Component\Form\ChoiceList\ChoiceListInterface', $field1->getConfig()->getOption('choice_list')); + $this->assertSame($field1->getConfig()->getOption('choice_list'), $field2->getConfig()->getOption('choice_list')); + } + protected function createRegistryMock($name, $em) { $registry = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry'); diff --git a/src/Symfony/Bridge/Twig/Extension/FormExtension.php b/src/Symfony/Bridge/Twig/Extension/FormExtension.php index 9c7339f702..38270f9f5b 100644 --- a/src/Symfony/Bridge/Twig/Extension/FormExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/FormExtension.php @@ -13,7 +13,7 @@ namespace Symfony\Bridge\Twig\Extension; use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser; use Symfony\Bridge\Twig\Form\TwigRendererInterface; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; /** * FormExtension extends Twig with form capabilities. diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index b39dbf1c80..df0e571602 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig @@ -79,7 +79,8 @@ {{- block('choice_widget_options') -}} {%- else -%} - + {% set attr = choice.attr %} + {%- endif -%} {% endfor %} {%- endblock choice_widget_options -%} @@ -355,3 +356,16 @@ {%- endif -%} {%- endfor -%} {%- endblock button_attributes -%} + +{% block attributes -%} + {%- for attrname, attrvalue in attr -%} + {{- " " -}} + {%- if attrname in ['placeholder', 'title'] -%} + {{- attrname }}="{{ attrvalue|trans({}, translation_domain) }}" + {%- elseif attrvalue is sameas(true) -%} + {{- attrname }}="{{ attrname }}" + {%- elseif attrvalue is not sameas(false) -%} + {{- attrname }}="{{ attrvalue }}" + {%- endif -%} + {%- endfor -%} +{%- endblock attributes -%} diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php index a7a9311d51..81402efffb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php @@ -1,11 +1,13 @@ - + $choice): ?> - + block($form, 'choice_widget_options', array('choices' => $choice)) ?> - + diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php new file mode 100644 index 0000000000..0dfc0f9945 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php @@ -0,0 +1,136 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList; + +use Symfony\Component\Form\Exception\InvalidArgumentException; + +/** + * A list of choices with arbitrary data types. + * + * The user of this class is responsible for assigning string values to the + * choices. Both the choices and their values are passed to the constructor. + * Each choice must have a corresponding value (with the same array key) in + * the value array. + * + * @author Bernhard Schussek + */ +class ArrayChoiceList implements ChoiceListInterface +{ + /** + * The choices in the list. + * + * @var array + */ + protected $choices = array(); + + /** + * The values of the choices. + * + * @var string[] + */ + protected $values = array(); + + /** + * Creates a list with the given choices and values. + * + * The given choice array must have the same array keys as the value array. + * + * @param array $choices The selectable choices + * @param string[] $values The string values of the choices + * + * @throws InvalidArgumentException If the keys of the choices don't match + * the keys of the values + */ + public function __construct(array $choices, array $values) + { + $choiceKeys = array_keys($choices); + $valueKeys = array_keys($values); + + if ($choiceKeys !== $valueKeys) { + throw new InvalidArgumentException(sprintf( + 'The keys of the choices and the values must match. The choice '. + 'keys are: "%s". The value keys are: "%s".', + implode('", "', $choiceKeys), + implode('", "', $valueKeys) + )); + } + + $this->choices = $choices; + $this->values = array_map('strval', $values); + } + + /** + * {@inheritdoc} + */ + public function getChoices() + { + return $this->choices; + } + + /** + * {@inheritdoc} + */ + public function getValues() + { + return $this->values; + } + + /** + * {@inheritdoc} + */ + public function getChoicesForValues(array $values) + { + $choices = array(); + + foreach ($values as $i => $givenValue) { + foreach ($this->values as $j => $value) { + if ($value !== (string) $givenValue) { + continue; + } + + $choices[$i] = $this->choices[$j]; + unset($values[$i]); + + if (0 === count($values)) { + break 2; + } + } + } + + return $choices; + } + + /** + * {@inheritdoc} + */ + public function getValuesForChoices(array $choices) + { + $values = array(); + + foreach ($choices as $i => $givenChoice) { + foreach ($this->choices as $j => $choice) { + if ($choice !== $givenChoice) { + continue; + } + + $values[$i] = $this->values[$j]; + unset($choices[$i]); + + if (0 === count($choices)) { + break 2; + } + } + } + + return $values; + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php new file mode 100644 index 0000000000..d79747e048 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList; + +use Symfony\Component\Form\Exception\InvalidArgumentException; + +/** + * A list of choices that can be stored in the keys of a PHP array. + * + * PHP arrays accept only strings and integers as array keys. Other scalar types + * are cast to integers and strings according to the description of + * {@link toArrayKey()}. This implementation applies the same casting rules for + * the choices passed to the constructor and to {@link getValuesForChoices()}. + * + * By default, the choices are cast to strings and used as values. Optionally, + * you may pass custom values. The keys of the value array must match the keys + * of the choice array. + * + * Example: + * + * ```php + * $choices = array('' => 'Don\'t know', 0 => 'No', 1 => 'Yes'); + * $choiceList = new ArrayKeyChoiceList(array_keys($choices)); + * + * $values = $choiceList->getValues() + * // => array('', '0', '1') + * + * $selectedValues = $choiceList->getValuesForChoices(array(true)); + * // => array('1') + * ``` + * + * @author Bernhard Schussek + * + * @deprecated Added for backwards compatibility in Symfony 2.7, to be removed + * in Symfony 3.0. + */ +class ArrayKeyChoiceList implements ChoiceListInterface +{ + /** + * The selectable choices. + * + * @var array + */ + private $choices = array(); + + /** + * The values of the choices. + * + * @var string[] + */ + private $values = array(); + + /** + * Casts the given choice to an array key. + * + * PHP arrays accept only strings and integers as array keys. Integer + * strings such as "42" are automatically cast to integers. The boolean + * values "true" and "false" are cast to the integers 1 and 0. Every other + * scalar value is cast to a string. + * + * @param mixed $choice The choice + * + * @return int|string The choice as PHP array key + * + * @throws InvalidArgumentException If the choice is not scalar + */ + public static function toArrayKey($choice) + { + if (!is_scalar($choice) && null !== $choice) { + throw new InvalidArgumentException(sprintf( + 'The value of type "%s" cannot be converted to a valid array key.', + gettype($choice) + )); + } + + if (is_bool($choice) || (string) (int) $choice === (string) $choice) { + return (int) $choice; + } + + return (string) $choice; + } + + /** + * Creates a list with the given choices and values. + * + * The given choice array must have the same array keys as the value array. + * Each choice must be castable to an integer/string according to the + * casting rules described in {@link toArrayKey()}. + * + * If no values are given, the choices are cast to strings and used as + * values. + * + * @param array $choices The selectable choices + * @param string[] $values Optional. The string values of the choices + * + * @throws InvalidArgumentException If the keys of the choices don't match + * the keys of the values or if any of the + * choices is not scalar + */ + public function __construct(array $choices, array $values = array()) + { + if (empty($values)) { + // The cast to strings happens later + $values = $choices; + } else { + $choiceKeys = array_keys($choices); + $valueKeys = array_keys($values); + + if ($choiceKeys !== $valueKeys) { + throw new InvalidArgumentException( + sprintf( + 'The keys of the choices and the values must match. The choice '. + 'keys are: "%s". The value keys are: "%s".', + implode('", "', $choiceKeys), + implode('", "', $valueKeys) + ) + ); + } + } + + $this->choices = array_map(array(__CLASS__, 'toArrayKey'), $choices); + $this->values = array_map('strval', $values); + } + + /** + * {@inheritdoc} + */ + public function getChoices() + { + return $this->choices; + } + + /** + * {@inheritdoc} + */ + public function getValues() + { + return $this->values; + } + + /** + * {@inheritdoc} + */ + public function getChoicesForValues(array $values) + { + $values = array_map('strval', $values); + + // The values are identical to the choices, so we can just return them + // to improve performance a little bit + return array_map(array(__CLASS__, 'toArrayKey'), array_intersect($values, $this->values)); + } + + /** + * {@inheritdoc} + */ + public function getValuesForChoices(array $choices) + { + $choices = array_map(array(__CLASS__, 'toArrayKey'), $choices); + + // The choices are identical to the values, so we can just return them + // to improve performance a little bit + return array_map('strval', array_intersect($choices, $this->choices)); + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php b/src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php new file mode 100644 index 0000000000..62f3158646 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList; + +/** + * A list of choices that can be selected in a choice field. + * + * A choice list assigns string values to each of a list of choices. These + * string values are displayed in the "value" attributes in HTML and submitted + * back to the server. + * + * The acceptable data types for the choices depend on the implementation. + * Values must always be strings and (within the list) free of duplicates. + * + * The choices returned by {@link getChoices()} and the values returned by + * {@link getValues()} must have the same array indices. + * + * @author Bernhard Schussek + */ +interface ChoiceListInterface +{ + /** + * Returns all selectable choices. + * + * The keys of the choices correspond to the keys of the values returned by + * {@link getValues()}. + * + * @return array The selectable choices + */ + public function getChoices(); + + /** + * Returns the values for the choices. + * + * The keys of the values correspond to the keys of the choices returned by + * {@link getChoices()}. + * + * @return string[] The choice values + */ + public function getValues(); + + /** + * Returns the choices corresponding to the given values. + * + * The choices are returned with the same keys and in the same order as the + * corresponding values in the given array. + * + * @param string[] $values An array of choice values. Non-existing values in + * this array are ignored + * + * @return array An array of choices + */ + public function getChoicesForValues(array $values); + + /** + * Returns the values corresponding to the given choices. + * + * The values are returned with the same keys and in the same order as the + * corresponding choices in the given array. + * + * @param array $choices An array of choices. Non-existing choices in this + * array are ignored + * + * @return string[] An array of choice values + */ + public function getValuesForChoices(array $choices); +} diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php new file mode 100644 index 0000000000..fb43ac8759 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php @@ -0,0 +1,189 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +/** + * Caches the choice lists created by the decorated factory. + * + * @author Bernhard Schussek + */ +class CachingFactoryDecorator implements ChoiceListFactoryInterface +{ + /** + * @var ChoiceListFactoryInterface + */ + private $decoratedFactory; + + /** + * @var ChoiceListInterface[] + */ + private $lists = array(); + + /** + * @var ChoiceListView[] + */ + private $views = array(); + + /** + * Generates a SHA-256 hash for the given value. + * + * Optionally, a namespace string can be passed. Calling this method will + * the same values, but different namespaces, will return different hashes. + * + * @param mixed $value The value to hash + * @param string $namespace Optional. The namespace + * + * @return string The SHA-256 hash + * + * @internal Should not be used by user-land code. + */ + public static function generateHash($value, $namespace = '') + { + if (is_object($value)) { + $value = spl_object_hash($value); + } elseif (is_array($value)) { + array_walk_recursive($value, function (&$v) { + if (is_object($v)) { + $v = spl_object_hash($v); + } + }); + } + + return hash('sha256', $namespace.':'.json_encode($value)); + } + + /** + * Decorates the given factory. + * + * @param ChoiceListFactoryInterface $decoratedFactory The decorated factory + */ + public function __construct(ChoiceListFactoryInterface $decoratedFactory) + { + $this->decoratedFactory = $decoratedFactory; + } + + /** + * Returns the decorated factory. + * + * @return ChoiceListFactoryInterface The decorated factory + */ + public function getDecoratedFactory() + { + return $this->decoratedFactory; + } + + /** + * {@inheritdoc} + */ + public function createListFromChoices($choices, $value = null) + { + if (!is_array($choices) && !$choices instanceof \Traversable) { + throw new UnexpectedTypeException($choices, 'array or \Traversable'); + } + + if ($choices instanceof \Traversable) { + $choices = iterator_to_array($choices); + } + + // The value is not validated on purpose. The decorated factory may + // decide which values to accept and which not. + + // We ignore the choice groups for caching. If two choice lists are + // requested with the same choices, but a different grouping, the same + // choice list is returned. + DefaultChoiceListFactory::flatten($choices, $flatChoices); + + $hash = self::generateHash(array($flatChoices, $value), 'fromChoices'); + + if (!isset($this->lists[$hash])) { + $this->lists[$hash] = $this->decoratedFactory->createListFromChoices($choices, $value); + } + + return $this->lists[$hash]; + } + + /** + * {@inheritdoc} + * + * @deprecated Added for backwards compatibility in Symfony 2.7, to be + * removed in Symfony 3.0. + */ + public function createListFromFlippedChoices($choices, $value = null) + { + if (!is_array($choices) && !$choices instanceof \Traversable) { + throw new UnexpectedTypeException($choices, 'array or \Traversable'); + } + + if ($choices instanceof \Traversable) { + $choices = iterator_to_array($choices); + } + + // The value is not validated on purpose. The decorated factory may + // decide which values to accept and which not. + + // We ignore the choice groups for caching. If two choice lists are + // requested with the same choices, but a different grouping, the same + // choice list is returned. + DefaultChoiceListFactory::flattenFlipped($choices, $flatChoices); + + $hash = self::generateHash(array($flatChoices, $value), 'fromFlippedChoices'); + + if (!isset($this->lists[$hash])) { + $this->lists[$hash] = $this->decoratedFactory->createListFromFlippedChoices($choices, $value); + } + + return $this->lists[$hash]; + } + + /** + * {@inheritdoc} + */ + public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null) + { + $hash = self::generateHash(array($loader, $value), 'fromLoader'); + + if (!isset($this->lists[$hash])) { + $this->lists[$hash] = $this->decoratedFactory->createListFromLoader($loader, $value); + } + + return $this->lists[$hash]; + } + + /** + * {@inheritdoc} + */ + public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null) + { + // The input is not validated on purpose. This way, the decorated + // factory may decide which input to accept and which not. + + $hash = self::generateHash(array($list, $preferredChoices, $label, $index, $groupBy, $attr)); + + if (!isset($this->views[$hash])) { + $this->views[$hash] = $this->decoratedFactory->createView( + $list, + $preferredChoices, + $label, + $index, + $groupBy, + $attr + ); + } + + return $this->views[$hash]; + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php b/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php new file mode 100644 index 0000000000..60239423f3 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; + +/** + * Creates {@link ChoiceListInterface} instances. + * + * @author Bernhard Schussek + */ +interface ChoiceListFactoryInterface +{ + /** + * Creates a choice list for the given choices. + * + * The choices should be passed in the values of the choices array. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as first and the array key as the second + * argument. + * + * @param array|\Traversable $choices The choices + * @param null|callable $value The callable generating the choice + * values + * + * @return ChoiceListInterface The choice list + */ + public function createListFromChoices($choices, $value = null); + + /** + * Creates a choice list for the given choices. + * + * The choices should be passed in the keys of the choices array. Since the + * choices array will be flipped, the entries of the array must be strings + * or integers. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as first and the array key as the second + * argument. + * + * @param array|\Traversable $choices The choices + * @param null|callable $value The callable generating the choice + * values + * + * @return ChoiceListInterface The choice list + * + * @deprecated Added for backwards compatibility in Symfony 2.7, to be + * removed in Symfony 3.0. + */ + public function createListFromFlippedChoices($choices, $value = null); + + /** + * Creates a choice list that is loaded with the given loader. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as first and the array key as the second + * argument. + * + * @param ChoiceLoaderInterface $loader The choice loader + * @param null|callable $value The callable generating the choice + * values + * + * @return ChoiceListInterface The choice list + */ + public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null); + + /** + * Creates a view for the given choice list. + * + * Callables may be passed for all optional arguments. The callables receive + * the choice as first and the array key as the second argument. + * + * * The callable for the label and the name should return the generated + * label/choice name. + * * The callable for the preferred choices should return true or false, + * depending on whether the choice should be preferred or not. + * * The callable for the grouping should return the group name or null if + * a choice should not be grouped. + * * The callable for the attributes should return an array of HTML + * attributes that will be inserted in the tag of the choice. + * + * If no callable is passed, the labels will be generated from the choice + * keys. The view indices will be generated using an incrementing integer + * by default. + * + * The preferred choices can also be passed as array. Each choice that is + * contained in that array will be marked as preferred. + * + * The groups can be passed as a multi-dimensional array. In that case, a + * group will be created for each array entry containing a nested array. + * For all other entries, the choice for the corresponding key will be + * inserted at that position. + * + * The attributes can be passed as multi-dimensional array. The keys should + * match the keys of the choices. The values should be arrays of HTML + * attributes that should be added to the respective choice. + * + * @param ChoiceListInterface $list The choice list + * @param null|array|callable $preferredChoices The preferred choices + * @param null|callable $label The callable generating + * the choice labels + * @param null|callable $index The callable generating + * the view indices + * @param null|array|\Traversable|callable $groupBy The callable generating + * the group names + * @param null|array|callable $attr The callable generating + * the HTML attributes + * + * @return ChoiceListView The choice list view + */ + public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null); +} diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php new file mode 100644 index 0000000000..dd191eea39 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -0,0 +1,414 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\ArrayKeyChoiceList; +use Symfony\Component\Form\ChoiceList\ArrayChoiceList; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\LazyChoiceList; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface as LegacyChoiceListInterface; + +/** + * Default implementation of {@link ChoiceListFactoryInterface}. + * + * @author Bernhard Schussek + */ +class DefaultChoiceListFactory implements ChoiceListFactoryInterface +{ + /** + * Flattens an array into the given output variable. + * + * @param array $array The array to flatten + * @param array $output The flattened output + * + * @internal Should not be used by user-land code + */ + public static function flatten(array $array, &$output) + { + if (null === $output) { + $output = array(); + } + + foreach ($array as $key => $value) { + if (is_array($value)) { + self::flatten($value, $output); + continue; + } + + $output[$key] = $value; + } + } + + /** + * Flattens and flips an array into the given output variable. + * + * During the flattening, the keys and values of the input array are + * flipped. + * + * @param array $array The array to flatten + * @param array $output The flattened output + * + * @internal Should not be used by user-land code + */ + public static function flattenFlipped(array $array, &$output) + { + if (null === $output) { + $output = array(); + } + + foreach ($array as $key => $value) { + if (is_array($value)) { + self::flattenFlipped($value, $output); + continue; + } + + $output[$value] = $key; + } + } + + /** + * {@inheritdoc} + */ + public function createListFromChoices($choices, $value = null) + { + if (!is_array($choices) && !$choices instanceof \Traversable) { + throw new UnexpectedTypeException($choices, 'array or \Traversable'); + } + + if (null !== $value && !is_callable($value)) { + throw new UnexpectedTypeException($value, 'null or callable'); + } + + if ($choices instanceof \Traversable) { + $choices = iterator_to_array($choices); + } + + // If the choices are given as recursive array (i.e. with explicit + // choice groups), flatten the array. The grouping information is needed + // in the view only. + self::flatten($choices, $flatChoices); + + // If no values are given, use incrementing integers as values + // We can not use the choices themselves, because we don't know whether + // choices can be converted to (duplicate-free) strings + if (null === $value) { + $values = $flatChoices; + $i = 0; + + foreach ($values as $key => $value) { + $values[$key] = (string) $i++; + } + + return new ArrayChoiceList($flatChoices, $values); + } + + // Can't use array_map(), because array_map() doesn't pass the key + // Can't use array_walk(), which ignores the return value of the + // closure + $values = array(); + foreach ($flatChoices as $key => $choice) { + $values[$key] = call_user_func($value, $choice, $key); + } + + return new ArrayChoiceList($flatChoices, $values); + } + + /** + * {@inheritdoc} + * + * @deprecated Added for backwards compatibility in Symfony 2.7, to be + * removed in Symfony 3.0. + */ + public function createListFromFlippedChoices($choices, $value = null) + { + if (!is_array($choices) && !$choices instanceof \Traversable) { + throw new UnexpectedTypeException($choices, 'array or \Traversable'); + } + + if (null !== $value && !is_callable($value)) { + throw new UnexpectedTypeException($value, 'null or callable'); + } + + if ($choices instanceof \Traversable) { + $choices = iterator_to_array($choices); + } + + // If the choices are given as recursive array (i.e. with explicit + // choice groups), flatten the array. The grouping information is needed + // in the view only. + self::flattenFlipped($choices, $flatChoices); + + // If no values are given, use the choices as values + // Since the choices are stored in the collection keys, i.e. they are + // strings or integers, we are guaranteed to be able to convert them + // to strings + if (null === $value) { + $values = array_map('strval', $flatChoices); + + return new ArrayKeyChoiceList($flatChoices, $values); + } + + // Can't use array_map(), because array_map() doesn't pass the key + // Can't use array_walk(), which ignores the return value of the + // closure + $values = array(); + foreach ($flatChoices as $key => $choice) { + $values[$key] = call_user_func($value, $choice, $key); + } + + return new ArrayKeyChoiceList($flatChoices, $values); + } + + /** + * {@inheritdoc} + */ + public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null) + { + if (null !== $value && !is_callable($value)) { + throw new UnexpectedTypeException($value, 'null or callable'); + } + + return new LazyChoiceList($loader, $value); + } + + /** + * {@inheritdoc} + */ + public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null) + { + if (null !== $preferredChoices && !is_array($preferredChoices) && !is_callable($preferredChoices)) { + throw new UnexpectedTypeException($preferredChoices, 'null, array or callable'); + } + + if (null !== $label && !is_callable($label)) { + throw new UnexpectedTypeException($label, 'null or callable'); + } + + if (null !== $index && !is_callable($index)) { + throw new UnexpectedTypeException($index, 'null or callable'); + } + + if (null !== $groupBy && !is_array($groupBy) && !$groupBy instanceof \Traversable && !is_callable($groupBy)) { + throw new UnexpectedTypeException($groupBy, 'null, array, \Traversable or callable'); + } + + if (null !== $attr && !is_array($attr) && !is_callable($attr)) { + throw new UnexpectedTypeException($attr, 'null, array or callable'); + } + + // Backwards compatibility + if ($list instanceof LegacyChoiceListInterface && null === $preferredChoices + && null === $label && null === $index && null === $groupBy && null === $attr) { + return new ChoiceListView($list->getRemainingViews(), $list->getPreferredViews()); + } + + $preferredViews = array(); + $otherViews = array(); + $choices = $list->getChoices(); + $values = $list->getValues(); + + if (!is_callable($preferredChoices) && !empty($preferredChoices)) { + $preferredChoices = function ($choice) use ($preferredChoices) { + return false !== array_search($choice, $preferredChoices, true); + }; + } + + // The names are generated from an incrementing integer by default + if (null === $index) { + $i = 0; + $index = function () use (&$i) { + return $i++; + }; + } + + // If $groupBy is not given, no grouping is done + if (empty($groupBy)) { + foreach ($choices as $key => $choice) { + self::addChoiceView( + $choice, + $key, + $label, + $values, + $index, + $attr, + $preferredChoices, + $preferredViews, + $otherViews + ); + } + + return new ChoiceListView($otherViews, $preferredViews); + } + + // If $groupBy is a callable, choices are added to the group with the + // name returned by the callable. If the callable returns null, the + // choice is not added to any group + if (is_callable($groupBy)) { + foreach ($choices as $key => $choice) { + self::addChoiceViewGroupedBy( + $groupBy, + $choice, + $key, + $label, + $values, + $index, + $attr, + $preferredChoices, + $preferredViews, + $otherViews + ); + } + } else { + // If $groupBy is passed as array, use that array as template for + // constructing the groups + self::addChoiceViewsGroupedBy( + $groupBy, + $label, + $choices, + $values, + $index, + $attr, + $preferredChoices, + $preferredViews, + $otherViews + ); + } + + // Remove any empty group views that may have been created by + // addChoiceViewGroupedBy() + foreach ($preferredViews as $key => $view) { + if ($view instanceof ChoiceGroupView && 0 === count($view->choices)) { + unset($preferredViews[$key]); + } + } + + foreach ($otherViews as $key => $view) { + if ($view instanceof ChoiceGroupView && 0 === count($view->choices)) { + unset($otherViews[$key]); + } + } + + return new ChoiceListView($otherViews, $preferredViews); + } + + private static function addChoiceView($choice, $key, $label, $values, $index, $attr, $isPreferred, &$preferredViews, &$otherViews) + { + $view = new ChoiceView( + // If the labels are null, use the choice key by default + null === $label ? (string) $key : (string) call_user_func($label, $choice, $key), + $values[$key], + $choice, + // The attributes may be a callable or a mapping from choice indices + // to nested arrays + is_callable($attr) ? call_user_func($attr, $choice, $key) : (isset($attr[$key]) ? $attr[$key] : array()) + ); + + // $isPreferred may be null if no choices are preferred + if ($isPreferred && call_user_func($isPreferred, $choice, $key)) { + $preferredViews[call_user_func($index, $choice, $key)] = $view; + } else { + $otherViews[call_user_func($index, $choice, $key)] = $view; + } + } + + private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $values, $index, $attr, $isPreferred, &$preferredViews, &$otherViews) + { + foreach ($groupBy as $key => $content) { + // Add the contents of groups to new ChoiceGroupView instances + if (is_array($content)) { + $preferredViewsForGroup = array(); + $otherViewsForGroup = array(); + + self::addChoiceViewsGroupedBy( + $content, + $label, + $choices, + $values, + $index, + $attr, + $isPreferred, + $preferredViewsForGroup, + $otherViewsForGroup + ); + + if (count($preferredViewsForGroup) > 0) { + $preferredViews[$key] = new ChoiceGroupView($key, $preferredViewsForGroup); + } + + if (count($otherViewsForGroup) > 0) { + $otherViews[$key] = new ChoiceGroupView($key, $otherViewsForGroup); + } + + continue; + } + + // Add ungrouped items directly + self::addChoiceView( + $choices[$key], + $key, + $label, + $values, + $index, + $attr, + $isPreferred, + $preferredViews, + $otherViews + ); + } + } + + private static function addChoiceViewGroupedBy($groupBy, $choice, $key, $label, $values, $index, $attr, $isPreferred, &$preferredViews, &$otherViews) + { + $groupLabel = call_user_func($groupBy, $choice, $key); + + if (null === $groupLabel) { + // If the callable returns null, don't group the choice + self::addChoiceView( + $choice, + $key, + $label, + $values, + $index, + $attr, + $isPreferred, + $preferredViews, + $otherViews + ); + + return; + } + + // Initialize the group views if necessary. Unnnecessarily built group + // views will be cleaned up at the end of createView() + if (!isset($preferredViews[$groupLabel])) { + $preferredViews[$groupLabel] = new ChoiceGroupView($groupLabel); + $otherViews[$groupLabel] = new ChoiceGroupView($groupLabel); + } + + self::addChoiceView( + $choice, + $key, + $label, + $values, + $index, + $attr, + $isPreferred, + $preferredViews[$groupLabel]->choices, + $otherViews[$groupLabel]->choices + ); + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php new file mode 100644 index 0000000000..bf91d85eea --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php @@ -0,0 +1,226 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; +use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyAccess\PropertyPath; + +/** + * Adds property path support to a choice list factory. + * + * Pass the decorated factory to the constructor: + * + * ```php + * $decorator = new PropertyAccessDecorator($factory); + * ``` + * + * You can now pass property paths for generating choice values, labels, view + * indices, HTML attributes and for determining the preferred choices and the + * choice groups: + * + * ```php + * // extract values from the $value property + * $list = $createListFromChoices($objects, 'value'); + * ``` + * + * @author Bernhard Schussek + */ +class PropertyAccessDecorator implements ChoiceListFactoryInterface +{ + /** + * @var ChoiceListFactoryInterface + */ + private $decoratedFactory; + + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + /** + * Decorates the given factory. + * + * @param ChoiceListFactoryInterface $decoratedFactory The decorated factory + * @param null|PropertyAccessorInterface $propertyAccessor The used property accessor + */ + public function __construct(ChoiceListFactoryInterface $decoratedFactory, PropertyAccessorInterface $propertyAccessor = null) + { + $this->decoratedFactory = $decoratedFactory; + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + } + + /** + * Returns the decorated factory. + * + * @return ChoiceListFactoryInterface The decorated factory + */ + public function getDecoratedFactory() + { + return $this->decoratedFactory; + } + + /** + * {@inheritdoc} + * + * @param array|\Traversable $choices The choices + * @param null|callable|string|PropertyPath $value The callable or path for + * generating the choice values + * + * @return ChoiceListInterface The choice list + */ + public function createListFromChoices($choices, $value = null) + { + if (is_string($value)) { + $value = new PropertyPath($value); + } + + if ($value instanceof PropertyPath) { + $accessor = $this->propertyAccessor; + $value = function ($choice) use ($accessor, $value) { + return $accessor->getValue($choice, $value); + }; + } + + return $this->decoratedFactory->createListFromChoices($choices, $value); + } + + /** + * {@inheritdoc} + * + * @param array|\Traversable $choices The choices + * @param null|callable|string|PropertyPath $value The callable or path for + * generating the choice values + * + * @return ChoiceListInterface The choice list + * + * @deprecated Added for backwards compatibility in Symfony 2.7, to be + * removed in Symfony 3.0. + */ + public function createListFromFlippedChoices($choices, $value = null) + { + // Property paths are not supported here, because array keys can never + // be objects + return $this->decoratedFactory->createListFromFlippedChoices($choices, $value); + } + + /** + * {@inheritdoc} + * + * @param ChoiceLoaderInterface $loader The choice loader + * @param null|callable|string|PropertyPath $value The callable or path for + * generating the choice values + * + * @return ChoiceListInterface The choice list + */ + public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null) + { + if (is_string($value)) { + $value = new PropertyPath($value); + } + + if ($value instanceof PropertyPath) { + $accessor = $this->propertyAccessor; + $value = function ($choice) use ($accessor, $value) { + return $accessor->getValue($choice, $value); + }; + } + + return $this->decoratedFactory->createListFromLoader($loader, $value); + } + + /** + * {@inheritdoc} + * + * @param ChoiceListInterface $list The choice list + * @param null|array|callable|PropertyPath $preferredChoices The preferred choices + * @param null|callable|PropertyPath $label The callable or path + * generating the choice labels + * @param null|callable|PropertyPath $index The callable or path + * generating the view indices + * @param null|array|\Traversable|callable|PropertyPath $groupBy The callable or path + * generating the group names + * @param null|array|callable|PropertyPath $attr The callable or path + * generating the HTML attributes + * + * @return ChoiceListView The choice list view + */ + public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null) + { + $accessor = $this->propertyAccessor; + + if (is_string($label)) { + $label = new PropertyPath($label); + } + + if ($label instanceof PropertyPath) { + $label = function ($choice) use ($accessor, $label) { + return $accessor->getValue($choice, $label); + }; + } + + if (is_string($preferredChoices)) { + $preferredChoices = new PropertyPath($preferredChoices); + } + + if ($preferredChoices instanceof PropertyPath) { + $preferredChoices = function ($choice) use ($accessor, $preferredChoices) { + try { + return $accessor->getValue($choice, $preferredChoices); + } catch (UnexpectedTypeException $e) { + // Assume not preferred if not readable + return false; + } + }; + } + + if (is_string($index)) { + $index = new PropertyPath($index); + } + + if ($index instanceof PropertyPath) { + $index = function ($choice) use ($accessor, $index) { + return $accessor->getValue($choice, $index); + }; + } + + if (is_string($groupBy)) { + $groupBy = new PropertyPath($groupBy); + } + + if ($groupBy instanceof PropertyPath) { + $groupBy = function ($choice) use ($accessor, $groupBy) { + try { + return $accessor->getValue($choice, $groupBy); + } catch (UnexpectedTypeException $e) { + // Don't group if path is not readable + } + }; + } + + if (is_string($attr)) { + $attr = new PropertyPath($attr); + } + + if ($attr instanceof PropertyPath) { + $attr = function ($choice) use ($accessor, $attr) { + return $accessor->getValue($choice, $attr); + }; + } + + return $this->decoratedFactory->createView($list, $preferredChoices, $label, $index, $groupBy, $attr); + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php new file mode 100644 index 0000000000..91e6bfe408 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList; + +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; + +/** + * A choice list that loads its choices lazily. + * + * The choices are fetched using a {@link ChoiceLoaderInterface} instance. + * If only {@link getChoicesForValues()} or {@link getValuesForChoices()} is + * called, the choice list is only loaded partially for improved performance. + * + * Once {@link getChoices()} or {@link getValues()} is called, the list is + * loaded fully. + * + * @author Bernhard Schussek + */ +class LazyChoiceList implements ChoiceListInterface +{ + /** + * The choice loader. + * + * @var ChoiceLoaderInterface + */ + private $loader; + + /** + * The callable creating string values for each choice. + * + * If null, choices are simply cast to strings. + * + * @var null|callable + */ + private $value; + + /** + * @var ChoiceListInterface + */ + private $loadedList; + + /** + * Creates a lazily-loaded list using the given loader. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as first and the array key as the second + * argument. + * + * @param ChoiceLoaderInterface $loader The choice loader + * @param null|callable $value The callable generating the choice + * values + */ + public function __construct(ChoiceLoaderInterface $loader, $value = null) + { + $this->loader = $loader; + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function getChoices() + { + if (!$this->loadedList) { + $this->loadedList = $this->loader->loadChoiceList($this->value); + } + + return $this->loadedList->getChoices(); + } + + /** + * {@inheritdoc} + */ + public function getValues() + { + if (!$this->loadedList) { + $this->loadedList = $this->loader->loadChoiceList($this->value); + } + + return $this->loadedList->getValues(); + } + + /** + * {@inheritdoc} + */ + public function getChoicesForValues(array $values) + { + if (!$this->loadedList) { + return $this->loader->loadChoicesForValues($values, $this->value); + } + + return $this->loadedList->getChoicesForValues($values); + } + + /** + * {@inheritdoc} + */ + public function getValuesForChoices(array $choices) + { + if (!$this->loadedList) { + return $this->loader->loadValuesForChoices($choices, $this->value); + } + + return $this->loadedList->getValuesForChoices($choices); + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.php b/src/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.php new file mode 100644 index 0000000000..9171fe3f16 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Loader/ChoiceLoaderInterface.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\Loader; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; + +/** + * Loads a choice list. + * + * The methods {@link loadChoicesForValues()} and {@link loadValuesForChoices()} + * can be used to load the list only partially in cases where a fully-loaded + * list is not necessary. + * + * @author Bernhard Schussek + */ +interface ChoiceLoaderInterface +{ + /** + * Loads a list of choices. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as first and the array key as the second + * argument. + * + * @param null|callable $value The callable which generates the values + * from choices + * + * @return ChoiceListInterface The loaded choice list + */ + public function loadChoiceList($value = null); + + /** + * Loads the choices corresponding to the given values. + * + * The choices are returned with the same keys and in the same order as the + * corresponding values in the given array. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as first and the array key as the second + * argument. + * + * @param string[] $values An array of choice values. Non-existing + * values in this array are ignored + * @param null|callable $value The callable generating the choice values + * + * @return array An array of choices + */ + public function loadChoicesForValues(array $values, $value = null); + + /** + * Loads the values corresponding to the given choices. + * + * The values are returned with the same keys and in the same order as the + * corresponding choices in the given array. + * + * Optionally, a callable can be passed for generating the choice values. + * The callable receives the choice as first and the array key as the second + * argument. + * + * @param array $choices An array of choices. Non-existing choices in + * this array are ignored + * @param null|callable $value The callable generating the choice values + * + * @return string[] An array of choice values + */ + public function loadValuesForChoices(array $choices, $value = null); +} diff --git a/src/Symfony/Component/Form/ChoiceList/View/ChoiceGroupView.php b/src/Symfony/Component/Form/ChoiceList/View/ChoiceGroupView.php new file mode 100644 index 0000000000..8e59620369 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/View/ChoiceGroupView.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\View; + +/** + * Represents a group of choices in templates. + * + * @author Bernhard Schussek + */ +class ChoiceGroupView implements \IteratorAggregate +{ + /** + * The label of the group + * + * @var string + */ + public $label; + + /** + * The choice views in the group + * + * @var ChoiceGroupView[]|ChoiceView[] + */ + public $choices; + + /** + * Creates a new choice group view. + * + * @param string $label The label of the group. + * @param ChoiceGroupView[]|ChoiceView[] $choices The choice views in the + * group. + */ + public function __construct($label, array $choices = array()) + { + $this->label = $label; + $this->choices = $choices; + } + + /** + * {@inheritdoc} + */ + public function getIterator() + { + return new \ArrayIterator($this->choices); + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/View/ChoiceListView.php b/src/Symfony/Component/Form/ChoiceList/View/ChoiceListView.php new file mode 100644 index 0000000000..9641f4b1d9 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/View/ChoiceListView.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\View; + +/** + * Represents a choice list in templates. + * + * A choice list contains choices and optionally preferred choices which are + * displayed in the very beginning of the list. Both choices and preferred + * choices may be grouped in {@link ChoiceGroupView} instances. + * + * @author Bernhard Schussek + */ +class ChoiceListView +{ + /** + * The choices. + * + * @var ChoiceGroupView[]|ChoiceView[] + */ + public $choices; + + /** + * The preferred choices. + * + * @var ChoiceGroupView[]|ChoiceView[] + */ + public $preferredChoices; + + /** + * Creates a new choice list view. + * + * @param ChoiceGroupView[]|ChoiceView[] $choices The choice views. + * @param ChoiceGroupView[]|ChoiceView[] $preferredChoices The preferred + * choice views. + */ + public function __construct(array $choices = array(), array $preferredChoices = array()) + { + $this->choices = $choices; + $this->preferredChoices = $preferredChoices; + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/View/ChoiceView.php b/src/Symfony/Component/Form/ChoiceList/View/ChoiceView.php new file mode 100644 index 0000000000..ded2a55b30 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/View/ChoiceView.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\ChoiceList\View; + +/** + * Represents a choice in templates. + * + * @author Bernhard Schussek + */ +class ChoiceView +{ + /** + * The label displayed to humans. + * + * @var string + */ + public $label; + + /** + * The view representation of the choice. + * + * @var string + */ + public $value; + + /** + * The original choice value. + * + * @var mixed + */ + public $data; + + /** + * Additional attributes for the HTML tag. + * + * @var array + */ + public $attr; + + /** + * Creates a new choice view. + * + * @param string $label The label displayed to humans + * @param string $value The view representation of the choice + * @param mixed $data The original choice + * @param array $attr Additional attributes for the HTML tag + */ + public function __construct($label, $value, $data, array $attr = array()) + { + $this->label = $label; + $this->value = $value; + $this->data = $data; + $this->attr = $attr; + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php index 9d2a1c42a4..2f7b287b63 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php @@ -29,10 +29,13 @@ use Symfony\Component\Form\Extension\Core\View\ChoiceView; * * $choices = array(true, false); * $labels = array('Agree', 'Disagree'); - * $choiceList = new ChoiceList($choices, $labels); + * $choiceList = new ArrayChoiceList($choices, $labels); * * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayChoiceList} instead. */ class ChoiceList implements ChoiceListInterface { diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php index 8f09179a2a..22354e09d8 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php @@ -25,23 +25,13 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList; * in the HTML "value" attribute. * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\ChoiceListInterface} + * instead. */ -interface ChoiceListInterface +interface ChoiceListInterface extends \Symfony\Component\Form\ChoiceList\ChoiceListInterface { - /** - * Returns the list of choices. - * - * @return array The choices with their indices as keys - */ - public function getChoices(); - - /** - * Returns the values for the choices. - * - * @return array The values with the corresponding choice indices as keys - */ - public function getValues(); - /** * Returns the choice views of the preferred choices as nested array with * the choice groups as top-level keys. @@ -92,37 +82,6 @@ interface ChoiceListInterface */ public function getRemainingViews(); - /** - * Returns the choices corresponding to the given values. - * - * The choices can have any data type. - * - * The choices must be returned with the same keys and in the same order - * as the corresponding values in the given array. - * - * @param array $values An array of choice values. Not existing values in - * this array are ignored - * - * @return array An array of choices with ascending, 0-based numeric keys - */ - public function getChoicesForValues(array $values); - - /** - * Returns the values corresponding to the given choices. - * - * The values must be strings. - * - * The values must be returned with the same keys and in the same order - * as the corresponding choices in the given array. - * - * @param array $choices An array of choices. Not existing choices in this - * array are ignored - * - * @return array An array of choice values with ascending, 0-based numeric - * keys - */ - public function getValuesForChoices(array $choices); - /** * Returns the indices corresponding to the given choices. * diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php index ee136f7978..24232bc1d6 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php @@ -21,6 +21,10 @@ use Symfony\Component\Form\Exception\InvalidArgumentException; * which should return a ChoiceListInterface instance. * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\LazyChoiceList} + * instead. */ abstract class LazyChoiceList implements ChoiceListInterface { diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php index a20d194555..606de43af3 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php @@ -32,6 +32,10 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; * * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayChoiceList} + * instead. */ class ObjectChoiceList extends ChoiceList { diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php index 8d4ddd1242..50a3eb5f4a 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php @@ -28,6 +28,10 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList; * * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayKeyChoiceList} + * instead. */ class SimpleChoiceList extends ChoiceList { diff --git a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php index a0153a57eb..231994258e 100644 --- a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php +++ b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php @@ -12,7 +12,12 @@ namespace Symfony\Component\Form\Extension\Core; use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** * Represents the main form extension, which loads the core functionality. @@ -21,13 +26,29 @@ use Symfony\Component\PropertyAccess\PropertyAccess; */ class CoreExtension extends AbstractExtension { + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + /** + * @var ChoiceListFactoryInterface + */ + private $choiceListFactory; + + public function __construct(PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null) + { + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + $this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor)); + } + protected function loadTypes() { return array( - new Type\FormType(PropertyAccess::createPropertyAccessor()), + new Type\FormType($this->propertyAccessor), new Type\BirthdayType(), new Type\CheckboxType(), - new Type\ChoiceType(), + new Type\ChoiceType($this->choiceListFactory), new Type\CollectionType(), new Type\CountryType(), new Type\DateType(), diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/CheckboxListMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/CheckboxListMapper.php new file mode 100644 index 0000000000..d87196475f --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/CheckboxListMapper.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataMapper; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\DataMapperInterface; +use Symfony\Component\Form\Exception; +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * Maps choices to/from checkbox forms. + * + * A {@link ChoiceListInterface} implementation is used to find the + * corresponding string values for the choices. Each checkbox form whose "value" + * option corresponds to any of the selected values is marked as selected. + * + * @author Bernhard Schussek + */ +class CheckboxListMapper implements DataMapperInterface +{ + /** + * @var ChoiceListInterface + */ + private $choiceList; + + public function __construct(ChoiceListInterface $choiceList) + { + $this->choiceList = $choiceList; + } + + /** + * {@inheritdoc} + */ + public function mapDataToForms($choices, $checkboxes) + { + if (null === $choices) { + $choices = array(); + } + + if (!is_array($choices)) { + throw new TransformationFailedException('Expected an array.'); + } + + try { + $valueMap = array_flip($this->choiceList->getValuesForChoices($choices)); + } catch (\Exception $e) { + throw new TransformationFailedException( + 'Can not read the choices from the choice list.', + $e->getCode(), + $e + ); + } + + foreach ($checkboxes as $checkbox) { + $value = $checkbox->getConfig()->getOption('value'); + $checkbox->setData(isset($valueMap[$value]) ? true : false); + } + } + + /** + * {@inheritdoc} + */ + public function mapFormsToData($checkboxes, &$choices) + { + $values = array(); + + foreach ($checkboxes as $checkbox) { + if ($checkbox->getData()) { + // construct an array of choice values + $values[] = $checkbox->getConfig()->getOption('value'); + } + } + + try { + $choices = $this->choiceList->getChoicesForValues($values); + } catch (\Exception $e) { + throw new TransformationFailedException( + 'Can not read the values from the choice list.', + $e->getCode(), + $e + ); + } + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php index 2208f26d1e..736752a41e 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php @@ -17,7 +17,7 @@ use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** - * A data mapper using property paths to read/write data. + * Maps arrays/objects to/from forms using property paths. * * @author Bernhard Schussek */ @@ -31,7 +31,7 @@ class PropertyPathMapper implements DataMapperInterface /** * Creates a new property path mapper. * - * @param PropertyAccessorInterface $propertyAccessor + * @param PropertyAccessorInterface $propertyAccessor The property accessor */ public function __construct(PropertyAccessorInterface $propertyAccessor = null) { diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php new file mode 100644 index 0000000000..aecdb2fad0 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataMapper; + +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\DataMapperInterface; + +/** + * Maps choices to/from radio forms. + * + * A {@link ChoiceListInterface} implementation is used to find the + * corresponding string values for the choices. The radio form whose "value" + * option corresponds to the selected value is marked as selected. + * + * @author Bernhard Schussek + */ +class RadioListMapper implements DataMapperInterface +{ + /** + * @var ChoiceListInterface + */ + private $choiceList; + + public function __construct(ChoiceListInterface $choiceList) + { + $this->choiceList = $choiceList; + } + + /** + * {@inheritdoc} + */ + public function mapDataToForms($choice, $radios) + { + $valueMap = array_flip($this->choiceList->getValuesForChoices(array($choice))); + + foreach ($radios as $radio) { + $value = $radio->getConfig()->getOption('value'); + $radio->setData(isset($valueMap[$value]) ? true : false); + } + } + + /** + * {@inheritdoc} + */ + public function mapFormsToData($radios, &$choice) + { + $choice = null; + + foreach ($radios as $radio) { + if ($radio->getData()) { + if ('placeholder' === $radio->getName()) { + $choice = null; + + return; + } + + $value = $radio->getConfig()->getOption('value'); + $choice = current($this->choiceList->getChoicesForValues(array($value))); + + return; + } + } + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php index a91ed55c31..a0b5039317 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php @@ -11,12 +11,16 @@ namespace Symfony\Component\Form\Extension\Core\DataTransformer; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; /** * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\LazyChoiceList} + * instead. */ class ChoiceToBooleanArrayTransformer implements DataTransformerInterface { diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php index 087faf4d3b..1c83782621 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php @@ -13,7 +13,7 @@ namespace Symfony\Component\Form\Extension\Core\DataTransformer; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; /** * @author Bernhard Schussek @@ -43,7 +43,7 @@ class ChoiceToValueTransformer implements DataTransformerInterface throw new TransformationFailedException('Expected a scalar.'); } - // These are now valid ChoiceList values, so we can return null + // These are now valid ArrayChoiceList values, so we can return null // right away if ('' === $value || null === $value) { return; diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php index f1f13fda28..c38c363329 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php @@ -11,12 +11,16 @@ namespace Symfony\Component\Form\Extension\Core\DataTransformer; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; /** * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\ArrayChoiceList\LazyChoiceList} + * instead. */ class ChoicesToBooleanArrayTransformer implements DataTransformerInterface { diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php index 0ee0b0fefd..0a1f2f0288 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToValuesTransformer.php @@ -13,7 +13,7 @@ namespace Symfony\Component\Form\Extension\Core\DataTransformer; use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\DataTransformerInterface; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; /** * @author Bernhard Schussek diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php index b201802fbc..297987f799 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Form\Extension\Core\EventListener; -use Symfony\Component\Form\Exception\TransformationFailedException; -use Symfony\Component\Form\FormEvents; -use Symfony\Component\Form\FormEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; /** * Takes care of converting the input from a list of checkboxes to a correctly diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php index c5f871756b..d5067b6e33 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Form\Extension\Core\EventListener; -use Symfony\Component\Form\FormEvents; -use Symfony\Component\Form\FormEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; /** * Takes care of converting the input from a single radio button diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index 5b52b4ad96..7e80a00bde 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -12,19 +12,25 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\Core\DataMapper\RadioListMapper; +use Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; -use Symfony\Component\Form\Exception\LogicException; -use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; -use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener; -use Symfony\Component\Form\Extension\Core\EventListener\FixCheckboxInputListener; use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener; use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer; -use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToBooleanArrayTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer; -use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToBooleanArrayTransformer; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -33,54 +39,111 @@ class ChoiceType extends AbstractType /** * Caches created choice lists. * - * @var array + * @var ChoiceListFactoryInterface */ - private $choiceListCache = array(); + private $choiceListFactory; + + public function __construct(ChoiceListFactoryInterface $choiceListFactory = null) + { + $this->choiceListFactory = $choiceListFactory ?: new PropertyAccessDecorator(new DefaultChoiceListFactory()); + } /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { - if (!$options['choice_list'] && !is_array($options['choices']) && !$options['choices'] instanceof \Traversable) { - throw new LogicException('Either the option "choices" or "choice_list" must be set.'); - } - if ($options['expanded']) { + $builder->setDataMapper($options['multiple'] + ? new CheckboxListMapper($options['choice_list']) + : new RadioListMapper($options['choice_list'])); + // Initialize all choices before doing the index check below. // This helps in cases where index checks are optimized for non // initialized choice lists. For example, when using an SQL driver, // the index check would read in one SQL query and the initialization // requires another SQL query. When the initialization is done first, // one SQL query is sufficient. - $preferredViews = $options['choice_list']->getPreferredViews(); - $remainingViews = $options['choice_list']->getRemainingViews(); + + $choiceListView = $this->createChoiceListView($options['choice_list'], $options); + $builder->setAttribute('choice_list_view', $choiceListView); // Check if the choices already contain the empty value - // Only add the empty value option if this is not the case + // Only add the placeholder option if this is not the case if (null !== $options['placeholder'] && 0 === count($options['choice_list']->getChoicesForValues(array('')))) { - $placeholderView = new ChoiceView(null, '', $options['placeholder']); + $placeholderView = new ChoiceView($options['placeholder'], '', null); - // "placeholder" is a reserved index - $this->addSubForms($builder, array('placeholder' => $placeholderView), $options); + // "placeholder" is a reserved name + $this->addSubForm($builder, 'placeholder', $placeholderView, $options); } - $this->addSubForms($builder, $preferredViews, $options); - $this->addSubForms($builder, $remainingViews, $options); + $this->addSubForms($builder, $choiceListView->preferredChoices, $options); + $this->addSubForms($builder, $choiceListView->choices, $options); - if ($options['multiple']) { - $builder->addViewTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list'])); - $builder->addEventSubscriber(new FixCheckboxInputListener($options['choice_list']), 10); - } else { - $builder->addViewTransformer(new ChoiceToBooleanArrayTransformer($options['choice_list'], $builder->has('placeholder'))); - $builder->addEventSubscriber(new FixRadioInputListener($options['choice_list'], $builder->has('placeholder')), 10); + // Make sure that scalar, submitted values are converted to arrays + // which can be submitted to the checkboxes/radio buttons + $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { + $form = $event->getForm(); + $data = $event->getData(); + + // Convert the submitted data to a string, if scalar, before + // casting it to an array + if (!is_array($data)) { + $data = (array) (string) $data; + } + + // A map from submitted values to integers + $valueMap = array_flip($data); + + // Make a copy of the value map to determine whether any unknown + // values were submitted + $unknownValues = $valueMap; + + // Reconstruct the data as mapping from child names to values + $data = array(); + + foreach ($form as $child) { + $value = $child->getConfig()->getOption('value'); + + // Add the value to $data with the child's name as key + if (isset($valueMap[$value])) { + $data[$child->getName()] = $value; + unset($unknownValues[$value]); + continue; + } + } + + // The empty value is always known, independent of whether a + // field exists for it or not + unset($unknownValues['']); + + // Throw exception if unknown values were submitted + if (count($unknownValues) > 0) { + throw new TransformationFailedException(sprintf( + 'The choices "%s" do not exist in the choice list.', + implode('", "', array_keys($unknownValues)) + )); + } + + $event->setData($data); + }); + + if (!$options['multiple']) { + // For radio lists, transform empty arrays to null + // This is kind of a hack necessary because the RadioListMapper + // is not invoked for forms without choices + $builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) { + if (array() === $event->getData()) { + $event->setData(null); + } + }); } + } elseif ($options['multiple']) { + // tag without "multiple" option + $builder->addViewTransformer(new ChoiceToValueTransformer($options['choice_list'])); } if ($options['multiple'] && $options['by_reference']) { @@ -95,11 +158,16 @@ class ChoiceType extends AbstractType */ public function buildView(FormView $view, FormInterface $form, array $options) { + /** @var ChoiceListView $choiceListView */ + $choiceListView = $form->getConfig()->hasAttribute('choice_list_view') + ? $form->getConfig()->getAttribute('choice_list_view') + : $this->createChoiceListView($options['choice_list'], $options); + $view->vars = array_replace($view->vars, array( 'multiple' => $options['multiple'], 'expanded' => $options['expanded'], - 'preferred_choices' => $options['choice_list']->getPreferredViews(), - 'choices' => $options['choice_list']->getRemainingViews(), + 'preferred_choices' => $choiceListView->preferredChoices, + 'choices' => $choiceListView->choices, 'separator' => '-------------------', 'placeholder' => null, )); @@ -163,20 +231,39 @@ class ChoiceType extends AbstractType */ public function configureOptions(OptionsResolver $resolver) { - $choiceListCache = &$this->choiceListCache; + $choiceListFactory = $this->choiceListFactory; + + $choiceList = function (Options $options) use ($choiceListFactory) { + if (null !== $options['choice_loader']) { + // Due to a bug in OptionsResolver, the choices haven't been + // validated yet at this point. Remove the if statement once that + // bug is resolved + if (!$options['choice_loader'] instanceof ChoiceLoaderInterface) { + return; + } + + return $choiceListFactory->createListFromLoader( + $options['choice_loader'], + $options['choice_value'] + ); + } - $choiceList = function (Options $options) use (&$choiceListCache) { // Harden against NULL values (like in EntityType and ModelType) $choices = null !== $options['choices'] ? $options['choices'] : array(); - // Reuse existing choice lists in order to increase performance - $hash = hash('sha256', serialize(array($choices, $options['preferred_choices']))); - - if (!isset($choiceListCache[$hash])) { - $choiceListCache[$hash] = new SimpleChoiceList($choices, $options['preferred_choices']); + // Due to a bug in OptionsResolver, the choices haven't been + // validated yet at this point. Remove the if statement once that + // bug is resolved + if (!is_array($choices) && !$choices instanceof \Traversable) { + return; } - return $choiceListCache[$hash]; + // BC when choices are in the keys, not in the values + if (!$options['choices_as_values']) { + return $choiceListFactory->createListFromFlippedChoices($choices, $options['choice_value']); + } + + return $choiceListFactory->createListFromChoices($choices, $options['choice_value']); }; $emptyData = function (Options $options) { @@ -219,9 +306,16 @@ class ChoiceType extends AbstractType $resolver->setDefaults(array( 'multiple' => false, 'expanded' => false, - 'choice_list' => $choiceList, + 'choice_list' => $choiceList, // deprecated 'choices' => array(), + 'choices_as_values' => false, + 'choice_loader' => null, + 'choice_label' => null, + 'choice_name' => null, + 'choice_value' => null, + 'choice_attr' => null, 'preferred_choices' => array(), + 'group_by' => null, 'empty_data' => $emptyData, 'empty_value' => $emptyValue, // deprecated 'placeholder' => $placeholder, @@ -236,7 +330,16 @@ class ChoiceType extends AbstractType $resolver->setNormalizer('empty_value', $placeholderNormalizer); $resolver->setNormalizer('placeholder', $placeholderNormalizer); - $resolver->setAllowedTypes('choice_list', array('null', 'Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface')); + $resolver->setAllowedTypes('choice_list', array('null', 'Symfony\Component\Form\ChoiceList\ChoiceListInterface')); + $resolver->setAllowedTypes('choices', array('null', 'array', '\Traversable')); + $resolver->setAllowedTypes('choices_as_values', 'bool'); + $resolver->setAllowedTypes('choice_loader', array('null', 'Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface')); + $resolver->setAllowedTypes('choice_label', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); + $resolver->setAllowedTypes('choice_name', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); + $resolver->setAllowedTypes('choice_value', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); + $resolver->setAllowedTypes('choice_attr', array('null', 'array', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); + $resolver->setAllowedTypes('preferred_choices', array('array', '\Traversable', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); + $resolver->setAllowedTypes('group_by', array('null', 'array', '\Traversable', 'string', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); } /** @@ -247,6 +350,21 @@ class ChoiceType extends AbstractType return 'choice'; } + private static function flipRecursive($choices, &$output = array()) + { + foreach ($choices as $key => $value) { + if (is_array($value)) { + $output[$key] = array(); + self::flipRecursive($value, $output[$key]); + continue; + } + + $output[$value] = $key; + } + + return $output; + } + /** * Adds the sub fields for an expanded choice field. * @@ -256,29 +374,69 @@ class ChoiceType extends AbstractType */ private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options) { - foreach ($choiceViews as $i => $choiceView) { + foreach ($choiceViews as $name => $choiceView) { + // Flatten groups if (is_array($choiceView)) { - // Flatten groups $this->addSubForms($builder, $choiceView, $options); - } else { - $choiceOpts = array( - 'value' => $choiceView->value, - 'label' => $choiceView->label, - 'translation_domain' => $options['translation_domain'], - 'block_name' => 'entry', - ); - - if ($options['multiple']) { - $choiceType = 'checkbox'; - // The user can check 0 or more checkboxes. If required - // is true, he is required to check all of them. - $choiceOpts['required'] = false; - } else { - $choiceType = 'radio'; - } - - $builder->add($i, $choiceType, $choiceOpts); + continue; } + + if ($choiceView instanceof ChoiceGroupView) { + $this->addSubForms($builder, $choiceView->choices, $options); + continue; + } + + $this->addSubForm($builder, $name, $choiceView, $options); } } + + /** + * @param FormBuilderInterface $builder + * @param $name + * @param $choiceView + * @param array $options + * + * @return mixed + */ + private function addSubForm(FormBuilderInterface $builder, $name, ChoiceView $choiceView, array $options) + { + $choiceOpts = array( + 'value' => $choiceView->value, + 'label' => $choiceView->label, + 'attr' => $choiceView->attr, + 'translation_domain' => $options['translation_domain'], + 'block_name' => 'entry', + ); + + if ($options['multiple']) { + $choiceType = 'checkbox'; + // The user can check 0 or more checkboxes. If required + // is true, he is required to check all of them. + $choiceOpts['required'] = false; + } else { + $choiceType = 'radio'; + } + + $builder->add($name, $choiceType, $choiceOpts); + } + + private function createChoiceListView(ChoiceListInterface $choiceList, array $options) + { + // If no explicit grouping information is given, use the structural + // information from the "choices" option for creating groups + if (!$options['group_by'] && $options['choices']) { + $options['group_by'] = !$options['choices_as_values'] + ? ChoiceType::flipRecursive($options['choices']) + : $options['choices']; + } + + return $this->choiceListFactory->createView( + $choiceList, + $options['preferred_choices'], + $options['choice_label'], + $options['choice_name'], + $options['group_by'], + $options['choice_attr'] + ); + } } diff --git a/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php b/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php index 97cdd214c2..65d7af2464 100644 --- a/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php +++ b/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php @@ -16,29 +16,8 @@ namespace Symfony\Component\Form\Extension\Core\View; * * @author Bernhard Schussek */ -class ChoiceView +class ChoiceView extends \Symfony\Component\Form\ChoiceList\View\ChoiceView { - /** - * The original choice value. - * - * @var mixed - */ - public $data; - - /** - * The view representation of the choice. - * - * @var string - */ - public $value; - - /** - * The label displayed to humans. - * - * @var string - */ - public $label; - /** * Creates a new ChoiceView. * @@ -48,8 +27,6 @@ class ChoiceView */ public function __construct($data, $value, $label) { - $this->data = $data; - $this->value = $value; - $this->label = $label; + parent::__construct($label, $value, $data); } } diff --git a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php index 6375542b28..3bf84d71c8 100644 --- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php @@ -516,6 +516,28 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg ); } + public function testSingleChoiceAttributes() + { + $form = $this->factory->createNamed('name', 'choice', '&a', array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'multiple' => false, + 'expanded' => false, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/select + [@name="name"] + [not(@required)] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + public function testSingleChoiceWithPreferred() { $form = $this->factory->createNamed('name', 'choice', '&a', array( @@ -776,6 +798,30 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg ); } + public function testMultipleChoiceAttributes() + { + $form = $this->factory->createNamed('name', 'choice', array('&a'), array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'required' => true, + 'multiple' => true, + 'expanded' => false, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/select + [@name="name[]"] + [@required="required"] + [@multiple="multiple"] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + public function testMultipleChoiceSkipsPlaceholder() { $form = $this->factory->createNamed('name', 'choice', array('&a'), array( @@ -842,6 +888,29 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg ); } + public function testSingleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', 'choice', '&a', array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'multiple' => false, + 'expanded' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][@class="foo&bar"][not(@checked)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] + [count(./input)=3] +' + ); + } + public function testSingleChoiceExpandedWithPlaceholder() { $form = $this->factory->createNamed('name', 'choice', '&a', array( @@ -914,6 +983,32 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg ); } + public function testMultipleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', 'choice', array('&a', '&c'), array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B', '&c' => 'Choice&C'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'multiple' => true, + 'expanded' => true, + 'required' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_1"][@class="foo&bar"][not(@checked)][not(@required)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] + /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)] + /following-sibling::label[@for="name_2"][.="[trans]Choice&C[/trans]"] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] + [count(./input)=4] +' + ); + } + public function testCountry() { $form = $this->factory->createNamed('name', 'country', 'AT'); diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/AbstractChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/AbstractChoiceListTest.php new file mode 100644 index 0000000000..0805238f7f --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/AbstractChoiceListTest.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\ChoiceList; + +/** + * @author Bernhard Schussek + */ +abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + protected $list; + + /** + * @var array + */ + protected $choices; + + /** + * @var array + */ + protected $values; + + /** + * @var mixed + */ + protected $choice1; + + /** + * @var mixed + */ + protected $choice2; + + /** + * @var mixed + */ + protected $choice3; + + /** + * @var mixed + */ + protected $choice4; + + /** + * @var string + */ + protected $value1; + + /** + * @var string + */ + protected $value2; + + /** + * @var string + */ + protected $value3; + + /** + * @var string + */ + protected $value4; + + protected function setUp() + { + parent::setUp(); + + $this->list = $this->createChoiceList(); + + $this->choices = $this->getChoices(); + $this->values = $this->getValues(); + + // allow access to the individual entries without relying on their indices + reset($this->choices); + reset($this->values); + + for ($i = 1; $i <= 4; ++$i) { + $this->{'choice'.$i} = current($this->choices); + $this->{'value'.$i} = current($this->values); + + next($this->choices); + next($this->values); + } + } + + public function testGetChoices() + { + $this->assertSame($this->choices, $this->list->getChoices()); + } + + public function testGetValues() + { + $this->assertSame($this->values, $this->list->getValues()); + } + + public function testGetChoicesForValues() + { + $values = array($this->value1, $this->value2); + $this->assertSame(array($this->choice1, $this->choice2), $this->list->getChoicesForValues($values)); + } + + public function testGetChoicesForValuesPreservesKeys() + { + $values = array(5 => $this->value1, 8 => $this->value2); + $this->assertSame(array(5 => $this->choice1, 8 => $this->choice2), $this->list->getChoicesForValues($values)); + } + + public function testGetChoicesForValuesPreservesOrder() + { + $values = array($this->value2, $this->value1); + $this->assertSame(array($this->choice2, $this->choice1), $this->list->getChoicesForValues($values)); + } + + public function testGetChoicesForValuesIgnoresNonExistingValues() + { + $values = array($this->value1, $this->value2, 'foobar'); + $this->assertSame(array($this->choice1, $this->choice2), $this->list->getChoicesForValues($values)); + } + + // https://github.com/symfony/symfony/issues/3446 + public function testGetChoicesForValuesEmpty() + { + $this->assertSame(array(), $this->list->getChoicesForValues(array())); + } + + public function testGetValuesForChoices() + { + $choices = array($this->choice1, $this->choice2); + $this->assertSame(array($this->value1, $this->value2), $this->list->getValuesForChoices($choices)); + } + + public function testGetValuesForChoicesPreservesKeys() + { + $choices = array(5 => $this->choice1, 8 => $this->choice2); + $this->assertSame(array(5 => $this->value1, 8 => $this->value2), $this->list->getValuesForChoices($choices)); + } + + public function testGetValuesForChoicesPreservesOrder() + { + $choices = array($this->choice2, $this->choice1); + $this->assertSame(array($this->value2, $this->value1), $this->list->getValuesForChoices($choices)); + } + + public function testGetValuesForChoicesIgnoresNonExistingChoices() + { + $choices = array($this->choice1, $this->choice2, 'foobar'); + $this->assertSame(array($this->value1, $this->value2), $this->list->getValuesForChoices($choices)); + } + + public function testGetValuesForChoicesEmpty() + { + $this->assertSame(array(), $this->list->getValuesForChoices(array())); + } + + /** + * @return \Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + */ + abstract protected function createChoiceList(); + + abstract protected function getChoices(); + + abstract protected function getValues(); +} diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php new file mode 100644 index 0000000000..34b22fe041 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\ChoiceList; + +use Symfony\Component\Form\ChoiceList\ArrayChoiceList; + +/** + * @author Bernhard Schussek + */ +class ArrayChoiceListTest extends AbstractChoiceListTest +{ + private $object; + + protected function setUp() + { + parent::setUp(); + + $this->object = new \stdClass(); + } + + protected function createChoiceList() + { + return new ArrayChoiceList($this->getChoices(), $this->getValues()); + } + + protected function getChoices() + { + return array(0, 1, '1', 'a', false, true, $this->object); + } + + protected function getValues() + { + return array('0', '1', '2', '3', '4', '5', '6'); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException + */ + public function testFailIfKeyMismatch() + { + new ArrayChoiceList(array(0 => 'a', 1 => 'b'), array(1 => 'a', 2 => 'b')); + } +} diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php new file mode 100644 index 0000000000..74cf2afb4a --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php @@ -0,0 +1,187 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\ChoiceList; + +use Symfony\Component\Form\ChoiceList\ArrayKeyChoiceList; + +/** + * @author Bernhard Schussek + */ +class ArrayKeyChoiceListTest extends AbstractChoiceListTest +{ + private $object; + + protected function setUp() + { + parent::setUp(); + + $this->object = new \stdClass(); + } + + protected function createChoiceList() + { + return new ArrayKeyChoiceList($this->getChoices(), $this->getValues()); + } + + protected function getChoices() + { + return array(0, 1, 'a', 'b', ''); + } + + protected function getValues() + { + return array('0', '1', 'a', 'b', ''); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException + */ + public function testFailIfKeyMismatch() + { + new ArrayKeyChoiceList(array(0 => 'a', 1 => 'b'), array(1 => 'a', 2 => 'b')); + } + + public function testUseChoicesAsValuesByDefault() + { + $list = new ArrayKeyChoiceList(array(1 => '', 3 => 0, 7 => '1', 10 => 1.23)); + + $this->assertSame(array(1 => '', 3 => '0', 7 => '1', 10 => '1.23'), $list->getValues()); + } + + public function testNoChoices() + { + $list = new ArrayKeyChoiceList(array()); + + $this->assertSame(array(), $list->getValues()); + } + + public function testGetChoicesForValuesConvertsValuesToStrings() + { + $this->assertSame(array(0), $this->list->getChoicesForValues(array(0))); + $this->assertSame(array(0), $this->list->getChoicesForValues(array('0'))); + $this->assertSame(array(1), $this->list->getChoicesForValues(array(1))); + $this->assertSame(array(1), $this->list->getChoicesForValues(array('1'))); + $this->assertSame(array('a'), $this->list->getChoicesForValues(array('a'))); + $this->assertSame(array('b'), $this->list->getChoicesForValues(array('b'))); + $this->assertSame(array(''), $this->list->getChoicesForValues(array(''))); + // "1" === (string) true + $this->assertSame(array(1), $this->list->getChoicesForValues(array(true))); + // "" === (string) false + $this->assertSame(array(''), $this->list->getChoicesForValues(array(false))); + // "" === (string) null + $this->assertSame(array(''), $this->list->getChoicesForValues(array(null))); + $this->assertSame(array(), $this->list->getChoicesForValues(array(1.23))); + } + + public function testGetValuesForChoicesConvertsChoicesToArrayKeys() + { + $this->assertSame(array('0'), $this->list->getValuesForChoices(array(0))); + $this->assertSame(array('0'), $this->list->getValuesForChoices(array('0'))); + $this->assertSame(array('1'), $this->list->getValuesForChoices(array(1))); + $this->assertSame(array('1'), $this->list->getValuesForChoices(array('1'))); + $this->assertSame(array('a'), $this->list->getValuesForChoices(array('a'))); + $this->assertSame(array('b'), $this->list->getValuesForChoices(array('b'))); + // Always cast booleans to 0 and 1, because: + // array(true => 'Yes', false => 'No') === array(1 => 'Yes', 0 => 'No') + // see ChoiceTypeTest::testSetDataSingleNonExpandedAcceptsBoolean + $this->assertSame(array('0'), $this->list->getValuesForChoices(array(false))); + $this->assertSame(array('1'), $this->list->getValuesForChoices(array(true))); + } + + /** + * @dataProvider provideConvertibleChoices + */ + public function testConvertChoicesIfNecessary(array $choices, array $converted) + { + $list = new ArrayKeyChoiceList($choices, range(0, count($choices) - 1)); + + $this->assertSame($converted, $list->getChoices()); + } + + public function provideConvertibleChoices() + { + return array( + array(array(0), array(0)), + array(array(1), array(1)), + array(array('0'), array(0)), + array(array('1'), array(1)), + array(array('1.23'), array('1.23')), + array(array('foobar'), array('foobar')), + // The default value of choice fields is NULL. It should be treated + // like the empty value for this choice list type + array(array(null), array('')), + array(array(1.23), array('1.23')), + // Always cast booleans to 0 and 1, because: + // array(true => 'Yes', false => 'No') === array(1 => 'Yes', 0 => 'No') + // see ChoiceTypeTest::testSetDataSingleNonExpandedAcceptsBoolean + array(array(true), array(1)), + array(array(false), array(0)), + ); + } + + /** + * @dataProvider provideInvalidChoices + * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException + */ + public function testFailIfInvalidChoices(array $choices) + { + new ArrayKeyChoiceList($choices, range(0, count($choices) - 1)); + } + + /** + * @dataProvider provideInvalidChoices + * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException + */ + public function testGetValuesForChoicesFailsIfInvalidChoices(array $choices) + { + $this->list->getValuesForChoices($choices); + } + + public function provideInvalidChoices() + { + return array( + array(array(new \stdClass())), + array(array(array(1, 2))), + ); + } + + /** + * @dataProvider provideConvertibleValues + */ + public function testConvertValuesToStrings(array $values, array $converted) + { + $list = new ArrayKeyChoiceList(range(0, count($values) - 1), $values); + + $this->assertSame($converted, $list->getValues()); + } + + public function provideConvertibleValues() + { + return array( + array(array(0), array('0')), + array(array(1), array('1')), + array(array('0'), array('0')), + array(array('1'), array('1')), + array(array('1.23'), array('1.23')), + array(array('foobar'), array('foobar')), + // The default value of choice fields is NULL. It should be treated + // like the empty value for this choice list type + array(array(null), array('')), + array(array(1.23), array('1.23')), + // Always cast booleans to 0 and 1, because: + // array(true => 'Yes', false => 'No') === array(1 => 'Yes', 0 => 'No') + // see ChoiceTypeTest::testSetDataSingleNonExpandedAcceptsBoolean + array(array(true), array('1')), + array(array(false), array('')), + ); + } +} diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php new file mode 100644 index 0000000000..031cced280 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php @@ -0,0 +1,668 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; + +/** + * @author Bernhard Schussek + */ +class CachingFactoryDecoratorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $decoratedFactory; + + /** + * @var CachingFactoryDecorator + */ + private $factory; + + protected function setUp() + { + $this->decoratedFactory = $this->getMock('Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface'); + $this->factory = new CachingFactoryDecorator($this->decoratedFactory); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateFromChoicesFailsIfChoicesNotArrayOrTraversable() + { + $this->factory->createListFromChoices('foobar'); + } + + public function testCreateFromChoicesEmpty() + { + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromChoices') + ->with(array()) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromChoices(array())); + $this->assertSame($list, $this->factory->createListFromChoices(array())); + } + + public function testCreateFromChoicesComparesTraversableChoicesAsArray() + { + // The top-most traversable is converted to an array + $choices1 = new \ArrayIterator(array('A' => 'a')); + $choices2 = array('A' => 'a'); + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices2) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromChoices($choices1)); + $this->assertSame($list, $this->factory->createListFromChoices($choices2)); + } + + public function testCreateFromChoicesFlattensChoices() + { + $choices1 = array('key' => array('A' => 'a')); + $choices2 = array('A' => 'a'); + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices1) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromChoices($choices1)); + $this->assertSame($list, $this->factory->createListFromChoices($choices2)); + } + + /** + * @dataProvider provideSameChoices + */ + public function testCreateFromChoicesSameChoices($choice1, $choice2) + { + $choices1 = array($choice1); + $choices2 = array($choice2); + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices1) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromChoices($choices1)); + $this->assertSame($list, $this->factory->createListFromChoices($choices2)); + } + + /** + * @dataProvider provideDistinguishedChoices + */ + public function testCreateFromChoicesDifferentChoices($choice1, $choice2) + { + $choices1 = array($choice1); + $choices2 = array($choice2); + $list1 = new \stdClass(); + $list2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createListFromChoices') + ->with($choices1) + ->will($this->returnValue($list1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createListFromChoices') + ->with($choices2) + ->will($this->returnValue($list2)); + + $this->assertSame($list1, $this->factory->createListFromChoices($choices1)); + $this->assertSame($list2, $this->factory->createListFromChoices($choices2)); + } + + public function testCreateFromChoicesSameValueClosure() + { + $choices = array(1); + $list = new \stdClass(); + $closure = function () {}; + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices, $closure) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromChoices($choices, $closure)); + $this->assertSame($list, $this->factory->createListFromChoices($choices, $closure)); + } + + public function testCreateFromChoicesDifferentValueClosure() + { + $choices = array(1); + $list1 = new \stdClass(); + $list2 = new \stdClass(); + $closure1 = function () {}; + $closure2 = function () {}; + + $this->decoratedFactory->expects($this->at(0)) + ->method('createListFromChoices') + ->with($choices, $closure1) + ->will($this->returnValue($list1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createListFromChoices') + ->with($choices, $closure2) + ->will($this->returnValue($list2)); + + $this->assertSame($list1, $this->factory->createListFromChoices($choices, $closure1)); + $this->assertSame($list2, $this->factory->createListFromChoices($choices, $closure2)); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateFromFlippedChoicesFailsIfChoicesNotArrayOrTraversable() + { + $this->factory->createListFromFlippedChoices('foobar'); + } + + public function testCreateFromFlippedChoicesEmpty() + { + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromFlippedChoices') + ->with(array()) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromFlippedChoices(array())); + $this->assertSame($list, $this->factory->createListFromFlippedChoices(array())); + } + + public function testCreateFromFlippedChoicesComparesTraversableChoicesAsArray() + { + // The top-most traversable is converted to an array + $choices1 = new \ArrayIterator(array('a' => 'A')); + $choices2 = array('a' => 'A'); + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromFlippedChoices') + ->with($choices2) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices1)); + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices2)); + } + + public function testCreateFromFlippedChoicesFlattensChoices() + { + $choices1 = array('key' => array('a' => 'A')); + $choices2 = array('a' => 'A'); + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromFlippedChoices') + ->with($choices1) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices1)); + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices2)); + } + + /** + * @dataProvider provideSameKeyChoices + */ + public function testCreateFromFlippedChoicesSameChoices($choice1, $choice2) + { + $choices1 = array($choice1); + $choices2 = array($choice2); + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromFlippedChoices') + ->with($choices1) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices1)); + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices2)); + } + + /** + * @dataProvider provideDistinguishedKeyChoices + */ + public function testCreateFromFlippedChoicesDifferentChoices($choice1, $choice2) + { + $choices1 = array($choice1); + $choices2 = array($choice2); + $list1 = new \stdClass(); + $list2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createListFromFlippedChoices') + ->with($choices1) + ->will($this->returnValue($list1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createListFromFlippedChoices') + ->with($choices2) + ->will($this->returnValue($list2)); + + $this->assertSame($list1, $this->factory->createListFromFlippedChoices($choices1)); + $this->assertSame($list2, $this->factory->createListFromFlippedChoices($choices2)); + } + + public function testCreateFromFlippedChoicesSameValueClosure() + { + $choices = array(1); + $list = new \stdClass(); + $closure = function () {}; + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromFlippedChoices') + ->with($choices, $closure) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices, $closure)); + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices, $closure)); + } + + public function testCreateFromFlippedChoicesDifferentValueClosure() + { + $choices = array(1); + $list1 = new \stdClass(); + $list2 = new \stdClass(); + $closure1 = function () {}; + $closure2 = function () {}; + + $this->decoratedFactory->expects($this->at(0)) + ->method('createListFromFlippedChoices') + ->with($choices, $closure1) + ->will($this->returnValue($list1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createListFromFlippedChoices') + ->with($choices, $closure2) + ->will($this->returnValue($list2)); + + $this->assertSame($list1, $this->factory->createListFromFlippedChoices($choices, $closure1)); + $this->assertSame($list2, $this->factory->createListFromFlippedChoices($choices, $closure2)); + } + + public function testCreateFromLoaderSameLoader() + { + $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromLoader') + ->with($loader) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromLoader($loader)); + $this->assertSame($list, $this->factory->createListFromLoader($loader)); + } + + public function testCreateFromLoaderDifferentLoader() + { + $loader1 = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + $loader2 = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + $list1 = new \stdClass(); + $list2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createListFromLoader') + ->with($loader1) + ->will($this->returnValue($list1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createListFromLoader') + ->with($loader2) + ->will($this->returnValue($list2)); + + $this->assertSame($list1, $this->factory->createListFromLoader($loader1)); + $this->assertSame($list2, $this->factory->createListFromLoader($loader2)); + } + + public function testCreateFromLoaderSameValueClosure() + { + $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + $list = new \stdClass(); + $closure = function () {}; + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromLoader') + ->with($loader, $closure) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromLoader($loader, $closure)); + $this->assertSame($list, $this->factory->createListFromLoader($loader, $closure)); + } + + public function testCreateFromLoaderDifferentValueClosure() + { + $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + $list1 = new \stdClass(); + $list2 = new \stdClass(); + $closure1 = function () {}; + $closure2 = function () {}; + + $this->decoratedFactory->expects($this->at(0)) + ->method('createListFromLoader') + ->with($loader, $closure1) + ->will($this->returnValue($list1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createListFromLoader') + ->with($loader, $closure2) + ->will($this->returnValue($list2)); + + $this->assertSame($list1, $this->factory->createListFromLoader($loader, $closure1)); + $this->assertSame($list2, $this->factory->createListFromLoader($loader, $closure2)); + } + + public function testCreateViewSamePreferredChoices() + { + $preferred = array('a'); + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, $preferred) + ->will($this->returnValue($view)); + + $this->assertSame($view, $this->factory->createView($list, $preferred)); + $this->assertSame($view, $this->factory->createView($list, $preferred)); + } + + public function testCreateViewDifferentPreferredChoices() + { + $preferred1 = array('a'); + $preferred2 = array('b'); + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view1 = new \stdClass(); + $view2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createView') + ->with($list, $preferred1) + ->will($this->returnValue($view1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createView') + ->with($list, $preferred2) + ->will($this->returnValue($view2)); + + $this->assertSame($view1, $this->factory->createView($list, $preferred1)); + $this->assertSame($view2, $this->factory->createView($list, $preferred2)); + } + + public function testCreateViewSamePreferredChoicesClosure() + { + $preferred = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, $preferred) + ->will($this->returnValue($view)); + + $this->assertSame($view, $this->factory->createView($list, $preferred)); + $this->assertSame($view, $this->factory->createView($list, $preferred)); + } + + public function testCreateViewDifferentPreferredChoicesClosure() + { + $preferred1 = function () {}; + $preferred2 = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view1 = new \stdClass(); + $view2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createView') + ->with($list, $preferred1) + ->will($this->returnValue($view1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createView') + ->with($list, $preferred2) + ->will($this->returnValue($view2)); + + $this->assertSame($view1, $this->factory->createView($list, $preferred1)); + $this->assertSame($view2, $this->factory->createView($list, $preferred2)); + } + + public function testCreateViewSameLabelClosure() + { + $labels = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, $labels) + ->will($this->returnValue($view)); + + $this->assertSame($view, $this->factory->createView($list, null, $labels)); + $this->assertSame($view, $this->factory->createView($list, null, $labels)); + } + + public function testCreateViewDifferentLabelClosure() + { + $labels1 = function () {}; + $labels2 = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view1 = new \stdClass(); + $view2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createView') + ->with($list, null, $labels1) + ->will($this->returnValue($view1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createView') + ->with($list, null, $labels2) + ->will($this->returnValue($view2)); + + $this->assertSame($view1, $this->factory->createView($list, null, $labels1)); + $this->assertSame($view2, $this->factory->createView($list, null, $labels2)); + } + + public function testCreateViewSameIndexClosure() + { + $index = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, $index) + ->will($this->returnValue($view)); + + $this->assertSame($view, $this->factory->createView($list, null, null, $index)); + $this->assertSame($view, $this->factory->createView($list, null, null, $index)); + } + + public function testCreateViewDifferentIndexClosure() + { + $index1 = function () {}; + $index2 = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view1 = new \stdClass(); + $view2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createView') + ->with($list, null, null, $index1) + ->will($this->returnValue($view1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createView') + ->with($list, null, null, $index2) + ->will($this->returnValue($view2)); + + $this->assertSame($view1, $this->factory->createView($list, null, null, $index1)); + $this->assertSame($view2, $this->factory->createView($list, null, null, $index2)); + } + + public function testCreateViewSameGroupByClosure() + { + $groupBy = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, null, $groupBy) + ->will($this->returnValue($view)); + + $this->assertSame($view, $this->factory->createView($list, null, null, null, $groupBy)); + $this->assertSame($view, $this->factory->createView($list, null, null, null, $groupBy)); + } + + public function testCreateViewDifferentGroupByClosure() + { + $groupBy1 = function () {}; + $groupBy2 = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view1 = new \stdClass(); + $view2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createView') + ->with($list, null, null, null, $groupBy1) + ->will($this->returnValue($view1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createView') + ->with($list, null, null, null, $groupBy2) + ->will($this->returnValue($view2)); + + $this->assertSame($view1, $this->factory->createView($list, null, null, null, $groupBy1)); + $this->assertSame($view2, $this->factory->createView($list, null, null, null, $groupBy2)); + } + + public function testCreateViewSameAttributes() + { + $attr = array('class' => 'foobar'); + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, null, null, $attr) + ->will($this->returnValue($view)); + + $this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr)); + $this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr)); + } + + public function testCreateViewDifferentAttributes() + { + $attr1 = array('class' => 'foobar1'); + $attr2 = array('class' => 'foobar2'); + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view1 = new \stdClass(); + $view2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createView') + ->with($list, null, null, null, null, $attr1) + ->will($this->returnValue($view1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createView') + ->with($list, null, null, null, null, $attr2) + ->will($this->returnValue($view2)); + + $this->assertSame($view1, $this->factory->createView($list, null, null, null, null, $attr1)); + $this->assertSame($view2, $this->factory->createView($list, null, null, null, null, $attr2)); + } + + public function testCreateViewSameAttributesClosure() + { + $attr = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, null, null, $attr) + ->will($this->returnValue($view)); + + $this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr)); + $this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr)); + } + + public function testCreateViewDifferentAttributesClosure() + { + $attr1 = function () {}; + $attr2 = function () {}; + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $view1 = new \stdClass(); + $view2 = new \stdClass(); + + $this->decoratedFactory->expects($this->at(0)) + ->method('createView') + ->with($list, null, null, null, null, $attr1) + ->will($this->returnValue($view1)); + $this->decoratedFactory->expects($this->at(1)) + ->method('createView') + ->with($list, null, null, null, null, $attr2) + ->will($this->returnValue($view2)); + + $this->assertSame($view1, $this->factory->createView($list, null, null, null, null, $attr1)); + $this->assertSame($view2, $this->factory->createView($list, null, null, null, null, $attr2)); + } + + public function provideSameChoices() + { + $object = (object) array('foo' => 'bar'); + + return array( + array(0, 0), + array('a', 'a'), + // https://github.com/symfony/symfony/issues/10409 + array(chr(181).'meter', chr(181).'meter'), // UTF-8 + array($object, $object), + ); + } + + public function provideDistinguishedChoices() + { + return array( + array(0, false), + array(0, null), + array(0, '0'), + array(0, ''), + array(1, true), + array(1, '1'), + array(1, 'a'), + array('', false), + array('', null), + array(false, null), + // Same properties, but not identical + array((object) array('foo' => 'bar'), (object) array('foo' => 'bar')), + ); + } + + public function provideSameKeyChoices() + { + // Only test types here that can be used as array keys + return array( + array(0, 0), + array(0, '0'), + array('a', 'a'), + array(chr(181).'meter', chr(181).'meter'), + ); + } + + public function provideDistinguishedKeyChoices() + { + // Only test types here that can be used as array keys + return array( + array(0, ''), + array(1, 'a'), + array('', 'a'), + ); + } +} diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php new file mode 100644 index 0000000000..42f745e29b --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php @@ -0,0 +1,970 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\ArrayChoiceList; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\LazyChoiceList; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; +use Symfony\Component\Form\ChoiceList\View\ChoiceListView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; + +class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase +{ + private $obj1; + + private $obj2; + + private $obj3; + + private $obj4; + + private $list; + + /** + * @var DefaultChoiceListFactory + */ + private $factory; + + public function getValue($object) + { + return $object->value; + } + + public function getScalarValue($choice) + { + switch ($choice) { + case 'a': return 'a'; + case 'b': return 'b'; + case 'c': return '1'; + case 'd': return '2'; + } + } + + public function getLabel($object) + { + return $object->label; + } + + public function getFormIndex($object) + { + return $object->index; + } + + public function isPreferred($object) + { + return $this->obj2 === $object || $this->obj3 === $object; + } + + public function getAttr($object) + { + return $object->attr; + } + + public function getGroup($object) + { + return $this->obj1 === $object || $this->obj2 === $object ? 'Group 1' : 'Group 2'; + } + + protected function setUp() + { + $this->obj1 = (object) array('label' => 'A', 'index' => 'w', 'value' => 'a', 'preferred' => false, 'group' => 'Group 1', 'attr' => array()); + $this->obj2 = (object) array('label' => 'B', 'index' => 'x', 'value' => 'b', 'preferred' => true, 'group' => 'Group 1', 'attr' => array('attr1' => 'value1')); + $this->obj3 = (object) array('label' => 'C', 'index' => 'y', 'value' => 1, 'preferred' => true, 'group' => 'Group 2', 'attr' => array('attr2' => 'value2')); + $this->obj4 = (object) array('label' => 'D', 'index' => 'z', 'value' => 2, 'preferred' => false, 'group' => 'Group 2', 'attr' => array()); + $this->list = new ArrayChoiceList( + array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4), + array('A' => '0', 'B' => '1', 'C' => '2', 'D' => '3') + ); + $this->factory = new DefaultChoiceListFactory(); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateFromChoicesFailsIfChoicesNotArrayOrTraversable() + { + $this->factory->createListFromChoices('foobar'); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateFromChoicesFailsIfValuesNotCallableOrString() + { + $this->factory->createListFromChoices(array(), new \stdClass()); + } + + public function testCreateFromChoicesEmpty() + { + $list = $this->factory->createListFromChoices(array()); + + $this->assertSame(array(), $list->getChoices()); + $this->assertSame(array(), $list->getValues()); + } + + public function testCreateFromChoicesFlat() + { + $list = $this->factory->createListFromChoices( + array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4) + ); + + $this->assertObjectListWithGeneratedValues($list); + } + + public function testCreateFromChoicesFlatTraversable() + { + $list = $this->factory->createListFromChoices( + new \ArrayIterator(array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4)) + ); + + $this->assertObjectListWithGeneratedValues($list); + } + + public function testCreateFromChoicesFlatValuesAsCallable() + { + $list = $this->factory->createListFromChoices( + array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4), + array($this, 'getValue') + ); + + $this->assertObjectListWithCustomValues($list); + } + + public function testCreateFromChoicesFlatValuesAsClosure() + { + $list = $this->factory->createListFromChoices( + array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4), + function ($object) { return $object->value; } + ); + + $this->assertObjectListWithCustomValues($list); + } + + public function testCreateFromChoicesFlatValuesClosureReceivesKey() + { + $list = $this->factory->createListFromChoices( + array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4), + function ($object, $key) { + switch ($key) { + case 'A': return 'a'; + case 'B': return 'b'; + case 'C': return '1'; + case 'D': return '2'; + } + } + ); + + $this->assertObjectListWithCustomValues($list); + } + + public function testCreateFromChoicesGrouped() + { + $list = $this->factory->createListFromChoices( + array( + 'Group 1' => array('A' => $this->obj1, 'B' => $this->obj2), + 'Group 2' => array('C' => $this->obj3, 'D' => $this->obj4), + ) + ); + + $this->assertObjectListWithGeneratedValues($list); + } + + public function testCreateFromChoicesGroupedTraversable() + { + $list = $this->factory->createListFromChoices( + new \ArrayIterator(array( + 'Group 1' => array('A' => $this->obj1, 'B' => $this->obj2), + 'Group 2' => array('C' => $this->obj3, 'D' => $this->obj4), + )) + ); + + $this->assertObjectListWithGeneratedValues($list); + } + + public function testCreateFromChoicesGroupedValuesAsCallable() + { + $list = $this->factory->createListFromChoices( + array( + 'Group 1' => array('A' => $this->obj1, 'B' => $this->obj2), + 'Group 2' => array('C' => $this->obj3, 'D' => $this->obj4), + ), + array($this, 'getValue') + ); + + $this->assertObjectListWithCustomValues($list); + } + + public function testCreateFromChoicesGroupedValuesAsClosure() + { + $list = $this->factory->createListFromChoices( + array( + 'Group 1' => array('A' => $this->obj1, 'B' => $this->obj2), + 'Group 2' => array('C' => $this->obj3, 'D' => $this->obj4), + ), + function ($object) { return $object->value; } + ); + + $this->assertObjectListWithCustomValues($list); + } + + public function testCreateFromChoicesGroupedValuesAsClosureReceivesKey() + { + $list = $this->factory->createListFromChoices( + array( + 'Group 1' => array('A' => $this->obj1, 'B' => $this->obj2), + 'Group 2' => array('C' => $this->obj3, 'D' => $this->obj4), + ), + function ($object, $key) { + switch ($key) { + case 'A': return 'a'; + case 'B': return 'b'; + case 'C': return '1'; + case 'D': return '2'; + } + } + ); + + $this->assertObjectListWithCustomValues($list); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateFromFlippedChoicesFailsIfChoicesNotArrayOrTraversable() + { + $this->factory->createListFromFlippedChoices('foobar'); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateFromFlippedChoicesFailsIfValuesNotCallableOrString() + { + $this->factory->createListFromFlippedChoices(array(), new \stdClass()); + } + + public function testCreateFromFlippedChoicesEmpty() + { + $list = $this->factory->createListFromFlippedChoices(array()); + + $this->assertSame(array(), $list->getChoices()); + $this->assertSame(array(), $list->getValues()); + } + + public function testCreateFromFlippedChoicesFlat() + { + $list = $this->factory->createListFromFlippedChoices( + array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D') + ); + + $this->assertScalarListWithGeneratedValues($list); + } + + public function testCreateFromFlippedChoicesFlatTraversable() + { + $list = $this->factory->createListFromFlippedChoices( + new \ArrayIterator(array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D')) + ); + + $this->assertScalarListWithGeneratedValues($list); + } + + public function testCreateFromFlippedChoicesFlatValuesAsCallable() + { + $list = $this->factory->createListFromFlippedChoices( + array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D'), + array($this, 'getScalarValue') + ); + + $this->assertScalarListWithCustomValues($list); + } + + public function testCreateFromFlippedChoicesFlatValuesAsClosure() + { + $list = $this->factory->createListFromFlippedChoices( + array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D'), + function ($choice) { + switch ($choice) { + case 'a': return 'a'; + case 'b': return 'b'; + case 'c': return '1'; + case 'd': return '2'; + } + } + ); + + $this->assertScalarListWithCustomValues($list); + } + + public function testCreateFromFlippedChoicesFlatValuesClosureReceivesKey() + { + $list = $this->factory->createListFromFlippedChoices( + array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D'), + function ($choice, $key) { + switch ($key) { + case 'A': return 'a'; + case 'B': return 'b'; + case 'C': return '1'; + case 'D': return '2'; + } + } + ); + + $this->assertScalarListWithCustomValues($list); + } + + public function testCreateFromFlippedChoicesGrouped() + { + $list = $this->factory->createListFromFlippedChoices( + array( + 'Group 1' => array('a' => 'A', 'b' => 'B'), + 'Group 2' => array('c' => 'C', 'd' => 'D'), + ) + ); + + $this->assertScalarListWithGeneratedValues($list); + } + + public function testCreateFromFlippedChoicesGroupedTraversable() + { + $list = $this->factory->createListFromFlippedChoices( + new \ArrayIterator(array( + 'Group 1' => array('a' => 'A', 'b' => 'B'), + 'Group 2' => array('c' => 'C', 'd' => 'D'), + )) + ); + + $this->assertScalarListWithGeneratedValues($list); + } + + public function testCreateFromFlippedChoicesGroupedValuesAsCallable() + { + $list = $this->factory->createListFromFlippedChoices( + array( + 'Group 1' => array('a' => 'A', 'b' => 'B'), + 'Group 2' => array('c' => 'C', 'd' => 'D'), + ), + array($this, 'getScalarValue') + ); + + $this->assertScalarListWithCustomValues($list); + } + + public function testCreateFromFlippedChoicesGroupedValuesAsClosure() + { + $list = $this->factory->createListFromFlippedChoices( + array( + 'Group 1' => array('a' => 'A', 'b' => 'B'), + 'Group 2' => array('c' => 'C', 'd' => 'D'), + ), + function ($choice) { + switch ($choice) { + case 'a': return 'a'; + case 'b': return 'b'; + case 'c': return '1'; + case 'd': return '2'; + } + } + ); + + $this->assertScalarListWithCustomValues($list); + } + + public function testCreateFromFlippedChoicesGroupedValuesAsClosureReceivesKey() + { + $list = $this->factory->createListFromFlippedChoices( + array( + 'Group 1' => array('a' => 'A', 'b' => 'B'), + 'Group 2' => array('c' => 'C', 'd' => 'D'), + ), + function ($choice, $key) { + switch ($key) { + case 'A': return 'a'; + case 'B': return 'b'; + case 'C': return '1'; + case 'D': return '2'; + } + } + ); + + $this->assertScalarListWithCustomValues($list); + } + + public function testCreateFromLoader() + { + $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + + $list = $this->factory->createListFromLoader($loader); + + $this->assertEquals(new LazyChoiceList($loader), $list); + } + + public function testCreateFromLoaderWithValues() + { + $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + + $value = function () {}; + $list = $this->factory->createListFromLoader($loader, $value); + + $this->assertEquals(new LazyChoiceList($loader, $value), $list); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateFromLoaderFailsIfValuesNotCallableOrString() + { + $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + + $this->factory->createListFromLoader($loader, new \stdClass()); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateViewFailsIfPreferredChoicesInvalid() + { + $this->factory->createView($this->list, new \stdClass()); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateViewFailsIfLabelInvalid() + { + $this->factory->createView($this->list, null, new \stdClass()); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateViewFailsIfIndexInvalid() + { + $this->factory->createView($this->list, null, null, new \stdClass()); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateViewFailsIfGroupByInvalid() + { + $this->factory->createView($this->list, null, null, null, new \stdClass()); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testCreateViewFailsIfAttrInvalid() + { + $this->factory->createView($this->list, null, null, null, null, new \stdClass()); + } + + public function testCreateViewFlat() + { + $view = $this->factory->createView($this->list); + + $this->assertEquals(new ChoiceListView( + array( + 0 => new ChoiceView('A', '0', $this->obj1), + 1 => new ChoiceView('B', '1', $this->obj2), + 2 => new ChoiceView('C', '2', $this->obj3), + 3 => new ChoiceView('D', '3', $this->obj4), + ), array() + ), $view); + } + + public function testCreateViewFlatPreferredChoices() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3) + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatPreferredChoicesEmptyArray() + { + $view = $this->factory->createView( + $this->list, + array() + ); + + $this->assertEquals(new ChoiceListView( + array( + 0 => new ChoiceView('A', '0', $this->obj1), + 1 => new ChoiceView('B', '1', $this->obj2), + 2 => new ChoiceView('C', '2', $this->obj3), + 3 => new ChoiceView('D', '3', $this->obj4), + ), array() + ), $view); + } + + public function testCreateViewFlatPreferredChoicesAsCallable() + { + $view = $this->factory->createView( + $this->list, + array($this, 'isPreferred') + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatPreferredChoicesAsClosure() + { + $obj2 = $this->obj2; + $obj3 = $this->obj3; + + $view = $this->factory->createView( + $this->list, + function ($object) use ($obj2, $obj3) { + return $obj2 === $object || $obj3 === $object; + } + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatPreferredChoicesClosureReceivesKey() + { + $obj2 = $this->obj2; + $obj3 = $this->obj3; + + $view = $this->factory->createView( + $this->list, + function ($object, $key) use ($obj2, $obj3) { + return 'B' === $key || 'C' === $key; + } + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatLabelAsCallable() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + array($this, 'getLabel') + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatLabelAsClosure() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + function ($object) { + return $object->label; + } + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatLabelClosureReceivesKey() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + function ($object, $key) { + return $key; + } + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatIndexAsCallable() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + array($this, 'getFormIndex') + ); + + $this->assertFlatViewWithCustomIndices($view); + } + + public function testCreateViewFlatIndexAsClosure() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + function ($object) { + return $object->index; + } + ); + + $this->assertFlatViewWithCustomIndices($view); + } + + public function testCreateViewFlatIndexClosureReceivesKey() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + function ($object, $key) { + switch ($key) { + case 'A': return 'w'; + case 'B': return 'x'; + case 'C': return 'y'; + case 'D': return 'z'; + } + } + ); + + $this->assertFlatViewWithCustomIndices($view); + } + + public function testCreateViewFlatGroupByAsArray() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + array( + 'Group 1' => array('A' => true, 'B' => true), + 'Group 2' => array('C' => true, 'D' => true), + ) + ); + + $this->assertGroupedView($view); + } + + public function testCreateViewFlatGroupByAsTraversable() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + new \ArrayIterator(array( + 'Group 1' => array('A' => true, 'B' => true), + 'Group 2' => array('C' => true, 'D' => true), + )) + ); + + $this->assertGroupedView($view); + } + + public function testCreateViewFlatGroupByEmpty() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + array() // ignored + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatGroupByAsCallable() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + array($this, 'getGroup') + ); + + $this->assertGroupedView($view); + } + + public function testCreateViewFlatGroupByAsClosure() + { + $obj1 = $this->obj1; + $obj2 = $this->obj2; + + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + function ($object) use ($obj1, $obj2) { + return $obj1 === $object || $obj2 === $object ? 'Group 1' + : 'Group 2'; + } + ); + + $this->assertGroupedView($view); + } + + public function testCreateViewFlatGroupByClosureReceivesKey() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + function ($object, $key) { + return 'A' === $key || 'B' === $key ? 'Group 1' : 'Group 2'; + } + ); + + $this->assertGroupedView($view); + } + + public function testCreateViewFlatAttrAsArray() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + null, // group + array( + 'B' => array('attr1' => 'value1'), + 'C' => array('attr2' => 'value2') + ) + ); + + $this->assertFlatViewWithAttr($view); + } + + public function testCreateViewFlatAttrEmpty() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + null, // group + array() + ); + + $this->assertFlatView($view); + } + + public function testCreateViewFlatAttrAsCallable() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + null, // group + array($this, 'getAttr') + ); + + $this->assertFlatViewWithAttr($view); + } + + public function testCreateViewFlatAttrAsClosure() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + null, // group + function ($object) { + return $object->attr; + } + ); + + $this->assertFlatViewWithAttr($view); + } + + public function testCreateViewFlatAttrClosureReceivesKey() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + null, // group + function ($object, $key) { + switch ($key) { + case 'B': return array('attr1' => 'value1'); + case 'C': return array('attr2' => 'value2'); + default: return array(); + } + } + ); + + $this->assertFlatViewWithAttr($view); + } + + public function testCreateViewForLegacyChoiceList() + { + $preferred = array(new ChoiceView('Preferred', 'x', 'x')); + $other = array(new ChoiceView('Other', 'y', 'y')); + + $list = $this->getMock('Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface'); + + $list->expects($this->once()) + ->method('getPreferredViews') + ->will($this->returnValue($preferred)); + $list->expects($this->once()) + ->method('getRemainingViews') + ->will($this->returnValue($other)); + + $view = $this->factory->createView($list); + + $this->assertSame($other, $view->choices); + $this->assertSame($preferred, $view->preferredChoices); + } + + private function assertScalarListWithGeneratedValues(ChoiceListInterface $list) + { + $this->assertSame(array( + 'A' => 'a', + 'B' => 'b', + 'C' => 'c', + 'D' => 'd', + ), $list->getChoices()); + + $this->assertSame(array( + 'A' => 'a', + 'B' => 'b', + 'C' => 'c', + 'D' => 'd', + ), $list->getValues()); + } + + private function assertObjectListWithGeneratedValues(ChoiceListInterface $list) + { + $this->assertSame(array( + 'A' => $this->obj1, + 'B' => $this->obj2, + 'C' => $this->obj3, + 'D' => $this->obj4, + ), $list->getChoices()); + + $this->assertSame(array( + 'A' => '0', + 'B' => '1', + 'C' => '2', + 'D' => '3', + ), $list->getValues()); + } + + private function assertScalarListWithCustomValues(ChoiceListInterface $list) + { + $this->assertSame(array( + 'A' => 'a', + 'B' => 'b', + 'C' => 'c', + 'D' => 'd', + ), $list->getChoices()); + + $this->assertSame(array( + 'A' => 'a', + 'B' => 'b', + 'C' => '1', + 'D' => '2', + ), $list->getValues()); + } + + private function assertObjectListWithCustomValues(ChoiceListInterface $list) + { + $this->assertSame(array( + 'A' => $this->obj1, + 'B' => $this->obj2, + 'C' => $this->obj3, + 'D' => $this->obj4, + ), $list->getChoices()); + + $this->assertSame(array( + 'A' => 'a', + 'B' => 'b', + 'C' => '1', + 'D' => '2', + ), $list->getValues()); + } + + private function assertFlatView($view) + { + $this->assertEquals(new ChoiceListView( + array( + 0 => new ChoiceView('A', '0', $this->obj1), + 3 => new ChoiceView('D', '3', $this->obj4), + ), array( + 1 => new ChoiceView('B', '1', $this->obj2), + 2 => new ChoiceView('C', '2', $this->obj3), + ) + ), $view); + } + + private function assertFlatViewWithCustomIndices($view) + { + $this->assertEquals(new ChoiceListView( + array( + 'w' => new ChoiceView('A', '0', $this->obj1), + 'z' => new ChoiceView('D', '3', $this->obj4), + ), array( + 'x' => new ChoiceView('B', '1', $this->obj2), + 'y' => new ChoiceView('C', '2', $this->obj3), + ) + ), $view); + } + + private function assertFlatViewWithAttr($view) + { + $this->assertEquals(new ChoiceListView( + array( + 0 => new ChoiceView('A', '0', $this->obj1), + 3 => new ChoiceView('D', '3', $this->obj4), + ), array( + 1 => new ChoiceView( + 'B', + '1', + $this->obj2, + array('attr1' => 'value1') + ), + 2 => new ChoiceView( + 'C', + '2', + $this->obj3, + array('attr2' => 'value2') + ), + ) + ), $view); + } + + private function assertGroupedView($view) + { + $this->assertEquals(new ChoiceListView( + array( + 'Group 1' => new ChoiceGroupView( + 'Group 1', + array(0 => new ChoiceView('A', '0', $this->obj1)) + ), + 'Group 2' => new ChoiceGroupView( + 'Group 2', + array(3 => new ChoiceView('D', '3', $this->obj4)) + ), + ), array( + 'Group 1' => new ChoiceGroupView( + 'Group 1', + array(1 => new ChoiceView('B', '1', $this->obj2)) + ), + 'Group 2' => new ChoiceGroupView( + 'Group 2', + array(2 => new ChoiceView('C', '2', $this->obj3)) + ), + ) + ), $view); + } +} diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/PropertyAccessDecoratorTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/PropertyAccessDecoratorTest.php new file mode 100644 index 0000000000..8697a9cac5 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/PropertyAccessDecoratorTest.php @@ -0,0 +1,338 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\ChoiceList\Factory; + +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; +use Symfony\Component\PropertyAccess\PropertyPath; + +/** + * @author Bernhard Schussek + */ +class PropertyAccessDecoratorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $decoratedFactory; + + /** + * @var PropertyAccessDecorator + */ + private $factory; + + protected function setUp() + { + $this->decoratedFactory = $this->getMock('Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface'); + $this->factory = new PropertyAccessDecorator($this->decoratedFactory); + } + + public function testCreateFromChoicesPropertyPath() + { + $choices = array((object) array('property' => 'value')); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($choices, $callback) { + return array_map($callback, $choices); + })); + + $this->assertSame(array('value'), $this->factory->createListFromChoices($choices, 'property')); + } + + public function testCreateFromChoicesPropertyPathInstance() + { + $choices = array((object) array('property' => 'value')); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($choices, $callback) { + return array_map($callback, $choices); + })); + + $this->assertSame(array('value'), $this->factory->createListFromChoices($choices, new PropertyPath('property'))); + } + + public function testCreateFromFlippedChoices() + { + // Property paths are not supported here, because array keys can never + // be objects anyway + $choices = array('a' => 'A'); + $value = 'foobar'; + $list = new \stdClass(); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromFlippedChoices') + ->with($choices, $value) + ->will($this->returnValue($list)); + + $this->assertSame($list, $this->factory->createListFromFlippedChoices($choices, $value)); + } + + public function testCreateFromLoaderPropertyPath() + { + $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromLoader') + ->with($loader, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($loader, $callback) { + return $callback((object) array('property' => 'value')); + })); + + $this->assertSame('value', $this->factory->createListFromLoader($loader, 'property')); + } + + public function testCreateFromLoaderPropertyPathInstance() + { + $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createListFromLoader') + ->with($loader, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($loader, $callback) { + return $callback((object) array('property' => 'value')); + })); + + $this->assertSame('value', $this->factory->createListFromLoader($loader, new PropertyPath('property'))); + } + + public function testCreateViewPreferredChoicesAsPropertyPath() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred) { + return $preferred((object) array('property' => true)); + })); + + $this->assertTrue($this->factory->createView( + $list, + 'property' + )); + } + + public function testCreateViewPreferredChoicesAsPropertyPathInstance() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred) { + return $preferred((object) array('property' => true)); + })); + + $this->assertTrue($this->factory->createView( + $list, + new PropertyPath('property') + )); + } + + // https://github.com/symfony/symfony/issues/5494 + public function testCreateViewAssumeNullIfPreferredChoicesPropertyPathUnreadable() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred) { + return $preferred((object) array('category' => null)); + })); + + $this->assertFalse($this->factory->createView( + $list, + 'category.preferred' + )); + } + + public function testCreateViewLabelsAsPropertyPath() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label) { + return $label((object) array('property' => 'label')); + })); + + $this->assertSame('label', $this->factory->createView( + $list, + null, // preferred choices + 'property' + )); + } + + public function testCreateViewLabelsAsPropertyPathInstance() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label) { + return $label((object) array('property' => 'label')); + })); + + $this->assertSame('label', $this->factory->createView( + $list, + null, // preferred choices + new PropertyPath('property') + )); + } + + public function testCreateViewIndicesAsPropertyPath() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label, $index) { + return $index((object) array('property' => 'index')); + })); + + $this->assertSame('index', $this->factory->createView( + $list, + null, // preferred choices + null, // label + 'property' + )); + } + + public function testCreateViewIndicesAsPropertyPathInstance() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label, $index) { + return $index((object) array('property' => 'index')); + })); + + $this->assertSame('index', $this->factory->createView( + $list, + null, // preferred choices + null, // label + new PropertyPath('property') + )); + } + + public function testCreateViewGroupsAsPropertyPath() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label, $index, $groupBy) { + return $groupBy((object) array('property' => 'group')); + })); + + $this->assertSame('group', $this->factory->createView( + $list, + null, // preferred choices + null, // label + null, // index + 'property' + )); + } + + public function testCreateViewGroupsAsPropertyPathInstance() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label, $index, $groupBy) { + return $groupBy((object) array('property' => 'group')); + })); + + $this->assertSame('group', $this->factory->createView( + $list, + null, // preferred choices + null, // label + null, // index + new PropertyPath('property') + )); + } + + // https://github.com/symfony/symfony/issues/5494 + public function testCreateViewAssumeNullIfGroupsPropertyPathUnreadable() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label, $index, $groupBy) { + return $groupBy((object) array('group' => null)); + })); + + $this->assertNull($this->factory->createView( + $list, + null, // preferred choices + null, // label + null, // index + 'group.name' + )); + } + + public function testCreateViewAttrAsPropertyPath() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, null, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label, $index, $groupBy, $attr) { + return $attr((object) array('property' => 'attr')); + })); + + $this->assertSame('attr', $this->factory->createView( + $list, + null, // preferred choices + null, // label + null, // index + null, // groups + 'property' + )); + } + + public function testCreateViewAttrAsPropertyPathInstance() + { + $list = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + + $this->decoratedFactory->expects($this->once()) + ->method('createView') + ->with($list, null, null, null, null, $this->isInstanceOf('\Closure')) + ->will($this->returnCallback(function ($list, $preferred, $label, $index, $groupBy, $attr) { + return $attr((object) array('property' => 'attr')); + })); + + $this->assertSame('attr', $this->factory->createView( + $list, + null, // preferred choices + null, // label + null, // index + null, // groups + new PropertyPath('property') + )); + } +} diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/LazyChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/LazyChoiceListTest.php new file mode 100644 index 0000000000..2993721c82 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ChoiceList/LazyChoiceListTest.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\ChoiceList; + +use Symfony\Component\Form\ChoiceList\LazyChoiceList; + +/** + * @author Bernhard Schussek + */ +class LazyChoiceListTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var LazyChoiceList + */ + private $list; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $innerList; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $loader; + + private $value; + + protected function setUp() + { + $this->innerList = $this->getMock('Symfony\Component\Form\ChoiceList\ChoiceListInterface'); + $this->loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); + $this->value = function () {}; + $this->list = new LazyChoiceList($this->loader, $this->value); + } + + public function testGetChoicesLoadsInnerListOnFirstCall() + { + $this->loader->expects($this->once()) + ->method('loadChoiceList') + ->with($this->value) + ->will($this->returnValue($this->innerList)); + + $this->innerList->expects($this->exactly(2)) + ->method('getChoices') + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->list->getChoices()); + $this->assertSame('RESULT', $this->list->getChoices()); + } + + public function testGetValuesLoadsInnerListOnFirstCall() + { + $this->loader->expects($this->once()) + ->method('loadChoiceList') + ->with($this->value) + ->will($this->returnValue($this->innerList)); + + $this->innerList->expects($this->exactly(2)) + ->method('getValues') + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->list->getValues()); + $this->assertSame('RESULT', $this->list->getValues()); + } + + public function testGetChoicesForValuesForwardsCallIfListNotLoaded() + { + $this->loader->expects($this->exactly(2)) + ->method('loadChoicesForValues') + ->with(array('a', 'b')) + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->list->getChoicesForValues(array('a', 'b'))); + $this->assertSame('RESULT', $this->list->getChoicesForValues(array('a', 'b'))); + } + + public function testGetChoicesForValuesUsesLoadedList() + { + $this->loader->expects($this->once()) + ->method('loadChoiceList') + ->with($this->value) + ->will($this->returnValue($this->innerList)); + + $this->loader->expects($this->never()) + ->method('loadChoicesForValues'); + + $this->innerList->expects($this->exactly(2)) + ->method('getChoicesForValues') + ->with(array('a', 'b')) + ->will($this->returnValue('RESULT')); + + // load choice list + $this->list->getChoices(); + + $this->assertSame('RESULT', $this->list->getChoicesForValues(array('a', 'b'))); + $this->assertSame('RESULT', $this->list->getChoicesForValues(array('a', 'b'))); + } + + public function testGetValuesForChoicesForwardsCallIfListNotLoaded() + { + $this->loader->expects($this->exactly(2)) + ->method('loadValuesForChoices') + ->with(array('a', 'b')) + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->list->getValuesForChoices(array('a', 'b'))); + $this->assertSame('RESULT', $this->list->getValuesForChoices(array('a', 'b'))); + } + + public function testGetValuesForChoicesUsesLoadedList() + { + $this->loader->expects($this->once()) + ->method('loadChoiceList') + ->with($this->value) + ->will($this->returnValue($this->innerList)); + + $this->loader->expects($this->never()) + ->method('loadValuesForChoices'); + + $this->innerList->expects($this->exactly(2)) + ->method('getValuesForChoices') + ->with(array('a', 'b')) + ->will($this->returnValue('RESULT')); + + // load choice list + $this->list->getChoices(); + + $this->assertSame('RESULT', $this->list->getValuesForChoices(array('a', 'b'))); + $this->assertSame('RESULT', $this->list->getValuesForChoices(array('a', 'b'))); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php index 17972cbc0a..6a0b6db2ec 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -11,8 +11,9 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase { @@ -66,6 +67,16 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->objectChoices = null; } + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testChoicesOptionExpectsArrayOrTraversable() + { + $this->factory->create('choice', null, array( + 'choices' => new \stdClass(), + )); + } + /** * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException */ @@ -76,6 +87,16 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase )); } + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testChoiceLoaderOptionExpectsChoiceLoaderInterface() + { + $this->factory->create('choice', null, array( + 'choice_loader' => new \stdClass(), + )); + } + public function testChoiceListAndChoicesCanBeEmpty() { $this->factory->create('choice'); @@ -236,7 +257,118 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertFalse($form->isSynchronized()); } + public function testSubmitSingleNonExpandedNull() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->choices, + )); + + $form->submit(null); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleNonExpandedNullNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => array(), + )); + + $form->submit(null); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + + public function testSubmitSingleNonExpandedEmpty() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->choices, + )); + + $form->submit(''); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleNonExpandedEmptyNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => array(), + )); + + $form->submit(''); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + + public function testSubmitSingleNonExpandedFalse() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->choices, + )); + + $form->submit(false); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleNonExpandedFalseNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => array(), + )); + + $form->submit(false); + + $this->assertNull($form->getData()); + $this->assertSame('', $form->getViewData()); + } + public function testSubmitSingleNonExpandedObjectChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->objectChoices, + 'choices_as_values' => true, + 'choice_label' => 'name', + 'choice_value' => 'id', + )); + + // "id" value of the second entry + $form->submit('2'); + + $this->assertEquals($this->objectChoices[1], $form->getData()); + $this->assertEquals('2', $form->getViewData()); + } + + public function testSubmitSingleNonExpandedObjectChoicesBc() { $form = $this->factory->create('choice', null, array( 'multiple' => false, @@ -273,6 +405,37 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertEquals(array('a', 'b'), $form->getViewData()); } + public function testSubmitMultipleNonExpandedEmpty() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => true, + 'expanded' => false, + 'choices' => $this->choices, + )); + + $form->submit(array()); + + $this->assertSame(array(), $form->getData()); + $this->assertSame(array(), $form->getViewData()); + } + + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitMultipleNonExpandedEmptyNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => true, + 'expanded' => false, + 'choices' => array(), + )); + + $form->submit(array()); + + $this->assertSame(array(), $form->getData()); + $this->assertSame(array(), $form->getViewData()); + } + public function testSubmitMultipleNonExpandedInvalidScalarChoice() { $form = $this->factory->create('choice', null, array( @@ -304,6 +467,23 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase } public function testSubmitMultipleNonExpandedObjectChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => true, + 'expanded' => false, + 'choices' => $this->objectChoices, + 'choices_as_values' => true, + 'choice_label' => 'name', + 'choice_value' => 'id', + )); + + $form->submit(array('2', '3')); + + $this->assertEquals(array($this->objectChoices[1], $this->objectChoices[2]), $form->getData()); + $this->assertEquals(array('2', '3'), $form->getViewData()); + } + + public function testSubmitMultipleNonExpandedObjectChoicesBc() { $form = $this->factory->create('choice', null, array( 'multiple' => true, @@ -337,13 +517,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $form->submit('b'); $this->assertSame('b', $form->getData()); - $this->assertSame(array( - 0 => false, - 1 => true, - 2 => false, - 3 => false, - 4 => false, - ), $form->getViewData()); + $this->assertSame('b', $form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -399,14 +573,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $form->submit('b'); $this->assertSame('b', $form->getData()); - $this->assertSame(array( - 0 => false, - 1 => true, - 2 => false, - 3 => false, - 4 => false, - 'placeholder' => false, - ), $form->getViewData()); + $this->assertSame('b', $form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -464,13 +631,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $form->submit(null); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -486,6 +647,26 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedRequiredNullNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => true, + 'choices' => array(), + )); + + $form->submit(null); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedRequiredEmpty() { $form = $this->factory->create('choice', null, array( @@ -498,13 +679,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $form->submit(''); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -520,6 +695,26 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedRequiredEmptyNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => true, + 'choices' => array(), + )); + + $form->submit(''); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedRequiredFalse() { $form = $this->factory->create('choice', null, array( @@ -532,13 +727,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $form->submit(false); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -554,6 +743,26 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedRequiredFalseNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => true, + 'choices' => array(), + )); + + $form->submit(false); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedNonRequiredNull() { $form = $this->factory->create('choice', null, array( @@ -566,14 +775,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $form->submit(null); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - 'placeholder' => true, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -591,6 +793,26 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedNonRequiredNullNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => false, + 'choices' => array(), + )); + + $form->submit(null); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedNonRequiredEmpty() { $form = $this->factory->create('choice', null, array( @@ -603,14 +825,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $form->submit(''); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - 'placeholder' => true, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -628,6 +843,26 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedNonRequiredEmptyNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => false, + 'choices' => array(), + )); + + $form->submit(''); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedNonRequiredFalse() { $form = $this->factory->create('choice', null, array( @@ -640,14 +875,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $form->submit(false); $this->assertNull($form->getData()); - $this->assertSame(array( - 0 => false, - 1 => false, - 2 => false, - 3 => false, - 4 => false, - 'placeholder' => true, - ), $form->getViewData()); + $this->assertNull($form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -665,6 +893,26 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitSingleExpandedNonRequiredFalseNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'required' => false, + 'choices' => array(), + )); + + $form->submit(false); + + $this->assertNull($form->getData()); + $this->assertNull($form->getViewData()); + $this->assertEmpty($form->getExtraData()); + $this->assertTrue($form->isSynchronized()); + } + public function testSubmitSingleExpandedWithEmptyChild() { $form = $this->factory->create('choice', null, array( @@ -686,6 +934,32 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase } public function testSubmitSingleExpandedObjectChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => false, + 'expanded' => true, + 'choices' => $this->objectChoices, + 'choices_as_values' => true, + 'choice_label' => 'name', + 'choice_value' => 'id', + )); + + $form->submit('2'); + + $this->assertSame($this->objectChoices[1], $form->getData()); + $this->assertFalse($form[0]->getData()); + $this->assertTrue($form[1]->getData()); + $this->assertFalse($form[2]->getData()); + $this->assertFalse($form[3]->getData()); + $this->assertFalse($form[4]->getData()); + $this->assertNull($form[0]->getViewData()); + $this->assertSame('2', $form[1]->getViewData()); + $this->assertNull($form[2]->getViewData()); + $this->assertNull($form[3]->getViewData()); + $this->assertNull($form[4]->getViewData()); + } + + public function testSubmitSingleExpandedObjectChoicesBc() { $form = $this->factory->create('choice', null, array( 'multiple' => false, @@ -750,13 +1024,7 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $form->submit(array('a', 'c')); $this->assertSame(array('a', 'c'), $form->getData()); - $this->assertSame(array( - 0 => true, - 1 => false, - 2 => true, - 3 => false, - 4 => false, - ), $form->getViewData()); + $this->assertSame(array('a', 'c'), $form->getViewData()); $this->assertEmpty($form->getExtraData()); $this->assertTrue($form->isSynchronized()); @@ -849,6 +1117,22 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertNull($form[4]->getViewData()); } + // In edge cases (for example, when choices are loaded dynamically by a + // loader), the choices may be empty. Make sure to behave the same as when + // choices are available. + public function testSubmitMultipleExpandedEmptyNoChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => true, + 'expanded' => true, + 'choices' => array(), + )); + + $form->submit(array()); + + $this->assertSame(array(), $form->getData()); + } + public function testSubmitMultipleExpandedWithEmptyChild() { $form = $this->factory->create('choice', null, array( @@ -873,6 +1157,32 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase } public function testSubmitMultipleExpandedObjectChoices() + { + $form = $this->factory->create('choice', null, array( + 'multiple' => true, + 'expanded' => true, + 'choices' => $this->objectChoices, + 'choices_as_values' => true, + 'choice_label' => 'name', + 'choice_value' => 'id', + )); + + $form->submit(array('1', '2')); + + $this->assertSame(array($this->objectChoices[0], $this->objectChoices[1]), $form->getData()); + $this->assertTrue($form[0]->getData()); + $this->assertTrue($form[1]->getData()); + $this->assertFalse($form[2]->getData()); + $this->assertFalse($form[3]->getData()); + $this->assertFalse($form[4]->getData()); + $this->assertSame('1', $form[0]->getViewData()); + $this->assertSame('2', $form[1]->getViewData()); + $this->assertNull($form[2]->getViewData()); + $this->assertNull($form[3]->getViewData()); + $this->assertNull($form[4]->getViewData()); + } + + public function testSubmitMultipleExpandedObjectChoicesBc() { $form = $this->factory->create('choice', null, array( 'multiple' => true, @@ -1134,10 +1444,10 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('a', 'a', 'A'), - new ChoiceView('b', 'b', 'B'), - new ChoiceView('c', 'c', 'C'), - new ChoiceView('d', 'd', 'D'), + new ChoiceView('A', 'a', 'a'), + new ChoiceView('B', 'b', 'b'), + new ChoiceView('C', 'c', 'c'), + new ChoiceView('D', 'd', 'd'), ), $view->vars['choices']); } @@ -1151,12 +1461,12 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $view = $form->createView(); $this->assertEquals(array( - 0 => new ChoiceView('a', 'a', 'A'), - 2 => new ChoiceView('c', 'c', 'C'), + 0 => new ChoiceView('A', 'a', 'a'), + 2 => new ChoiceView('C', 'c', 'c'), ), $view->vars['choices']); $this->assertEquals(array( - 1 => new ChoiceView('b', 'b', 'B'), - 3 => new ChoiceView('d', 'd', 'D'), + 1 => new ChoiceView('B', 'b', 'b'), + 3 => new ChoiceView('D', 'd', 'd'), ), $view->vars['preferred_choices']); } @@ -1169,21 +1479,21 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $view = $form->createView(); $this->assertEquals(array( - 'Symfony' => array( - 0 => new ChoiceView('a', 'a', 'Bernhard'), - 2 => new ChoiceView('c', 'c', 'Kris'), - ), - 'Doctrine' => array( - 4 => new ChoiceView('e', 'e', 'Roman'), - ), + 'Symfony' => new ChoiceGroupView('Symfony', array( + 0 => new ChoiceView('Bernhard', 'a', 'a'), + 2 => new ChoiceView('Kris', 'c', 'c'), + )), + 'Doctrine' => new ChoiceGroupView('Doctrine', array( + 4 => new ChoiceView('Roman', 'e', 'e'), + )), ), $view->vars['choices']); $this->assertEquals(array( - 'Symfony' => array( - 1 => new ChoiceView('b', 'b', 'Fabien'), - ), - 'Doctrine' => array( - 3 => new ChoiceView('d', 'd', 'Jon'), - ), + 'Symfony' => new ChoiceGroupView('Symfony', array( + 1 => new ChoiceView('Fabien', 'b', 'b'), + )), + 'Doctrine' => new ChoiceGroupView('Doctrine', array( + 3 => new ChoiceView('Jon', 'd', 'd'), + )), ), $view->vars['preferred_choices']); } @@ -1194,15 +1504,18 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $obj3 = (object) array('value' => 'c', 'label' => 'C'); $obj4 = (object) array('value' => 'd', 'label' => 'D'); $form = $this->factory->create('choice', null, array( - 'choice_list' => new ObjectChoiceList(array($obj1, $obj2, $obj3, $obj4), 'label', array(), null, 'value'), + 'choices' => array($obj1, $obj2, $obj3, $obj4), + 'choices_as_values' => true, + 'choice_label' => 'label', + 'choice_value' => 'value', )); $view = $form->createView(); $this->assertEquals(array( - new ChoiceView($obj1, 'a', 'A'), - new ChoiceView($obj2, 'b', 'B'), - new ChoiceView($obj3, 'c', 'C'), - new ChoiceView($obj4, 'd', 'D'), + new ChoiceView('A', 'a', $obj1), + new ChoiceView('B', 'b', $obj2), + new ChoiceView('C', 'c', $obj3), + new ChoiceView('D', 'd', $obj4), ), $view->vars['choices']); } @@ -1226,47 +1539,6 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase )); } - // https://github.com/symfony/symfony/issues/10409 - public function testReuseNonUtf8ChoiceLists() - { - $form1 = $this->factory->createNamed('name', 'choice', null, array( - 'choices' => array( - 'meter' => 'm', - 'millimeter' => 'mm', - 'micrometer' => chr(181).'meter', - ), - )); - - $form2 = $this->factory->createNamed('name', 'choice', null, array( - 'choices' => array( - 'meter' => 'm', - 'millimeter' => 'mm', - 'micrometer' => chr(181).'meter', - ), - )); - - $form3 = $this->factory->createNamed('name', 'choice', null, array( - 'choices' => array( - 'meter' => 'm', - 'millimeter' => 'mm', - 'micrometer' => null, - ), - )); - - // $form1 and $form2 use the same ChoiceList - $this->assertSame( - $form1->getConfig()->getOption('choice_list'), - $form2->getConfig()->getOption('choice_list') - ); - - // $form3 doesn't, but used to use the same when using json_encode() - // instead of serialize for the hashing algorithm - $this->assertNotSame( - $form1->getConfig()->getOption('choice_list'), - $form3->getConfig()->getOption('choice_list') - ); - } - public function testInitializeWithDefaultObjectChoice() { $obj1 = (object) array('value' => 'a', 'label' => 'A'); @@ -1275,7 +1547,10 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $obj4 = (object) array('value' => 'd', 'label' => 'D'); $form = $this->factory->create('choice', null, array( - 'choice_list' => new ObjectChoiceList(array($obj1, $obj2, $obj3, $obj4), 'label', array(), null, 'value'), + 'choices' => array($obj1, $obj2, $obj3, $obj4), + 'choices_as_values' => true, + 'choice_label' => 'label', + 'choice_value' => 'value', // Used to break because "data_class" was inferred, which needs to // remain null in every case (because it refers to the view format) 'data' => $obj3, diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/CountryTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/CountryTypeTest.php index 7c2cebb542..3b684f133e 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/CountryTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/CountryTypeTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Test\TypeTestCase as TestCase; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Intl\Util\IntlTestHelper; class CountryTypeTest extends TestCase @@ -31,11 +31,11 @@ class CountryTypeTest extends TestCase $choices = $view->vars['choices']; // Don't check objects for identity - $this->assertContains(new ChoiceView('DE', 'DE', 'Germany'), $choices, '', false, false); - $this->assertContains(new ChoiceView('GB', 'GB', 'United Kingdom'), $choices, '', false, false); - $this->assertContains(new ChoiceView('US', 'US', 'United States'), $choices, '', false, false); - $this->assertContains(new ChoiceView('FR', 'FR', 'France'), $choices, '', false, false); - $this->assertContains(new ChoiceView('MY', 'MY', 'Malaysia'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Germany', 'DE', 'DE'), $choices, '', false, false); + $this->assertContains(new ChoiceView('United Kingdom', 'GB', 'GB'), $choices, '', false, false); + $this->assertContains(new ChoiceView('United States', 'US', 'US'), $choices, '', false, false); + $this->assertContains(new ChoiceView('France', 'FR', 'FR'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Malaysia', 'MY', 'MY'), $choices, '', false, false); } public function testUnknownCountryIsNotIncluded() diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/CurrencyTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/CurrencyTypeTest.php index 702262f580..802c715b0c 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/CurrencyTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/CurrencyTypeTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Test\TypeTestCase as TestCase; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Intl\Util\IntlTestHelper; class CurrencyTypeTest extends TestCase @@ -30,8 +30,8 @@ class CurrencyTypeTest extends TestCase $view = $form->createView(); $choices = $view->vars['choices']; - $this->assertContains(new ChoiceView('EUR', 'EUR', 'Euro'), $choices, '', false, false); - $this->assertContains(new ChoiceView('USD', 'USD', 'US Dollar'), $choices, '', false, false); - $this->assertContains(new ChoiceView('SIT', 'SIT', 'Slovenian Tolar'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Euro', 'EUR', 'EUR'), $choices, '', false, false); + $this->assertContains(new ChoiceView('US Dollar', 'USD', 'USD'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Slovenian Tolar', 'SIT', 'SIT'), $choices, '', false, false); } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php index d8b3312b1f..a658b90465 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\FormError; use Symfony\Component\Form\Test\TypeTestCase as TestCase; use Symfony\Component\Intl\Util\IntlTestHelper; @@ -490,8 +490,8 @@ class DateTypeTest extends TestCase $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('6', '6', '06'), - new ChoiceView('7', '7', '07'), + new ChoiceView('06', '6', '6'), + new ChoiceView('07', '7', '7'), ), $view['month']->vars['choices']); } @@ -505,8 +505,8 @@ class DateTypeTest extends TestCase $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('1', '1', 'Jän'), - new ChoiceView('4', '4', 'Apr.'), + new ChoiceView('Jän', '1', '1'), + new ChoiceView('Apr.', '4', '4'), ), $view['month']->vars['choices']); } @@ -520,8 +520,8 @@ class DateTypeTest extends TestCase $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('1', '1', 'Jänner'), - new ChoiceView('4', '4', 'April'), + new ChoiceView('Jänner', '1', '1'), + new ChoiceView('April', '4', '4'), ), $view['month']->vars['choices']); } @@ -535,8 +535,8 @@ class DateTypeTest extends TestCase $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('1', '1', 'Jänner'), - new ChoiceView('4', '4', 'April'), + new ChoiceView('Jänner', '1', '1'), + new ChoiceView('April', '4', '4'), ), $view['month']->vars['choices']); } @@ -549,8 +549,8 @@ class DateTypeTest extends TestCase $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('6', '6', '06'), - new ChoiceView('7', '7', '07'), + new ChoiceView('06', '6', '6'), + new ChoiceView('07', '7', '7'), ), $view['day']->vars['choices']); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/LanguageTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/LanguageTypeTest.php index e234811887..9445c74fd6 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/LanguageTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/LanguageTypeTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Test\TypeTestCase as TestCase; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Intl\Util\IntlTestHelper; class LanguageTypeTest extends TestCase @@ -30,11 +30,11 @@ class LanguageTypeTest extends TestCase $view = $form->createView(); $choices = $view->vars['choices']; - $this->assertContains(new ChoiceView('en', 'en', 'English'), $choices, '', false, false); - $this->assertContains(new ChoiceView('en_GB', 'en_GB', 'British English'), $choices, '', false, false); - $this->assertContains(new ChoiceView('en_US', 'en_US', 'American English'), $choices, '', false, false); - $this->assertContains(new ChoiceView('fr', 'fr', 'French'), $choices, '', false, false); - $this->assertContains(new ChoiceView('my', 'my', 'Burmese'), $choices, '', false, false); + $this->assertContains(new ChoiceView('English', 'en', 'en'), $choices, '', false, false); + $this->assertContains(new ChoiceView('British English', 'en_GB', 'en_GB'), $choices, '', false, false); + $this->assertContains(new ChoiceView('American English', 'en_US', 'en_US'), $choices, '', false, false); + $this->assertContains(new ChoiceView('French', 'fr', 'fr'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Burmese', 'my', 'my'), $choices, '', false, false); } public function testMultipleLanguagesIsNotIncluded() @@ -43,6 +43,6 @@ class LanguageTypeTest extends TestCase $view = $form->createView(); $choices = $view->vars['choices']; - $this->assertNotContains(new ChoiceView('mul', 'mul', 'Mehrsprachig'), $choices, '', false, false); + $this->assertNotContains(new ChoiceView('Mehrsprachig', 'mul', 'mul'), $choices, '', false, false); } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/LocaleTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/LocaleTypeTest.php index 6c1951a4e9..0b729a3b31 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/LocaleTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/LocaleTypeTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Test\TypeTestCase as TestCase; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Intl\Util\IntlTestHelper; class LocaleTypeTest extends TestCase @@ -30,8 +30,8 @@ class LocaleTypeTest extends TestCase $view = $form->createView(); $choices = $view->vars['choices']; - $this->assertContains(new ChoiceView('en', 'en', 'English'), $choices, '', false, false); - $this->assertContains(new ChoiceView('en_GB', 'en_GB', 'English (United Kingdom)'), $choices, '', false, false); - $this->assertContains(new ChoiceView('zh_Hant_MO', 'zh_Hant_MO', 'Chinese (Traditional, Macau SAR China)'), $choices, '', false, false); + $this->assertContains(new ChoiceView('English', 'en', 'en'), $choices, '', false, false); + $this->assertContains(new ChoiceView('English (United Kingdom)', 'en_GB', 'en_GB'), $choices, '', false, false); + $this->assertContains(new ChoiceView('Chinese (Traditional, Macau SAR China)', 'zh_Hant_MO', 'zh_Hant_MO'), $choices, '', false, false); } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php index dfa8fbc5a1..c3754695b1 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimeTypeTest.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\FormError; use Symfony\Component\Form\Test\TypeTestCase as TestCase; use Symfony\Component\Intl\Util\IntlTestHelper; @@ -319,8 +319,8 @@ class TimeTypeTest extends TestCase $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('6', '6', '06'), - new ChoiceView('7', '7', '07'), + new ChoiceView('06', '6', '6'), + new ChoiceView('07', '7', '7'), ), $view['hour']->vars['choices']); } @@ -333,8 +333,8 @@ class TimeTypeTest extends TestCase $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('6', '6', '06'), - new ChoiceView('7', '7', '07'), + new ChoiceView('06', '6', '6'), + new ChoiceView('07', '7', '7'), ), $view['minute']->vars['choices']); } @@ -348,8 +348,8 @@ class TimeTypeTest extends TestCase $view = $form->createView(); $this->assertEquals(array( - new ChoiceView('6', '6', '06'), - new ChoiceView('7', '7', '07'), + new ChoiceView('06', '6', '6'), + new ChoiceView('07', '7', '7'), ), $view['second']->vars['choices']); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimezoneTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimezoneTypeTest.php index 81df20cbb9..7839954740 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimezoneTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TimezoneTypeTest.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; class TimezoneTypeTest extends \Symfony\Component\Form\Test\TypeTestCase { @@ -22,9 +22,9 @@ class TimezoneTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $choices = $view->vars['choices']; $this->assertArrayHasKey('Africa', $choices); - $this->assertContains(new ChoiceView('Africa/Kinshasa', 'Africa/Kinshasa', 'Kinshasa'), $choices['Africa'], '', false, false); + $this->assertContains(new ChoiceView('Kinshasa', 'Africa/Kinshasa', 'Africa/Kinshasa'), $choices['Africa'], '', false, false); $this->assertArrayHasKey('America', $choices); - $this->assertContains(new ChoiceView('America/New_York', 'America/New_York', 'New York'), $choices['America'], '', false, false); + $this->assertContains(new ChoiceView('New York', 'America/New_York', 'America/New_York'), $choices['America'], '', false, false); } } From 3846b3750ad60e02e55f4dc7b91d06366a1695f8 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 25 Mar 2015 11:41:36 +0100 Subject: [PATCH 04/60] [DoctrineBridge] Fixed: don't cache choice lists if query builders are constructed dynamically --- ...iceLoader.php => DoctrineChoiceLoader.php} | 101 +++++++++--------- .../Form/ChoiceList/EntityChoiceList.php | 2 +- .../Doctrine/Form/Type/DoctrineType.php | 63 ++++++----- .../Bridge/Doctrine/Form/Type/EntityType.php | 65 ++--------- 4 files changed, 97 insertions(+), 134 deletions(-) rename src/Symfony/Bridge/Doctrine/Form/ChoiceList/{EntityChoiceLoader.php => DoctrineChoiceLoader.php} (71%) diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php similarity index 71% rename from src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceLoader.php rename to src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php index d1f7971e63..c00c258ca5 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -23,7 +23,7 @@ use Symfony\Component\Form\Exception\RuntimeException; * * @author Bernhard Schussek */ -class EntityChoiceLoader implements ChoiceLoaderInterface +class DoctrineChoiceLoader implements ChoiceLoaderInterface { /** * @var ChoiceListFactoryInterface @@ -48,7 +48,7 @@ class EntityChoiceLoader implements ChoiceLoaderInterface /** * @var null|EntityLoaderInterface */ - private $entityLoader; + private $objectLoader; /** * The identifier field, unless the identifier is composite @@ -70,20 +70,20 @@ class EntityChoiceLoader implements ChoiceLoaderInterface private $choiceList; /** - * Returns the value of the identifier field of an entity. + * Returns the value of the identifier field of an object. * - * Doctrine must know about this entity, that is, the entity must already + * Doctrine must know about this object, that is, the object must already * be persisted or added to the identity map before. Otherwise an * exception is thrown. * - * This method assumes that the entity has a single-column identifier and + * This method assumes that the object has a single-column identifier and * will return a single value instead of an array. * - * @param object $object The entity for which to get the identifier + * @param object $object The object for which to get the identifier * * @return int|string The identifier value * - * @throws RuntimeException If the entity does not exist in Doctrine's identity map + * @throws RuntimeException If the object does not exist in Doctrine's identity map * * @internal Should not be accessed by user-land code. This method is public * only to be usable as callback. @@ -106,22 +106,23 @@ class EntityChoiceLoader implements ChoiceLoaderInterface * Creates a new choice loader. * * Optionally, an implementation of {@link EntityLoaderInterface} can be - * passed which optimizes the entity loading for one of the Doctrine + * passed which optimizes the object loading for one of the Doctrine * mapper implementations. * * @param ChoiceListFactoryInterface $factory The factory for creating * the loaded choice list * @param ObjectManager $manager The object manager - * @param string $class The entity class name - * @param null|EntityLoaderInterface $entityLoader The entity loader + * @param string $class The class name of the + * loaded objects + * @param null|EntityLoaderInterface $objectLoader The objects loader */ - public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, EntityLoaderInterface $entityLoader = null) + public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, EntityLoaderInterface $objectLoader = null) { $this->factory = $factory; $this->manager = $manager; $this->classMetadata = $manager->getClassMetadata($class); $this->class = $this->classMetadata->getName(); - $this->entityLoader = $entityLoader; + $this->objectLoader = $objectLoader; $identifier = $this->classMetadata->getIdentifierFieldNames(); @@ -140,51 +141,51 @@ class EntityChoiceLoader implements ChoiceLoaderInterface return $this->choiceList; } - $entities = $this->entityLoader - ? $this->entityLoader->getEntities() + $objects = $this->objectLoader + ? $this->objectLoader->getEntities() : $this->manager->getRepository($this->class)->findAll(); // If the class has a multi-column identifier, we cannot index the - // entities by their IDs + // objects by their IDs if ($this->compositeId) { - $this->choiceList = $this->factory->createListFromChoices($entities, $value); + $this->choiceList = $this->factory->createListFromChoices($objects, $value); return $this->choiceList; } - // Index the entities by ID - $entitiesById = array(); + // Index the objects by ID + $objectsById = array(); - foreach ($entities as $entity) { - $id = self::getIdValue($this->manager, $this->classMetadata, $entity); - $entitiesById[$id] = $entity; + foreach ($objects as $object) { + $id = self::getIdValue($this->manager, $this->classMetadata, $object); + $objectsById[$id] = $object; } - $this->choiceList = $this->factory->createListFromChoices($entitiesById, $value); + $this->choiceList = $this->factory->createListFromChoices($objectsById, $value); return $this->choiceList; } /** - * Loads the values corresponding to the given entities. + * Loads the values corresponding to the given objects. * * The values are returned with the same keys and in the same order as the - * corresponding entities in the given array. + * corresponding objects in the given array. * * Optionally, a callable can be passed for generating the choice values. - * The callable receives the entity as first and the array key as the second + * The callable receives the object as first and the array key as the second * argument. * - * @param array $entities An array of entities. Non-existing entities - * in this array are ignored + * @param array $objects An array of objects. Non-existing objects in + * this array are ignored * @param null|callable $value The callable generating the choice values * * @return string[] An array of choice values */ - public function loadValuesForChoices(array $entities, $value = null) + public function loadValuesForChoices(array $objects, $value = null) { // Performance optimization - if (empty($entities)) { + if (empty($objects)) { return array(); } @@ -195,41 +196,41 @@ class EntityChoiceLoader implements ChoiceLoaderInterface if (!$this->choiceList && !$this->compositeId) { $values = array(); - // Maintain order and indices of the given entities - foreach ($entities as $i => $entity) { - if ($entity instanceof $this->class) { + // Maintain order and indices of the given objects + foreach ($objects as $i => $object) { + if ($object instanceof $this->class) { // Make sure to convert to the right format - $values[$i] = (string) self::getIdValue($this->manager, $this->classMetadata, $entity); + $values[$i] = (string) self::getIdValue($this->manager, $this->classMetadata, $object); } } return $values; } - return $this->loadChoiceList($value)->getValuesForChoices($entities); + return $this->loadChoiceList($value)->getValuesForChoices($objects); } /** - * Loads the entities corresponding to the given values. + * Loads the objects corresponding to the given values. * - * The entities are returned with the same keys and in the same order as the + * The objects are returned with the same keys and in the same order as the * corresponding values in the given array. * * Optionally, a callable can be passed for generating the choice values. - * The callable receives the entity as first and the array key as the second + * The callable receives the object as first and the array key as the second * argument. * * @param string[] $values An array of choice values. Non-existing * values in this array are ignored * @param null|callable $value The callable generating the choice values * - * @return array An array of entities + * @return array An array of objects */ public function loadChoicesForValues(array $values, $value = null) { // Performance optimization // Also prevents the generation of "WHERE id IN ()" queries through the - // entity loader. At least with MySQL and on the development machine + // object loader. At least with MySQL and on the development machine // this was tested on, no exception was thrown for such invalid // statements, consequently no test fails when this code is removed. // https://github.com/symfony/symfony/pull/8981#issuecomment-24230557 @@ -237,29 +238,29 @@ class EntityChoiceLoader implements ChoiceLoaderInterface return array(); } - // Optimize performance in case we have an entity loader and + // Optimize performance in case we have an object loader and // a single-field identifier - if (!$this->choiceList && !$this->compositeId && $this->entityLoader) { - $unorderedEntities = $this->entityLoader->getEntitiesByIds($this->idField, $values); - $entitiesById = array(); - $entities = array(); + if (!$this->choiceList && !$this->compositeId && $this->objectLoader) { + $unorderedObjects = $this->objectLoader->getEntitiesByIds($this->idField, $values); + $objectsById = array(); + $objects = array(); // Maintain order and indices from the given $values // An alternative approach to the following loop is to add the // "INDEX BY" clause to the Doctrine query in the loader, // but I'm not sure whether that's doable in a generic fashion. - foreach ($unorderedEntities as $entity) { - $id = self::getIdValue($this->manager, $this->classMetadata, $entity); - $entitiesById[$id] = $entity; + foreach ($unorderedObjects as $object) { + $id = self::getIdValue($this->manager, $this->classMetadata, $object); + $objectsById[$id] = $object; } foreach ($values as $i => $id) { - if (isset($entitiesById[$id])) { - $entities[$i] = $entitiesById[$id]; + if (isset($objectsById[$id])) { + $objects[$i] = $objectsById[$id]; } } - return $entities; + return $objects; } return $this->loadChoiceList($value)->getChoicesForValues($values); diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php index 3566a33d7d..f3d4ff48f6 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php @@ -24,7 +24,7 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; * @author Bernhard Schussek * * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. - * Use {@link EntityChoiceLoader} instead. + * Use {@link DoctrineChoiceLoader} instead. */ class EntityChoiceList extends ObjectChoiceList { diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 6c90a2eeb0..76e78cb1f0 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -13,7 +13,7 @@ namespace Symfony\Bridge\Doctrine\Form\Type; use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\Common\Persistence\ObjectManager; -use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceLoader; +use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener; @@ -41,7 +41,7 @@ abstract class DoctrineType extends AbstractType private $choiceListFactory; /** - * @var EntityChoiceLoader[] + * @var DoctrineChoiceLoader[] */ private $choiceLoaders = array(); @@ -71,32 +71,43 @@ abstract class DoctrineType extends AbstractType $choiceLoader = function (Options $options) use ($choiceListFactory, &$choiceLoaders, $type) { // Unless the choices are given explicitly, load them on demand if (null === $options['choices']) { - $hash = CachingFactoryDecorator::generateHash(array( - $options['em'], - $options['class'], - $options['query_builder'], - $options['loader'], - )); - - if (!isset($choiceLoaders[$hash])) { - if ($options['loader']) { - $loader = $options['loader']; - } elseif (null !== $options['query_builder']) { - $loader = $type->getLoader($options['em'], $options['query_builder'], $options['class']); - } else { - $queryBuilder = $options['em']->getRepository($options['class'])->createQueryBuilder('e'); - $loader = $type->getLoader($options['em'], $queryBuilder, $options['class']); - } - - $choiceLoaders[$hash] = new EntityChoiceLoader( - $choiceListFactory, + // Don't cache if the query builder is constructed dynamically + if ($options['query_builder'] instanceof \Closure) { + $hash = null; + } else { + $hash = CachingFactoryDecorator::generateHash(array( $options['em'], $options['class'], - $loader - ); + $options['query_builder'], + $options['loader'], + )); + + if (isset($choiceLoaders[$hash])) { + return $choiceLoaders[$hash]; + } } - return $choiceLoaders[$hash]; + if ($options['loader']) { + $entityLoader = $options['loader']; + } elseif (null !== $options['query_builder']) { + $entityLoader = $type->getLoader($options['em'], $options['query_builder'], $options['class']); + } else { + $queryBuilder = $options['em']->getRepository($options['class'])->createQueryBuilder('e'); + $entityLoader = $type->getLoader($options['em'], $queryBuilder, $options['class']); + } + + $choiceLoader = new DoctrineChoiceLoader( + $choiceListFactory, + $options['em'], + $options['class'], + $entityLoader + ); + + if (null !== $hash) { + $choiceLoaders[$hash] = $choiceLoader; + } + + return $choiceLoader; } }; @@ -131,7 +142,7 @@ abstract class DoctrineType extends AbstractType }; // The choices are always indexed by ID (see "choices" normalizer - // and EntityChoiceLoader), unless the ID is composite. Then they + // and DoctrineChoiceLoader), unless the ID is composite. Then they // are indexed by an incrementing integer. // Use the ID/incrementing integer as choice value. $choiceValue = function ($entity, $key) { @@ -181,7 +192,7 @@ abstract class DoctrineType extends AbstractType $entitiesById = array(); foreach ($entities as $entity) { - $id = EntityChoiceLoader::getIdValue($om, $classMetadata, $entity); + $id = DoctrineChoiceLoader::getIdValue($om, $classMetadata, $entity); $entitiesById[$id] = $entity; } diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php index e9b89302db..675c289c76 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php @@ -17,71 +17,22 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; class EntityType extends DoctrineType { - /** - * @var ORMQueryBuilderLoader[] - */ - private $loaderCache = array(); - /** * Return the default loader object. * - * @param ObjectManager $manager - * @param mixed $queryBuilder - * @param string $class + * @param ObjectManager $manager + * @param QueryBuilder|\Closure $queryBuilder + * @param string $class * * @return ORMQueryBuilderLoader */ public function getLoader(ObjectManager $manager, $queryBuilder, $class) { - if (!$queryBuilder instanceof QueryBuilder) { - return new ORMQueryBuilderLoader( - $queryBuilder, - $manager, - $class - ); - } - - $queryBuilderHash = $this->getQueryBuilderHash($queryBuilder); - $loaderHash = $this->getLoaderHash($manager, $queryBuilderHash, $class); - - if (!isset($this->loaderCache[$loaderHash])) { - $this->loaderCache[$loaderHash] = new ORMQueryBuilderLoader( - $queryBuilder, - $manager, - $class - ); - } - - return $this->loaderCache[$loaderHash]; - } - - /** - * @param QueryBuilder $queryBuilder - * - * @return string - */ - private function getQueryBuilderHash(QueryBuilder $queryBuilder) - { - return hash('sha256', json_encode(array( - 'sql' => $queryBuilder->getQuery()->getSQL(), - 'parameters' => $queryBuilder->getParameters(), - ))); - } - - /** - * @param ObjectManager $manager - * @param string $queryBuilderHash - * @param string $class - * - * @return string - */ - private function getLoaderHash(ObjectManager $manager, $queryBuilderHash, $class) - { - return hash('sha256', json_encode(array( - 'manager' => spl_object_hash($manager), - 'queryBuilder' => $queryBuilderHash, - 'class' => $class, - ))); + return new ORMQueryBuilderLoader( + $queryBuilder, + $manager, + $class + ); } public function getName() From e6739bf05e07ebd5d0ba22f76bb7247af69d62de Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 25 Mar 2015 14:31:10 +0100 Subject: [PATCH 05/60] [DoctrineBridge] DoctrineType now respects the "query_builder" option when caching the choice loader --- .../Form/ChoiceList/ORMQueryBuilderLoader.php | 2 + .../Doctrine/Form/Type/DoctrineType.php | 56 ++++++++++++------- .../Bridge/Doctrine/Form/Type/EntityType.php | 6 +- .../Tests/Form/Type/EntityTypeTest.php | 31 +++++----- 4 files changed, 57 insertions(+), 38 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php index 9cfdd1fe48..9d34601c9f 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php @@ -51,6 +51,8 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder or \Closure'); } + // This block is not executed anymore since Symfony 2.7. The query + // builder closure is already invoked in DoctrineType if ($queryBuilder instanceof \Closure) { if (!$manager instanceof EntityManager) { throw new UnexpectedTypeException($manager, 'Doctrine\ORM\EntityManager'); diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 76e78cb1f0..ed8ded5bad 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\Form\Type; use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; @@ -23,6 +24,7 @@ use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; use Symfony\Component\Form\Exception\RuntimeException; +use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -71,20 +73,24 @@ abstract class DoctrineType extends AbstractType $choiceLoader = function (Options $options) use ($choiceListFactory, &$choiceLoaders, $type) { // Unless the choices are given explicitly, load them on demand if (null === $options['choices']) { - // Don't cache if the query builder is constructed dynamically - if ($options['query_builder'] instanceof \Closure) { - $hash = null; - } else { - $hash = CachingFactoryDecorator::generateHash(array( - $options['em'], - $options['class'], - $options['query_builder'], - $options['loader'], - )); + // We consider two query builders with an equal SQL string and + // equal parameters to be equal + $qbParts = $options['query_builder'] + ? array( + $options['query_builder']->getQuery()->getSQL(), + $options['query_builder']->getParameters()->toArray(), + ) + : null; - if (isset($choiceLoaders[$hash])) { - return $choiceLoaders[$hash]; - } + $hash = CachingFactoryDecorator::generateHash(array( + $options['em'], + $options['class'], + $qbParts, + $options['loader'], + )); + + if (isset($choiceLoaders[$hash])) { + return $choiceLoaders[$hash]; } if ($options['loader']) { @@ -96,18 +102,14 @@ abstract class DoctrineType extends AbstractType $entityLoader = $type->getLoader($options['em'], $queryBuilder, $options['class']); } - $choiceLoader = new DoctrineChoiceLoader( + $choiceLoaders[$hash] = new DoctrineChoiceLoader( $choiceListFactory, $options['em'], $options['class'], $entityLoader ); - if (null !== $hash) { - $choiceLoaders[$hash] = $choiceLoader; - } - - return $choiceLoader; + return $choiceLoaders[$hash]; } }; @@ -199,6 +201,20 @@ abstract class DoctrineType extends AbstractType return $entitiesById; }; + // Invoke the query builder closure so that we can cache choice lists + // for equal query builders + $queryBuilderNormalizer = function (Options $options, $queryBuilder) { + if (is_callable($queryBuilder)) { + $queryBuilder = call_user_func($queryBuilder, $options['em']->getRepository($options['class'])); + + if (!$queryBuilder instanceof QueryBuilder) { + throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder'); + } + } + + return $queryBuilder; + }; + $resolver->setDefaults(array( 'em' => null, 'property' => null, // deprecated, use "choice_label" @@ -216,9 +232,11 @@ abstract class DoctrineType extends AbstractType $resolver->setNormalizer('em', $emNormalizer); $resolver->setNormalizer('choices', $choicesNormalizer); + $resolver->setNormalizer('query_builder', $queryBuilderNormalizer); $resolver->setAllowedTypes('em', array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager')); $resolver->setAllowedTypes('loader', array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface')); + $resolver->setAllowedTypes('query_builder', array('null', 'callable', 'Doctrine\ORM\QueryBuilder')); } /** diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php index 675c289c76..236b9290c7 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php @@ -28,11 +28,7 @@ class EntityType extends DoctrineType */ public function getLoader(ObjectManager $manager, $queryBuilder, $class) { - return new ORMQueryBuilderLoader( - $queryBuilder, - $manager, - $class - ); + return new ORMQueryBuilderLoader($queryBuilder, $manager, $class); } public function getName() diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index 9f1591f308..7d80819f6b 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -14,6 +14,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\Type; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Tools\SchemaTool; use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; use Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser; @@ -28,6 +29,7 @@ use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\Forms; use Symfony\Component\Form\Test\TypeTestCase; +use Symfony\Component\OptionsResolver\Options; use Symfony\Component\PropertyAccess\PropertyAccess; class EntityTypeTest extends TypeTestCase @@ -172,7 +174,7 @@ class EntityTypeTest extends TypeTestCase } /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException */ public function testConfigureQueryBuilderWithNonQueryBuilderAndNonClosure() { @@ -786,8 +788,7 @@ class EntityTypeTest extends TypeTestCase $this->persist(array($entity1, $entity2, $entity3)); - $repository = $this->em->getRepository(self::SINGLE_IDENT_CLASS); - $qb = $repository->createQueryBuilder('e')->where('e.id IN (1, 2)'); + $repo = $this->em->getRepository(self::SINGLE_IDENT_CLASS); $entityType = new EntityType( $this->emRegistry, @@ -806,19 +807,23 @@ class EntityTypeTest extends TypeTestCase $formBuilder->add('property1', 'entity', array( 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'query_builder' => $qb, + 'query_builder' => $repo->createQueryBuilder('e')->where('e.id IN (1, 2)'), )); $formBuilder->add('property2', 'entity', array( 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'query_builder' => $qb, + 'query_builder' => function (EntityRepository $repo) { + return $repo->createQueryBuilder('e')->where('e.id IN (1, 2)'); + }, )); $formBuilder->add('property3', 'entity', array( 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'query_builder' => $qb, + 'query_builder' => function (EntityRepository $repo) { + return $repo->createQueryBuilder('e')->where('e.id IN (1, 2)'); + }, )); $form = $formBuilder->getForm(); @@ -829,15 +834,13 @@ class EntityTypeTest extends TypeTestCase 'property3' => 2, )); - $reflectionClass = new \ReflectionObject($entityType); - $reflectionProperty = $reflectionClass->getProperty('loaderCache'); - $reflectionProperty->setAccessible(true); + $choiceList1 = $form->get('property1')->getConfig()->getOption('choice_list'); + $choiceList2 = $form->get('property2')->getConfig()->getOption('choice_list'); + $choiceList3 = $form->get('property3')->getConfig()->getOption('choice_list'); - $loaders = $reflectionProperty->getValue($entityType); - - $reflectionProperty->setAccessible(false); - - $this->assertCount(1, $loaders); + $this->assertInstanceOf('Symfony\Component\Form\ChoiceList\ChoiceListInterface', $choiceList1); + $this->assertSame($choiceList1, $choiceList2); + $this->assertSame($choiceList1, $choiceList3); } public function testCacheChoiceLists() From a289deb97358cf7295bbd1410d954f2d66c5346e Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 25 Mar 2015 15:45:45 +0100 Subject: [PATCH 06/60] [Form] Fixed new ArrayChoiceList to compare choices by their values, if enabled --- .../Form/ChoiceList/ArrayChoiceList.php | 60 +++++++++---- .../Form/ChoiceList/ArrayKeyChoiceList.php | 84 +++++++------------ .../Factory/DefaultChoiceListFactory.php | 48 ++--------- .../Tests/ChoiceList/ArrayChoiceListTest.php | 45 +++++++++- .../ChoiceList/ArrayKeyChoiceListTest.php | 57 +++++++------ .../Factory/DefaultChoiceListFactoryTest.php | 3 +- 6 files changed, 155 insertions(+), 142 deletions(-) diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php index 0dfc0f9945..a3987cc02c 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\ChoiceList; -use Symfony\Component\Form\Exception\InvalidArgumentException; +use Symfony\Component\Form\Exception\UnexpectedTypeException; /** * A list of choices with arbitrary data types. @@ -39,33 +39,46 @@ class ArrayChoiceList implements ChoiceListInterface */ protected $values = array(); + /** + * The callback for creating the value for a choice. + * + * @var callable + */ + protected $valueCallback; + /** * Creates a list with the given choices and values. * * The given choice array must have the same array keys as the value array. * - * @param array $choices The selectable choices - * @param string[] $values The string values of the choices - * - * @throws InvalidArgumentException If the keys of the choices don't match - * the keys of the values + * @param array $choices The selectable choices + * @param callable $value The callable for creating the value for a + * choice. If `null` is passed, incrementing + * integers are used as values + * @param bool $compareByValue Whether to use the value callback to + * compare choices. If `null`, choices are + * compared by identity */ - public function __construct(array $choices, array $values) + public function __construct(array $choices, $value = null, $compareByValue = false) { - $choiceKeys = array_keys($choices); - $valueKeys = array_keys($values); - - if ($choiceKeys !== $valueKeys) { - throw new InvalidArgumentException(sprintf( - 'The keys of the choices and the values must match. The choice '. - 'keys are: "%s". The value keys are: "%s".', - implode('", "', $choiceKeys), - implode('", "', $valueKeys) - )); + if (null !== $value && !is_callable($value)) { + throw new UnexpectedTypeException($value, 'null or callable'); } $this->choices = $choices; - $this->values = array_map('strval', $values); + $this->values = array(); + $this->valueCallback = $compareByValue ? $value : null; + + if (null === $value) { + $i = 0; + foreach ($this->choices as $key => $choice) { + $this->values[$key] = (string) $i++; + } + } else { + foreach ($choices as $key => $choice) { + $this->values[$key] = (string) call_user_func($value, $choice, $key); + } + } } /** @@ -116,6 +129,17 @@ class ArrayChoiceList implements ChoiceListInterface { $values = array(); + // Use the value callback to compare choices by their values, if present + if ($this->valueCallback) { + $givenValues = array(); + foreach ($choices as $key => $choice) { + $givenValues[$key] = (string) call_user_func($this->valueCallback, $choice, $key); + } + + return array_intersect($givenValues, $this->values); + } + + // Otherwise compare choices by identity foreach ($choices as $i => $givenChoice) { foreach ($this->choices as $j => $choice) { if ($choice !== $givenChoice) { diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php index d79747e048..918c278f06 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php @@ -43,21 +43,14 @@ use Symfony\Component\Form\Exception\InvalidArgumentException; * @deprecated Added for backwards compatibility in Symfony 2.7, to be removed * in Symfony 3.0. */ -class ArrayKeyChoiceList implements ChoiceListInterface +class ArrayKeyChoiceList extends ArrayChoiceList { /** - * The selectable choices. + * Whether the choices are used as values. * - * @var array + * @var bool */ - private $choices = array(); - - /** - * The values of the choices. - * - * @var string[] - */ - private $values = array(); + private $useChoicesAsValues = false; /** * Casts the given choice to an array key. @@ -100,51 +93,26 @@ class ArrayKeyChoiceList implements ChoiceListInterface * values. * * @param array $choices The selectable choices - * @param string[] $values Optional. The string values of the choices + * @param callable $value The callable for creating the value for a + * choice. If `null` is passed, the choices are + * cast to strings and used as values * * @throws InvalidArgumentException If the keys of the choices don't match * the keys of the values or if any of the * choices is not scalar */ - public function __construct(array $choices, array $values = array()) + public function __construct(array $choices, $value = null) { - if (empty($values)) { - // The cast to strings happens later - $values = $choices; - } else { - $choiceKeys = array_keys($choices); - $valueKeys = array_keys($values); + $choices = array_map(array(__CLASS__, 'toArrayKey'), $choices); - if ($choiceKeys !== $valueKeys) { - throw new InvalidArgumentException( - sprintf( - 'The keys of the choices and the values must match. The choice '. - 'keys are: "%s". The value keys are: "%s".', - implode('", "', $choiceKeys), - implode('", "', $valueKeys) - ) - ); - } + if (null === $value) { + $value = function ($choice) { + return (string) $choice; + }; + $this->useChoicesAsValues = true; } - $this->choices = array_map(array(__CLASS__, 'toArrayKey'), $choices); - $this->values = array_map('strval', $values); - } - - /** - * {@inheritdoc} - */ - public function getChoices() - { - return $this->choices; - } - - /** - * {@inheritdoc} - */ - public function getValues() - { - return $this->values; + parent::__construct($choices, $value); } /** @@ -152,11 +120,15 @@ class ArrayKeyChoiceList implements ChoiceListInterface */ public function getChoicesForValues(array $values) { - $values = array_map('strval', $values); + if ($this->useChoicesAsValues) { + $values = array_map('strval', $values); - // The values are identical to the choices, so we can just return them - // to improve performance a little bit - return array_map(array(__CLASS__, 'toArrayKey'), array_intersect($values, $this->values)); + // If the values are identical to the choices, so we can just return + // them to improve performance a little bit + return array_map(array(__CLASS__, 'toArrayKey'), array_intersect($values, $this->values)); + } + + return parent::getChoicesForValues($values); } /** @@ -166,8 +138,12 @@ class ArrayKeyChoiceList implements ChoiceListInterface { $choices = array_map(array(__CLASS__, 'toArrayKey'), $choices); - // The choices are identical to the values, so we can just return them - // to improve performance a little bit - return array_map('strval', array_intersect($choices, $this->choices)); + if ($this->useChoicesAsValues) { + // If the choices are identical to the values, we can just return + // them to improve performance a little bit + return array_map('strval', array_intersect($choices, $this->choices)); + } + + return parent::getValuesForChoices($choices); } } diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php index dd191eea39..f0ea07cdf6 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -89,10 +89,6 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface throw new UnexpectedTypeException($choices, 'array or \Traversable'); } - if (null !== $value && !is_callable($value)) { - throw new UnexpectedTypeException($value, 'null or callable'); - } - if ($choices instanceof \Traversable) { $choices = iterator_to_array($choices); } @@ -102,29 +98,7 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface // in the view only. self::flatten($choices, $flatChoices); - // If no values are given, use incrementing integers as values - // We can not use the choices themselves, because we don't know whether - // choices can be converted to (duplicate-free) strings - if (null === $value) { - $values = $flatChoices; - $i = 0; - - foreach ($values as $key => $value) { - $values[$key] = (string) $i++; - } - - return new ArrayChoiceList($flatChoices, $values); - } - - // Can't use array_map(), because array_map() doesn't pass the key - // Can't use array_walk(), which ignores the return value of the - // closure - $values = array(); - foreach ($flatChoices as $key => $choice) { - $values[$key] = call_user_func($value, $choice, $key); - } - - return new ArrayChoiceList($flatChoices, $values); + return new ArrayChoiceList($flatChoices, $value); } /** @@ -139,10 +113,6 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface throw new UnexpectedTypeException($choices, 'array or \Traversable'); } - if (null !== $value && !is_callable($value)) { - throw new UnexpectedTypeException($value, 'null or callable'); - } - if ($choices instanceof \Traversable) { $choices = iterator_to_array($choices); } @@ -157,20 +127,12 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface // strings or integers, we are guaranteed to be able to convert them // to strings if (null === $value) { - $values = array_map('strval', $flatChoices); - - return new ArrayKeyChoiceList($flatChoices, $values); + $value = function ($choice) { + return (string) $choice; + }; } - // Can't use array_map(), because array_map() doesn't pass the key - // Can't use array_walk(), which ignores the return value of the - // closure - $values = array(); - foreach ($flatChoices as $key => $choice) { - $values[$key] = call_user_func($value, $choice, $key); - } - - return new ArrayKeyChoiceList($flatChoices, $values); + return new ArrayKeyChoiceList($flatChoices, $value); } /** diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php index 34b22fe041..0dffd08374 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php @@ -29,7 +29,9 @@ class ArrayChoiceListTest extends AbstractChoiceListTest protected function createChoiceList() { - return new ArrayChoiceList($this->getChoices(), $this->getValues()); + $i = 0; + + return new ArrayChoiceList($this->getChoices()); } protected function getChoices() @@ -49,4 +51,45 @@ class ArrayChoiceListTest extends AbstractChoiceListTest { new ArrayChoiceList(array(0 => 'a', 1 => 'b'), array(1 => 'a', 2 => 'b')); } + + public function testCreateChoiceListWithValueCallback() + { + $callback = function ($choice, $key) { + return $key.':'.$choice; + }; + + $choiceList = new ArrayChoiceList(array(2 => 'foo', 7 => 'bar', 10 => 'baz'), $callback); + + $this->assertSame(array(2 => '2:foo', 7 => '7:bar', 10 => '10:baz'), $choiceList->getValues()); + $this->assertSame(array(1 => 'foo', 2 => 'baz'), $choiceList->getChoicesForValues(array(1 => '2:foo', 2 => '10:baz'))); + $this->assertSame(array(1 => '2:foo', 2 => '10:baz'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => 'baz'))); + } + + public function testCompareChoicesByIdentityByDefault() + { + $callback = function ($choice) { + return $choice->value; + }; + + $obj1 = (object) array('value' => 'value1'); + $obj2 = (object) array('value' => 'value2'); + + $choiceList = new ArrayChoiceList(array($obj1, $obj2), $callback); + $this->assertSame(array(2 => 'value2'), $choiceList->getValuesForChoices(array(2 => $obj2))); + $this->assertSame(array(), $choiceList->getValuesForChoices(array(2 => (object) array('value' => 'value2')))); + } + + public function testCompareChoicesWithValueCallbackIfCompareByValue() + { + $callback = function ($choice) { + return $choice->value; + }; + + $obj1 = (object) array('value' => 'value1'); + $obj2 = (object) array('value' => 'value2'); + + $choiceList = new ArrayChoiceList(array($obj1, $obj2), $callback, true); + $this->assertSame(array(2 => 'value2'), $choiceList->getValuesForChoices(array(2 => $obj2))); + $this->assertSame(array(2 => 'value2'), $choiceList->getValuesForChoices(array(2 => (object) array('value' => 'value2')))); + } } diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php index 74cf2afb4a..78263502d6 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php @@ -29,7 +29,7 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest protected function createChoiceList() { - return new ArrayKeyChoiceList($this->getChoices(), $this->getValues()); + return new ArrayKeyChoiceList($this->getChoices()); } protected function getChoices() @@ -42,14 +42,6 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest return array('0', '1', 'a', 'b', ''); } - /** - * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException - */ - public function testFailIfKeyMismatch() - { - new ArrayKeyChoiceList(array(0 => 'a', 1 => 'b'), array(1 => 'a', 2 => 'b')); - } - public function testUseChoicesAsValuesByDefault() { $list = new ArrayKeyChoiceList(array(1 => '', 3 => 0, 7 => '1', 10 => 1.23)); @@ -102,7 +94,7 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest */ public function testConvertChoicesIfNecessary(array $choices, array $converted) { - $list = new ArrayKeyChoiceList($choices, range(0, count($choices) - 1)); + $list = new ArrayKeyChoiceList($choices); $this->assertSame($converted, $list->getChoices()); } @@ -134,7 +126,7 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest */ public function testFailIfInvalidChoices(array $choices) { - new ArrayKeyChoiceList($choices, range(0, count($choices) - 1)); + new ArrayKeyChoiceList($choices); } /** @@ -157,31 +149,48 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest /** * @dataProvider provideConvertibleValues */ - public function testConvertValuesToStrings(array $values, array $converted) + public function testConvertValuesToStrings($value, $converted) { - $list = new ArrayKeyChoiceList(range(0, count($values) - 1), $values); + $callback = function () use ($value) { + return $value; + }; - $this->assertSame($converted, $list->getValues()); + $list = new ArrayKeyChoiceList(array('choice'), $callback); + + $this->assertSame(array($converted), $list->getValues()); } public function provideConvertibleValues() { return array( - array(array(0), array('0')), - array(array(1), array('1')), - array(array('0'), array('0')), - array(array('1'), array('1')), - array(array('1.23'), array('1.23')), - array(array('foobar'), array('foobar')), + array(0, '0'), + array(1, '1'), + array('0', '0'), + array('1', '1'), + array('1.23', '1.23'), + array('foobar', 'foobar'), // The default value of choice fields is NULL. It should be treated // like the empty value for this choice list type - array(array(null), array('')), - array(array(1.23), array('1.23')), + array(null, ''), + array(1.23, '1.23'), // Always cast booleans to 0 and 1, because: // array(true => 'Yes', false => 'No') === array(1 => 'Yes', 0 => 'No') // see ChoiceTypeTest::testSetDataSingleNonExpandedAcceptsBoolean - array(array(true), array('1')), - array(array(false), array('')), + array(true, '1'), + array(false, ''), ); } + + public function testCreateChoiceListWithValueCallback() + { + $callback = function ($choice, $key) { + return $key.':'.$choice; + }; + + $choiceList = new ArrayKeyChoiceList(array(2 => 'foo', 7 => 'bar', 10 => 'baz'), $callback); + + $this->assertSame(array(2 => '2:foo', 7 => '7:bar', 10 => '10:baz'), $choiceList->getValues()); + $this->assertSame(array(1 => 'foo', 2 => 'baz'), $choiceList->getChoicesForValues(array(1 => '2:foo', 2 => '10:baz'))); + $this->assertSame(array(1 => '2:foo', 2 => '10:baz'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => 'baz'))); + } } diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php index 42f745e29b..b144699892 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php @@ -83,8 +83,7 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->obj3 = (object) array('label' => 'C', 'index' => 'y', 'value' => 1, 'preferred' => true, 'group' => 'Group 2', 'attr' => array('attr2' => 'value2')); $this->obj4 = (object) array('label' => 'D', 'index' => 'z', 'value' => 2, 'preferred' => false, 'group' => 'Group 2', 'attr' => array()); $this->list = new ArrayChoiceList( - array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4), - array('A' => '0', 'B' => '1', 'C' => '2', 'D' => '3') + array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4) ); $this->factory = new DefaultChoiceListFactory(); } From 26eba769b5a1f9a13df71675f80f0269d89b1c2b Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 26 Mar 2015 10:52:07 +0100 Subject: [PATCH 07/60] [Form] Fixed regression: Choices are compared by their values if a value callback is given --- .../Form/ChoiceList/DoctrineChoiceLoader.php | 97 ++--------- .../Doctrine/Form/ChoiceList/IdReader.php | 125 ++++++++++++++ .../Doctrine/Form/Type/DoctrineType.php | 130 +++++++++----- .../Tests/Form/Type/EntityTypeTest.php | 1 - .../Form/ChoiceList/ArrayChoiceList.php | 22 ++- .../Factory/DefaultChoiceListFactory.php | 28 +-- .../Factory/PropertyAccessDecorator.php | 10 +- .../Form/ChoiceList/LazyChoiceList.php | 10 +- .../Tests/ChoiceList/ArrayChoiceListTest.php | 24 +-- .../ChoiceList/ArrayKeyChoiceListTest.php | 10 +- .../Factory/DefaultChoiceListFactoryTest.php | 163 +++++++++--------- 11 files changed, 361 insertions(+), 259 deletions(-) create mode 100644 src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php index c00c258ca5..4b10b45855 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -11,12 +11,10 @@ namespace Symfony\Bridge\Doctrine\Form\ChoiceList; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; -use Symfony\Component\Form\Exception\RuntimeException; /** * Loads choices using a Doctrine object manager. @@ -41,67 +39,20 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface private $class; /** - * @var ClassMetadata + * @var IdReader */ - private $classMetadata; + private $idReader; /** * @var null|EntityLoaderInterface */ private $objectLoader; - /** - * The identifier field, unless the identifier is composite - * - * @var null|string - */ - private $idField = null; - - /** - * Whether to use the identifier for value generation - * - * @var bool - */ - private $compositeId = true; - /** * @var ChoiceListInterface */ private $choiceList; - /** - * Returns the value of the identifier field of an object. - * - * Doctrine must know about this object, that is, the object must already - * be persisted or added to the identity map before. Otherwise an - * exception is thrown. - * - * This method assumes that the object has a single-column identifier and - * will return a single value instead of an array. - * - * @param object $object The object for which to get the identifier - * - * @return int|string The identifier value - * - * @throws RuntimeException If the object does not exist in Doctrine's identity map - * - * @internal Should not be accessed by user-land code. This method is public - * only to be usable as callback. - */ - public static function getIdValue(ObjectManager $om, ClassMetadata $classMetadata, $object) - { - if (!$om->contains($object)) { - throw new RuntimeException( - 'Entities passed to the choice field must be managed. Maybe '. - 'persist them in the entity manager?' - ); - } - - $om->initializeObject($object); - - return current($classMetadata->getIdentifierValues($object)); - } - /** * Creates a new choice loader. * @@ -114,22 +65,17 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface * @param ObjectManager $manager The object manager * @param string $class The class name of the * loaded objects + * @param IdReader $idReader The reader for the object + * IDs. * @param null|EntityLoaderInterface $objectLoader The objects loader */ - public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, EntityLoaderInterface $objectLoader = null) + public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, IdReader $idReader, EntityLoaderInterface $objectLoader = null) { $this->factory = $factory; $this->manager = $manager; - $this->classMetadata = $manager->getClassMetadata($class); - $this->class = $this->classMetadata->getName(); + $this->class = $manager->getClassMetadata($class)->getName(); + $this->idReader = $idReader; $this->objectLoader = $objectLoader; - - $identifier = $this->classMetadata->getIdentifierFieldNames(); - - if (1 === count($identifier)) { - $this->idField = $identifier[0]; - $this->compositeId = false; - } } /** @@ -145,23 +91,7 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface ? $this->objectLoader->getEntities() : $this->manager->getRepository($this->class)->findAll(); - // If the class has a multi-column identifier, we cannot index the - // objects by their IDs - if ($this->compositeId) { - $this->choiceList = $this->factory->createListFromChoices($objects, $value); - - return $this->choiceList; - } - - // Index the objects by ID - $objectsById = array(); - - foreach ($objects as $object) { - $id = self::getIdValue($this->manager, $this->classMetadata, $object); - $objectsById[$id] = $object; - } - - $this->choiceList = $this->factory->createListFromChoices($objectsById, $value); + $this->choiceList = $this->factory->createListFromChoices($objects, $value); return $this->choiceList; } @@ -193,14 +123,14 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface // know that the IDs are used as values // Attention: This optimization does not check choices for existence - if (!$this->choiceList && !$this->compositeId) { + if (!$this->choiceList && $this->idReader->isSingleId()) { $values = array(); // Maintain order and indices of the given objects foreach ($objects as $i => $object) { if ($object instanceof $this->class) { // Make sure to convert to the right format - $values[$i] = (string) self::getIdValue($this->manager, $this->classMetadata, $object); + $values[$i] = (string) $this->idReader->getIdValue($object); } } @@ -240,8 +170,8 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface // Optimize performance in case we have an object loader and // a single-field identifier - if (!$this->choiceList && !$this->compositeId && $this->objectLoader) { - $unorderedObjects = $this->objectLoader->getEntitiesByIds($this->idField, $values); + if (!$this->choiceList && $this->objectLoader && $this->idReader->isSingleId()) { + $unorderedObjects = $this->objectLoader->getEntitiesByIds($this->idReader->getIdField(), $values); $objectsById = array(); $objects = array(); @@ -250,8 +180,7 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface // "INDEX BY" clause to the Doctrine query in the loader, // but I'm not sure whether that's doable in a generic fashion. foreach ($unorderedObjects as $object) { - $id = self::getIdValue($this->manager, $this->classMetadata, $object); - $objectsById[$id] = $object; + $objectsById[$this->idReader->getIdValue($object)] = $object; } foreach ($values as $i => $id) { diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php new file mode 100644 index 0000000000..f6164725fd --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Form\ChoiceList; + +use Doctrine\Common\Persistence\Mapping\ClassMetadata; +use Doctrine\Common\Persistence\ObjectManager; +use Symfony\Component\Form\Exception\RuntimeException; + +/** + * A utility for reading object IDs. + * + * @since 1.0 + * @author Bernhard Schussek + * + * @internal This class is meant for internal use only. + */ +class IdReader +{ + /** + * @var ObjectManager + */ + private $om; + + /** + * @var ClassMetadata + */ + private $classMetadata; + + /** + * @var bool + */ + private $singleId; + + /** + * @var bool + */ + private $intId; + + /** + * @var string + */ + private $idField; + + public function __construct(ObjectManager $om, ClassMetadata $classMetadata) + { + $ids = $classMetadata->getIdentifierFieldNames(); + $idType = $classMetadata->getTypeOfField(current($ids)); + + $this->om = $om; + $this->classMetadata = $classMetadata; + $this->singleId = 1 === count($ids); + $this->intId = $this->singleId && 1 === count($ids) && in_array($idType, array('integer', 'smallint', 'bigint')); + $this->idField = current($ids); + } + + /** + * Returns whether the class has a single-column ID. + * + * @return bool Returns `true` if the class has a single-column ID and + * `false` otherwise. + */ + public function isSingleId() + { + return $this->singleId; + } + + /** + * Returns whether the class has a single-column integer ID. + * + * @return bool Returns `true` if the class has a single-column integer ID + * and `false` otherwise. + */ + public function isIntId() + { + return $this->intId; + } + + /** + * Returns the ID value for an object. + * + * This method assumes that the object has a single-column ID. + * + * @param object $object The object. + * + * @return mixed The ID value. + */ + public function getIdValue($object) + { + if (!$object) { + return null; + } + + if (!$this->om->contains($object)) { + throw new RuntimeException( + 'Entities passed to the choice field must be managed. Maybe '. + 'persist them in the entity manager?' + ); + } + + $this->om->initializeObject($object); + + return current($this->classMetadata->getIdentifierValues($object)); + } + + /** + * Returns the name of the ID field. + * + * This method assumes that the object has a single-column ID. + * + * @return string The name of the ID field. + */ + public function getIdField() + { + return $this->idField; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index ed8ded5bad..6020d95e92 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -16,6 +16,7 @@ use Doctrine\Common\Persistence\ObjectManager; use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; +use Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader; use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener; use Symfony\Component\Form\AbstractType; @@ -42,11 +43,55 @@ abstract class DoctrineType extends AbstractType */ private $choiceListFactory; + /** + * @var IdReader[] + */ + private $idReaders = array(); + /** * @var DoctrineChoiceLoader[] */ private $choiceLoaders = array(); + /** + * Creates the label for a choice. + * + * For backwards compatibility, objects are cast to strings by default. + * + * @param object $choice The object. + * + * @return string The string representation of the object. + * + * @internal This method is public to be usable as callback. It should not + * be used in user code. + */ + public static function createChoiceLabel($choice) + { + return (string) $choice; + } + + /** + * Creates the field name for a choice. + * + * This method is used to generate field names if the underlying object has + * a single-column integer ID. In that case, the value of the field is + * the ID of the object. That ID is also used as field name. + * + * @param object $choice The object. + * @param int|string $key The choice key. + * @param string $value The choice value. Corresponds to the object's + * ID here. + * + * @return string The field name. + * + * @internal This method is public to be usable as callback. It should not + * be used in user code. + */ + public static function createChoiceName($choice, $key, $value) + { + return (string) $value; + } + public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null) { $this->registry = $registry; @@ -67,9 +112,30 @@ abstract class DoctrineType extends AbstractType { $registry = $this->registry; $choiceListFactory = $this->choiceListFactory; + $idReaders = &$this->idReaders; $choiceLoaders = &$this->choiceLoaders; $type = $this; + $idReader = function (Options $options) use (&$idReaders) { + $hash = CachingFactoryDecorator::generateHash(array( + $options['em'], + $options['class'], + )); + + // The ID reader is a utility that is needed to read the object IDs + // when generating the field values. The callback generating the + // field values has no access to the object manager or the class + // of the field, so we store that information in the reader. + // The reader is cached so that two choice lists for the same class + // (and hence with the same reader) can successfully be cached. + if (!isset($idReaders[$hash])) { + $classMetadata = $options['em']->getClassMetadata($options['class']); + $idReaders[$hash] = new IdReader($options['em'], $classMetadata); + } + + return $idReaders[$hash]; + }; + $choiceLoader = function (Options $options) use ($choiceListFactory, &$choiceLoaders, $type) { // Unless the choices are given explicitly, load them on demand if (null === $options['choices']) { @@ -106,6 +172,7 @@ abstract class DoctrineType extends AbstractType $choiceListFactory, $options['em'], $options['class'], + $options['id_reader'], $entityLoader ); @@ -120,24 +187,18 @@ abstract class DoctrineType extends AbstractType } // BC: use __toString() by default - return function ($entity) { - return (string) $entity; - }; + return array(__CLASS__, 'createChoiceLabel'); }; $choiceName = function (Options $options) { - /** @var ObjectManager $om */ - $om = $options['em']; - $classMetadata = $om->getClassMetadata($options['class']); - $ids = $classMetadata->getIdentifierFieldNames(); - $idType = $classMetadata->getTypeOfField(current($ids)); + /** @var IdReader $idReader */ + $idReader = $options['id_reader']; - // If the entity has a single-column, numeric ID, use that ID as - // field name - if (1 === count($ids) && in_array($idType, array('integer', 'smallint', 'bigint'))) { - return function ($entity, $id) { - return $id; - }; + // If the object has a single-column, numeric ID, use that ID as + // field name. We can only use numeric IDs as names, as we cannot + // guarantee that a non-numeric ID contains a valid form name + if ($idReader->isIntId()) { + return array(__CLASS__, 'createChoiceName'); } // Otherwise, an incrementing integer is used as name automatically @@ -147,8 +208,16 @@ abstract class DoctrineType extends AbstractType // and DoctrineChoiceLoader), unless the ID is composite. Then they // are indexed by an incrementing integer. // Use the ID/incrementing integer as choice value. - $choiceValue = function ($entity, $key) { - return $key; + $choiceValue = function (Options $options) { + /** @var IdReader $idReader */ + $idReader = $options['id_reader']; + + // If the entity has a single-column ID, use that ID as value + if ($idReader->isSingleId()) { + return array($idReader, 'getIdValue'); + } + + // Otherwise, an incrementing integer is used as value automatically }; $emNormalizer = function (Options $options, $em) use ($registry) { @@ -174,33 +243,6 @@ abstract class DoctrineType extends AbstractType return $em; }; - $choicesNormalizer = function (Options $options, $entities) { - if (null === $entities || 0 === count($entities)) { - return $entities; - } - - // Make sure that the entities are indexed by their ID - /** @var ObjectManager $om */ - $om = $options['em']; - $classMetadata = $om->getClassMetadata($options['class']); - $ids = $classMetadata->getIdentifierFieldNames(); - - // We cannot use composite IDs as indices. In that case, keep the - // given indices - if (count($ids) > 1) { - return $entities; - } - - $entitiesById = array(); - - foreach ($entities as $entity) { - $id = DoctrineChoiceLoader::getIdValue($om, $classMetadata, $entity); - $entitiesById[$id] = $entity; - } - - return $entitiesById; - }; - // Invoke the query builder closure so that we can cache choice lists // for equal query builders $queryBuilderNormalizer = function (Options $options, $queryBuilder) { @@ -226,12 +268,12 @@ abstract class DoctrineType extends AbstractType 'choice_label' => $choiceLabel, 'choice_name' => $choiceName, 'choice_value' => $choiceValue, + 'id_reader' => $idReader, )); $resolver->setRequired(array('class')); $resolver->setNormalizer('em', $emNormalizer); - $resolver->setNormalizer('choices', $choicesNormalizer); $resolver->setNormalizer('query_builder', $queryBuilderNormalizer); $resolver->setAllowedTypes('em', array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager')); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index 7d80819f6b..95e11aff6f 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -29,7 +29,6 @@ use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\Forms; use Symfony\Component\Form\Test\TypeTestCase; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\PropertyAccess\PropertyAccess; class EntityTypeTest extends TypeTestCase diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php index a3987cc02c..515cd15a83 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php @@ -51,15 +51,12 @@ class ArrayChoiceList implements ChoiceListInterface * * The given choice array must have the same array keys as the value array. * - * @param array $choices The selectable choices - * @param callable $value The callable for creating the value for a - * choice. If `null` is passed, incrementing - * integers are used as values - * @param bool $compareByValue Whether to use the value callback to - * compare choices. If `null`, choices are - * compared by identity + * @param array $choices The selectable choices + * @param callable $value The callable for creating the value for a + * choice. If `null` is passed, incrementing + * integers are used as values */ - public function __construct(array $choices, $value = null, $compareByValue = false) + public function __construct(array $choices, $value = null) { if (null !== $value && !is_callable($value)) { throw new UnexpectedTypeException($value, 'null or callable'); @@ -67,7 +64,7 @@ class ArrayChoiceList implements ChoiceListInterface $this->choices = $choices; $this->values = array(); - $this->valueCallback = $compareByValue ? $value : null; + $this->valueCallback = $value; if (null === $value) { $i = 0; @@ -76,7 +73,7 @@ class ArrayChoiceList implements ChoiceListInterface } } else { foreach ($choices as $key => $choice) { - $this->values[$key] = (string) call_user_func($value, $choice, $key); + $this->values[$key] = (string) call_user_func($value, $choice); } } } @@ -132,8 +129,9 @@ class ArrayChoiceList implements ChoiceListInterface // Use the value callback to compare choices by their values, if present if ($this->valueCallback) { $givenValues = array(); - foreach ($choices as $key => $choice) { - $givenValues[$key] = (string) call_user_func($this->valueCallback, $choice, $key); + + foreach ($choices as $i => $givenChoice) { + $givenValues[$i] = (string) call_user_func($this->valueCallback, $givenChoice); } return array_intersect($givenValues, $this->values); diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php index f0ea07cdf6..d974bf7d4f 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -191,10 +191,7 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface // The names are generated from an incrementing integer by default if (null === $index) { - $i = 0; - $index = function () use (&$i) { - return $i++; - }; + $index = 0; } // If $groupBy is not given, no grouping is done @@ -267,27 +264,30 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface return new ChoiceListView($otherViews, $preferredViews); } - private static function addChoiceView($choice, $key, $label, $values, $index, $attr, $isPreferred, &$preferredViews, &$otherViews) + private static function addChoiceView($choice, $key, $label, $values, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews) { + $value = $values[$key]; + $nextIndex = is_int($index) ? $index++ : call_user_func($index, $choice, $key, $value); + $view = new ChoiceView( // If the labels are null, use the choice key by default - null === $label ? (string) $key : (string) call_user_func($label, $choice, $key), - $values[$key], + null === $label ? (string) $key : (string) call_user_func($label, $choice, $key, $value), + $value, $choice, // The attributes may be a callable or a mapping from choice indices // to nested arrays - is_callable($attr) ? call_user_func($attr, $choice, $key) : (isset($attr[$key]) ? $attr[$key] : array()) + is_callable($attr) ? call_user_func($attr, $choice, $key, $value) : (isset($attr[$key]) ? $attr[$key] : array()) ); // $isPreferred may be null if no choices are preferred - if ($isPreferred && call_user_func($isPreferred, $choice, $key)) { - $preferredViews[call_user_func($index, $choice, $key)] = $view; + if ($isPreferred && call_user_func($isPreferred, $choice, $key, $value)) { + $preferredViews[$nextIndex] = $view; } else { - $otherViews[call_user_func($index, $choice, $key)] = $view; + $otherViews[$nextIndex] = $view; } } - private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $values, $index, $attr, $isPreferred, &$preferredViews, &$otherViews) + private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $values, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews) { foreach ($groupBy as $key => $content) { // Add the contents of groups to new ChoiceGroupView instances @@ -333,9 +333,9 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface } } - private static function addChoiceViewGroupedBy($groupBy, $choice, $key, $label, $values, $index, $attr, $isPreferred, &$preferredViews, &$otherViews) + private static function addChoiceViewGroupedBy($groupBy, $choice, $key, $label, $values, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews) { - $groupLabel = call_user_func($groupBy, $choice, $key); + $groupLabel = call_user_func($groupBy, $choice, $key, $values[$key]); if (null === $groupLabel) { // If the callable returns null, don't group the choice diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php index bf91d85eea..131690a6ff 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php @@ -91,7 +91,15 @@ class PropertyAccessDecorator implements ChoiceListFactoryInterface if ($value instanceof PropertyPath) { $accessor = $this->propertyAccessor; $value = function ($choice) use ($accessor, $value) { - return $accessor->getValue($choice, $value); + // The callable may be invoked with a non-object/array value + // when such values are passed to + // ChoiceListInterface::getValuesForChoices(). Handle this case + // so that the call to getValue() doesn't break. + if (is_object($choice) || is_array($choice)) { + return $accessor->getValue($choice, $value); + } + + return null; }; } diff --git a/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php index 91e6bfe408..3dea398c6d 100644 --- a/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php @@ -43,6 +43,13 @@ class LazyChoiceList implements ChoiceListInterface */ private $value; + /** + * Whether to use the value callback to compare choices. + * + * @var bool + */ + private $compareByValue; + /** * @var ChoiceListInterface */ @@ -59,10 +66,11 @@ class LazyChoiceList implements ChoiceListInterface * @param null|callable $value The callable generating the choice * values */ - public function __construct(ChoiceLoaderInterface $loader, $value = null) + public function __construct(ChoiceLoaderInterface $loader, $value = null, $compareByValue = false) { $this->loader = $loader; $this->value = $value; + $this->compareByValue = $compareByValue; } /** diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php index 0dffd08374..129a093b89 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayChoiceListTest.php @@ -54,15 +54,15 @@ class ArrayChoiceListTest extends AbstractChoiceListTest public function testCreateChoiceListWithValueCallback() { - $callback = function ($choice, $key) { - return $key.':'.$choice; + $callback = function ($choice) { + return ':'.$choice; }; $choiceList = new ArrayChoiceList(array(2 => 'foo', 7 => 'bar', 10 => 'baz'), $callback); - $this->assertSame(array(2 => '2:foo', 7 => '7:bar', 10 => '10:baz'), $choiceList->getValues()); - $this->assertSame(array(1 => 'foo', 2 => 'baz'), $choiceList->getChoicesForValues(array(1 => '2:foo', 2 => '10:baz'))); - $this->assertSame(array(1 => '2:foo', 2 => '10:baz'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => 'baz'))); + $this->assertSame(array(2 => ':foo', 7 => ':bar', 10 => ':baz'), $choiceList->getValues()); + $this->assertSame(array(1 => 'foo', 2 => 'baz'), $choiceList->getChoicesForValues(array(1 => ':foo', 2 => ':baz'))); + $this->assertSame(array(1 => ':foo', 2 => ':baz'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => 'baz'))); } public function testCompareChoicesByIdentityByDefault() @@ -76,20 +76,6 @@ class ArrayChoiceListTest extends AbstractChoiceListTest $choiceList = new ArrayChoiceList(array($obj1, $obj2), $callback); $this->assertSame(array(2 => 'value2'), $choiceList->getValuesForChoices(array(2 => $obj2))); - $this->assertSame(array(), $choiceList->getValuesForChoices(array(2 => (object) array('value' => 'value2')))); - } - - public function testCompareChoicesWithValueCallbackIfCompareByValue() - { - $callback = function ($choice) { - return $choice->value; - }; - - $obj1 = (object) array('value' => 'value1'); - $obj2 = (object) array('value' => 'value2'); - - $choiceList = new ArrayChoiceList(array($obj1, $obj2), $callback, true); - $this->assertSame(array(2 => 'value2'), $choiceList->getValuesForChoices(array(2 => $obj2))); $this->assertSame(array(2 => 'value2'), $choiceList->getValuesForChoices(array(2 => (object) array('value' => 'value2')))); } } diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php index 78263502d6..5024a60db7 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/ArrayKeyChoiceListTest.php @@ -183,14 +183,14 @@ class ArrayKeyChoiceListTest extends AbstractChoiceListTest public function testCreateChoiceListWithValueCallback() { - $callback = function ($choice, $key) { - return $key.':'.$choice; + $callback = function ($choice) { + return ':'.$choice; }; $choiceList = new ArrayKeyChoiceList(array(2 => 'foo', 7 => 'bar', 10 => 'baz'), $callback); - $this->assertSame(array(2 => '2:foo', 7 => '7:bar', 10 => '10:baz'), $choiceList->getValues()); - $this->assertSame(array(1 => 'foo', 2 => 'baz'), $choiceList->getChoicesForValues(array(1 => '2:foo', 2 => '10:baz'))); - $this->assertSame(array(1 => '2:foo', 2 => '10:baz'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => 'baz'))); + $this->assertSame(array(2 => ':foo', 7 => ':bar', 10 => ':baz'), $choiceList->getValues()); + $this->assertSame(array(1 => 'foo', 2 => 'baz'), $choiceList->getChoicesForValues(array(1 => ':foo', 2 => ':baz'))); + $this->assertSame(array(1 => ':foo', 2 => ':baz'), $choiceList->getValuesForChoices(array(1 => 'foo', 2 => 'baz'))); } } diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php index b144699892..360d46729f 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php @@ -150,23 +150,6 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->assertObjectListWithCustomValues($list); } - public function testCreateFromChoicesFlatValuesClosureReceivesKey() - { - $list = $this->factory->createListFromChoices( - array('A' => $this->obj1, 'B' => $this->obj2, 'C' => $this->obj3, 'D' => $this->obj4), - function ($object, $key) { - switch ($key) { - case 'A': return 'a'; - case 'B': return 'b'; - case 'C': return '1'; - case 'D': return '2'; - } - } - ); - - $this->assertObjectListWithCustomValues($list); - } - public function testCreateFromChoicesGrouped() { $list = $this->factory->createListFromChoices( @@ -217,26 +200,6 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->assertObjectListWithCustomValues($list); } - public function testCreateFromChoicesGroupedValuesAsClosureReceivesKey() - { - $list = $this->factory->createListFromChoices( - array( - 'Group 1' => array('A' => $this->obj1, 'B' => $this->obj2), - 'Group 2' => array('C' => $this->obj3, 'D' => $this->obj4), - ), - function ($object, $key) { - switch ($key) { - case 'A': return 'a'; - case 'B': return 'b'; - case 'C': return '1'; - case 'D': return '2'; - } - } - ); - - $this->assertObjectListWithCustomValues($list); - } - /** * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException */ @@ -306,23 +269,6 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->assertScalarListWithCustomValues($list); } - public function testCreateFromFlippedChoicesFlatValuesClosureReceivesKey() - { - $list = $this->factory->createListFromFlippedChoices( - array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D'), - function ($choice, $key) { - switch ($key) { - case 'A': return 'a'; - case 'B': return 'b'; - case 'C': return '1'; - case 'D': return '2'; - } - } - ); - - $this->assertScalarListWithCustomValues($list); - } - public function testCreateFromFlippedChoicesGrouped() { $list = $this->factory->createListFromFlippedChoices( @@ -380,26 +326,6 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->assertScalarListWithCustomValues($list); } - public function testCreateFromFlippedChoicesGroupedValuesAsClosureReceivesKey() - { - $list = $this->factory->createListFromFlippedChoices( - array( - 'Group 1' => array('a' => 'A', 'b' => 'B'), - 'Group 2' => array('c' => 'C', 'd' => 'D'), - ), - function ($choice, $key) { - switch ($key) { - case 'A': return 'a'; - case 'B': return 'b'; - case 'C': return '1'; - case 'D': return '2'; - } - } - ); - - $this->assertScalarListWithCustomValues($list); - } - public function testCreateFromLoader() { $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); @@ -537,12 +463,9 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase public function testCreateViewFlatPreferredChoicesClosureReceivesKey() { - $obj2 = $this->obj2; - $obj3 = $this->obj3; - $view = $this->factory->createView( $this->list, - function ($object, $key) use ($obj2, $obj3) { + function ($object, $key) { return 'B' === $key || 'C' === $key; } ); @@ -550,6 +473,18 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->assertFlatView($view); } + public function testCreateViewFlatPreferredChoicesClosureReceivesValue() + { + $view = $this->factory->createView( + $this->list, + function ($object, $key, $value) { + return '1' === $value || '2' === $value; + } + ); + + $this->assertFlatView($view); + } + public function testCreateViewFlatLabelAsCallable() { $view = $this->factory->createView( @@ -587,6 +522,24 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->assertFlatView($view); } + public function testCreateViewFlatLabelClosureReceivesValue() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + function ($object, $key, $value) { + switch ($value) { + case '0': return 'A'; + case '1': return 'B'; + case '2': return 'C'; + case '3': return 'D'; + } + } + ); + + $this->assertFlatView($view); + } + public function testCreateViewFlatIndexAsCallable() { $view = $this->factory->createView( @@ -632,6 +585,25 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->assertFlatViewWithCustomIndices($view); } + public function testCreateViewFlatIndexClosureReceivesValue() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + function ($object, $key, $value) { + switch ($value) { + case '0': return 'w'; + case '1': return 'x'; + case '2': return 'y'; + case '3': return 'z'; + } + } + ); + + $this->assertFlatViewWithCustomIndices($view); + } + public function testCreateViewFlatGroupByAsArray() { $view = $this->factory->createView( @@ -724,6 +696,21 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->assertGroupedView($view); } + public function testCreateViewFlatGroupByClosureReceivesValue() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + function ($object, $key, $value) { + return '0' === $value || '1' === $value ? 'Group 1' : 'Group 2'; + } + ); + + $this->assertGroupedView($view); + } + public function testCreateViewFlatAttrAsArray() { $view = $this->factory->createView( @@ -805,6 +792,26 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->assertFlatViewWithAttr($view); } + public function testCreateViewFlatAttrClosureReceivesValue() + { + $view = $this->factory->createView( + $this->list, + array($this->obj2, $this->obj3), + null, // label + null, // index + null, // group + function ($object, $key, $value) { + switch ($value) { + case '1': return array('attr1' => 'value1'); + case '2': return array('attr2' => 'value2'); + default: return array(); + } + } + ); + + $this->assertFlatViewWithAttr($view); + } + public function testCreateViewForLegacyChoiceList() { $preferred = array(new ChoiceView('Preferred', 'x', 'x')); From 52885f0ade390e2a657f3c5707b5aa467dcf4c71 Mon Sep 17 00:00:00 2001 From: Joseph Bielawski Date: Tue, 31 Mar 2015 10:41:35 +0200 Subject: [PATCH 08/60] [DoctrineBridge] Add missing variable declaration in testcase --- .../Bridge/Doctrine/Tests/ContainerAwareEventManagerTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Symfony/Bridge/Doctrine/Tests/ContainerAwareEventManagerTest.php b/src/Symfony/Bridge/Doctrine/Tests/ContainerAwareEventManagerTest.php index 5185a75998..55991dbf4f 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/ContainerAwareEventManagerTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/ContainerAwareEventManagerTest.php @@ -16,6 +16,9 @@ use Symfony\Component\DependencyInjection\Container; class ContainerAwareEventManagerTest extends \PHPUnit_Framework_TestCase { + private $container; + private $evm; + protected function setUp() { $this->container = new Container(); From 13ec9ffafc57a6d715bd8b638c51f58f64bbbe76 Mon Sep 17 00:00:00 2001 From: stloyd Date: Tue, 31 Mar 2015 15:01:43 +0200 Subject: [PATCH 09/60] Remove dead code from FrameworkExtension & LogoutUrlHelper --- .../FrameworkBundle/DependencyInjection/FrameworkExtension.php | 1 - .../SecurityBundle/Templating/Helper/LogoutUrlHelper.php | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index c1d6e30b6d..29722dad32 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -706,7 +706,6 @@ class FrameworkExtension extends Extension ; foreach ($finder as $file) { - list($domain, $locale, $format) = explode('.', $file->getBasename(), 3); $files[] = (string) $file; } diff --git a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php index 144d11acd5..ee3d3ddc66 100644 --- a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php +++ b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php @@ -25,9 +25,6 @@ use Symfony\Component\Templating\Helper\Helper; class LogoutUrlHelper extends Helper { private $generator; - private $listeners = array(); - private $router; - private $tokenStorage; /** * Constructor. From 64d6e76e42c356a2e129c6bd5b1f0cdd18e7880e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 26 Mar 2015 20:35:25 +0100 Subject: [PATCH 10/60] [VarDumper] Add casters for Reflection* classes --- .../VarDumper/Caster/ReflectionCaster.php | 208 +++++++++++++++++- .../VarDumper/Cloner/AbstractCloner.php | 8 +- .../Tests/Caster/ReflectionCasterTest.php | 70 ++++++ .../VarDumper/Tests/CliDumperTest.php | 31 ++- .../VarDumper/Tests/HtmlDumperTest.php | 30 ++- 5 files changed, 320 insertions(+), 27 deletions(-) create mode 100644 src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php diff --git a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php index 94a25a89e4..092073940e 100644 --- a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php @@ -20,19 +20,221 @@ use Symfony\Component\VarDumper\Cloner\Stub; */ class ReflectionCaster { + private static $extraMap = array( + 'docComment' => 'getDocComment', + 'extension' => 'getExtensionName', + 'isDisabled' => 'isDisabled', + 'isDeprecated' => 'isDeprecated', + 'isInternal' => 'isInternal', + 'isUserDefined' => 'isUserDefined', + 'isGenerator' => 'isGenerator', + 'isVariadic' => 'isVariadic', + ); + + /** + * @deprecated since Symfony 2.7, to be removed in 3.0. + */ public static function castReflector(\Reflector $c, array $a, Stub $stub, $isNested) { - $a["\0~\0reflection"] = $c->__toString(); + trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); + $a[Caster::PREFIX_VIRTUAL.'reflection'] = $c->__toString(); return $a; } public static function castClosure(\Closure $c, array $a, Stub $stub, $isNested) { + $prefix = Caster::PREFIX_VIRTUAL; + $c = new \ReflectionFunction($c); + $stub->class = 'Closure'; // HHVM generates unique class names for closures - $a = static::castReflector(new \ReflectionFunction($c), $a, $stub, $isNested); - unset($a["\0+\0000"], $a['name'], $a["\0+\0this"], $a["\0+\0parameter"]); + $a = static::castFunctionAbstract($c, $a, $stub, $isNested); + + if (isset($a[$prefix.'parameters'])) { + foreach ($a[$prefix.'parameters'] as &$v) { + $param = $v; + $v = array(); + foreach (static::castParameter($param, array(), $stub, true) as $k => $param) { + if ("\0" === $k[0]) { + $v[substr($k, 3)] = $param; + } + } + unset($v['position'], $v['isVariadic'], $v['byReference'], $v); + } + } + + if ($f = $c->getFileName()) { + $a[$prefix.'file'] = $f; + $a[$prefix.'line'] = $c->getStartLine().' to '.$c->getEndLine(); + } + + $prefix = Caster::PREFIX_DYNAMIC; + unset($a['name'], $a[$prefix.'0'], $a[$prefix.'this'], $a[$prefix.'parameter'], $a[Caster::PREFIX_VIRTUAL.'extra']); return $a; } + + public static function castClass(\ReflectionClass $c, array $a, Stub $stub, $isNested, $filter = 0) + { + $prefix = Caster::PREFIX_VIRTUAL; + + if ($n = \Reflection::getModifierNames($c->getModifiers())) { + $a[$prefix.'modifiers'] = implode(' ', $n); + } + + self::addMap($a, $c, array( + 'extends' => 'getParentClass', + 'implements' => 'getInterfaceNames', + 'constants' => 'getConstants', + )); + + foreach ($c->getProperties() as $n) { + $a[$prefix.'properties'][$n->name] = $n; + } + + foreach ($c->getMethods() as $n) { + $a[$prefix.'methods'][$n->name] = $n; + } + + if (!($filter & Caster::EXCLUDE_VERBOSE) && !$isNested) { + self::addExtra($a, $c); + } + + return $a; + } + + public static function castFunctionAbstract(\ReflectionFunctionAbstract $c, array $a, Stub $stub, $isNested, $filter = 0) + { + $prefix = Caster::PREFIX_VIRTUAL; + + self::addMap($a, $c, array( + 'returnsReference' => 'returnsReference', + 'class' => 'getClosureScopeClass', + 'this' => 'getClosureThis', + )); + + if (isset($a[$prefix.'this'])) { + $a[$prefix.'this'] = new CutStub($a[$prefix.'this']); + } + + foreach ($c->getParameters() as $v) { + $k = '$'.$v->name; + if ($v->isPassedByReference()) { + $k = '&'.$k; + } + if (method_exists($v, 'isVariadic') && $v->isVariadic()) { + $k = '...'.$k; + } + $a[$prefix.'parameters'][$k] = $v; + } + + if ($v = $c->getStaticVariables()) { + foreach ($v as $k => &$v) { + $a[$prefix.'use']['$'.$k] =& $v; + } + unset($v); + } + + if (!($filter & Caster::EXCLUDE_VERBOSE) && !$isNested) { + self::addExtra($a, $c); + } + + return $a; + } + + public static function castMethod(\ReflectionMethod $c, array $a, Stub $stub, $isNested) + { + $a[Caster::PREFIX_VIRTUAL.'modifiers'] = implode(' ', \Reflection::getModifierNames($c->getModifiers())); + + return $a; + } + + public static function castParameter(\ReflectionParameter $c, array $a, Stub $stub, $isNested) + { + $prefix = Caster::PREFIX_VIRTUAL; + + self::addMap($a, $c, array( + 'position' => 'getPosition', + 'isVariadic' => 'isVariadic', + 'byReference' => 'isPassedByReference', + )); + + try { + if ($c->isArray()) { + $a[$prefix.'typeHint'] = 'array'; + } elseif (method_exists($c, 'isCallable') && $c->isCallable()) { + $a[$prefix.'typeHint'] = 'callable'; + } elseif ($v = $c->getClass()) { + $a[$prefix.'typeHint'] = $v->name; + } + } catch (\ReflectionException $e) { + } + + try { + $a[$prefix.'default'] = $v = $c->getDefaultValue(); + if (method_exists($c, 'isDefaultValueConstant') && $c->isDefaultValueConstant()) { + $a[$prefix.'default'] = new ConstStub($c->getDefaultValueConstantName(), $v); + } + } catch (\ReflectionException $e) { + } + + return $a; + } + + public static function castProperty(\ReflectionProperty $c, array $a, Stub $stub, $isNested) + { + $a[Caster::PREFIX_VIRTUAL.'modifiers'] = implode(' ', \Reflection::getModifierNames($c->getModifiers())); + self::addExtra($a, $c); + + return $a; + } + + public static function castExtension(\ReflectionExtension $c, array $a, Stub $stub, $isNested) + { + self::addMap($a, $c, array( + 'version' => 'getVersion', + 'dependencies' => 'getDependencies', + 'iniEntries' => 'getIniEntries', + 'isPersistent' => 'isPersistent', + 'isTemporary' => 'isTemporary', + 'constants' => 'getConstants', + 'functions' => 'getFunctions', + 'classes' => 'getClasses', + )); + + return $a; + } + + public static function castZendExtension(\ReflectionZendExtension $c, array $a, Stub $stub, $isNested) + { + self::addMap($a, $c, array( + 'version' => 'getVersion', + 'author' => 'getAuthor', + 'copyright' => 'getCopyright', + 'url' => 'getURL', + )); + + return $a; + } + + private static function addExtra(&$a, \Reflector $c) + { + $a =& $a[Caster::PREFIX_VIRTUAL.'extra']; + + if (method_exists($c, 'getFileName') && $m = $c->getFileName()) { + $a['file'] = $m; + $a['line'] = $c->getStartLine().' to '.$c->getEndLine(); + } + + self::addMap($a, $c, self::$extraMap, ''); + } + + private static function addMap(&$a, \Reflector $c, $map, $prefix = Caster::PREFIX_VIRTUAL) + { + foreach ($map as $k => $m) { + if (method_exists($c, $m) && false !== ($m = $c->$m()) && null !== $m) { + $a[$prefix.$k] = $m instanceof \Reflector ? $m->name : $m; + } + } + } } diff --git a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php index 7c641770fe..e4f7a7c053 100644 --- a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php +++ b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php @@ -26,7 +26,13 @@ abstract class AbstractCloner implements ClonerInterface 'Symfony\Component\VarDumper\Caster\ConstStub' => 'Symfony\Component\VarDumper\Caster\StubCaster::castStub', 'Closure' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castClosure', - 'Reflector' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castReflector', + 'ReflectionClass' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castClass', + 'ReflectionFunctionAbstract' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castFunctionAbstract', + 'ReflectionMethod' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castMethod', + 'ReflectionParameter' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castParameter', + 'ReflectionProperty' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castProperty', + 'ReflectionExtension' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castExtension', + 'ReflectionZendExtension' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castZendExtension', 'Doctrine\Common\Persistence\ObjectManager' => 'Symfony\Component\VarDumper\Caster\StubCaster::cutInternals', 'Doctrine\Common\Proxy\Proxy' => 'Symfony\Component\VarDumper\Caster\DoctrineCaster::castCommonProxy', diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php new file mode 100644 index 0000000000..be4947278d --- /dev/null +++ b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper\Tests\Caster; + +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Dumper\CliDumper; +use Symfony\Component\VarDumper\Test\VarDumperTestCase; + +/** + * @author Nicolas Grekas + */ +class ReflectionCasterTest extends VarDumperTestCase +{ + public function testReflectionCaster() + { + $var = new \ReflectionClass('ReflectionClass'); + + $this->assertDumpMatchesFormat( + <<<'EOTXT' +ReflectionClass { + +name: "ReflectionClass" + implements: array:1 [ + 0 => "Reflector" + ] + constants: array:3 [ + "IS_IMPLICIT_ABSTRACT" => 16 + "IS_EXPLICIT_ABSTRACT" => 32 + "IS_FINAL" => 64 + ] + properties: array:1 [ + "name" => ReflectionProperty { + +name: "name" + +class: "ReflectionClass" + modifiers: "public" + extra: null + } + ] + methods: array:%d [ +%A + "export" => ReflectionMethod { + +name: "export" + +class: "ReflectionClass" + parameters: array:2 [ + "$argument" => ReflectionParameter { + +name: "argument" + position: 0 + } + "$return" => ReflectionParameter { + +name: "return" + position: 1 + } + ] + modifiers: "public static" + } +%A +} +EOTXT + , $var + ); + } +} diff --git a/src/Symfony/Component/VarDumper/Tests/CliDumperTest.php b/src/Symfony/Component/VarDumper/Tests/CliDumperTest.php index 66c6eb72fb..b72247e1c0 100644 --- a/src/Symfony/Component/VarDumper/Tests/CliDumperTest.php +++ b/src/Symfony/Component/VarDumper/Tests/CliDumperTest.php @@ -39,11 +39,19 @@ class CliDumperTest extends VarDumperTestCase ob_start(); $dumper->dump($data); $out = ob_get_clean(); - $closureLabel = PHP_VERSION_ID >= 50400 ? 'public method' : 'function'; $out = preg_replace('/[ \t]+$/m', '', $out); $intMax = PHP_INT_MAX; $res1 = (int) $var['res']; $res2 = (int) $var[8]; + $closure54 = ''; + + if (PHP_VERSION_ID >= 50400) { + $closure54 = <<assertStringMatchesFormat( << Closure {#%d - reflection: """ - Closure [ {$closureLabel} Symfony\Component\VarDumper\Tests\Fixture\{closure} ] { - @@ {$var['file']} {$var['line']} - {$var['line']} - - - Parameters [2] { - Parameter #0 [ \$a ] - Parameter #1 [ PDO or NULL &\$b = NULL ] - } - } - """ + "closure" => Closure {#%d{$closure54} + parameters: array:2 [ + "\$a" => [] + "&\$b" => array:2 [ + "typeHint" => "PDO" + "default" => null + ] + ] + file: "{$var['file']}" + line: "{$var['line']} to {$var['line']}" } "line" => {$var['line']} "nobj" => array:1 [ diff --git a/src/Symfony/Component/VarDumper/Tests/HtmlDumperTest.php b/src/Symfony/Component/VarDumper/Tests/HtmlDumperTest.php index 22083b5161..2148cbf07f 100644 --- a/src/Symfony/Component/VarDumper/Tests/HtmlDumperTest.php +++ b/src/Symfony/Component/VarDumper/Tests/HtmlDumperTest.php @@ -47,6 +47,15 @@ class HtmlDumperTest extends \PHPUnit_Framework_TestCase $dumpId = $dumpId[0]; $res1 = (int) $var['res']; $res2 = (int) $var[8]; + $closure54 = ''; + + if (PHP_VERSION_ID >= 50400) { + $closure54 = <<class: "Symfony\Component\VarDumper\Tests\HtmlDumperTest" + this: HtmlDumperTest {#%d …} +EOTXT; + } $this->assertStringMatchesFormat( <<foo: "foo" +"bar": "bar" } - "closure" => Closure {#%d - reflection: """ - Closure [ <user> {$closureLabel} Symfony\Component\VarDumper\Tests\Fixture\{closure} ] { - @@ {$var['file']} {$var['line']} - {$var['line']} - - - Parameters [2] { - Parameter #0 [ <required> \$a ] - Parameter #1 [ <optional> PDO or NULL &\$b = NULL ] - } - } - """ + "closure" => Closure {#%d{$closure54} + parameters: array:2 [ + "\$a" => [] + "&\$b" => array:2 [ + "typeHint" => "PDO" + "default" => null + ] + ] + file: "{$var['file']}" + line: "{$var['line']} to {$var['line']}" } "line" => {$var['line']} "nobj" => array:1 [ From d6179c830be7f2245ad56b6a800a33275a802689 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 26 Mar 2015 10:59:50 +0100 Subject: [PATCH 11/60] [Form] Fixed PR comments --- .../Form/ChoiceList/DoctrineChoiceLoader.php | 38 ++------- .../Form/ChoiceList/EntityChoiceList.php | 2 + .../Form/ChoiceList/ORMQueryBuilderLoader.php | 18 ++-- .../Doctrine/Form/Type/DoctrineType.php | 65 ++++++++++----- .../Bridge/Doctrine/Form/Type/EntityType.php | 6 +- .../Form/ChoiceList/ArrayChoiceList.php | 8 +- .../Form/ChoiceList/ArrayKeyChoiceList.php | 4 +- .../Factory/CachingFactoryDecorator.php | 8 -- .../Factory/DefaultChoiceListFactory.php | 34 +------- .../Factory/PropertyAccessDecorator.php | 16 ++-- .../Form/ChoiceList/LazyChoiceList.php | 2 +- .../Extension/Core/ChoiceList/ChoiceList.php | 4 +- .../Core/ChoiceList/ChoiceListInterface.php | 7 +- .../Core/ChoiceList/LazyChoiceList.php | 7 +- .../Core/ChoiceList/ObjectChoiceList.php | 4 +- .../Core/ChoiceList/SimpleChoiceList.php | 2 +- .../Core/DataMapper/RadioListMapper.php | 2 - .../ChoiceToBooleanArrayTransformer.php | 4 +- .../ChoicesToBooleanArrayTransformer.php | 4 +- .../FixCheckboxInputListener.php | 6 ++ .../EventListener/FixRadioInputListener.php | 6 ++ .../Form/Extension/Core/Type/ChoiceType.php | 24 ++---- .../Form/Extension/Core/View/ChoiceView.php | 9 +- .../Factory/CachingFactoryDecoratorTest.php | 16 ---- .../Factory/DefaultChoiceListFactoryTest.php | 82 ------------------- 25 files changed, 134 insertions(+), 244 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php index 4b10b45855..5456c0eedb 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -97,25 +97,12 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface } /** - * Loads the values corresponding to the given objects. - * - * The values are returned with the same keys and in the same order as the - * corresponding objects in the given array. - * - * Optionally, a callable can be passed for generating the choice values. - * The callable receives the object as first and the array key as the second - * argument. - * - * @param array $objects An array of objects. Non-existing objects in - * this array are ignored - * @param null|callable $value The callable generating the choice values - * - * @return string[] An array of choice values + * {@inheritdoc} */ - public function loadValuesForChoices(array $objects, $value = null) + public function loadValuesForChoices(array $choices, $value = null) { // Performance optimization - if (empty($objects)) { + if (empty($choices)) { return array(); } @@ -127,7 +114,7 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface $values = array(); // Maintain order and indices of the given objects - foreach ($objects as $i => $object) { + foreach ($choices as $i => $object) { if ($object instanceof $this->class) { // Make sure to convert to the right format $values[$i] = (string) $this->idReader->getIdValue($object); @@ -137,24 +124,11 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface return $values; } - return $this->loadChoiceList($value)->getValuesForChoices($objects); + return $this->loadChoiceList($value)->getValuesForChoices($choices); } /** - * Loads the objects corresponding to the given values. - * - * The objects are returned with the same keys and in the same order as the - * corresponding values in the given array. - * - * Optionally, a callable can be passed for generating the choice values. - * The callable receives the object as first and the array key as the second - * argument. - * - * @param string[] $values An array of choice values. Non-existing - * values in this array are ignored - * @param null|callable $value The callable generating the choice values - * - * @return array An array of objects + * {@inheritdoc} */ public function loadChoicesForValues(array $values, $value = null) { diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php index f3d4ff48f6..1d4232306d 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php @@ -129,6 +129,8 @@ class EntityChoiceList extends ObjectChoiceList } parent::__construct($entities, $labelPath, $preferredEntities, $groupPath, null, $propertyAccessor); + + trigger_error('The '.__CLASS__.' class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader instead.', E_USER_DEPRECATED); } /** diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php index 9d34601c9f..92f00cb243 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php @@ -37,9 +37,14 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface /** * Construct an ORM Query Builder Loader. * - * @param QueryBuilder|\Closure $queryBuilder - * @param EntityManager $manager - * @param string $class + * @param QueryBuilder|\Closure $queryBuilder The query builder or a closure + * for creating the query builder. + * Passing a closure is + * deprecated and will not be + * supported anymore as of + * Symfony 3.0. + * @param EntityManager $manager Deprecated. + * @param string $class Deprecated. * * @throws UnexpectedTypeException */ @@ -51,13 +56,16 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder or \Closure'); } - // This block is not executed anymore since Symfony 2.7. The query - // builder closure is already invoked in DoctrineType if ($queryBuilder instanceof \Closure) { + trigger_error('Passing a QueryBuilder closure to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); + if (!$manager instanceof EntityManager) { throw new UnexpectedTypeException($manager, 'Doctrine\ORM\EntityManager'); } + trigger_error('Passing an EntityManager to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); + trigger_error('Passing a class to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); + $queryBuilder = $queryBuilder($manager->getRepository($class)); if (!$queryBuilder instanceof QueryBuilder) { diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 6020d95e92..b8d03c0eb1 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -116,27 +116,10 @@ abstract class DoctrineType extends AbstractType $choiceLoaders = &$this->choiceLoaders; $type = $this; - $idReader = function (Options $options) use (&$idReaders) { - $hash = CachingFactoryDecorator::generateHash(array( - $options['em'], - $options['class'], - )); - - // The ID reader is a utility that is needed to read the object IDs - // when generating the field values. The callback generating the - // field values has no access to the object manager or the class - // of the field, so we store that information in the reader. - // The reader is cached so that two choice lists for the same class - // (and hence with the same reader) can successfully be cached. - if (!isset($idReaders[$hash])) { - $classMetadata = $options['em']->getClassMetadata($options['class']); - $idReaders[$hash] = new IdReader($options['em'], $classMetadata); - } - - return $idReaders[$hash]; - }; - $choiceLoader = function (Options $options) use ($choiceListFactory, &$choiceLoaders, $type) { + // This closure and the "query_builder" options should be pushed to + // EntityType in Symfony 3.0 as they are specific to the ORM + // Unless the choices are given explicitly, load them on demand if (null === $options['choices']) { // We consider two query builders with an equal SQL string and @@ -243,6 +226,13 @@ abstract class DoctrineType extends AbstractType return $em; }; + // deprecation note + $propertyNormalizer = function (Options $options, $propertyName) { + trigger_error('The "property" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_label" instead.', E_USER_DEPRECATED); + + return $propertyName; + }; + // Invoke the query builder closure so that we can cache choice lists // for equal query builders $queryBuilderNormalizer = function (Options $options, $queryBuilder) { @@ -257,6 +247,35 @@ abstract class DoctrineType extends AbstractType return $queryBuilder; }; + // deprecation note + $loaderNormalizer = function (Options $options, $loader) { + trigger_error('The "loader" option is deprecated since version 2.7 and will be removed in 3.0. Override getLoader() instead.', E_USER_DEPRECATED); + + return $loader; + }; + + // Set the "id_reader" option via the normalizer. This option is not + // supposed to be set by the user. + $idReaderNormalizer = function (Options $options) use (&$idReaders) { + $hash = CachingFactoryDecorator::generateHash(array( + $options['em'], + $options['class'], + )); + + // The ID reader is a utility that is needed to read the object IDs + // when generating the field values. The callback generating the + // field values has no access to the object manager or the class + // of the field, so we store that information in the reader. + // The reader is cached so that two choice lists for the same class + // (and hence with the same reader) can successfully be cached. + if (!isset($idReaders[$hash])) { + $classMetadata = $options['em']->getClassMetadata($options['class']); + $idReaders[$hash] = new IdReader($options['em'], $classMetadata); + } + + return $idReaders[$hash]; + }; + $resolver->setDefaults(array( 'em' => null, 'property' => null, // deprecated, use "choice_label" @@ -268,17 +287,19 @@ abstract class DoctrineType extends AbstractType 'choice_label' => $choiceLabel, 'choice_name' => $choiceName, 'choice_value' => $choiceValue, - 'id_reader' => $idReader, + 'id_reader' => null, // internal )); $resolver->setRequired(array('class')); $resolver->setNormalizer('em', $emNormalizer); + $resolver->setNormalizer('property', $propertyNormalizer); $resolver->setNormalizer('query_builder', $queryBuilderNormalizer); + $resolver->setNormalizer('loader', $loaderNormalizer); + $resolver->setNormalizer('id_reader', $idReaderNormalizer); $resolver->setAllowedTypes('em', array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager')); $resolver->setAllowedTypes('loader', array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface')); - $resolver->setAllowedTypes('query_builder', array('null', 'callable', 'Doctrine\ORM\QueryBuilder')); } /** diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php index 236b9290c7..87b3ee42cb 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php @@ -20,9 +20,9 @@ class EntityType extends DoctrineType /** * Return the default loader object. * - * @param ObjectManager $manager - * @param QueryBuilder|\Closure $queryBuilder - * @param string $class + * @param ObjectManager $manager + * @param QueryBuilder $queryBuilder + * @param string $class * * @return ORMQueryBuilderLoader */ diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php index 515cd15a83..f55154b085 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php @@ -51,10 +51,10 @@ class ArrayChoiceList implements ChoiceListInterface * * The given choice array must have the same array keys as the value array. * - * @param array $choices The selectable choices - * @param callable $value The callable for creating the value for a - * choice. If `null` is passed, incrementing - * integers are used as values + * @param array $choices The selectable choices + * @param callable|null $value The callable for creating the value for a + * choice. If `null` is passed, incrementing + * integers are used as values */ public function __construct(array $choices, $value = null) { diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php index 918c278f06..7973072f32 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php @@ -41,7 +41,7 @@ use Symfony\Component\Form\Exception\InvalidArgumentException; * @author Bernhard Schussek * * @deprecated Added for backwards compatibility in Symfony 2.7, to be removed - * in Symfony 3.0. + * in Symfony 3.0. Use {@link ArrayChoiceList} instead. */ class ArrayKeyChoiceList extends ArrayChoiceList { @@ -113,6 +113,8 @@ class ArrayKeyChoiceList extends ArrayChoiceList } parent::__construct($choices, $value); + + trigger_error('The '.__CLASS__.' class was added for backwards compatibility in version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\ArrayChoiceList instead.', E_USER_DEPRECATED); } /** diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php index fb43ac8759..3a2702a335 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php @@ -91,10 +91,6 @@ class CachingFactoryDecorator implements ChoiceListFactoryInterface */ public function createListFromChoices($choices, $value = null) { - if (!is_array($choices) && !$choices instanceof \Traversable) { - throw new UnexpectedTypeException($choices, 'array or \Traversable'); - } - if ($choices instanceof \Traversable) { $choices = iterator_to_array($choices); } @@ -124,10 +120,6 @@ class CachingFactoryDecorator implements ChoiceListFactoryInterface */ public function createListFromFlippedChoices($choices, $value = null) { - if (!is_array($choices) && !$choices instanceof \Traversable) { - throw new UnexpectedTypeException($choices, 'array or \Traversable'); - } - if ($choices instanceof \Traversable) { $choices = iterator_to_array($choices); } diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php index d974bf7d4f..31527a9f34 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -85,10 +85,6 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface */ public function createListFromChoices($choices, $value = null) { - if (!is_array($choices) && !$choices instanceof \Traversable) { - throw new UnexpectedTypeException($choices, 'array or \Traversable'); - } - if ($choices instanceof \Traversable) { $choices = iterator_to_array($choices); } @@ -109,10 +105,6 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface */ public function createListFromFlippedChoices($choices, $value = null) { - if (!is_array($choices) && !$choices instanceof \Traversable) { - throw new UnexpectedTypeException($choices, 'array or \Traversable'); - } - if ($choices instanceof \Traversable) { $choices = iterator_to_array($choices); } @@ -140,10 +132,6 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface */ public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null) { - if (null !== $value && !is_callable($value)) { - throw new UnexpectedTypeException($value, 'null or callable'); - } - return new LazyChoiceList($loader, $value); } @@ -152,26 +140,6 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface */ public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null) { - if (null !== $preferredChoices && !is_array($preferredChoices) && !is_callable($preferredChoices)) { - throw new UnexpectedTypeException($preferredChoices, 'null, array or callable'); - } - - if (null !== $label && !is_callable($label)) { - throw new UnexpectedTypeException($label, 'null or callable'); - } - - if (null !== $index && !is_callable($index)) { - throw new UnexpectedTypeException($index, 'null or callable'); - } - - if (null !== $groupBy && !is_array($groupBy) && !$groupBy instanceof \Traversable && !is_callable($groupBy)) { - throw new UnexpectedTypeException($groupBy, 'null, array, \Traversable or callable'); - } - - if (null !== $attr && !is_array($attr) && !is_callable($attr)) { - throw new UnexpectedTypeException($attr, 'null, array or callable'); - } - // Backwards compatibility if ($list instanceof LegacyChoiceListInterface && null === $preferredChoices && null === $label && null === $index && null === $groupBy && null === $attr) { @@ -247,7 +215,7 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface ); } - // Remove any empty group views that may have been created by + // Remove any empty group view that may have been created by // addChoiceViewGroupedBy() foreach ($preferredViews as $key => $view) { if ($view instanceof ChoiceGroupView && 0 === count($view->choices)) { diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php index 131690a6ff..f6fd823784 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php @@ -153,16 +153,12 @@ class PropertyAccessDecorator implements ChoiceListFactoryInterface /** * {@inheritdoc} * - * @param ChoiceListInterface $list The choice list - * @param null|array|callable|PropertyPath $preferredChoices The preferred choices - * @param null|callable|PropertyPath $label The callable or path - * generating the choice labels - * @param null|callable|PropertyPath $index The callable or path - * generating the view indices - * @param null|array|\Traversable|callable|PropertyPath $groupBy The callable or path - * generating the group names - * @param null|array|callable|PropertyPath $attr The callable or path - * generating the HTML attributes + * @param ChoiceListInterface $list The choice list + * @param null|array|callable|string|PropertyPath $preferredChoices The preferred choices + * @param null|callable|string|PropertyPath $label The callable or path generating the choice labels + * @param null|callable|string|PropertyPath $index The callable or path generating the view indices + * @param null|array|\Traversable|callable|string|PropertyPath $groupBy The callable or path generating the group names + * @param null|array|callable|string|PropertyPath $attr The callable or path generating the HTML attributes * * @return ChoiceListView The choice list view */ diff --git a/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php index 3dea398c6d..092e2c4644 100644 --- a/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php @@ -51,7 +51,7 @@ class LazyChoiceList implements ChoiceListInterface private $compareByValue; /** - * @var ChoiceListInterface + * @var ChoiceListInterface|null */ private $loadedList; diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php index 2f7b287b63..817e03ec72 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php @@ -35,7 +35,7 @@ use Symfony\Component\Form\Extension\Core\View\ChoiceView; * @author Bernhard Schussek * * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. - * Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayChoiceList} instead. + * Use {@link \Symfony\Component\Form\ChoiceList\ArrayChoiceList} instead. */ class ChoiceList implements ChoiceListInterface { @@ -92,6 +92,8 @@ class ChoiceList implements ChoiceListInterface } $this->initialize($choices, $labels, $preferredChoices); + + trigger_error('The '.__CLASS__.' class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\ArrayChoiceList instead.', E_USER_DEPRECATED); } /** diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php index 22354e09d8..aef70aef87 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface as BaseChoiceListInterface; + /** * Contains choices that can be selected in a form field. * @@ -27,10 +29,9 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList; * @author Bernhard Schussek * * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. - * Use {@link \Symfony\Component\Form\ArrayChoiceList\ChoiceListInterface} - * instead. + * Use {@link BaseChoiceListInterface} instead. */ -interface ChoiceListInterface extends \Symfony\Component\Form\ChoiceList\ChoiceListInterface +interface ChoiceListInterface extends BaseChoiceListInterface { /** * Returns the choice views of the preferred choices as nested array with diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php index 24232bc1d6..f3a7cc028a 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php @@ -23,7 +23,7 @@ use Symfony\Component\Form\Exception\InvalidArgumentException; * @author Bernhard Schussek * * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. - * Use {@link \Symfony\Component\Form\ArrayChoiceList\LazyChoiceList} + * Use {@link \Symfony\Component\Form\ChoiceList\LazyChoiceList} * instead. */ abstract class LazyChoiceList implements ChoiceListInterface @@ -35,6 +35,11 @@ abstract class LazyChoiceList implements ChoiceListInterface */ private $choiceList; + public function __construct() + { + trigger_error('The '.__CLASS__.' class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\LazyChoiceList instead.', E_USER_DEPRECATED); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php index 606de43af3..c356ce466b 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php @@ -34,7 +34,7 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; * @author Bernhard Schussek * * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. - * Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayChoiceList} + * Use {@link \Symfony\Component\Form\ChoiceList\ArrayChoiceList} * instead. */ class ObjectChoiceList extends ChoiceList @@ -97,6 +97,8 @@ class ObjectChoiceList extends ChoiceList $this->valuePath = null !== $valuePath ? new PropertyPath($valuePath) : null; parent::__construct($choices, array(), $preferredChoices); + + trigger_error('The '.__CLASS__.' class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\ArrayChoiceList instead.', E_USER_DEPRECATED); } /** diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php index 50a3eb5f4a..6dd8fb091e 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php @@ -30,7 +30,7 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList; * @author Bernhard Schussek * * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. - * Use {@link \Symfony\Component\Form\ArrayChoiceList\ArrayKeyChoiceList} + * Use {@link \Symfony\Component\Form\ChoiceList\ArrayChoiceList} * instead. */ class SimpleChoiceList extends ChoiceList diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php index aecdb2fad0..19db183a28 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/RadioListMapper.php @@ -58,8 +58,6 @@ class RadioListMapper implements DataMapperInterface foreach ($radios as $radio) { if ($radio->getData()) { if ('placeholder' === $radio->getName()) { - $choice = null; - return; } diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php index a0b5039317..108c1ca6a3 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToBooleanArrayTransformer.php @@ -19,7 +19,7 @@ use Symfony\Component\Form\Exception\TransformationFailedException; * @author Bernhard Schussek * * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. - * Use {@link \Symfony\Component\Form\ArrayChoiceList\LazyChoiceList} + * Use {@link \Symfony\Component\Form\ChoiceList\LazyChoiceList} * instead. */ class ChoiceToBooleanArrayTransformer implements DataTransformerInterface @@ -38,6 +38,8 @@ class ChoiceToBooleanArrayTransformer implements DataTransformerInterface { $this->choiceList = $choiceList; $this->placeholderPresent = $placeholderPresent; + + trigger_error('The class '.__CLASS__.' is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\LazyChoiceList instead.', E_USER_DEPRECATED); } /** diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php index c38c363329..a632bc03c7 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoicesToBooleanArrayTransformer.php @@ -19,7 +19,7 @@ use Symfony\Component\Form\Exception\TransformationFailedException; * @author Bernhard Schussek * * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. - * Use {@link \Symfony\Component\Form\ArrayChoiceList\LazyChoiceList} + * Use {@link \Symfony\Component\Form\ChoiceList\LazyChoiceList} * instead. */ class ChoicesToBooleanArrayTransformer implements DataTransformerInterface @@ -29,6 +29,8 @@ class ChoicesToBooleanArrayTransformer implements DataTransformerInterface public function __construct(ChoiceListInterface $choiceList) { $this->choiceList = $choiceList; + + trigger_error('The class '.__CLASS__.' is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\LazyChoiceList instead.', E_USER_DEPRECATED); } /** diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php index 297987f799..85b08c7b32 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/FixCheckboxInputListener.php @@ -22,6 +22,10 @@ use Symfony\Component\Form\FormEvents; * indexed array. * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper} + * instead. */ class FixCheckboxInputListener implements EventSubscriberInterface { @@ -35,6 +39,8 @@ class FixCheckboxInputListener implements EventSubscriberInterface public function __construct(ChoiceListInterface $choiceList) { $this->choiceList = $choiceList; + + trigger_error('The class '.__CLASS__.' is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper instead.', E_USER_DEPRECATED); } public function preSubmit(FormEvent $event) diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php index d5067b6e33..8641ea725d 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/FixRadioInputListener.php @@ -21,6 +21,10 @@ use Symfony\Component\Form\FormEvents; * to an array. * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Form\Extension\Core\DataMapper\RadioListMapper} + * instead. */ class FixRadioInputListener implements EventSubscriberInterface { @@ -38,6 +42,8 @@ class FixRadioInputListener implements EventSubscriberInterface { $this->choiceList = $choiceList; $this->placeholderPresent = $placeholderPresent; + + trigger_error('The class '.__CLASS__.' is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\Extension\Core\DataMapper\RadioListMapper instead.', E_USER_DEPRECATED); } public function preSubmit(FormEvent $event) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index 7e80a00bde..a950be8d89 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -233,15 +233,8 @@ class ChoiceType extends AbstractType { $choiceListFactory = $this->choiceListFactory; - $choiceList = function (Options $options) use ($choiceListFactory) { + $choiceList = function (Options $options, $choiceList) use ($choiceListFactory) { if (null !== $options['choice_loader']) { - // Due to a bug in OptionsResolver, the choices haven't been - // validated yet at this point. Remove the if statement once that - // bug is resolved - if (!$options['choice_loader'] instanceof ChoiceLoaderInterface) { - return; - } - return $choiceListFactory->createListFromLoader( $options['choice_loader'], $options['choice_value'] @@ -251,13 +244,6 @@ class ChoiceType extends AbstractType // Harden against NULL values (like in EntityType and ModelType) $choices = null !== $options['choices'] ? $options['choices'] : array(); - // Due to a bug in OptionsResolver, the choices haven't been - // validated yet at this point. Remove the if statement once that - // bug is resolved - if (!is_array($choices) && !$choices instanceof \Traversable) { - return; - } - // BC when choices are in the keys, not in the values if (!$options['choices_as_values']) { return $choiceListFactory->createListFromFlippedChoices($choices, $options['choice_value']); @@ -283,6 +269,13 @@ class ChoiceType extends AbstractType return $options['empty_value']; }; + // deprecation note + $choiceListNormalizer = function (Options $options, $choiceList) { + trigger_error('The "choice_list" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_loader" instead.', E_USER_DEPRECATED); + + return $choiceList; + }; + $placeholderNormalizer = function (Options $options, $placeholder) { if ($options['multiple']) { // never use an empty value for this case @@ -327,6 +320,7 @@ class ChoiceType extends AbstractType 'data_class' => null, )); + $resolver->setNormalizer('choice_list', $choiceListNormalizer); $resolver->setNormalizer('empty_value', $placeholderNormalizer); $resolver->setNormalizer('placeholder', $placeholderNormalizer); diff --git a/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php b/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php index 65d7af2464..0cbeecab9f 100644 --- a/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php +++ b/src/Symfony/Component/Form/Extension/Core/View/ChoiceView.php @@ -11,12 +11,17 @@ namespace Symfony\Component\Form\Extension\Core\View; +use Symfony\Component\Form\ChoiceList\View\ChoiceView as BaseChoiceView; + /** * Represents a choice in templates. * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link BaseChoiceView} instead. */ -class ChoiceView extends \Symfony\Component\Form\ChoiceList\View\ChoiceView +class ChoiceView extends BaseChoiceView { /** * Creates a new ChoiceView. @@ -28,5 +33,7 @@ class ChoiceView extends \Symfony\Component\Form\ChoiceList\View\ChoiceView public function __construct($data, $value, $label) { parent::__construct($label, $value, $data); + + trigger_error('The '.__CLASS__.' class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\View\ChoiceView instead.', E_USER_DEPRECATED); } } diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php index 031cced280..716468276a 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php @@ -34,14 +34,6 @@ class CachingFactoryDecoratorTest extends \PHPUnit_Framework_TestCase $this->factory = new CachingFactoryDecorator($this->decoratedFactory); } - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testCreateFromChoicesFailsIfChoicesNotArrayOrTraversable() - { - $this->factory->createListFromChoices('foobar'); - } - public function testCreateFromChoicesEmpty() { $list = new \stdClass(); @@ -163,14 +155,6 @@ class CachingFactoryDecoratorTest extends \PHPUnit_Framework_TestCase $this->assertSame($list2, $this->factory->createListFromChoices($choices, $closure2)); } - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testCreateFromFlippedChoicesFailsIfChoicesNotArrayOrTraversable() - { - $this->factory->createListFromFlippedChoices('foobar'); - } - public function testCreateFromFlippedChoicesEmpty() { $list = new \stdClass(); diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php index 360d46729f..a2b817ed8d 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php @@ -88,22 +88,6 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->factory = new DefaultChoiceListFactory(); } - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testCreateFromChoicesFailsIfChoicesNotArrayOrTraversable() - { - $this->factory->createListFromChoices('foobar'); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testCreateFromChoicesFailsIfValuesNotCallableOrString() - { - $this->factory->createListFromChoices(array(), new \stdClass()); - } - public function testCreateFromChoicesEmpty() { $list = $this->factory->createListFromChoices(array()); @@ -200,22 +184,6 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->assertObjectListWithCustomValues($list); } - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testCreateFromFlippedChoicesFailsIfChoicesNotArrayOrTraversable() - { - $this->factory->createListFromFlippedChoices('foobar'); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testCreateFromFlippedChoicesFailsIfValuesNotCallableOrString() - { - $this->factory->createListFromFlippedChoices(array(), new \stdClass()); - } - public function testCreateFromFlippedChoicesEmpty() { $list = $this->factory->createListFromFlippedChoices(array()); @@ -345,56 +313,6 @@ class DefaultChoiceListFactoryTest extends \PHPUnit_Framework_TestCase $this->assertEquals(new LazyChoiceList($loader, $value), $list); } - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testCreateFromLoaderFailsIfValuesNotCallableOrString() - { - $loader = $this->getMock('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'); - - $this->factory->createListFromLoader($loader, new \stdClass()); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testCreateViewFailsIfPreferredChoicesInvalid() - { - $this->factory->createView($this->list, new \stdClass()); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testCreateViewFailsIfLabelInvalid() - { - $this->factory->createView($this->list, null, new \stdClass()); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testCreateViewFailsIfIndexInvalid() - { - $this->factory->createView($this->list, null, null, new \stdClass()); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testCreateViewFailsIfGroupByInvalid() - { - $this->factory->createView($this->list, null, null, null, new \stdClass()); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testCreateViewFailsIfAttrInvalid() - { - $this->factory->createView($this->list, null, null, null, null, new \stdClass()); - } - public function testCreateViewFlat() { $view = $this->factory->createView($this->list); From 1d89922782922fe3bcacb09f91e8c3c992d29d3f Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 31 Mar 2015 12:25:01 +0200 Subject: [PATCH 12/60] [Form] Fixed tests using legacy functionality --- .../Doctrine/Form/Type/DoctrineType.php | 9 ++- .../GenericEntityChoiceListTest.php | 28 ++++---- .../LoadedEntityChoiceListCompositeIdTest.php | 1 + .../LoadedEntityChoiceListSingleIntIdTest.php | 1 + ...adedEntityChoiceListSingleStringIdTest.php | 1 + ...nloadedEntityChoiceListCompositeIdTest.php | 3 +- ...iceListCompositeIdWithQueryBuilderTest.php | 1 + ...nloadedEntityChoiceListSingleIntIdTest.php | 9 +-- ...iceListSingleIntIdWithQueryBuilderTest.php | 1 + ...adedEntityChoiceListSingleStringIdTest.php | 3 +- ...ListSingleStringIdWithQueryBuilderTest.php | 1 + .../Tests/Form/Type/EntityTypeTest.php | 72 ++++++++++++------- .../Form/ChoiceList/ArrayKeyChoiceList.php | 5 -- .../ChoiceToValueTransformer.php | 12 +--- .../Form/Extension/Core/Type/ChoiceType.php | 47 ++++++------ .../ChoiceList/AbstractChoiceListTest.php | 56 ++++----------- .../Core/ChoiceList/ChoiceListTest.php | 13 ++-- .../Core/ChoiceList/LazyChoiceListTest.php | 20 ++++-- .../Core/ChoiceList/ObjectChoiceListTest.php | 25 ++++--- .../Core/ChoiceList/SimpleChoiceListTest.php | 9 ++- .../SimpleNumericChoiceListTest.php | 13 ++-- .../ChoiceToValueTransformerTest.php | 16 ++--- .../ChoicesToValuesTransformerTest.php | 9 ++- .../FixRadioInputListenerTest.php | 15 ++-- .../Extension/Core/Type/ChoiceTypeTest.php | 28 ++++++-- 25 files changed, 210 insertions(+), 188 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index b8d03c0eb1..a478574190 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -228,7 +228,9 @@ abstract class DoctrineType extends AbstractType // deprecation note $propertyNormalizer = function (Options $options, $propertyName) { - trigger_error('The "property" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_label" instead.', E_USER_DEPRECATED); + if ($propertyName) { + trigger_error('The "property" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_label" instead.', E_USER_DEPRECATED); + } return $propertyName; }; @@ -249,7 +251,9 @@ abstract class DoctrineType extends AbstractType // deprecation note $loaderNormalizer = function (Options $options, $loader) { - trigger_error('The "loader" option is deprecated since version 2.7 and will be removed in 3.0. Override getLoader() instead.', E_USER_DEPRECATED); + if ($loader) { + trigger_error('The "loader" option is deprecated since version 2.7 and will be removed in 3.0. Override getLoader() instead.', E_USER_DEPRECATED); + } return $loader; }; @@ -300,6 +304,7 @@ abstract class DoctrineType extends AbstractType $resolver->setAllowedTypes('em', array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager')); $resolver->setAllowedTypes('loader', array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface')); + $resolver->setAllowedTypes('query_builder', array('null', 'callable', 'Doctrine\ORM\QueryBuilder')); } /** diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/GenericEntityChoiceListTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/GenericEntityChoiceListTest.php index 9b60c87661..3226d69d1a 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/GenericEntityChoiceListTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/GenericEntityChoiceListTest.php @@ -19,6 +19,9 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList; use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Doctrine\ORM\Tools\SchemaTool; +/** + * @group legacy + */ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase { const SINGLE_INT_ID_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity'; @@ -36,6 +39,8 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase protected function setUp() { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + $this->em = DoctrineTestHelper::createTestEntityManager(); $schemaTool = new SchemaTool($this->em); @@ -70,7 +75,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase * @expectedException \Symfony\Component\Form\Exception\StringCastException * @expectedMessage Entity "Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity" passed to the choice field must have a "__toString()" method defined (or you can also override the "property" option). */ - public function testEntitiesMustHaveAToStringMethod() + public function testLegacyEntitiesMustHaveAToStringMethod() { $entity1 = new SingleIntIdNoToStringEntity(1, 'Foo'); $entity2 = new SingleIntIdNoToStringEntity(2, 'Bar'); @@ -96,7 +101,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase /** * @expectedException \Symfony\Component\Form\Exception\RuntimeException */ - public function testChoicesMustBeManaged() + public function testLegacyChoicesMustBeManaged() { $entity1 = new SingleIntIdEntity(1, 'Foo'); $entity2 = new SingleIntIdEntity(2, 'Bar'); @@ -118,7 +123,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase $choiceList->getChoices(); } - public function testInitExplicitChoices() + public function testLegacyInitExplicitChoices() { $entity1 = new SingleIntIdEntity(1, 'Foo'); $entity2 = new SingleIntIdEntity(2, 'Bar'); @@ -141,7 +146,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array(1 => $entity1, 2 => $entity2), $choiceList->getChoices()); } - public function testInitEmptyChoices() + public function testLegacyInitEmptyChoices() { $entity1 = new SingleIntIdEntity(1, 'Foo'); $entity2 = new SingleIntIdEntity(2, 'Bar'); @@ -161,7 +166,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array(), $choiceList->getChoices()); } - public function testInitNestedChoices() + public function testLegacyInitNestedChoices() { $entity1 = new SingleIntIdEntity(1, 'Foo'); $entity2 = new SingleIntIdEntity(2, 'Bar'); @@ -189,7 +194,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase ), $choiceList->getRemainingViews()); } - public function testGroupByPropertyPath() + public function testLegacyGroupByPropertyPath() { $item1 = new GroupableEntity(1, 'Foo', 'Group1'); $item2 = new GroupableEntity(2, 'Bar', 'Group1'); @@ -224,7 +229,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase ), $choiceList->getRemainingViews()); } - public function testGroupByInvalidPropertyPathReturnsFlatChoices() + public function testLegacyGroupByInvalidPropertyPathReturnsFlatChoices() { $item1 = new GroupableEntity(1, 'Foo', 'Group1'); $item2 = new GroupableEntity(2, 'Bar', 'Group1'); @@ -251,7 +256,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase ), $choiceList->getChoices()); } - public function testInitShorthandEntityName() + public function testLegacyInitShorthandEntityName() { $item1 = new SingleIntIdEntity(1, 'Foo'); $item2 = new SingleIntIdEntity(2, 'Bar'); @@ -267,13 +272,8 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertEquals(array(1, 2), $choiceList->getValuesForChoices(array($item1, $item2))); } - /** - * @group legacy - */ - public function testLegacyInitShorthandEntityName() + public function testLegacyInitShorthandEntityName2() { - $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); - $item1 = new SingleIntIdEntity(1, 'Foo'); $item2 = new SingleIntIdEntity(2, 'Bar'); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListCompositeIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListCompositeIdTest.php index 90cbf1d7c8..a2ee7cdc8a 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListCompositeIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListCompositeIdTest.php @@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList; /** * @author Bernhard Schussek + * @group legacy */ class LoadedEntityChoiceListCompositeIdTest extends AbstractEntityChoiceListCompositeIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleIntIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleIntIdTest.php index 52d04c3879..f655784004 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleIntIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleIntIdTest.php @@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList; /** * @author Bernhard Schussek + * @group legacy */ class LoadedEntityChoiceListSingleIntIdTest extends AbstractEntityChoiceListSingleIntIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleStringIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleStringIdTest.php index 690d4b3d23..629b399ac3 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleStringIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleStringIdTest.php @@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList; /** * @author Bernhard Schussek + * @group legacy */ class LoadedEntityChoiceListSingleStringIdTest extends AbstractEntityChoiceListSingleStringIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdTest.php index 5740a2ff94..15436a8627 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdTest.php @@ -13,10 +13,11 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList; /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListCompositeIdTest extends AbstractEntityChoiceListCompositeIdTest { - public function testGetIndicesForValuesIgnoresNonExistingValues() + public function testLegacyGetIndicesForValuesIgnoresNonExistingValues() { $this->markTestSkipped('Non-existing values are not detected for unloaded choice lists.'); } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdWithQueryBuilderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdWithQueryBuilderTest.php index 9c72ccccd9..422295feb1 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdWithQueryBuilderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdWithQueryBuilderTest.php @@ -16,6 +16,7 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListCompositeIdWithQueryBuilderTest extends UnloadedEntityChoiceListCompositeIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdTest.php index dd53bf4226..2fa11f0d0b 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdTest.php @@ -13,17 +13,10 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList; /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListSingleIntIdTest extends AbstractEntityChoiceListSingleIntIdTest { - public function testGetIndicesForValuesIgnoresNonExistingValues() - { - $this->markTestSkipped('Non-existing values are not detected for unloaded choice lists.'); - } - - /** - * @group legacy - */ public function testLegacyGetIndicesForValuesIgnoresNonExistingValues() { $this->markTestSkipped('Non-existing values are not detected for unloaded choice lists.'); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdWithQueryBuilderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdWithQueryBuilderTest.php index fa5bb80ae7..c093782ff0 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdWithQueryBuilderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdWithQueryBuilderTest.php @@ -16,6 +16,7 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListSingleIntIdWithQueryBuilderTest extends UnloadedEntityChoiceListSingleIntIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdTest.php index 5b25b49a71..6600e49e89 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdTest.php @@ -13,10 +13,11 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList; /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListSingleStringIdTest extends AbstractEntityChoiceListSingleStringIdTest { - public function testGetIndicesForValuesIgnoresNonExistingValues() + public function testLegacyGetIndicesForValuesIgnoresNonExistingValues() { $this->markTestSkipped('Non-existing values are not detected for unloaded choice lists.'); } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdWithQueryBuilderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdWithQueryBuilderTest.php index 9fba5b9295..23329e80df 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdWithQueryBuilderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdWithQueryBuilderTest.php @@ -16,6 +16,7 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListSingleStringIdWithQueryBuilderTest extends UnloadedEntityChoiceListSingleStringIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index 95e11aff6f..27d1d88e43 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -131,7 +131,7 @@ class EntityTypeTest extends TypeTestCase 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'required' => false, - 'property' => 'name', + 'choice_label' => 'name', )); $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); @@ -165,7 +165,7 @@ class EntityTypeTest extends TypeTestCase 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'required' => false, - 'property' => 'name', + 'choice_label' => 'name', 'query_builder' => $qb, )); @@ -294,7 +294,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -316,7 +316,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => false, 'em' => 'default', 'class' => self::COMPOSITE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); // the collection key is used here @@ -340,7 +340,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit(array('1', '3')); @@ -365,7 +365,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $existing = new ArrayCollection(array(0 => $entity2)); @@ -396,7 +396,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => false, 'em' => 'default', 'class' => self::COMPOSITE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); // because of the composite key collection keys are used @@ -422,7 +422,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => false, 'em' => 'default', 'class' => self::COMPOSITE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $existing = new ArrayCollection(array(0 => $entity2)); @@ -452,7 +452,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => true, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -478,7 +478,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => true, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit(array('1', '3')); @@ -508,7 +508,7 @@ class EntityTypeTest extends TypeTestCase 'class' => self::SINGLE_IDENT_CLASS, // not all persisted entities should be displayed 'choices' => array($entity1, $entity2), - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -532,7 +532,7 @@ class EntityTypeTest extends TypeTestCase 'em' => 'default', 'class' => self::ITEM_GROUP_CLASS, 'choices' => array($item1, $item2, $item3, $item4), - 'property' => 'name', + 'choice_label' => 'name', 'group_by' => 'groupName', )); @@ -563,7 +563,7 @@ class EntityTypeTest extends TypeTestCase 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'preferred_choices' => array($entity3, $entity2), - 'property' => 'name', + 'choice_label' => 'name', )); $this->assertEquals(array(3 => new ChoiceView('Baz', '3', $entity3), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['preferred_choices']); @@ -583,7 +583,7 @@ class EntityTypeTest extends TypeTestCase 'class' => self::SINGLE_IDENT_CLASS, 'choices' => array($entity2, $entity3), 'preferred_choices' => array($entity3), - 'property' => 'name', + 'choice_label' => 'name', )); $this->assertEquals(array(3 => new ChoiceView('Baz', '3', $entity3)), $field->createView()->vars['preferred_choices']); @@ -602,7 +602,7 @@ class EntityTypeTest extends TypeTestCase 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'choices' => array($entity1, $entity2), - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('3'); @@ -623,7 +623,7 @@ class EntityTypeTest extends TypeTestCase 'em' => 'default', 'class' => self::COMPOSITE_IDENT_CLASS, 'choices' => array($entity1, $entity2), - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -647,7 +647,7 @@ class EntityTypeTest extends TypeTestCase 'class' => self::SINGLE_IDENT_CLASS, 'query_builder' => $repository->createQueryBuilder('e') ->where('e.id IN (1, 2)'), - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('3'); @@ -671,7 +671,7 @@ class EntityTypeTest extends TypeTestCase return $repository->createQueryBuilder('e') ->where('e.id IN (1, 2)'); }, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('3'); @@ -695,7 +695,7 @@ class EntityTypeTest extends TypeTestCase return $repository->createQueryBuilder('e') ->where('e.id1 IN (10, 50)'); }, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -715,7 +715,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_STRING_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('foo'); @@ -736,7 +736,7 @@ class EntityTypeTest extends TypeTestCase 'expanded' => false, 'em' => 'default', 'class' => self::COMPOSITE_STRING_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); // the collection key is used here @@ -760,7 +760,7 @@ class EntityTypeTest extends TypeTestCase $this->factory->createNamed('name', 'entity', null, array( 'class' => self::SINGLE_IDENT_CLASS, 'required' => false, - 'property' => 'name', + 'choice_label' => 'name', )); } @@ -775,7 +775,7 @@ class EntityTypeTest extends TypeTestCase $this->factory->createNamed('name', 'entity', null, array( 'em' => $this->em, 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); } @@ -852,20 +852,42 @@ class EntityTypeTest extends TypeTestCase 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'required' => false, - 'property' => 'name', + 'choice_label' => 'name', )); $field2 = $this->factory->createNamed('name', 'entity', null, array( 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'required' => false, - 'property' => 'name', + 'choice_label' => 'name', )); $this->assertInstanceOf('Symfony\Component\Form\ChoiceList\ChoiceListInterface', $field1->getConfig()->getOption('choice_list')); $this->assertSame($field1->getConfig()->getOption('choice_list'), $field2->getConfig()->getOption('choice_list')); } + /** + * @group legacy + */ + public function testLegacyPropertyOption() + { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + + $entity1 = new SingleIntIdEntity(1, 'Foo'); + $entity2 = new SingleIntIdEntity(2, 'Bar'); + + $this->persist(array($entity1, $entity2)); + + $field = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'required' => false, + 'property' => 'name', + )); + + $this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']); + } + protected function createRegistryMock($name, $em) { $registry = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry'); diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php index 7973072f32..30709108e8 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php @@ -39,9 +39,6 @@ use Symfony\Component\Form\Exception\InvalidArgumentException; * ``` * * @author Bernhard Schussek - * - * @deprecated Added for backwards compatibility in Symfony 2.7, to be removed - * in Symfony 3.0. Use {@link ArrayChoiceList} instead. */ class ArrayKeyChoiceList extends ArrayChoiceList { @@ -113,8 +110,6 @@ class ArrayKeyChoiceList extends ArrayChoiceList } parent::__construct($choices, $value); - - trigger_error('The '.__CLASS__.' class was added for backwards compatibility in version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\ArrayChoiceList instead.', E_USER_DEPRECATED); } /** diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php index 1c83782621..2b4d026db7 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ChoiceToValueTransformer.php @@ -43,20 +43,12 @@ class ChoiceToValueTransformer implements DataTransformerInterface throw new TransformationFailedException('Expected a scalar.'); } - // These are now valid ArrayChoiceList values, so we can return null - // right away - if ('' === $value || null === $value) { - return; - } - - $choices = $this->choiceList->getChoicesForValues(array($value)); + $choices = $this->choiceList->getChoicesForValues(array((string) $value)); if (1 !== count($choices)) { throw new TransformationFailedException(sprintf('The choice "%s" does not exist or is not unique', $value)); } - $choice = current($choices); - - return '' === $choice ? null : $choice; + return current($choices); } } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index a950be8d89..a597e1d4f4 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -233,25 +233,6 @@ class ChoiceType extends AbstractType { $choiceListFactory = $this->choiceListFactory; - $choiceList = function (Options $options, $choiceList) use ($choiceListFactory) { - if (null !== $options['choice_loader']) { - return $choiceListFactory->createListFromLoader( - $options['choice_loader'], - $options['choice_value'] - ); - } - - // Harden against NULL values (like in EntityType and ModelType) - $choices = null !== $options['choices'] ? $options['choices'] : array(); - - // BC when choices are in the keys, not in the values - if (!$options['choices_as_values']) { - return $choiceListFactory->createListFromFlippedChoices($choices, $options['choice_value']); - } - - return $choiceListFactory->createListFromChoices($choices, $options['choice_value']); - }; - $emptyData = function (Options $options) { if ($options['multiple'] || $options['expanded']) { return array(); @@ -269,11 +250,29 @@ class ChoiceType extends AbstractType return $options['empty_value']; }; - // deprecation note - $choiceListNormalizer = function (Options $options, $choiceList) { - trigger_error('The "choice_list" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_loader" instead.', E_USER_DEPRECATED); + $choiceListNormalizer = function (Options $options, $choiceList) use ($choiceListFactory) { + if ($choiceList) { + trigger_error('The "choice_list" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_loader" instead.', E_USER_DEPRECATED); - return $choiceList; + return $choiceList; + } + + if (null !== $options['choice_loader']) { + return $choiceListFactory->createListFromLoader( + $options['choice_loader'], + $options['choice_value'] + ); + } + + // Harden against NULL values (like in EntityType and ModelType) + $choices = null !== $options['choices'] ? $options['choices'] : array(); + + // BC when choices are in the keys, not in the values + if (!$options['choices_as_values']) { + return $choiceListFactory->createListFromFlippedChoices($choices, $options['choice_value']); + } + + return $choiceListFactory->createListFromChoices($choices, $options['choice_value']); }; $placeholderNormalizer = function (Options $options, $placeholder) { @@ -299,7 +298,7 @@ class ChoiceType extends AbstractType $resolver->setDefaults(array( 'multiple' => false, 'expanded' => false, - 'choice_list' => $choiceList, // deprecated + 'choice_list' => null, // deprecated 'choices' => array(), 'choices_as_values' => false, 'choice_loader' => null, diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/AbstractChoiceListTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/AbstractChoiceListTest.php index 68ef4dca4f..710c30c6c5 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/AbstractChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/AbstractChoiceListTest.php @@ -123,6 +123,8 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase protected function setUp() { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + parent::setUp(); $this->list = $this->createChoiceList(); @@ -151,19 +153,16 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase } } - public function testGetChoices() + public function testLegacyGetChoices() { $this->assertSame($this->choices, $this->list->getChoices()); } - public function testGetValues() + public function testLegacyGetValues() { $this->assertSame($this->values, $this->list->getValues()); } - /** - * @group legacy - */ public function testLegacyGetIndicesForChoices() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -172,9 +171,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array($this->index1, $this->index2), $this->list->getIndicesForChoices($choices)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForChoicesPreservesKeys() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -183,9 +179,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array(5 => $this->index1, 8 => $this->index2), $this->list->getIndicesForChoices($choices)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForChoicesPreservesOrder() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -194,9 +187,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array($this->index2, $this->index1), $this->list->getIndicesForChoices($choices)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForChoicesIgnoresNonExistingChoices() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -205,9 +195,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array($this->index1, $this->index2), $this->list->getIndicesForChoices($choices)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForChoicesEmpty() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -215,9 +202,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array(), $this->list->getIndicesForChoices(array())); } - /** - * @group legacy - */ public function testLegacyGetIndicesForValues() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -227,9 +211,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array($this->index1, $this->index2), $this->list->getIndicesForValues($values)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForValuesPreservesKeys() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -239,9 +220,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array(5 => $this->index1, 8 => $this->index2), $this->list->getIndicesForValues($values)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForValuesPreservesOrder() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -250,9 +228,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array($this->index2, $this->index1), $this->list->getIndicesForValues($values)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForValuesIgnoresNonExistingValues() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -261,9 +236,6 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array($this->index1, $this->index2), $this->list->getIndicesForValues($values)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForValuesEmpty() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -271,61 +243,61 @@ abstract class AbstractChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array(), $this->list->getIndicesForValues(array())); } - public function testGetChoicesForValues() + public function testLegacyGetChoicesForValues() { $values = array($this->value1, $this->value2); $this->assertSame(array($this->choice1, $this->choice2), $this->list->getChoicesForValues($values)); } - public function testGetChoicesForValuesPreservesKeys() + public function testLegacyGetChoicesForValuesPreservesKeys() { $values = array(5 => $this->value1, 8 => $this->value2); $this->assertSame(array(5 => $this->choice1, 8 => $this->choice2), $this->list->getChoicesForValues($values)); } - public function testGetChoicesForValuesPreservesOrder() + public function testLegacyGetChoicesForValuesPreservesOrder() { $values = array($this->value2, $this->value1); $this->assertSame(array($this->choice2, $this->choice1), $this->list->getChoicesForValues($values)); } - public function testGetChoicesForValuesIgnoresNonExistingValues() + public function testLegacyGetChoicesForValuesIgnoresNonExistingValues() { $values = array($this->value1, $this->value2, 'foobar'); $this->assertSame(array($this->choice1, $this->choice2), $this->list->getChoicesForValues($values)); } // https://github.com/symfony/symfony/issues/3446 - public function testGetChoicesForValuesEmpty() + public function testLegacyGetChoicesForValuesEmpty() { $this->assertSame(array(), $this->list->getChoicesForValues(array())); } - public function testGetValuesForChoices() + public function testLegacyGetValuesForChoices() { $choices = array($this->choice1, $this->choice2); $this->assertSame(array($this->value1, $this->value2), $this->list->getValuesForChoices($choices)); } - public function testGetValuesForChoicesPreservesKeys() + public function testLegacyGetValuesForChoicesPreservesKeys() { $choices = array(5 => $this->choice1, 8 => $this->choice2); $this->assertSame(array(5 => $this->value1, 8 => $this->value2), $this->list->getValuesForChoices($choices)); } - public function testGetValuesForChoicesPreservesOrder() + public function testLegacyGetValuesForChoicesPreservesOrder() { $choices = array($this->choice2, $this->choice1); $this->assertSame(array($this->value2, $this->value1), $this->list->getValuesForChoices($choices)); } - public function testGetValuesForChoicesIgnoresNonExistingChoices() + public function testLegacyGetValuesForChoicesIgnoresNonExistingChoices() { $choices = array($this->choice1, $this->choice2, 'foobar'); $this->assertSame(array($this->value1, $this->value2), $this->list->getValuesForChoices($choices)); } - public function testGetValuesForChoicesEmpty() + public function testLegacyGetValuesForChoicesEmpty() { $this->assertSame(array(), $this->list->getValuesForChoices(array())); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ChoiceListTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ChoiceListTest.php index 538bbc1b3d..25b4fdd45b 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ChoiceListTest.php @@ -14,6 +14,9 @@ namespace Symfony\Component\Form\Tests\Extension\Core\ChoiceList; use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; use Symfony\Component\Form\Extension\Core\View\ChoiceView; +/** + * @group legacy + */ class ChoiceListTest extends AbstractChoiceListTest { private $obj1; @@ -34,7 +37,7 @@ class ChoiceListTest extends AbstractChoiceListTest parent::setUp(); } - public function testInitArray() + public function testLegacyInitArray() { $this->list = new ChoiceList( array($this->obj1, $this->obj2, $this->obj3, $this->obj4), @@ -53,7 +56,7 @@ class ChoiceListTest extends AbstractChoiceListTest * choices parameter. A choice itself that is an object implementing \Traversable * is not treated as hierarchical structure, but as-is. */ - public function testInitNestedTraversable() + public function testLegacyInitNestedTraversable() { $traversableChoice = new \ArrayIterator(array($this->obj3, $this->obj4)); @@ -80,7 +83,7 @@ class ChoiceListTest extends AbstractChoiceListTest ), $this->list->getRemainingViews()); } - public function testInitNestedArray() + public function testLegacyInitNestedArray() { $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); $this->assertSame(array('0', '1', '2', '3'), $this->list->getValues()); @@ -97,7 +100,7 @@ class ChoiceListTest extends AbstractChoiceListTest /** * @expectedException \InvalidArgumentException */ - public function testInitWithInsufficientLabels() + public function testLegacyInitWithInsufficientLabels() { $this->list = new ChoiceList( array($this->obj1, $this->obj2), @@ -105,7 +108,7 @@ class ChoiceListTest extends AbstractChoiceListTest ); } - public function testInitWithLabelsContainingNull() + public function testLegacyInitWithLabelsContainingNull() { $this->list = new ChoiceList( array($this->obj1, $this->obj2), diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/LazyChoiceListTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/LazyChoiceListTest.php index 0e5e2e6527..15018b2830 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/LazyChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/LazyChoiceListTest.php @@ -15,8 +15,14 @@ use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; use Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList; use Symfony\Component\Form\Extension\Core\View\ChoiceView; +/** + * @group legacy + */ class LazyChoiceListTest extends \PHPUnit_Framework_TestCase { + /** + * @var LazyChoiceListTest_Impl + */ private $list; protected function setUp() @@ -37,22 +43,22 @@ class LazyChoiceListTest extends \PHPUnit_Framework_TestCase $this->list = null; } - public function testGetChoices() + public function testLegacyGetChoices() { $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c'), $this->list->getChoices()); } - public function testGetValues() + public function testLegacyGetValues() { $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c'), $this->list->getValues()); } - public function testGetPreferredViews() + public function testLegacyGetPreferredViews() { $this->assertEquals(array(1 => new ChoiceView('b', 'b', 'B')), $this->list->getPreferredViews()); } - public function testGetRemainingViews() + public function testLegacyGetRemainingViews() { $this->assertEquals(array(0 => new ChoiceView('a', 'a', 'A'), 2 => new ChoiceView('c', 'c', 'C')), $this->list->getRemainingViews()); } @@ -79,13 +85,13 @@ class LazyChoiceListTest extends \PHPUnit_Framework_TestCase $this->assertSame(array(1, 2), $this->list->getIndicesForValues($values)); } - public function testGetChoicesForValues() + public function testLegacyGetChoicesForValues() { $values = array('b', 'c'); $this->assertSame(array('b', 'c'), $this->list->getChoicesForValues($values)); } - public function testGetValuesForChoices() + public function testLegacyGetValuesForChoices() { $choices = array('b', 'c'); $this->assertSame(array('b', 'c'), $this->list->getValuesForChoices($choices)); @@ -94,7 +100,7 @@ class LazyChoiceListTest extends \PHPUnit_Framework_TestCase /** * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException */ - public function testLoadChoiceListShouldReturnChoiceList() + public function testLegacyLoadChoiceListShouldReturnChoiceList() { $list = new LazyChoiceListTest_InvalidImpl(); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ObjectChoiceListTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ObjectChoiceListTest.php index 2bb06349ae..63dc8a9ea1 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ObjectChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ObjectChoiceListTest.php @@ -29,6 +29,9 @@ class ObjectChoiceListTest_EntityWithToString } } +/** + * @group legacy + */ class ObjectChoiceListTest extends AbstractChoiceListTest { private $obj1; @@ -49,7 +52,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest parent::setUp(); } - public function testInitArray() + public function testLegacyInitArray() { $this->list = new ObjectChoiceList( array($this->obj1, $this->obj2, $this->obj3, $this->obj4), @@ -63,7 +66,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest $this->assertEquals(array(0 => new ChoiceView($this->obj1, '0', 'A'), 2 => new ChoiceView($this->obj3, '2', 'C'), 3 => new ChoiceView($this->obj4, '3', 'D')), $this->list->getRemainingViews()); } - public function testInitNestedArray() + public function testLegacyInitNestedArray() { $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices()); $this->assertSame(array('0', '1', '2', '3'), $this->list->getValues()); @@ -77,7 +80,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest ), $this->list->getRemainingViews()); } - public function testInitArrayWithGroupPath() + public function testLegacyInitArrayWithGroupPath() { $this->obj1 = (object) array('name' => 'A', 'category' => 'Group 1'); $this->obj2 = (object) array('name' => 'B', 'category' => 'Group 1'); @@ -115,7 +118,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest /** * @expectedException \InvalidArgumentException */ - public function testInitArrayWithGroupPathThrowsExceptionIfNestedArray() + public function testLegacyInitArrayWithGroupPathThrowsExceptionIfNestedArray() { $this->obj1 = (object) array('name' => 'A', 'category' => 'Group 1'); $this->obj2 = (object) array('name' => 'B', 'category' => 'Group 1'); @@ -133,7 +136,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest ); } - public function testInitArrayWithValuePath() + public function testLegacyInitArrayWithValuePath() { $this->obj1 = (object) array('name' => 'A', 'id' => 10); $this->obj2 = (object) array('name' => 'B', 'id' => 20); @@ -154,7 +157,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest $this->assertEquals(array(0 => new ChoiceView($this->obj1, '10', 'A'), 3 => new ChoiceView($this->obj4, '40', 'D')), $this->list->getRemainingViews()); } - public function testInitArrayUsesToString() + public function testLegacyInitArrayUsesToString() { $this->obj1 = new ObjectChoiceListTest_EntityWithToString('A'); $this->obj2 = new ObjectChoiceListTest_EntityWithToString('B'); @@ -173,7 +176,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest /** * @expectedException \Symfony\Component\Form\Exception\StringCastException */ - public function testInitArrayThrowsExceptionIfToStringNotFound() + public function testLegacyInitArrayThrowsExceptionIfToStringNotFound() { $this->obj1 = new ObjectChoiceListTest_EntityWithToString('A'); $this->obj2 = new ObjectChoiceListTest_EntityWithToString('B'); @@ -262,7 +265,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest $this->assertSame(array($this->index1, $this->index2), $this->list->getIndicesForChoices($choices)); } - public function testGetValuesForChoicesWithValuePath() + public function testLegacyGetValuesForChoicesWithValuePath() { $this->list = new ObjectChoiceList( array($this->obj1, $this->obj2, $this->obj3, $this->obj4), @@ -276,7 +279,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest $this->assertSame(array('A', 'B'), $this->list->getValuesForChoices($choices)); } - public function testGetValuesForChoicesWithValuePathPreservesKeys() + public function testLegacyGetValuesForChoicesWithValuePathPreservesKeys() { $this->list = new ObjectChoiceList( array($this->obj1, $this->obj2, $this->obj3, $this->obj4), @@ -290,7 +293,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest $this->assertSame(array(5 => 'A', 8 => 'B'), $this->list->getValuesForChoices($choices)); } - public function testGetValuesForChoicesWithValuePathPreservesOrder() + public function testLegacyGetValuesForChoicesWithValuePathPreservesOrder() { $this->list = new ObjectChoiceList( array($this->obj1, $this->obj2, $this->obj3, $this->obj4), @@ -304,7 +307,7 @@ class ObjectChoiceListTest extends AbstractChoiceListTest $this->assertSame(array('B', 'A'), $this->list->getValuesForChoices($choices)); } - public function testGetValuesForChoicesWithValuePathIgnoresNonExistingChoices() + public function testLegacyGetValuesForChoicesWithValuePathIgnoresNonExistingChoices() { $this->list = new ObjectChoiceList( array($this->obj1, $this->obj2, $this->obj3, $this->obj4), diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/SimpleChoiceListTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/SimpleChoiceListTest.php index 3a5804ef28..ddf714f793 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/SimpleChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/SimpleChoiceListTest.php @@ -14,9 +14,12 @@ namespace Symfony\Component\Form\Tests\Extension\Core\ChoiceList; use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; use Symfony\Component\Form\Extension\Core\View\ChoiceView; +/** + * @group legacy + */ class SimpleChoiceListTest extends AbstractChoiceListTest { - public function testInitArray() + public function testLegacyInitArray() { $choices = array('a' => 'A', 'b' => 'B', 'c' => 'C'); $this->list = new SimpleChoiceList($choices, array('b')); @@ -27,7 +30,7 @@ class SimpleChoiceListTest extends AbstractChoiceListTest $this->assertEquals(array(0 => new ChoiceView('a', 'a', 'A'), 2 => new ChoiceView('c', 'c', 'C')), $this->list->getRemainingViews()); } - public function testInitNestedArray() + public function testLegacyInitNestedArray() { $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd'), $this->list->getChoices()); $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd'), $this->list->getValues()); @@ -44,7 +47,7 @@ class SimpleChoiceListTest extends AbstractChoiceListTest /** * @dataProvider dirtyValuesProvider */ - public function testGetValuesForChoicesDealsWithDirtyValues($choice, $value) + public function testLegacyGetValuesForChoicesDealsWithDirtyValues($choice, $value) { $choices = array( '0' => 'Zero', diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/SimpleNumericChoiceListTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/SimpleNumericChoiceListTest.php index b351790c45..0fd5fb92d9 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/SimpleNumericChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/SimpleNumericChoiceListTest.php @@ -13,11 +13,11 @@ namespace Symfony\Component\Form\Tests\Extension\Core\ChoiceList; use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; +/** + * @group legacy + */ class SimpleNumericChoiceListTest extends AbstractChoiceListTest { - /** - * @group legacy - */ public function testLegacyGetIndicesForChoicesDealsWithNumericChoices() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -27,9 +27,6 @@ class SimpleNumericChoiceListTest extends AbstractChoiceListTest $this->assertSame(array(0, 1), $this->list->getIndicesForChoices($choices)); } - /** - * @group legacy - */ public function testLegacyGetIndicesForValuesDealsWithNumericValues() { $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); @@ -39,14 +36,14 @@ class SimpleNumericChoiceListTest extends AbstractChoiceListTest $this->assertSame(array(0, 1), $this->list->getIndicesForValues($values)); } - public function testGetChoicesForValuesDealsWithNumericValues() + public function testLegacyGetChoicesForValuesDealsWithNumericValues() { // Pass values as strings although they are integers $values = array('0', '1'); $this->assertSame(array(0, 1), $this->list->getChoicesForValues($values)); } - public function testGetValuesForChoicesDealsWithNumericValues() + public function testLegacyGetValuesForChoicesDealsWithNumericValues() { // Pass values as strings although they are integers $values = array('0', '1'); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/ChoiceToValueTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/ChoiceToValueTransformerTest.php index bbae0621ce..c58d072f47 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/ChoiceToValueTransformerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/ChoiceToValueTransformerTest.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\DataTransformer; -use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; +use Symfony\Component\Form\ChoiceList\ArrayChoiceList; use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer; class ChoiceToValueTransformerTest extends \PHPUnit_Framework_TestCase @@ -20,7 +20,8 @@ class ChoiceToValueTransformerTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $list = new SimpleChoiceList(array('' => 'A', 0 => 'B', 1 => 'C')); + $list = new ArrayChoiceList(array('', 0, 'X')); + $this->transformer = new ChoiceToValueTransformer($list); } @@ -33,9 +34,8 @@ class ChoiceToValueTransformerTest extends \PHPUnit_Framework_TestCase { return array( // more extensive test set can be found in FormUtilTest - array(0, '0'), - array(false, '0'), - array('', ''), + array('', '0'), + array(0, '1'), ); } @@ -52,9 +52,9 @@ class ChoiceToValueTransformerTest extends \PHPUnit_Framework_TestCase return array( // values are expected to be valid choice keys already and stay // the same - array('0', 0), - array('', null), - array(null, null), + array('0', ''), + array('1', 0), + array('2', 'X'), ); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/ChoicesToValuesTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/ChoicesToValuesTransformerTest.php index 87f5018b04..a7dc40aca2 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/ChoicesToValuesTransformerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/ChoicesToValuesTransformerTest.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\DataTransformer; -use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; +use Symfony\Component\Form\ChoiceList\ArrayChoiceList; use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer; class ChoicesToValuesTransformerTest extends \PHPUnit_Framework_TestCase @@ -20,7 +20,7 @@ class ChoicesToValuesTransformerTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $list = new SimpleChoiceList(array(0 => 'A', 1 => 'B', 2 => 'C')); + $list = new ArrayChoiceList(array('A', 'B', 'C')); $this->transformer = new ChoicesToValuesTransformer($list); } @@ -31,8 +31,7 @@ class ChoicesToValuesTransformerTest extends \PHPUnit_Framework_TestCase public function testTransform() { - // Value strategy in SimpleChoiceList is to copy and convert to string - $in = array(0, 1, 2); + $in = array('A', 'B', 'C'); $out = array('0', '1', '2'); $this->assertSame($out, $this->transformer->transform($in)); @@ -55,7 +54,7 @@ class ChoicesToValuesTransformerTest extends \PHPUnit_Framework_TestCase { // values are expected to be valid choices and stay the same $in = array('0', '1', '2'); - $out = array(0, 1, 2); + $out = array('A', 'B', 'C'); $this->assertSame($out, $this->transformer->reverseTransform($in)); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixRadioInputListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixRadioInputListenerTest.php index 426293395c..cae43b6e79 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixRadioInputListenerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixRadioInputListenerTest.php @@ -15,12 +15,17 @@ use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener; use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; +/** + * @group legacy + */ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase { private $choiceList; protected function setUp() { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + parent::setUp(); $this->choiceList = new SimpleChoiceList(array('' => 'Empty', 0 => 'A', 1 => 'B')); @@ -33,7 +38,7 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase $listener = null; } - public function testFixRadio() + public function testLegacyFixRadio() { $data = '1'; $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); @@ -46,7 +51,7 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals(array(2 => '1'), $event->getData()); } - public function testFixZero() + public function testLegacyFixZero() { $data = '0'; $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); @@ -59,7 +64,7 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals(array(1 => '0'), $event->getData()); } - public function testFixEmptyString() + public function testLegacyFixEmptyString() { $data = ''; $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); @@ -72,7 +77,7 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals(array(0 => ''), $event->getData()); } - public function testConvertEmptyStringToPlaceholderIfNotFound() + public function testLegacyConvertEmptyStringToPlaceholderIfNotFound() { $list = new SimpleChoiceList(array(0 => 'A', 1 => 'B')); @@ -86,7 +91,7 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase $this->assertEquals(array('placeholder' => ''), $event->getData()); } - public function testDontConvertEmptyStringToPlaceholderIfNoPlaceholderUsed() + public function testLegacyDontConvertEmptyStringToPlaceholderIfNoPlaceholderUsed() { $list = new SimpleChoiceList(array(0 => 'A', 1 => 'B')); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php index 6a0b6db2ec..d34d5b2184 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -368,8 +368,13 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertEquals('2', $form->getViewData()); } - public function testSubmitSingleNonExpandedObjectChoicesBc() + /** + * @group legacy + */ + public function testLegacySubmitSingleNonExpandedObjectChoices() { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + $form = $this->factory->create('choice', null, array( 'multiple' => false, 'expanded' => false, @@ -483,8 +488,13 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertEquals(array('2', '3'), $form->getViewData()); } - public function testSubmitMultipleNonExpandedObjectChoicesBc() + /** + * @group legacy + */ + public function testLegacySubmitMultipleNonExpandedObjectChoices() { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + $form = $this->factory->create('choice', null, array( 'multiple' => true, 'expanded' => false, @@ -959,8 +969,13 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertNull($form[4]->getViewData()); } - public function testSubmitSingleExpandedObjectChoicesBc() + /** + * @group legacy + */ + public function testLegacySubmitSingleExpandedObjectChoices() { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + $form = $this->factory->create('choice', null, array( 'multiple' => false, 'expanded' => true, @@ -1182,8 +1197,13 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertNull($form[4]->getViewData()); } - public function testSubmitMultipleExpandedObjectChoicesBc() + /** + * @group legacy + */ + public function testLegacySubmitMultipleExpandedObjectChoices() { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + $form = $this->factory->create('choice', null, array( 'multiple' => true, 'expanded' => true, From 7e0960d7168e845dc5d0ad30296e27397fce44c1 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 31 Mar 2015 14:56:46 +0200 Subject: [PATCH 13/60] [Form] Fixed failing layout tests --- .../Extension/FormExtensionDivLayoutTest.php | 4 +- .../Tests/AbstractBootstrap3LayoutTest.php | 130 ++++++++++++++++++ 2 files changed, 132 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php index 0c25ad44cd..334abd7092 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php @@ -17,8 +17,8 @@ use Symfony\Bridge\Twig\Form\TwigRendererEngine; use Symfony\Bridge\Twig\Extension\TranslationExtension; use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\FormView; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Tests\AbstractDivLayoutTest; class FormExtensionDivLayoutTest extends AbstractDivLayoutTest @@ -132,7 +132,7 @@ class FormExtensionDivLayoutTest extends AbstractDivLayoutTest */ public function testIsChoiceSelected($expected, $choice, $value) { - $choice = new ChoiceView($choice, $choice, $choice.' label'); + $choice = new ChoiceView($choice.' label', $choice, $choice); $this->assertSame($expected, $this->extension->isSelectedChoice($choice, $value)); } diff --git a/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php index b5354e0bd2..6cc7edd7a3 100644 --- a/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php @@ -231,6 +231,29 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest ); } + public function testSingleChoiceAttributes() + { + $form = $this->factory->createNamed('name', 'choice', '&a', array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'multiple' => false, + 'expanded' => false, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array('attr' => array('class' => 'my&class')), +'/select + [@name="name"] + [@class="my&class form-control"] + [not(@required)] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + public function testSingleChoiceWithPreferred() { $form = $this->factory->createNamed('name', 'choice', '&a', array( @@ -496,6 +519,31 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest ); } + public function testMultipleChoiceAttributes() + { + $form = $this->factory->createNamed('name', 'choice', array('&a'), array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'required' => true, + 'multiple' => true, + 'expanded' => false, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array('attr' => array('class' => 'my&class')), +'/select + [@name="name[]"] + [@class="my&class form-control"] + [@required="required"] + [@multiple="multiple"] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + public function testMultipleChoiceSkipsPlaceholder() { $form = $this->factory->createNamed('name', 'choice', array('&a'), array( @@ -577,6 +625,42 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest ); } + public function testSingleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', 'choice', '&a', array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'multiple' => false, + 'expanded' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./div + [@class="radio"] + [ + ./label + [.="[trans]Choice&A[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] + ] + ] + /following-sibling::div + [@class="radio"] + [ + ./label + [.="[trans]Choice&B[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)][@class="foo&bar"] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"][@class="form-control"] + ] +' + ); + } + public function testSingleChoiceExpandedWithPlaceholder() { $form = $this->factory->createNamed('name', 'choice', '&a', array( @@ -702,6 +786,52 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest ); } + public function testMultipleChoiceExpandedAttributes() + { + $form = $this->factory->createNamed('name', 'choice', array('&a', '&c'), array( + 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B', '&c' => 'Choice&C'), + 'choice_attr' => array('Choice&B' => array('class' => 'foo&bar')), + 'multiple' => true, + 'expanded' => true, + 'required' => true, + )); + + $this->assertWidgetMatchesXpath($form->createView(), array(), +'/div + [ + ./div + [@class="checkbox"] + [ + ./label + [.="[trans]Choice&A[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)] + ] + ] + /following-sibling::div + [@class="checkbox"] + [ + ./label + [.="[trans]Choice&B[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_1"][not(@checked)][not(@required)][@class="foo&bar"] + ] + ] + /following-sibling::div + [@class="checkbox"] + [ + ./label + [.="[trans]Choice&C[/trans]"] + [ + ./input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"][@class="form-control"] + ] +' + ); + } + public function testCountry() { $form = $this->factory->createNamed('name', 'country', 'AT'); From 94d18e961cc72008adf97c0557856da63760688d Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 31 Mar 2015 14:58:03 +0200 Subject: [PATCH 14/60] [Form] Fixed CS --- src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php | 2 +- .../Form/ChoiceList/Factory/CachingFactoryDecorator.php | 1 - .../Form/ChoiceList/Factory/DefaultChoiceListFactory.php | 1 - .../Form/ChoiceList/Factory/PropertyAccessDecorator.php | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php index f6164725fd..7b48005408 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php @@ -96,7 +96,7 @@ class IdReader public function getIdValue($object) { if (!$object) { - return null; + return; } if (!$this->om->contains($object)) { diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php index 3a2702a335..f9848c2d0e 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php @@ -14,7 +14,6 @@ namespace Symfony\Component\Form\ChoiceList\Factory; use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; use Symfony\Component\Form\ChoiceList\View\ChoiceListView; -use Symfony\Component\Form\Exception\UnexpectedTypeException; /** * Caches the choice lists created by the decorated factory. diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php index 31527a9f34..907829be0e 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -19,7 +19,6 @@ use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceListView; use Symfony\Component\Form\ChoiceList\View\ChoiceView; -use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface as LegacyChoiceListInterface; /** diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php index f6fd823784..0f4bcaa14e 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php @@ -99,7 +99,7 @@ class PropertyAccessDecorator implements ChoiceListFactoryInterface return $accessor->getValue($choice, $value); } - return null; + return; }; } From 2b5efbbd35311c98b649c7c3e05314e893c6d374 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 1 Apr 2015 13:20:04 +0200 Subject: [PATCH 15/60] [FrameworkBundle] Pushed symfony/form dependency from ~2.6 to to ~2.7 --- src/Symfony/Bundle/FrameworkBundle/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 98012bdacc..fe0cbbf099 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -41,7 +41,7 @@ "symfony/finder": "~2.0,>=2.0.5|~3.0.0", "symfony/intl": "~2.3|~3.0.0", "symfony/security": "~2.6|~3.0.0", - "symfony/form": "~2.6|~3.0.0", + "symfony/form": "~2.7|~3.0.0", "symfony/class-loader": "~2.1|~3.0.0", "symfony/expression-language": "~2.6|~3.0.0", "symfony/process": "~2.0,>=2.0.5|~3.0.0", From 7523f685ebf7c51941d547df213068cc08db97e4 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 1 Apr 2015 13:22:40 +0200 Subject: [PATCH 16/60] [TwigBridge] Pushed symfony/form dependency from ~2.6 to ~2.7 --- src/Symfony/Bridge/Twig/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 11e4e24240..85f9ea3cc2 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -23,7 +23,7 @@ "symfony/phpunit-bridge": "~2.7|~3.0.0", "symfony/asset": "~2.7|~3.0.0", "symfony/finder": "~2.3|~3.0.0", - "symfony/form": "~2.6,>=2.6.6|~3.0.0", + "symfony/form": "~2.7|~3.0.0", "symfony/http-kernel": "~2.3|~3.0.0", "symfony/intl": "~2.3|~3.0.0", "symfony/routing": "~2.2|~3.0.0", From 17d265a5266eba1938f6c0bd6c15271bbaadc5b4 Mon Sep 17 00:00:00 2001 From: Diego Saint Esteben Date: Tue, 31 Mar 2015 21:58:16 -0300 Subject: [PATCH 17/60] Added missing changelog entry --- src/Symfony/Component/VarDumper/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/Symfony/Component/VarDumper/CHANGELOG.md diff --git a/src/Symfony/Component/VarDumper/CHANGELOG.md b/src/Symfony/Component/VarDumper/CHANGELOG.md new file mode 100644 index 0000000000..6b08aa77ac --- /dev/null +++ b/src/Symfony/Component/VarDumper/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +2.7.0 +----- + + * deprecated Cloner\Data::getLimitedClone(). Use withMaxDepth, withMaxItemsPerDepth or withRefHandles instead. From 195c57e1f50765aff33137689b16e126a689056a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 16 Mar 2015 15:12:02 +0100 Subject: [PATCH 18/60] Safe escaping of fragments for eval() --- .../Component/HttpKernel/HttpCache/Esi.php | 62 +++++++++---------- .../HttpKernel/Tests/HttpCache/EsiTest.php | 4 +- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php index 54e841530f..58b6265656 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php @@ -29,6 +29,10 @@ use Symfony\Component\HttpKernel\HttpKernelInterface; class Esi { private $contentTypes; + private $phpEscapeMap = array( + array('', '', '', ''), + ); /** * Constructor. @@ -158,10 +162,34 @@ class Esi // we don't use a proper XML parser here as we can have ESI tags in a plain text response $content = $response->getContent(); - $content = str_replace(array('', ''), $content); - $content = preg_replace_callback('##', array($this, 'handleEsiIncludeTag'), $content); - $content = preg_replace('#]*(?:/|#', '', $content); $content = preg_replace('#.*?#', '', $content); + $content = preg_replace('#]*(?:/|#', '', $content); + + $chunks = preg_split('##', $content, -1, PREG_SPLIT_DELIM_CAPTURE); + $chunks[0] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[0]); + + $i = 1; + while (isset($chunks[$i])) { + $options = array(); + preg_match_all('/(src|onerror|alt)="([^"]*?)"/', $chunks[$i], $matches, PREG_SET_ORDER); + foreach ($matches as $set) { + $options[$set[1]] = $set[2]; + } + + if (!isset($options['src'])) { + throw new \RuntimeException('Unable to process an ESI tag without a "src" attribute.'); + } + + $chunks[$i] = sprintf('esi->handle($this, %s, %s, %s) ?>'."\n", + var_export($options['src'], true), + var_export(isset($options['alt']) ? $options['alt'] : '', true), + isset($options['onerror']) && 'continue' == $options['onerror'] ? 'true' : 'false' + ); + ++$i; + $chunks[$i] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[$i]); + ++$i; + } + $content = implode('', $chunks); $response->setContent($content); $response->headers->set('X-Body-Eval', 'ESI'); @@ -214,32 +242,4 @@ class Esi } } } - - /** - * Handles an ESI include tag (called internally). - * - * @param array $attributes An array containing the attributes. - * - * @return string The response content for the include. - * - * @throws \RuntimeException - */ - private function handleEsiIncludeTag($attributes) - { - $options = array(); - preg_match_all('/(src|onerror|alt)="([^"]*?)"/', $attributes[1], $matches, PREG_SET_ORDER); - foreach ($matches as $set) { - $options[$set[1]] = $set[2]; - } - - if (!isset($options['src'])) { - throw new \RuntimeException('Unable to process an ESI tag without a "src" attribute.'); - } - - return sprintf('esi->handle($this, %s, %s, %s) ?>'."\n", - var_export($options['src'], true), - var_export(isset($options['alt']) ? $options['alt'] : '', true), - isset($options['onerror']) && 'continue' == $options['onerror'] ? 'true' : 'false' - ); - } } diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php index ad400c69ae..d1411f016b 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php @@ -124,10 +124,10 @@ class EsiTest extends \PHPUnit_Framework_TestCase $esi = new Esi(); $request = Request::create('/'); - $response = new Response('foo <%= "lala" %>'); + $response = new Response(''); $esi->process($request, $response); - $this->assertEquals('foo php die("foo"); ?>= "lala" %>', $response->getContent()); + $this->assertEquals('php cript language=php>', $response->getContent()); } /** From 6c73f0ce9302a0091bbfbb96f317e400ce16ef84 Mon Sep 17 00:00:00 2001 From: James Gilliland Date: Tue, 17 Feb 2015 11:56:59 -0600 Subject: [PATCH 19/60] isFromTrustedProxy to confirm request came from a trusted proxy. --- .../Component/HttpFoundation/Request.php | 13 +++++-- .../HttpFoundation/Tests/RequestTest.php | 38 ++++++++++++------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index de798de27a..75ef72d0c7 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -791,7 +791,7 @@ class Request { $ip = $this->server->get('REMOTE_ADDR'); - if (!self::$trustedProxies) { + if (!$this->isFromTrustedProxy()) { return array($ip); } @@ -957,7 +957,7 @@ class Request */ public function getPort() { - if (self::$trustedProxies) { + if ($this->isFromTrustedProxy()) { if (self::$trustedHeaders[self::HEADER_CLIENT_PORT] && $port = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_PORT])) { return $port; } @@ -1138,7 +1138,7 @@ class Request */ public function isSecure() { - if (self::$trustedProxies && self::$trustedHeaders[self::HEADER_CLIENT_PROTO] && $proto = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_PROTO])) { + if ($this->isFromTrustedProxy() && self::$trustedHeaders[self::HEADER_CLIENT_PROTO] && $proto = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_PROTO])) { return in_array(strtolower(current(explode(',', $proto))), array('https', 'on', 'ssl', '1')); } @@ -1166,7 +1166,7 @@ class Request */ public function getHost() { - if (self::$trustedProxies && self::$trustedHeaders[self::HEADER_CLIENT_HOST] && $host = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_HOST])) { + if ($this->isFromTrustedProxy() && self::$trustedHeaders[self::HEADER_CLIENT_HOST] && $host = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_HOST])) { $elements = explode(',', $host); $host = $elements[count($elements) - 1]; @@ -1853,4 +1853,9 @@ class Request return false; } + + private function isFromTrustedProxy() + { + return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR'), self::$trustedProxies); + } } diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php index e8639d7be0..a1a123426b 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php @@ -722,35 +722,37 @@ class RequestTest extends \PHPUnit_Framework_TestCase 'HTTP_X_FORWARDED_PROTO' => 'https', 'HTTP_X_FORWARDED_PORT' => '8443', )); - $port = $request->getPort(); - - $this->assertEquals(8443, $port, 'With PROTO and PORT set PORT takes precedence.'); + $this->assertEquals(80, $request->getPort(), 'With PROTO and PORT on untrusted connection server value takes precedence.'); + $request->server->set('REMOTE_ADDR', '1.1.1.1'); + $this->assertEquals(8443, $request->getPort(), 'With PROTO and PORT set PORT takes precedence.'); $request = Request::create('http://example.com', 'GET', array(), array(), array(), array( 'HTTP_X_FORWARDED_PROTO' => 'https', )); - $port = $request->getPort(); - - $this->assertEquals(443, $port, 'With only PROTO set getPort() defaults to 443.'); + $this->assertEquals(80, $request->getPort(), 'With only PROTO set getPort() ignores trusted headers on untrusted connection.'); + $request->server->set('REMOTE_ADDR', '1.1.1.1'); + $this->assertEquals(443, $request->getPort(), 'With only PROTO set getPort() defaults to 443.'); $request = Request::create('http://example.com', 'GET', array(), array(), array(), array( 'HTTP_X_FORWARDED_PROTO' => 'http', )); - $port = $request->getPort(); - - $this->assertEquals(80, $port, 'If X_FORWARDED_PROTO is set to HTTP return 80.'); + $this->assertEquals(80, $request->getPort(), 'If X_FORWARDED_PROTO is set to HTTP getPort() ignores trusted headers on untrusted connection.'); + $request->server->set('REMOTE_ADDR', '1.1.1.1'); + $this->assertEquals(80, $request->getPort(), 'If X_FORWARDED_PROTO is set to HTTP getPort() returns port of the original request.'); $request = Request::create('http://example.com', 'GET', array(), array(), array(), array( 'HTTP_X_FORWARDED_PROTO' => 'On', )); - $port = $request->getPort(); - $this->assertEquals(443, $port, 'With only PROTO set and value is On, getPort() defaults to 443.'); + $this->assertEquals(80, $request->getPort(), 'With only PROTO set and value is On, getPort() ignores trusted headers on untrusted connection.'); + $request->server->set('REMOTE_ADDR', '1.1.1.1'); + $this->assertEquals(443, $request->getPort(), 'With only PROTO set and value is On, getPort() defaults to 443.'); $request = Request::create('http://example.com', 'GET', array(), array(), array(), array( 'HTTP_X_FORWARDED_PROTO' => '1', )); - $port = $request->getPort(); - $this->assertEquals(443, $port, 'With only PROTO set and value is 1, getPort() defaults to 443.'); + $this->assertEquals(80, $request->getPort(), 'With only PROTO set and value is 1, getPort() ignores trusted headers on untrusted connection.'); + $request->server->set('REMOTE_ADDR', '1.1.1.1'); + $this->assertEquals(443, $request->getPort(), 'With only PROTO set and value is 1, getPort() defaults to 443.'); $request = Request::create('http://example.com', 'GET', array(), array(), array(), array( 'HTTP_X_FORWARDED_PROTO' => 'something-else', @@ -1020,6 +1022,8 @@ class RequestTest extends \PHPUnit_Framework_TestCase $request->headers->set('X_FORWARDED_PROTO', 'https'); Request::setTrustedProxies(array('1.1.1.1')); + $this->assertFalse($request->isSecure()); + $request->server->set('REMOTE_ADDR', '1.1.1.1'); $this->assertTrue($request->isSecure()); Request::setTrustedProxies(array()); @@ -1455,7 +1459,15 @@ class RequestTest extends \PHPUnit_Framework_TestCase $this->assertEquals(443, $request->getPort()); $this->assertTrue($request->isSecure()); + // trusted proxy via setTrustedProxies() + Request::setTrustedProxies(array('3.3.3.4', '2.2.2.2')); + $this->assertEquals('3.3.3.3', $request->getClientIp()); + $this->assertEquals('example.com', $request->getHost()); + $this->assertEquals(80, $request->getPort()); + $this->assertFalse($request->isSecure()); + // check various X_FORWARDED_PROTO header values + Request::setTrustedProxies(array('3.3.3.3', '2.2.2.2')); $request->headers->set('X_FORWARDED_PROTO', 'ssl'); $this->assertTrue($request->isSecure()); From dcd79710330d126216f324855689cdbe3e4275f0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 1 Apr 2015 16:26:15 +0200 Subject: [PATCH 20/60] updated CHANGELOG for 2.3.27 --- CHANGELOG-2.3.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG-2.3.md b/CHANGELOG-2.3.md index 12b4efd248..675302718a 100644 --- a/CHANGELOG-2.3.md +++ b/CHANGELOG-2.3.md @@ -7,6 +7,16 @@ 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.27 (2015-04-01) + + * security #14167 CVE-2015-2308 (nicolas-grekas) + * security #14166 CVE-2015-2309 (neclimdul) + * bug #14010 Replace GET parameters when changed in form (WouterJ) + * bug #13991 [Dependency Injection] Improve PhpDumper Performance for huge Containers (BattleRattle) + * bug #13997 [2.3+][Form][DoctrineBridge] Improved loading of entities and documents (guilhermeblanco) + * bug #13953 [Translation][MoFileLoader] fixed load empty translation. (aitboudad) + * bug #13912 [DependencyInjection] Highest precedence for user parameters (lyrixx) + * 2.3.26 (2015-03-17) * bug #13927 Fixing wrong variable name from #13519 (weaverryan) From a7dcf0c15f9e3bdc112a25b6e40b7d3a1105299f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 1 Apr 2015 16:28:11 +0200 Subject: [PATCH 21/60] update CONTRIBUTORS for 2.3.27 --- CONTRIBUTORS.md | 57 ++++++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 09feceb7b6..d773ad7ebb 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -11,8 +11,8 @@ Symfony is the result of the work of many people who made the code better - Jordi Boggiano (seldaek) - Johannes S (johannes) - Kris Wallsmith (kriswallsmith) - - Christophe Coevoet (stof) - Nicolas Grekas (nicolas-grekas) + - Christophe Coevoet (stof) - Jakub Zalas (jakubzalas) - Pascal Borreli (pborreli) - Hugo Hamon (hhamon) @@ -21,9 +21,9 @@ Symfony is the result of the work of many people who made the code better - Ryan Weaver (weaverryan) - Lukas Kahwe Smith (lsmith) - Romain Neutron (romain) + - Christian Flothmann (xabbuh) - Jeremy Mikola (jmikola) - Jean-François Simon (jfsimon) - - Christian Flothmann (xabbuh) - Benjamin Eberlei (beberlei) - Igor Wiedler (igorw) - Martin Hasoň (hason) @@ -37,16 +37,16 @@ Symfony is the result of the work of many people who made the code better - stealth35 ‏ (stealth35) - Alexander Mols (asm89) - Bulat Shakirzyanov (avalanche123) + - Abdellatif Ait boudad (aitboudad) - Francis Besset (francisbesset) - Saša Stamenković (umpirsky) - Henrik Bjørnskov (henrikbjorn) - Miha Vrhovnik + - Kévin Dunglas (dunglas) + - Sarah Khalil (saro0h) - Konstantin Kudryashov (everzet) - Bilal Amarni (bamarni) - - Kévin Dunglas (dunglas) - Florin Patan (florinpatan) - - Abdellatif Ait Boudad (aitboudad) - - Sarah Khalil (saro0h) - Eric Clemmons (ericclemmons) - Andrej Hudec (pulzarraider) - Deni @@ -61,13 +61,14 @@ Symfony is the result of the work of many people who made the code better - Daniel Holmes (dholmes) - Bart van den Burg (burgov) - Jordan Alliot (jalliot) + - Kevin Bond (kbond) - John Wards (johnwards) - Fran Moreno (franmomu) - - Antoine Hérault (herzult) - - Kevin Bond (kbond) - - Toni Uebernickel (havvg) - Luis Cordova (cordoval) + - Antoine Hérault (herzult) + - Toni Uebernickel (havvg) - Arnaud Le Blanc (arnaud-lb) + - Gábor Egyed (1ed) - Tim Nagel (merk) - Brice BERNARD (brikou) - marc.weistroff @@ -78,7 +79,6 @@ Symfony is the result of the work of many people who made the code better - Colin Frei - Jérôme Tamarelle (gromnan) - Adrien Brault (adrienbrault) - - Gábor Egyed (1ed) - excelwebzone - Jacob Dreesen (jdreesen) - Fabien Pennequin (fabienpennequin) @@ -93,26 +93,29 @@ Symfony is the result of the work of many people who made the code better - Daniel Gomes (danielcsgomes) - Hidenori Goto (hidenorigoto) - David Buchmann (dbu) + - Guilherme Blanco (guilhermeblanco) - Jérémy DERUSSÉ (jderusse) - Pablo Godel (pgodel) - Eric GELOEN (gelo) - Jérémie Augustin (jaugustin) - Rafael Dohms (rdohms) - - Guilherme Blanco (guilhermeblanco) - Tigran Azatyan (tigranazatyan) - Javier Eguiluz (javier.eguiluz) - Arnaud Kleinpeter (nanocom) - Richard Shank (iampersistent) + - Dariusz Ruminski - Clemens Tolboom - Helmer Aaviksoo - Sebastiaan Stok (sstok) - Hiromi Hishida (77web) - Matthieu Ouellette-Vachon (maoueh) - Michał Pipa (michal.pipa) + - Issei Murasawa (issei_m) - Amal Raghav (kertz) - Jonathan Ingram (jonathaningram) - Artur Kotyrba - Rouven Weßling (realityking) + - Andréia Bohner (andreia) - Dmitrii Chekaliuk (lazyhammer) - Clément JOBEILI (dator) - Dorian Villet (gnutix) @@ -123,14 +126,12 @@ Symfony is the result of the work of many people who made the code better - Benjamin Dulau (dbenjamin) - Matthias Pigulla (mpdude) - Andreas Hucks (meandmymonkey) - - Andréia Bohner (andreia) - Noel Guilbert (noel) - Joel Wurtz (brouznouf) - Charles Sarrazin (csarrazi) - bronze1man - sun (sun) - Larry Garfield (crell) - - Issei Murasawa (issei_m) - Martin Schuhfuß (usefulthink) - Thomas Rabaix (rande) - Matthieu Bontemps (mbontemps) @@ -163,6 +164,7 @@ Symfony is the result of the work of many people who made the code better - Xavier Montaña Carreras (xmontana) - Michele Orselli (orso) - Chris Wilkinson (thewilkybarkid) + - Joshua Thijssen - Xavier Perez - Arjen Brouwer (arjenjb) - Katsuhiro OGAWA @@ -181,16 +183,18 @@ Symfony is the result of the work of many people who made the code better - Jeremy Livingston (jeremylivingston) - Nikita Konstantinov - Wodor Wodorski + - Matthieu Auger (matthieuauger) - julien pauli (jpauli) - Beau Simensen (simensen) - Robert Kiss (kepten) + - John Kary (johnkary) - Ruben Gonzalez (rubenrua) - Kim Hemsø Rasmussen (kimhemsoe) - Florian Lonqueu-Brochard (florianlb) - Tom Van Looy (tvlooy) - Wouter Van Hecke - - Joshua Thijssen - Peter Kruithof (pkruithof) + - Vladimir Reznichenko (kalessil) - Michael Holm (hollo) - Warnar Boekkooi (boekkooi) - Marc Weistroff (futurecat) @@ -222,7 +226,6 @@ Symfony is the result of the work of many people who made the code better - Marco Pivetta (ocramius) - Ricard Clau (ricardclau) - Erin Millard - - John Kary (johnkary) - Matthew Lewinski (lewinski) - alquerci - Francesco Levorato @@ -232,7 +235,6 @@ Symfony is the result of the work of many people who made the code better - Inal DJAFAR (inalgnu) - Christian Gärtner (dagardner) - Felix Labrecque - - Vladimir Reznichenko (kalessil) - Yaroslav Kiliba - Sébastien Lavoie (lavoiesl) - Terje Bråten @@ -283,7 +285,6 @@ Symfony is the result of the work of many people who made the code better - Brian King - Michel Salib (michelsalib) - geoffrey - - Matthieu Auger (matthieuauger) - Lorenz Schori - Jeanmonod David (jeanmonod) - Jan Schumann @@ -309,7 +310,6 @@ Symfony is the result of the work of many people who made the code better - Christian Schmidt - Marcin Sikoń (marphi) - franek (franek) - - Dariusz Ruminski - Adam Harvey - Diego Saint Esteben (dii3g0) - Alex Bakhturin @@ -336,6 +336,7 @@ Symfony is the result of the work of many people who made the code better - mmoreram - Markus Lanthaler (lanthaler) - Vicent Soria Durá (vicentgodella) + - Anthony Ferrara - Ioan Negulescu - Jakub Škvára (jskvara) - Daniel Beyer @@ -343,9 +344,11 @@ Symfony is the result of the work of many people who made the code better - alexpods - Erik Trapman (eriktrapman) - De Cock Xavier (xdecock) + - Scott Arciszewski - Norbert Orzechowicz (norzechowicz) - Tobias Nyholm (tobias) - Matthijs van den Bos (matthijs) + - Loick Piera (pyrech) - Lenard Palko - Nils Adermann (naderman) - Gábor Fási @@ -371,9 +374,11 @@ Symfony is the result of the work of many people who made the code better - Zach Badgett (zachbadgett) - Aurélien Fredouelle - Pavel Campr (pcampr) + - Maxime Steinhausser (ogizanagi) - Disquedur - Geoffrey Tran (geoff) - Jan Behrens + - Mantas Var (mvar) - Sebastian Krebs - Christopher Davis (chrisguitarguy) - Thomas Lallement (raziel057) @@ -387,6 +392,7 @@ Symfony is the result of the work of many people who made the code better - Max Rath (drak3) - Stéphane Escandell (sescandell) - Sinan Eldem + - Alexandre Dupuy (satchette) - Nahuel Cuesta (ncuesta) - Chris Boden (cboden) - Asmir Mustafic (goetas) @@ -484,7 +490,6 @@ Symfony is the result of the work of many people who made the code better - Xavier Lacot (xavier) - Olivier Maisonneuve (olineuve) - Francis Turmel (fturmel) - - Loick Piera (pyrech) - cgonzalez - Ben - Jayson Xu (superjavason) @@ -506,6 +511,7 @@ Symfony is the result of the work of many people who made the code better - Fabian Vogler (fabian) - Korvin Szanto - Maksim Kotlyar (makasim) + - Ivan Kurnosov - Neil Ferreira - Dmitry Parnas (parnas) - DQNEO @@ -516,6 +522,7 @@ Symfony is the result of the work of many people who made the code better - David Romaní - Patrick Allaert - Gustavo Falco (gfalco) + - Matt Robinson (inanimatt) - Aleksey Podskrebyshev - David Marín Carreño (davefx) - Jörn Lang (j.lang) @@ -580,6 +587,7 @@ Symfony is the result of the work of many people who made the code better - Michael Tibben - Sander Marechal - Radosław Benkel + - Marcos Sánchez - ttomor - Mei Gwilym (meigwilym) - Michael H. Arieli (excelwebzone) @@ -622,6 +630,7 @@ Symfony is the result of the work of many people who made the code better - nacho - Piotr Antosik (antek88) - Artem Lopata + - Samuel ROZE (sroze) - Marcos Quesada (marcos_quesada) - Matthew Vickery (mattvick) - Dan Finnie @@ -675,6 +684,7 @@ Symfony is the result of the work of many people who made the code better - Yannick - Eduardo García Sanz (coma) - Sebastian Grodzicki (sgrodzicki) + - Michael Lee (zerustech) - Roy Van Ginneken - David de Boer (ddeboer) - Gilles Doge (gido) @@ -721,6 +731,7 @@ Symfony is the result of the work of many people who made the code better - Malaney J. Hill - Christian Flach (cmfcmf) - Cédric Girard (enk_) + - Lars Ambrosius Wallenborn (larsborn) - Oriol Mangas Abellan (oriolman) - Sebastian Göttschkes (sgoettschkes) - Tatsuya Tsuruoka @@ -847,6 +858,7 @@ Symfony is the result of the work of many people who made the code better - Gunnar Lium (gunnarlium) - Tiago Garcia (tiagojsag) - Artiom + - Jakub Simon - Bouke Haarsma - Martin Eckhardt - Denis Zunke @@ -876,7 +888,6 @@ Symfony is the result of the work of many people who made the code better - Vasily Khayrulin (sirian) - Stefan Koopmanschap (skoop) - Stefan Hüsges (tronsha) - - Ivan Kurnosov - stloyd - Chris Tickner - Andrew Coulton @@ -905,7 +916,6 @@ Symfony is the result of the work of many people who made the code better - Julius Beckmann - Romain Dorgueil - Grayson Koonce (breerly) - - Matt Robinson (inanimatt) - Karim Cassam Chenaï (ka) - Nicolas Bastien (nicolas_bastien) - Andy Stanberry @@ -992,6 +1002,7 @@ Symfony is the result of the work of many people who made the code better - grifx - Robert Campbell - Matt Lehner + - Hidde Wieringa - Hein Zaw Htet™ - Ruben Kruiswijk - Michael J @@ -1048,9 +1059,7 @@ Symfony is the result of the work of many people who made the code better - Muriel (metalmumu) - Michaël Perrin (michael.perrin) - Michael Pohlers (mick_the_big) - - Mantas Var (mvar) - Cayetano Soriano Gallego (neoshadybeat) - - Maxime Steinhausser (ogizanagi) - Pablo Monterde Perez (plebs) - Jimmy Leger (redpanda) - Cyrille Jouineau (tuxosaurus) @@ -1128,6 +1137,7 @@ Symfony is the result of the work of many people who made the code better - Brian Freytag - Skorney - mieszko4 + - Neophy7e - Arrilot - Markus Staab - Pierre-Louis LAUNAY @@ -1140,6 +1150,8 @@ Symfony is the result of the work of many people who made the code better - Sema - Thorsten Hallwas - Michael Squires + - Norman Soetbeer + - Benjamin Long - Matt Janssen - Peter Gribanov - kwiateusz @@ -1235,6 +1247,7 @@ Symfony is the result of the work of many people who made the code better - Vincent (vincent1870) - Eugene Babushkin (warl) - Xavier Amado (xamado) + - Jesper Søndergaard Pedersen (zerrvox) - Florent Cailhol - szymek - craigmarvelley From ce4aab1508dd9642906fc9a120e0d8763e689896 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 1 Apr 2015 16:28:26 +0200 Subject: [PATCH 22/60] updated VERSION for 2.3.27 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 5e4fac51cd..e6363872e8 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -60,12 +60,12 @@ abstract class Kernel implements KernelInterface, TerminableInterface protected $startTime; protected $loadClassCache; - const VERSION = '2.3.27-DEV'; + const VERSION = '2.3.27'; const VERSION_ID = '20327'; const MAJOR_VERSION = '2'; const MINOR_VERSION = '3'; const RELEASE_VERSION = '27'; - const EXTRA_VERSION = 'DEV'; + const EXTRA_VERSION = ''; /** * Constructor. From feb48c26b3e4294294e0f12cc21b022fd636551a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 1 Apr 2015 17:29:10 +0200 Subject: [PATCH 23/60] bumped Symfony version to 2.3.28 --- src/Symfony/Component/HttpKernel/Kernel.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index e6363872e8..42a3f3b3f6 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -60,12 +60,12 @@ abstract class Kernel implements KernelInterface, TerminableInterface protected $startTime; protected $loadClassCache; - const VERSION = '2.3.27'; - const VERSION_ID = '20327'; + const VERSION = '2.3.28-DEV'; + const VERSION_ID = '20328'; const MAJOR_VERSION = '2'; const MINOR_VERSION = '3'; - const RELEASE_VERSION = '27'; - const EXTRA_VERSION = ''; + const RELEASE_VERSION = '28'; + const EXTRA_VERSION = 'DEV'; /** * Constructor. From 06f92fc2d5707dc0ea749cf948281d759e8b0cbf Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 16 Mar 2015 15:12:02 +0100 Subject: [PATCH 24/60] Safe escaping of fragments for eval() --- .../Component/HttpKernel/HttpCache/Ssi.php | 56 +++++++++---------- .../HttpKernel/Tests/HttpCache/SsiTest.php | 4 +- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php b/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php index 1fc203e595..7bb54cff62 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php @@ -23,6 +23,10 @@ use Symfony\Component\HttpKernel\HttpKernelInterface; class Ssi implements SurrogateInterface { private $contentTypes; + private $phpEscapeMap = array( + array('', '', '', ''), + ); /** * Constructor. @@ -122,8 +126,30 @@ class Ssi implements SurrogateInterface // we don't use a proper XML parser here as we can have SSI tags in a plain text response $content = $response->getContent(); - $content = str_replace(array('', ''), $content); - $content = preg_replace_callback('##', array($this, 'handleIncludeTag'), $content); + + $chunks = preg_split('##', $content, -1, PREG_SPLIT_DELIM_CAPTURE); + $chunks[0] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[0]); + + $i = 1; + while (isset($chunks[$i])) { + $options = array(); + preg_match_all('/(virtual)="([^"]*?)"/', $chunks[$i], $matches, PREG_SET_ORDER); + foreach ($matches as $set) { + $options[$set[1]] = $set[2]; + } + + if (!isset($options['virtual'])) { + throw new \RuntimeException('Unable to process an SSI tag without a "virtual" attribute.'); + } + + $chunks[$i] = sprintf('surrogate->handle($this, %s, \'\', false) ?>'."\n", + var_export($options['virtual'], true) + ); + ++$i; + $chunks[$i] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[$i]); + ++$i; + } + $content = implode('', $chunks); $response->setContent($content); $response->headers->set('X-Body-Eval', 'SSI'); @@ -166,30 +192,4 @@ class Ssi implements SurrogateInterface } } } - - /** - * Handles an SSI include tag (called internally). - * - * @param array $attributes An array containing the attributes. - * - * @return string The response content for the include. - * - * @throws \RuntimeException - */ - private function handleIncludeTag($attributes) - { - $options = array(); - preg_match_all('/(virtual)="([^"]*?)"/', $attributes[1], $matches, PREG_SET_ORDER); - foreach ($matches as $set) { - $options[$set[1]] = $set[2]; - } - - if (!isset($options['virtual'])) { - throw new \RuntimeException('Unable to process an SSI tag without a "virtual" attribute.'); - } - - return sprintf('surrogate->handle($this, %s, \'\', false) ?>'."\n", - var_export($options['virtual'], true) - ); - } } diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php index bb208cd86c..07b70dcee8 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php @@ -113,10 +113,10 @@ class SsiTest extends \PHPUnit_Framework_TestCase $ssi = new Ssi(); $request = Request::create('/'); - $response = new Response('foo <%= "lala" %>'); + $response = new Response(''); $ssi->process($request, $response); - $this->assertEquals('foo php die("foo"); ?>= "lala" %>', $response->getContent()); + $this->assertEquals('php cript language=php>', $response->getContent()); } /** From 4eb695a40e7d1467e8c929540e9f0368c31f7487 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 1 Apr 2015 18:54:41 +0200 Subject: [PATCH 25/60] updated CHANGELOG for 2.6.6 --- CHANGELOG-2.6.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG-2.6.md b/CHANGELOG-2.6.md index 56fd84de3c..8bd1c85aff 100644 --- a/CHANGELOG-2.6.md +++ b/CHANGELOG-2.6.md @@ -7,6 +7,26 @@ in 2.6 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.6.0...v2.6.1 +* 2.6.6 (2015-04-01) + + * security #14167 CVE-2015-2308 (nicolas-grekas) + * security #14166 CVE-2015-2309 (neclimdul) + * bug #14126 [VarDumper] Fix toggle action to see source excerpt (nicolas-grekas) + * bug #14114 [VarDumper] Fix dumping references as properties (nicolas-grekas) + * bug #14074 [VarDumper] Fix dumping ThrowingCasterException (nicolas-grekas) + * bug #12948 [Form] [TwigBridge] Bootstrap layout whitespace control (mvar) + * bug #14046 [Security] StringUtils::equals() arguments in RememberMe Cookie based implementation are confused (zerkms) + * bug #14010 Replace GET parameters when changed in form (WouterJ) + * bug #13991 [Dependency Injection] Improve PhpDumper Performance for huge Containers (BattleRattle) + * bug #13886 [FrameworkBundle][debug:config] added support for dynamic configurations... (aitboudad) + * bug #14013 [DependencyInjection] prevent inlining service configurators (xabbuh) + * bug #14012 [DomCrawler] Improve namespace discovery performance (jakzal) + * bug #13997 [2.3+][Form][DoctrineBridge] Improved loading of entities and documents (guilhermeblanco) + * bug #13978 [WebProfilerBundle] Fix javascript toolbar on IE8 (romainneutron) + * bug #13987 [WebProfilerBundle] fixed undefined buttons variable. (aitboudad) + * bug #13953 [Translation][MoFileLoader] fixed load empty translation. (aitboudad) + * bug #13912 [DependencyInjection] Highest precedence for user parameters (lyrixx) + * 2.6.5 (2015-03-17) * bug #13944 [HttpKernel] UriSigner::buildUrl - default params for http_build_query (Jakub Simon) From 48c9e835a877adfb023b8b6d033d9dd14f342b4b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 1 Apr 2015 18:55:26 +0200 Subject: [PATCH 26/60] updated VERSION for 2.6.6 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 97406d1bc4..d1442149b0 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -60,12 +60,12 @@ abstract class Kernel implements KernelInterface, TerminableInterface protected $startTime; protected $loadClassCache; - const VERSION = '2.6.6-DEV'; + const VERSION = '2.6.6'; const VERSION_ID = '20606'; const MAJOR_VERSION = '2'; const MINOR_VERSION = '6'; const RELEASE_VERSION = '6'; - const EXTRA_VERSION = 'DEV'; + const EXTRA_VERSION = ''; /** * Constructor. From de02c5f9623ad2599d063aed910958c52711c21d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 1 Apr 2015 20:51:37 +0200 Subject: [PATCH 27/60] bumped Symfony version to 2.6.7 --- src/Symfony/Component/HttpKernel/Kernel.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index d1442149b0..4680ed54ba 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -60,12 +60,12 @@ abstract class Kernel implements KernelInterface, TerminableInterface protected $startTime; protected $loadClassCache; - const VERSION = '2.6.6'; - const VERSION_ID = '20606'; + const VERSION = '2.6.7-DEV'; + const VERSION_ID = '20607'; const MAJOR_VERSION = '2'; const MINOR_VERSION = '6'; - const RELEASE_VERSION = '6'; - const EXTRA_VERSION = ''; + const RELEASE_VERSION = '7'; + const EXTRA_VERSION = 'DEV'; /** * Constructor. From 9d6c0b1c30724a99c1732d3b3bd0951d7f007a6f Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Thu, 2 Apr 2015 11:50:50 +0200 Subject: [PATCH 28/60] Fix the AJAX profiling The fix for IE8 which does not have the addEventListener method on XMLHttpRequest broke the feature for modern browsers because it was checking the existence on the wrong object. It is a method on the instance, not on the "class", and so should be checked on the prototype. --- .../Resources/views/Profiler/base_js.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig index 502ee36fcd..070c7888bc 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig @@ -196,7 +196,7 @@ } {% if excluded_ajax_paths is defined %} - if (window.XMLHttpRequest && XMLHttpRequest.addEventListener) { + if (window.XMLHttpRequest && XMLHttpRequest.prototype.addEventListener) { var proxied = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url, async, user, pass) { From dbd02b087175c7817a39d047c6f6e237b8adeca5 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Thu, 2 Apr 2015 13:05:24 +0200 Subject: [PATCH 29/60] Use specialized config methods instead of the generic ifTrue() method --- .../FrameworkBundle/DependencyInjection/Configuration.php | 7 ++----- .../TwigBundle/DependencyInjection/Configuration.php | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 2de60489a8..08bcc1c19d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -69,10 +69,7 @@ class Configuration implements ConfigurationInterface ->booleanNode('test')->end() ->scalarNode('default_locale')->defaultValue('en')->end() ->arrayNode('trusted_hosts') - ->beforeNormalization() - ->ifTrue(function ($v) { return is_string($v); }) - ->then(function ($v) { return array($v); }) - ->end() + ->beforeNormalization()->ifString()->then(function ($v) { return array($v); })->end() ->prototype('scalar')->end() ->end() ->end() @@ -263,7 +260,7 @@ class Configuration implements ConfigurationInterface ->addDefaultChildrenIfNoneSet() ->prototype('scalar')->defaultValue('FrameworkBundle:Form')->end() ->validate() - ->ifTrue(function ($v) {return !in_array('FrameworkBundle:Form', $v); }) + ->ifNotInArray(array('FrameworkBundle:Form')) ->then(function ($v) { return array_merge(array('FrameworkBundle:Form'), $v); }) diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php index a20ca8c1a8..8cc114ab29 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php @@ -58,7 +58,7 @@ class Configuration implements ConfigurationInterface ->prototype('scalar')->defaultValue('form_div_layout.html.twig')->end() ->example(array('MyBundle::form.html.twig')) ->validate() - ->ifTrue(function ($v) { return !in_array('form_div_layout.html.twig', $v); }) + ->ifNotInArray(array('form_div_layout.html.twig')) ->then(function ($v) { return array_merge(array('form_div_layout.html.twig'), $v); }) From 15ef182b9971472aa30753da6349c0d28f477d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Thu, 2 Apr 2015 14:57:42 +0200 Subject: [PATCH 30/60] [Debug] Made code in ErrorHandler easier to read --- src/Symfony/Component/Debug/ErrorHandler.php | 162 ++++++++++--------- 1 file changed, 84 insertions(+), 78 deletions(-) diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index 48e4a2d69e..280d5f8abc 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -356,69 +356,71 @@ class ErrorHandler $throw = $this->thrownErrors & $type & $level; $type &= $level | $this->screamedErrors; - if ($type && ($log || $throw)) { - if (PHP_VERSION_ID < 50400 && isset($context['GLOBALS']) && ($this->scopedErrors & $type)) { - $e = $context; // Whatever the signature of the method, - unset($e['GLOBALS'], $context); // $context is always a reference in 5.3 - $context = $e; - } + if (!$type || (!$log && !$throw)) { + return $type && $log; + } - if ($throw) { - if (($this->scopedErrors & $type) && class_exists('Symfony\Component\Debug\Exception\ContextErrorException')) { - // Checking for class existence is a work around for https://bugs.php.net/42098 - $throw = new ContextErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line, $context); - } else { - $throw = new \ErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line); - } + if (PHP_VERSION_ID < 50400 && isset($context['GLOBALS']) && ($this->scopedErrors & $type)) { + $e = $context; // Whatever the signature of the method, + unset($e['GLOBALS'], $context); // $context is always a reference in 5.3 + $context = $e; + } - if (PHP_VERSION_ID <= 50407 && (PHP_VERSION_ID >= 50400 || PHP_VERSION_ID <= 50317)) { - // Exceptions thrown from error handlers are sometimes not caught by the exception - // handler and shutdown handlers are bypassed before 5.4.8/5.3.18. - // We temporarily re-enable display_errors to prevent any blank page related to this bug. - - $throw->errorHandlerCanary = new ErrorHandlerCanary(); - } - - throw $throw; - } - - // For duplicated errors, log the trace only once - $e = md5("{$type}/{$line}/{$file}\x00{$message}", true); - $trace = true; - - if (!($this->tracedErrors & $type) || isset($this->loggedTraces[$e])) { - $trace = false; + if ($throw) { + if (($this->scopedErrors & $type) && class_exists('Symfony\Component\Debug\Exception\ContextErrorException')) { + // Checking for class existence is a work around for https://bugs.php.net/42098 + $throw = new ContextErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line, $context); } else { - $this->loggedTraces[$e] = 1; + $throw = new \ErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line); } - $e = compact('type', 'file', 'line', 'level'); + if (PHP_VERSION_ID <= 50407 && (PHP_VERSION_ID >= 50400 || PHP_VERSION_ID <= 50317)) { + // Exceptions thrown from error handlers are sometimes not caught by the exception + // handler and shutdown handlers are bypassed before 5.4.8/5.3.18. + // We temporarily re-enable display_errors to prevent any blank page related to this bug. - if ($type & $level) { - if ($this->scopedErrors & $type) { - $e['context'] = $context; - if ($trace) { - $e['stack'] = debug_backtrace(true); // Provide object - } - } elseif ($trace) { - $e['stack'] = debug_backtrace(PHP_VERSION_ID >= 50306 ? DEBUG_BACKTRACE_IGNORE_ARGS : false); - } + $throw->errorHandlerCanary = new ErrorHandlerCanary(); } - if ($this->isRecursive) { - $log = 0; - } elseif (self::$stackedErrorLevels) { - self::$stackedErrors[] = array($this->loggers[$type], $message, $e); - } else { - try { - $this->isRecursive = true; - $this->loggers[$type][0]->log($this->loggers[$type][1], $message, $e); - $this->isRecursive = false; - } catch (\Exception $e) { - $this->isRecursive = false; + throw $throw; + } - throw $e; + // For duplicated errors, log the trace only once + $e = md5("{$type}/{$line}/{$file}\x00{$message}", true); + $trace = true; + + if (!($this->tracedErrors & $type) || isset($this->loggedTraces[$e])) { + $trace = false; + } else { + $this->loggedTraces[$e] = 1; + } + + $e = compact('type', 'file', 'line', 'level'); + + if ($type & $level) { + if ($this->scopedErrors & $type) { + $e['context'] = $context; + if ($trace) { + $e['stack'] = debug_backtrace(true); // Provide object } + } elseif ($trace) { + $e['stack'] = debug_backtrace(PHP_VERSION_ID >= 50306 ? DEBUG_BACKTRACE_IGNORE_ARGS : false); + } + } + + if ($this->isRecursive) { + $log = 0; + } elseif (self::$stackedErrorLevels) { + self::$stackedErrors[] = array($this->loggers[$type], $message, $e); + } else { + try { + $this->isRecursive = true; + $this->loggers[$type][0]->log($this->loggers[$type][1], $message, $e); + $this->isRecursive = false; + } catch (\Exception $e) { + $this->isRecursive = false; + + throw $e; } } @@ -487,40 +489,44 @@ class ErrorHandler public static function handleFatalError(array $error = null) { self::$reservedMemory = ''; + $handler = set_error_handler('var_dump', 0); $handler = is_array($handler) ? $handler[0] : null; restore_error_handler(); - if ($handler instanceof self) { - if (null === $error) { - $error = error_get_last(); - } - try { - while (self::$stackedErrorLevels) { - static::unstackErrors(); - } - } catch (\Exception $exception) { - // Handled below - } + if (!$handler instanceof self) { + return; + } - if ($error && ($error['type'] & (E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR))) { - // Let's not throw anymore but keep logging - $handler->throwAt(0, true); + if (null === $error) { + $error = error_get_last(); + } - if (0 === strpos($error['message'], 'Allowed memory') || 0 === strpos($error['message'], 'Out of memory')) { - $exception = new OutOfMemoryException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, false); - } else { - $exception = new FatalErrorException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, true); - } - } elseif (!isset($exception)) { - return; + try { + while (self::$stackedErrorLevels) { + static::unstackErrors(); } + } catch (\Exception $exception) { + // Handled below + } - try { - $handler->handleException($exception, $error); - } catch (FatalErrorException $e) { - // Ignore this re-throw + if ($error && ($error['type'] & (E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR))) { + // Let's not throw anymore but keep logging + $handler->throwAt(0, true); + + if (0 === strpos($error['message'], 'Allowed memory') || 0 === strpos($error['message'], 'Out of memory')) { + $exception = new OutOfMemoryException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, false); + } else { + $exception = new FatalErrorException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, true); } + } elseif (!isset($exception)) { + return; + } + + try { + $handler->handleException($exception, $error); + } catch (FatalErrorException $e) { + // Ignore this re-throw } } From a95f7f38af2c7c6ce8266cf162a6eaff9bbcc45b Mon Sep 17 00:00:00 2001 From: stloyd Date: Tue, 31 Mar 2015 15:09:45 +0200 Subject: [PATCH 31/60] Fix some phpdocs for Twig extensions & templating helpers --- src/Symfony/Bridge/Twig/Extension/CodeExtension.php | 3 +++ src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php | 3 +++ src/Symfony/Bridge/Twig/Extension/RoutingExtension.php | 4 +--- src/Symfony/Bridge/Twig/Extension/SecurityExtension.php | 4 +--- src/Symfony/Bridge/Twig/Extension/TranslationExtension.php | 4 +--- src/Symfony/Bridge/Twig/Extension/YamlExtension.php | 4 +--- .../FrameworkBundle/Templating/Helper/ActionsHelper.php | 4 +--- .../Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php | 4 +--- .../FrameworkBundle/Templating/Helper/RequestHelper.php | 4 +--- .../Bundle/FrameworkBundle/Templating/Helper/RouterHelper.php | 4 +--- .../FrameworkBundle/Templating/Helper/SessionHelper.php | 4 +--- .../FrameworkBundle/Templating/Helper/TranslatorHelper.php | 4 +--- .../SecurityBundle/Templating/Helper/LogoutUrlHelper.php | 4 +--- .../SecurityBundle/Templating/Helper/SecurityHelper.php | 4 +--- src/Symfony/Bundle/TwigBundle/Extension/ActionsExtension.php | 3 +++ src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php | 4 +--- 16 files changed, 22 insertions(+), 39 deletions(-) diff --git a/src/Symfony/Bridge/Twig/Extension/CodeExtension.php b/src/Symfony/Bridge/Twig/Extension/CodeExtension.php index 4a0c1e9c76..b5f619d327 100644 --- a/src/Symfony/Bridge/Twig/Extension/CodeExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/CodeExtension.php @@ -210,6 +210,9 @@ class CodeExtension extends \Twig_Extension }, $text); } + /** + * {@inheritdoc} + */ public function getName() { return 'code'; diff --git a/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php b/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php index 4d9919d16c..1da12aaf5e 100644 --- a/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php @@ -81,6 +81,9 @@ class HttpKernelExtension extends \Twig_Extension return new ControllerReference($controller, $attributes, $query); } + /** + * {@inheritdoc} + */ public function getName() { return 'http_kernel'; diff --git a/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php b/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php index 237c36c190..7469183e75 100644 --- a/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php @@ -89,9 +89,7 @@ class RoutingExtension extends \Twig_Extension } /** - * Returns the name of the extension. - * - * @return string The extension name + * {@inheritdoc} */ public function getName() { diff --git a/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php b/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php index edba0e7b8b..49863a4e3f 100644 --- a/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php @@ -52,9 +52,7 @@ class SecurityExtension extends \Twig_Extension } /** - * Returns the name of the extension. - * - * @return string The extension name + * {@inheritdoc} */ public function getName() { diff --git a/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php b/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php index 0fde3a675d..f1f2fbd20b 100644 --- a/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php @@ -99,9 +99,7 @@ class TranslationExtension extends \Twig_Extension } /** - * Returns the name of the extension. - * - * @return string The extension name + * {@inheritdoc} */ public function getName() { diff --git a/src/Symfony/Bridge/Twig/Extension/YamlExtension.php b/src/Symfony/Bridge/Twig/Extension/YamlExtension.php index 63d658da3b..fc9bf0e9e3 100644 --- a/src/Symfony/Bridge/Twig/Extension/YamlExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/YamlExtension.php @@ -56,9 +56,7 @@ class YamlExtension extends \Twig_Extension } /** - * Returns the name of the extension. - * - * @return string The extension name + * {@inheritdoc} */ public function getName() { diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/ActionsHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/ActionsHelper.php index d4a855d53b..8f0d54eada 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/ActionsHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/ActionsHelper.php @@ -58,9 +58,7 @@ class ActionsHelper extends Helper } /** - * Returns the canonical name of this helper. - * - * @return string The canonical name + * {@inheritdoc} */ public function getName() { diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php index 94a9af0cc0..e7042e0dc3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php @@ -205,9 +205,7 @@ class CodeHelper extends Helper } /** - * Returns the canonical name of this helper. - * - * @return string The canonical name + * {@inheritdoc} */ public function getName() { diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RequestHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RequestHelper.php index 9f001f7f3f..b7fbfcdca7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RequestHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RequestHelper.php @@ -59,9 +59,7 @@ class RequestHelper extends Helper } /** - * Returns the canonical name of this helper. - * - * @return string The canonical name + * {@inheritdoc} */ public function getName() { diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RouterHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RouterHelper.php index 830e5c9019..d54caaf395 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RouterHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RouterHelper.php @@ -50,9 +50,7 @@ class RouterHelper extends Helper } /** - * Returns the canonical name of this helper. - * - * @return string The canonical name + * {@inheritdoc} */ public function getName() { diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SessionHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SessionHelper.php index c4362df1db..675fe9dc90 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SessionHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SessionHelper.php @@ -62,9 +62,7 @@ class SessionHelper extends Helper } /** - * Returns the canonical name of this helper. - * - * @return string The canonical name + * {@inheritdoc} */ public function getName() { diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php index 30816d736f..2c2641a885 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php @@ -50,9 +50,7 @@ class TranslatorHelper extends Helper } /** - * Returns the canonical name of this helper. - * - * @return string The canonical name + * {@inheritdoc} */ public function getName() { diff --git a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php index 1fef75ca09..9514ebf616 100644 --- a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php +++ b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php @@ -114,9 +114,7 @@ class LogoutUrlHelper extends Helper } /** - * Returns the canonical name of this helper. - * - * @return string The canonical name + * {@inheritdoc} */ public function getName() { diff --git a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/SecurityHelper.php b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/SecurityHelper.php index 7ca0e7bb66..a43f2b446d 100644 --- a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/SecurityHelper.php +++ b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/SecurityHelper.php @@ -48,9 +48,7 @@ class SecurityHelper extends Helper } /** - * Returns the canonical name of this helper. - * - * @return string The canonical name + * {@inheritdoc} */ public function getName() { diff --git a/src/Symfony/Bundle/TwigBundle/Extension/ActionsExtension.php b/src/Symfony/Bundle/TwigBundle/Extension/ActionsExtension.php index 3b08bd6859..76f4dff5c3 100644 --- a/src/Symfony/Bundle/TwigBundle/Extension/ActionsExtension.php +++ b/src/Symfony/Bundle/TwigBundle/Extension/ActionsExtension.php @@ -62,6 +62,9 @@ class ActionsExtension extends \Twig_Extension ); } + /** + * {@inheritdoc} + */ public function getName() { return 'actions'; diff --git a/src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php b/src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php index 929680597c..4f40c7a0d8 100644 --- a/src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php +++ b/src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php @@ -68,9 +68,7 @@ class AssetsExtension extends \Twig_Extension } /** - * Returns the name of the extension. - * - * @return string The extension name + * {@inheritdoc} */ public function getName() { From 8f723385139f485a5850b05de58d8794cf8f7423 Mon Sep 17 00:00:00 2001 From: Abdellatif Ait boudad Date: Mon, 30 Mar 2015 20:06:15 +0100 Subject: [PATCH 32/60] [FrameworkBundle] Move lint commands to lint namespace. --- src/Symfony/Bridge/Twig/Command/LintCommand.php | 17 +++++++++++------ .../Twig/Tests/Command/LintCommandTest.php | 2 +- .../FrameworkBundle/Command/YamlLintCommand.php | 7 ++++++- .../Bundle/FrameworkBundle/composer.json | 2 +- .../Bundle/TwigBundle/Command/LintCommand.php | 2 +- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Symfony/Bridge/Twig/Command/LintCommand.php b/src/Symfony/Bridge/Twig/Command/LintCommand.php index 449422b6d0..ccf6b8764d 100644 --- a/src/Symfony/Bridge/Twig/Command/LintCommand.php +++ b/src/Symfony/Bridge/Twig/Command/LintCommand.php @@ -35,13 +35,13 @@ class LintCommand extends Command /** * {@inheritdoc} */ - public function __construct($name = 'twig:lint') + public function __construct($name = 'lint:twig') { parent::__construct($name); } /** - * Sets the twig environment + * Sets the twig environment. * * @param \Twig_Environment $twig */ @@ -61,6 +61,7 @@ class LintCommand extends Command protected function configure() { $this + ->setAliases(array('twig:lint')) ->setDescription('Lints a template and outputs encountered errors') ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt') ->addArgument('filename', InputArgument::IS_ARRAY) @@ -87,6 +88,10 @@ EOF protected function execute(InputInterface $input, OutputInterface $output) { + if (false !== strpos($input->getFirstArgument(), ':l')) { + $output->writeln('The use of "twig:lint" command is deprecated since version 2.7 and will be removed in 3.0. Use the "lint:twig" instead.'); + } + $twig = $this->getTwigEnvironment(); if (null === $twig) { @@ -99,7 +104,7 @@ EOF if (0 === count($filenames)) { if (0 !== ftell(STDIN)) { - throw new \RuntimeException("Please provide a filename or pipe template content to STDIN."); + throw new \RuntimeException('Please provide a filename or pipe template content to STDIN.'); } $template = ''; @@ -210,14 +215,14 @@ EOF $line = $exception->getTemplateLine(); if ($file) { - $output->writeln(sprintf("KO in %s (line %s)", $file, $line)); + $output->writeln(sprintf('KO in %s (line %s)', $file, $line)); } else { - $output->writeln(sprintf("KO (line %s)", $line)); + $output->writeln(sprintf('KO (line %s)', $line)); } foreach ($this->getContext($template, $line) as $no => $code) { $output->writeln(sprintf( - "%s %-6s %s", + '%s %-6s %s', $no == $line ? '>>' : ' ', $no, $code diff --git a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php index 9a159b7623..a86f74b548 100644 --- a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php @@ -80,7 +80,7 @@ class LintCommandTest extends \PHPUnit_Framework_TestCase $application = new Application(); $application->add($command); - $command = $application->find('twig:lint'); + $command = $application->find('lint:twig'); return new CommandTester($command); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php index 6565ef85bc..8a6fdd33ce 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php @@ -33,7 +33,8 @@ class YamlLintCommand extends Command protected function configure() { $this - ->setName('yaml:lint') + ->setName('lint:yaml') + ->setAliases(array('yaml:lint')) ->setDescription('Lints a file and outputs encountered errors') ->addArgument('filename', null, 'A file or a directory or STDIN') ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt') @@ -65,6 +66,10 @@ EOF protected function execute(InputInterface $input, OutputInterface $output) { + if (false !== strpos($input->getFirstArgument(), ':l')) { + $output->writeln('The use of "yaml:lint" command is deprecated since version 2.7 and will be removed in 3.0. Use the "lint:yaml" instead.'); + } + $filename = $input->getArgument('filename'); if (!$filename) { diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 98012bdacc..acee5a9ad0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -53,7 +53,7 @@ "symfony/finder": "For using the translation loader and cache warmer", "symfony/form": "For using forms", "symfony/validator": "For using validation", - "symfony/yaml": "For using the debug:config and yaml:lint commands", + "symfony/yaml": "For using the debug:config and lint:yaml commands", "doctrine/cache": "For using alternative cache drivers" }, "autoload": { diff --git a/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php b/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php index a5759d8c71..60afc27ea5 100644 --- a/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php +++ b/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php @@ -59,7 +59,7 @@ class LintCommand extends BaseLintCommand implements ContainerAwareInterface Or all template files in a bundle: php %command.full_name% @AcmeDemoBundle - + EOF ) ; From d2f85ed5f7fbab3d89a6ac9d4dc3e3aa7b6cf187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Thu, 2 Apr 2015 16:50:50 +0200 Subject: [PATCH 33/60] [Debug] Updated the default log level when a PHP error occurs According to https://github.com/Seldaek/monolog#log-levels the level `NOTICE` means "Normal but significant events". So when a PHP notice occurs, it's not a "normal" event, but an error. That's why all PHP errors should use at lease the `WARNING` error level. --- src/Symfony/Component/Debug/ErrorHandler.php | 6 +++--- src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index 43929b3a53..d65dc1fdd9 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -71,9 +71,9 @@ class ErrorHandler private $loggers = array( E_DEPRECATED => array(null, LogLevel::INFO), E_USER_DEPRECATED => array(null, LogLevel::INFO), - E_NOTICE => array(null, LogLevel::NOTICE), - E_USER_NOTICE => array(null, LogLevel::NOTICE), - E_STRICT => array(null, LogLevel::NOTICE), + E_NOTICE => array(null, LogLevel::WARNING), + E_USER_NOTICE => array(null, LogLevel::WARNING), + E_STRICT => array(null, LogLevel::WARNING), E_WARNING => array(null, LogLevel::WARNING), E_USER_WARNING => array(null, LogLevel::WARNING), E_COMPILE_WARNING => array(null, LogLevel::WARNING), diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index fafbdc7322..a37ffc7966 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -141,9 +141,9 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase $loggers = array( E_DEPRECATED => array(null, LogLevel::INFO), E_USER_DEPRECATED => array(null, LogLevel::INFO), - E_NOTICE => array($logger, LogLevel::NOTICE), + E_NOTICE => array($logger, LogLevel::WARNING), E_USER_NOTICE => array($logger, LogLevel::CRITICAL), - E_STRICT => array(null, LogLevel::NOTICE), + E_STRICT => array(null, LogLevel::WARNING), E_WARNING => array(null, LogLevel::WARNING), E_USER_WARNING => array(null, LogLevel::WARNING), E_COMPILE_WARNING => array(null, LogLevel::WARNING), From b56a804f9395b8d6f147660b733bdc64a19fa8e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Thu, 2 Apr 2015 21:18:30 +0200 Subject: [PATCH 34/60] [Debug] Renamed "context" key to "scope_vars" to avoid any ambiguity --- src/Symfony/Component/Debug/ErrorHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index 280d5f8abc..a8f865a399 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -399,7 +399,7 @@ class ErrorHandler if ($type & $level) { if ($this->scopedErrors & $type) { - $e['context'] = $context; + $e['scope_vars'] = $context; if ($trace) { $e['stack'] = debug_backtrace(true); // Provide object } From 6f5e95b762ae0c00585a814efbec7cc6b46b47ce Mon Sep 17 00:00:00 2001 From: Gerben Wijnja Date: Fri, 3 Apr 2015 01:10:26 +0200 Subject: [PATCH 35/60] [StringUtil] Fixed singularification of 'movies' The word 'movies' was singularified to 'movy'. There seem to be only two words ending in 'ovies', which are 'movies' and 'anchovies'. The singular of the latter is 'anchovy'. All other words ending in 'vies' singularify to 'vy', so the word 'movies' is an exception to the general rule. --- src/Symfony/Component/PropertyAccess/StringUtil.php | 3 +++ src/Symfony/Component/PropertyAccess/Tests/StringUtilTest.php | 1 + 2 files changed, 4 insertions(+) diff --git a/src/Symfony/Component/PropertyAccess/StringUtil.php b/src/Symfony/Component/PropertyAccess/StringUtil.php index 5fa1b1734f..2160f0f422 100644 --- a/src/Symfony/Component/PropertyAccess/StringUtil.php +++ b/src/Symfony/Component/PropertyAccess/StringUtil.php @@ -60,6 +60,9 @@ class StringUtil // indices (index), appendices (appendix), prices (price) array('seci', 4, false, true, array('ex', 'ix', 'ice')), + // movies (movie) + array('seivom', 6, true, true, 'movie'), + // babies (baby) array('sei', 3, false, true, 'y'), diff --git a/src/Symfony/Component/PropertyAccess/Tests/StringUtilTest.php b/src/Symfony/Component/PropertyAccess/Tests/StringUtilTest.php index 7c6376dbd6..73922cd72a 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/StringUtilTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/StringUtilTest.php @@ -99,6 +99,7 @@ class StringUtilTest extends \PHPUnit_Framework_TestCase array('men', 'man'), array('mice', 'mouse'), array('moves', 'move'), + array('movies', 'movie'), array('nebulae', 'nebula'), array('neuroses', array('neuros', 'neurose', 'neurosis')), array('oases', array('oas', 'oase', 'oasis')), From bb020f4a729b7178ebabd26cbab6bb225deb3f09 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 3 Apr 2015 07:27:47 +0200 Subject: [PATCH 36/60] [HttpKernel] Embed the original exception as previous to bounced exceptions --- .../HttpKernel/EventListener/ExceptionListener.php | 13 ++++++++++++- .../Tests/EventListener/ExceptionListenerTest.php | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php b/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php index c7f4fda12f..8db846d8b6 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php @@ -62,7 +62,18 @@ class ExceptionListener implements EventSubscriberInterface // set handling to false otherwise it wont be able to handle further more $handling = false; - // throwing $e, not $exception, is on purpose: fixing error handling code paths is the most important + $wrapper = $e; + + while ($prev = $wrapper->getPrevious()) { + if ($exception === $wrapper = $prev) { + throw $e; + } + } + + $prev = new \ReflectionProperty('Exception', 'previous'); + $prev->setAccessible(true); + $prev->setValue($wrapper, $exception); + throw $e; } diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php index 86244df0fc..8fb00f51c1 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php @@ -57,6 +57,7 @@ class ExceptionListenerTest extends \PHPUnit_Framework_TestCase $this->fail('RuntimeException expected'); } catch (\RuntimeException $e) { $this->assertSame('bar', $e->getMessage()); + $this->assertSame('foo', $e->getPrevious()->getMessage()); } } @@ -77,6 +78,7 @@ class ExceptionListenerTest extends \PHPUnit_Framework_TestCase $this->fail('RuntimeException expected'); } catch (\RuntimeException $e) { $this->assertSame('bar', $e->getMessage()); + $this->assertSame('foo', $e->getPrevious()->getMessage()); } $this->assertEquals(3, $logger->countErrors()); From 9d0c0aeebad1f1c51cd2102660c81691c96f1cee Mon Sep 17 00:00:00 2001 From: Diego Saint Esteben Date: Tue, 31 Mar 2015 21:58:16 -0300 Subject: [PATCH 37/60] Added missing changelog entry --- src/Symfony/Component/VarDumper/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/Symfony/Component/VarDumper/CHANGELOG.md diff --git a/src/Symfony/Component/VarDumper/CHANGELOG.md b/src/Symfony/Component/VarDumper/CHANGELOG.md new file mode 100644 index 0000000000..6b08aa77ac --- /dev/null +++ b/src/Symfony/Component/VarDumper/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +2.7.0 +----- + + * deprecated Cloner\Data::getLimitedClone(). Use withMaxDepth, withMaxItemsPerDepth or withRefHandles instead. From 126514f2a3c8ae312f7c0dd2d5cd974388b7819b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Thu, 2 Apr 2015 18:36:00 +0200 Subject: [PATCH 38/60] [Debug] Rework a bit the PHP doc --- src/Symfony/Component/Debug/ErrorHandler.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index a8f865a399..715c99eee2 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -240,7 +240,7 @@ class ErrorHandler } /** - * Sets the error levels that are to be thrown. + * Sets the PHP error levels that should throw an exception when a PHP error occurs. * * @param int $levels A bit field of E_* constants for thrown errors * @param bool $replace Replace or amend the previous value @@ -263,7 +263,7 @@ class ErrorHandler } /** - * Sets the error levels that are logged or thrown with their local scope. + * Sets the PHP error levels for which local variables are preserved. * * @param int $levels A bit field of E_* constants for scoped errors * @param bool $replace Replace or amend the previous value @@ -282,7 +282,7 @@ class ErrorHandler } /** - * Sets the error levels that are logged with their stack trace. + * Sets the PHP error levels for which stack trace is preserved. * * @param int $levels A bit field of E_* constants for traced errors * @param bool $replace Replace or amend the previous value From dc282ec6790057910fe32bada246b51f07ecd775 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 3 Apr 2015 09:48:36 +0200 Subject: [PATCH 39/60] [Debug] Tweak docblocks --- src/Symfony/Component/Debug/ErrorHandler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index 715c99eee2..75c9caef71 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -240,7 +240,7 @@ class ErrorHandler } /** - * Sets the PHP error levels that should throw an exception when a PHP error occurs. + * Sets the PHP error levels that throw an exception when a PHP error occurs. * * @param int $levels A bit field of E_* constants for thrown errors * @param bool $replace Replace or amend the previous value @@ -282,7 +282,7 @@ class ErrorHandler } /** - * Sets the PHP error levels for which stack trace is preserved. + * Sets the PHP error levels for which the stack trace is preserved. * * @param int $levels A bit field of E_* constants for traced errors * @param bool $replace Replace or amend the previous value From 59f2172c013cf5b20e26f2dd57fd74e53d2648ac Mon Sep 17 00:00:00 2001 From: Abdellatif Ait boudad Date: Fri, 3 Apr 2015 08:53:14 +0100 Subject: [PATCH 40/60] [Translation][Profiler] fixed infiinite loop when collect msg from fallback. --- .../Component/Translation/DataCollectorTranslator.php | 2 ++ .../Translation/Tests/DataCollectorTranslatorTest.php | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Translation/DataCollectorTranslator.php b/src/Symfony/Component/Translation/DataCollectorTranslator.php index 99c26f182f..a94f9adabe 100644 --- a/src/Symfony/Component/Translation/DataCollectorTranslator.php +++ b/src/Symfony/Component/Translation/DataCollectorTranslator.php @@ -137,6 +137,8 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter $locale = $fallbackCatalogue->getLocale(); break; } + + $fallbackCatalogue = $fallbackCatalogue->getFallBackCatalogue(); } } else { $state = self::MESSAGE_MISSING; diff --git a/src/Symfony/Component/Translation/Tests/DataCollectorTranslatorTest.php b/src/Symfony/Component/Translation/Tests/DataCollectorTranslatorTest.php index 1e93cb4164..8e98ad31f4 100644 --- a/src/Symfony/Component/Translation/Tests/DataCollectorTranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/DataCollectorTranslatorTest.php @@ -26,11 +26,12 @@ class DataCollectorTranslatorTest extends \PHPUnit_Framework_TestCase public function testCollectMessages() { $collector = $this->createCollector(); - $collector->setFallbackLocales(array('fr')); + $collector->setFallbackLocales(array('fr', 'ru')); $collector->trans('foo'); $collector->trans('bar'); $collector->transChoice('choice', 0); + $collector->trans('bar_ru'); $expectedMessages = array(); $expectedMessages[] = array( @@ -54,6 +55,13 @@ class DataCollectorTranslatorTest extends \PHPUnit_Framework_TestCase 'domain' => 'messages', 'state' => DataCollectorTranslator::MESSAGE_MISSING, ); + $expectedMessages[] = array( + 'id' => 'bar_ru', + 'translation' => 'bar (ru)', + 'locale' => 'ru', + 'domain' => 'messages', + 'state' => DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK, + ); $this->assertEquals($expectedMessages, $collector->getCollectedMessages()); } @@ -64,6 +72,7 @@ class DataCollectorTranslatorTest extends \PHPUnit_Framework_TestCase $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', array('foo' => 'foo (en)'), 'en'); $translator->addResource('array', array('bar' => 'bar (fr)'), 'fr'); + $translator->addResource('array', array('bar_ru' => 'bar (ru)'), 'ru'); $collector = new DataCollectorTranslator($translator); From 24b780e1ed1466be57aada52e02b34852a3985fb Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 3 Apr 2015 09:54:01 +0200 Subject: [PATCH 41/60] Revert "Added missing changelog entry" This reverts commit 9d0c0aeebad1f1c51cd2102660c81691c96f1cee. --- src/Symfony/Component/VarDumper/CHANGELOG.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 src/Symfony/Component/VarDumper/CHANGELOG.md diff --git a/src/Symfony/Component/VarDumper/CHANGELOG.md b/src/Symfony/Component/VarDumper/CHANGELOG.md deleted file mode 100644 index 6b08aa77ac..0000000000 --- a/src/Symfony/Component/VarDumper/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -CHANGELOG -========= - -2.7.0 ------ - - * deprecated Cloner\Data::getLimitedClone(). Use withMaxDepth, withMaxItemsPerDepth or withRefHandles instead. From e9bc23b54af8ad3df06b2a1093d168f9d70554b3 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 30 Jan 2015 09:44:56 +0100 Subject: [PATCH 42/60] make date formats and number formats configurable This adds new Twig configuration options that make it possible to configure the format of both numbers and dates as well as timezones without the need to write custom code. For example, using the new configuration options can look like this: ```yaml twig: date: format: d.m.Y, H:i:s interval_format: %%d days timezone: Europe/Berlin number_format: decimals: 2 decimal_point: , thousands_separator: . ``` --- src/Symfony/Bundle/TwigBundle/CHANGELOG.md | 1 + .../DependencyInjection/Configuration.php | 30 ++++++++++++ .../Configurator/EnvironmentConfigurator.php | 48 +++++++++++++++++++ .../DependencyInjection/TwigExtension.php | 8 ++++ .../TwigBundle/Resources/config/twig.xml | 10 ++++ 5 files changed, 97 insertions(+) create mode 100644 src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php diff --git a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md index 82325777bc..90f7cbd1e0 100644 --- a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 2.7.0 ----- + * made it possible to configure the default formats for both the `date` and the `number_format` filter * added support for the new Asset component (from Twig bridge) * deprecated the assets extension (use the one from the Twig bridge instead) diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php index 7aaa48d476..794144eb5d 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php @@ -42,6 +42,7 @@ class Configuration implements ConfigurationInterface $this->addFormThemesSection($rootNode); $this->addGlobalsSection($rootNode); $this->addTwigOptions($rootNode); + $this->addTwigFormatOptions($rootNode); return $treeBuilder; } @@ -207,4 +208,33 @@ class Configuration implements ConfigurationInterface ->end() ; } + + private function addTwigFormatOptions(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('date') + ->info('The default format options used by the date filter') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('format')->defaultValue('F j, Y H:i')->end() + ->scalarNode('interval_format')->defaultValue('%d days')->end() + ->scalarNode('timezone') + ->info('The timezone used when formatting dates, when set to null, the timezone returned by date_default_timezone_get() is used') + ->defaultNull() + ->end() + ->end() + ->end() + ->arrayNode('number_format') + ->info('The default format options for the number_format filter') + ->addDefaultsIfNotSet() + ->children() + ->integerNode('decimals')->defaultValue(0)->end() + ->scalarNode('decimal_point')->defaultValue('.')->end() + ->scalarNode('thousands_separator')->defaultValue(',')->end() + ->end() + ->end() + ->end() + ; + } } diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php new file mode 100644 index 0000000000..21e9a1a25c --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\TwigBundle\DependencyInjection\Configurator; + +/** + * Twig environment configurator. + * + * @author Christian Flothmann + */ +class EnvironmentConfigurator +{ + private $dateFormat; + private $intervalFormat; + private $timezone; + private $decimals; + private $decimalPoint; + private $thousandsSeparator; + + public function __construct($dateFormat, $intervalFormat, $timezone, $decimals, $decimalPoint, $thousandsSeparator) + { + $this->dateFormat = $dateFormat; + $this->intervalFormat = $intervalFormat; + $this->timezone = $timezone; + $this->decimals = $decimals; + $this->decimalPoint = $decimalPoint; + $this->thousandsSeparator = $thousandsSeparator; + } + + public function configure(\Twig_Environment $environment) + { + $environment->getExtension('core')->setDateFormat($this->dateFormat, $this->intervalFormat); + + if (null !== $this->timezone) { + $environment->getExtension('core')->setTimezone($this->timezone); + } + + $environment->getExtension('core')->setNumberFormat($this->decimals, $this->decimalPoint, $this->thousandsSeparator); + } +} diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php index 70ed9eed9c..8b28042bd4 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php @@ -57,6 +57,14 @@ class TwigExtension extends Extension $container->setParameter('twig.form.resources', $config['form_themes']); + $envConfiguratorDefinition = $container->getDefinition('twig.configurator.environment'); + $envConfiguratorDefinition->replaceArgument(0, $config['date']['format']); + $envConfiguratorDefinition->replaceArgument(1, $config['date']['interval_format']); + $envConfiguratorDefinition->replaceArgument(2, $config['date']['timezone']); + $envConfiguratorDefinition->replaceArgument(3, $config['number_format']['decimals']); + $envConfiguratorDefinition->replaceArgument(4, $config['number_format']['decimal_point']); + $envConfiguratorDefinition->replaceArgument(5, $config['number_format']['thousands_separator']); + $twigFilesystemLoaderDefinition = $container->getDefinition('twig.loader.filesystem'); // register user-configured paths diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml index 5c84d25059..9e1a117774 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml @@ -35,6 +35,7 @@ app + @@ -163,5 +164,14 @@ %twig.exception_listener.controller% + + + + + + + + + From ab370409fec31d4c434bbf2b4c29de4724702011 Mon Sep 17 00:00:00 2001 From: Hugo Hamon Date: Thu, 2 Apr 2015 17:40:28 +0200 Subject: [PATCH 43/60] [FrameworkBundle] fixes displaying of deprecation notices. --- .../Resources/config/debug_prod.xml | 2 +- .../views/Collector/logger.html.twig | 34 +++++++++++-------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml index 39266eab03..9d70124fbe 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml @@ -16,7 +16,7 @@ - + null null true null diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig index 4ab569020f..4ae5063064 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig @@ -88,10 +88,13 @@ {% if collector.logs %}
    - {% for log in collector.logs if priority >= 0 and log.priority >= priority or priority < 0 and log.context.type|default(0) == priority %} -
  • - {{ logger.display_message(loop.index, log) }} -
  • + {% for log in collector.logs %} + {% set is_deprecation = log.context.level is defined and log.context.type is defined and (constant('E_DEPRECATED') == log.context.type or constant('E_USER_DEPRECATED') == log.context.type) %} + {% if priority == '-100' ? is_deprecation : log.priority >= priority %} +
  • + {{ logger.display_message(loop.index, log, is_deprecation) }} +
  • + {% endif %} {% else %}
  • No logs available for this priority.
  • {% endfor %} @@ -104,15 +107,18 @@ {% endblock %} -{% macro display_message(log_index, log) %} - {% if log.context.level is defined and log.context.type is defined and (constant('E_DEPRECATED') == log.context.type or constant('E_USER_DEPRECATED') == log.context.type) %} - DEPRECATION - {{ log.message }} - {% set id = 'sf-call-stack-' ~ log_index %} - - - + - - {% for index, call in log.context.stack if index > 1 %} +{% macro display_message(log_index, log, is_deprecation) %} + {% if is_deprecation %} + {% set stack = log.context.stack|default([]) %} + + {% if stack %} + + + + + + {% endif %} + + {% for index, call in stack if index > 1 %} {% if index == 2 %} {% endif %} {% endfor %} From dcf1801901eedb6438fffcd79c21bc3db250a785 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Fri, 3 Apr 2015 10:51:06 +0200 Subject: [PATCH 44/60] Tweaked some console command styles --- src/Symfony/Component/Console/Style/SymfonyStyle.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Console/Style/SymfonyStyle.php b/src/Symfony/Component/Console/Style/SymfonyStyle.php index 1b2e7368f6..f2e0279478 100644 --- a/src/Symfony/Component/Console/Style/SymfonyStyle.php +++ b/src/Symfony/Component/Console/Style/SymfonyStyle.php @@ -97,7 +97,7 @@ class SymfonyStyle extends OutputStyle */ public function title($message) { - $this->writeln(sprintf("\n%s\n%s\n", $message, str_repeat('=', strlen($message)))); + $this->writeln(sprintf("\n%s\n%s\n", $message, str_repeat('=', strlen($message)))); } /** @@ -105,7 +105,7 @@ class SymfonyStyle extends OutputStyle */ public function section($message) { - $this->writeln(sprintf("%s\n%s\n", $message, str_repeat('-', strlen($message)))); + $this->writeln(sprintf("%s\n%s\n", $message, str_repeat('-', strlen($message)))); } /** @@ -119,7 +119,7 @@ class SymfonyStyle extends OutputStyle $elements ); - $this->writeln(implode("\n\n", $elements)."\n"); + $this->writeln(implode("\n", $elements)."\n"); } /** @@ -183,6 +183,8 @@ class SymfonyStyle extends OutputStyle */ public function table(array $headers, array $rows) { + $headers = array_map(function ($value) { return sprintf('%s', $value); }, $headers); + $table = new Table($this); $table->setHeaders($headers); $table->setRows($rows); From f95d89c38248bc5630ec8c2ab6282026c029f083 Mon Sep 17 00:00:00 2001 From: WouterJ Date: Fri, 3 Apr 2015 14:47:25 +0200 Subject: [PATCH 45/60] Automatically start server:run if server:start failed --- .../Bundle/FrameworkBundle/Command/ServerStartCommand.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ServerStartCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ServerStartCommand.php index 7808560835..9110bb1b43 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ServerStartCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ServerStartCommand.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -75,6 +76,12 @@ EOF $output->writeln('This command needs the pcntl extension to run.'); $output->writeln('You can either install it or use the server:run command instead to run the built-in web server.'); + if ($this->getHelper('question')->ask($input, $output, new ConfirmationQuestion('Do you want to start server:run immediately? [Yn] ', true))) { + $command = $this->getApplication()->find('server:run'); + + return $command->run($input, $output); + } + return 1; } From 02cda0595c23a1811f2004295cd6abfb91f301cf Mon Sep 17 00:00:00 2001 From: Luis Cordova Date: Sun, 29 Mar 2015 03:09:29 -0500 Subject: [PATCH 46/60] [Enhancement] netbeans - force interactive shell when limited detection --- src/Symfony/Component/Console/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index fa14441421..9daff3f301 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -855,7 +855,7 @@ class Application $input->setInteractive(false); } elseif (function_exists('posix_isatty') && $this->getHelperSet()->has('dialog')) { $inputStream = $this->getHelperSet()->get('dialog')->getInputStream(); - if (!@posix_isatty($inputStream)) { + if (!@posix_isatty($inputStream) && false === getenv('SHELL_INTERACTIVE')) { $input->setInteractive(false); } } From 2a2f7e2c82509e59c5caaa14fc15c051c7fe7133 Mon Sep 17 00:00:00 2001 From: WouterJ Date: Tue, 17 Feb 2015 14:21:59 +0100 Subject: [PATCH 47/60] Deprecated precision option in favor of scale --- UPGRADE-3.0.md | 18 ++++++++++++++++++ src/Symfony/Component/Form/CHANGELOG.md | 1 + .../IntegerToLocalizedStringTransformer.php | 4 ++-- .../MoneyToLocalizedStringTransformer.php | 8 ++++---- .../NumberToLocalizedStringTransformer.php | 11 +++++++---- .../PercentToLocalizedStringTransformer.php | 16 ++++++++-------- .../Form/Extension/Core/Type/IntegerType.php | 17 +++++++++++++++-- .../Form/Extension/Core/Type/MoneyType.php | 19 +++++++++++++++++-- .../Form/Extension/Core/Type/NumberType.php | 17 +++++++++++++++-- .../Form/Extension/Core/Type/PercentType.php | 19 +++++++++++++++++-- ...NumberToLocalizedStringTransformerTest.php | 14 +++++++------- ...ercentToLocalizedStringTransformerTest.php | 4 ++-- .../Extension/Core/Type/NumberTypeTest.php | 6 +++--- 13 files changed, 116 insertions(+), 38 deletions(-) diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index 10ba022473..fd6b788d89 100644 --- a/UPGRADE-3.0.md +++ b/UPGRADE-3.0.md @@ -106,6 +106,24 @@ UPGRADE FROM 2.x to 3.0 ### Form + * The option "precision" was renamed to "scale". + + Before: + + ```php + $builder->add('length', 'number', array( + 'precision' => 3, + )); + ``` + + After: + + ```php + $builder->add('length', 'number', array( + 'scale' => 3, + )); + ``` + * The method `AbstractType::setDefaultOptions(OptionsResolverInterface $resolver)` and `AbstractTypeExtension::setDefaultOptions(OptionsResolverInterface $resolver)` have been renamed. You should use `AbstractType::configureOptions(OptionsResolver $resolver)` and diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 07c96077c2..9d8a790a33 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 2.7.0 ----- + * deprecated option "precision" in favor of "scale" * deprecated the overwriting of AbstractType::setDefaultOptions() in favor of overwriting AbstractType::configureOptions(). * deprecated the overwriting of AbstractTypeExtension::setDefaultOptions() in favor of overwriting AbstractTypeExtension::configureOptions(). diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php index a14074ce9f..eced0cc61d 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php @@ -22,11 +22,11 @@ class IntegerToLocalizedStringTransformer extends NumberToLocalizedStringTransfo /** * Constructs a transformer. * - * @param int $precision Unused. + * @param int $scale Unused. * @param bool $grouping Whether thousands should be grouped. * @param int $roundingMode One of the ROUND_ constants in this class. */ - public function __construct($precision = 0, $grouping = false, $roundingMode = self::ROUND_DOWN) + public function __construct($scale = 0, $grouping = false, $roundingMode = self::ROUND_DOWN) { if (null === $roundingMode) { $roundingMode = self::ROUND_DOWN; diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php index 7de1d0996b..2d3efdc35b 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php @@ -23,17 +23,17 @@ class MoneyToLocalizedStringTransformer extends NumberToLocalizedStringTransform { private $divisor; - public function __construct($precision = 2, $grouping = true, $roundingMode = self::ROUND_HALF_UP, $divisor = 1) + public function __construct($scale = 2, $grouping = true, $roundingMode = self::ROUND_HALF_UP, $divisor = 1) { if (null === $grouping) { $grouping = true; } - if (null === $precision) { - $precision = 2; + if (null === $scale) { + $scale = 2; } - parent::__construct($precision, $grouping, $roundingMode); + parent::__construct($scale, $grouping, $roundingMode); if (null === $divisor) { $divisor = 1; diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php index e6b0a14e09..8aef383fe4 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php @@ -93,13 +93,16 @@ class NumberToLocalizedStringTransformer implements DataTransformerInterface */ const ROUND_HALFDOWN = \NumberFormatter::ROUND_HALFDOWN; + /** + * @deprecated since version 2.7, will be replaced by a $scale private property in 3.0. + */ protected $precision; protected $grouping; protected $roundingMode; - public function __construct($precision = null, $grouping = false, $roundingMode = self::ROUND_HALF_UP) + public function __construct($scale = null, $grouping = false, $roundingMode = self::ROUND_HALF_UP) { if (null === $grouping) { $grouping = false; @@ -109,7 +112,7 @@ class NumberToLocalizedStringTransformer implements DataTransformerInterface $roundingMode = self::ROUND_HALF_UP; } - $this->precision = $precision; + $this->precision = $scale; $this->grouping = $grouping; $this->roundingMode = $roundingMode; } @@ -240,7 +243,7 @@ class NumberToLocalizedStringTransformer implements DataTransformerInterface } /** - * Rounds a number according to the configured precision and rounding mode. + * Rounds a number according to the configured scale and rounding mode. * * @param int|float $number A number. * @@ -249,7 +252,7 @@ class NumberToLocalizedStringTransformer implements DataTransformerInterface private function round($number) { if (null !== $this->precision && null !== $this->roundingMode) { - // shift number to maintain the correct precision during rounding + // shift number to maintain the correct scale during rounding $roundingCoef = pow(10, $this->precision); $number *= $roundingCoef; diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php index 1db54bf43b..fff191e543 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php @@ -33,22 +33,22 @@ class PercentToLocalizedStringTransformer implements DataTransformerInterface private $type; - private $precision; + private $scale; /** * Constructor. * * @see self::$types for a list of supported types * - * @param int $precision The precision - * @param string $type One of the supported types + * @param int $scale The scale + * @param string $type One of the supported types * * @throws UnexpectedTypeException if the given value of type is unknown */ - public function __construct($precision = null, $type = null) + public function __construct($scale = null, $type = null) { - if (null === $precision) { - $precision = 0; + if (null === $scale) { + $scale = 0; } if (null === $type) { @@ -60,7 +60,7 @@ class PercentToLocalizedStringTransformer implements DataTransformerInterface } $this->type = $type; - $this->precision = $precision; + $this->scale = $scale; } /** @@ -142,7 +142,7 @@ class PercentToLocalizedStringTransformer implements DataTransformerInterface { $formatter = new \NumberFormatter(\Locale::getDefault(), \NumberFormatter::DECIMAL); - $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->precision); + $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->scale); return $formatter; } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php b/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php index b36637ad67..16adc42d1c 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php @@ -14,6 +14,7 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer; +use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class IntegerType extends AbstractType @@ -25,7 +26,7 @@ class IntegerType extends AbstractType { $builder->addViewTransformer( new IntegerToLocalizedStringTransformer( - $options['precision'], + $options['scale'], $options['grouping'], $options['rounding_mode'] )); @@ -36,9 +37,19 @@ class IntegerType extends AbstractType */ public function configureOptions(OptionsResolver $resolver) { + $scale = function (Options $options) { + if (null !== $options['precision']) { + trigger_error('The form option "precision" is deprecated since version 2.7 and will be removed in 3.0. Use "scale" instead.', E_USER_DEPRECATED); + } + + return $options['precision']; + }; + $resolver->setDefaults(array( - // default precision is locale specific (usually around 3) + // deprecated as of Symfony 2.7, to be removed in Symfony 3.0. 'precision' => null, + // default scale is locale specific (usually around 3) + 'scale' => $scale, 'grouping' => false, // Integer cast rounds towards 0, so do the same when displaying fractions 'rounding_mode' => IntegerToLocalizedStringTransformer::ROUND_DOWN, @@ -56,6 +67,8 @@ class IntegerType extends AbstractType IntegerToLocalizedStringTransformer::ROUND_CEILING, ), )); + + $resolver->setAllowedTypes('scale', array('null', 'int')); } /** diff --git a/src/Symfony/Component/Form/Extension/Core/Type/MoneyType.php b/src/Symfony/Component/Form/Extension/Core/Type/MoneyType.php index 781fe7ac65..c29886e011 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/MoneyType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/MoneyType.php @@ -16,6 +16,7 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\Extension\Core\DataTransformer\MoneyToLocalizedStringTransformer; use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class MoneyType extends AbstractType @@ -29,7 +30,7 @@ class MoneyType extends AbstractType { $builder ->addViewTransformer(new MoneyToLocalizedStringTransformer( - $options['precision'], + $options['scale'], $options['grouping'], null, $options['divisor'] @@ -50,13 +51,27 @@ class MoneyType extends AbstractType */ public function configureOptions(OptionsResolver $resolver) { + $scale = function (Options $options) { + if (null !== $options['precision']) { + trigger_error('The form option "precision" is deprecated since version 2.7 and will be removed in 3.0. Use "scale" instead.', E_USER_DEPRECATED); + + return $options['precision']; + } + + return 2; + }; + $resolver->setDefaults(array( - 'precision' => 2, + // deprecated as of Symfony 2.7, to be removed in Symfony 3.0 + 'precision' => null, + 'scale' => $scale, 'grouping' => false, 'divisor' => 1, 'currency' => 'EUR', 'compound' => false, )); + + $resolver->setAllowedTypes('scale', 'int'); } /** diff --git a/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php b/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php index 946c9d44e4..0467e43687 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php @@ -14,6 +14,7 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\Extension\Core\DataTransformer\NumberToLocalizedStringTransformer; +use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class NumberType extends AbstractType @@ -24,7 +25,7 @@ class NumberType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options) { $builder->addViewTransformer(new NumberToLocalizedStringTransformer( - $options['precision'], + $options['scale'], $options['grouping'], $options['rounding_mode'] )); @@ -35,9 +36,19 @@ class NumberType extends AbstractType */ public function configureOptions(OptionsResolver $resolver) { + $scale = function (Options $options) { + if (null !== $options['precision']) { + trigger_error('The form option "precision" is deprecated since version 2.7 and will be removed in 3.0. Use "scale" instead.', E_USER_DEPRECATED); + } + + return $options['precision']; + }; + $resolver->setDefaults(array( - // default precision is locale specific (usually around 3) + // deprecated as of Symfony 2.7, to be removed in Symfony 3.0 'precision' => null, + // default scale is locale specific (usually around 3) + 'scale' => $scale, 'grouping' => false, 'rounding_mode' => NumberToLocalizedStringTransformer::ROUND_HALF_UP, 'compound' => false, @@ -54,6 +65,8 @@ class NumberType extends AbstractType NumberToLocalizedStringTransformer::ROUND_CEILING, ), )); + + $resolver->setAllowedTypes('scale', array('null', 'int')); } /** diff --git a/src/Symfony/Component/Form/Extension/Core/Type/PercentType.php b/src/Symfony/Component/Form/Extension/Core/Type/PercentType.php index 51632f031e..544cbafce8 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/PercentType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/PercentType.php @@ -14,6 +14,7 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\Extension\Core\DataTransformer\PercentToLocalizedStringTransformer; +use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class PercentType extends AbstractType @@ -23,7 +24,7 @@ class PercentType extends AbstractType */ public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->addViewTransformer(new PercentToLocalizedStringTransformer($options['precision'], $options['type'])); + $builder->addViewTransformer(new PercentToLocalizedStringTransformer($options['scale'], $options['type'])); } /** @@ -31,8 +32,20 @@ class PercentType extends AbstractType */ public function configureOptions(OptionsResolver $resolver) { + $scale = function (Options $options) { + if (null !== $options['precision']) { + trigger_error('The form option "precision" is deprecated since version 2.7 and will be removed in 3.0. Use "scale" instead.', E_USER_DEPRECATED); + + return $options['precision']; + } + + return 0; + }; + $resolver->setDefaults(array( - 'precision' => 0, + // deprecated as of Symfony 2.7, to be removed in Symfony 3.0. + 'precision' => null, + 'scale' => $scale, 'type' => 'fractional', 'compound' => false, )); @@ -43,6 +56,8 @@ class PercentType extends AbstractType 'integer', ), )); + + $resolver->setAllowedTypes('scale', 'int'); } /** diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php index d66216ac4c..47a0cd9574 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php @@ -74,7 +74,7 @@ class NumberToLocalizedStringTransformerTest extends \PHPUnit_Framework_TestCase $this->assertSame($to, $transformer->transform($from)); } - public function testTransformWithPrecision() + public function testTransformWithScale() { $transformer = new NumberToLocalizedStringTransformer(2); @@ -174,14 +174,14 @@ class NumberToLocalizedStringTransformerTest extends \PHPUnit_Framework_TestCase /** * @dataProvider transformWithRoundingProvider */ - public function testTransformWithRounding($precision, $input, $output, $roundingMode) + public function testTransformWithRounding($scale, $input, $output, $roundingMode) { - $transformer = new NumberToLocalizedStringTransformer($precision, null, $roundingMode); + $transformer = new NumberToLocalizedStringTransformer($scale, null, $roundingMode); $this->assertEquals($output, $transformer->transform($input)); } - public function testTransformDoesNotRoundIfNoPrecision() + public function testTransformDoesNotRoundIfNoScale() { $transformer = new NumberToLocalizedStringTransformer(null, null, NumberToLocalizedStringTransformer::ROUND_DOWN); @@ -327,14 +327,14 @@ class NumberToLocalizedStringTransformerTest extends \PHPUnit_Framework_TestCase /** * @dataProvider reverseTransformWithRoundingProvider */ - public function testReverseTransformWithRounding($precision, $input, $output, $roundingMode) + public function testReverseTransformWithRounding($scale, $input, $output, $roundingMode) { - $transformer = new NumberToLocalizedStringTransformer($precision, null, $roundingMode); + $transformer = new NumberToLocalizedStringTransformer($scale, null, $roundingMode); $this->assertEquals($output, $transformer->reverseTransform($input)); } - public function testReverseTransformDoesNotRoundIfNoPrecision() + public function testReverseTransformDoesNotRoundIfNoScale() { $transformer = new NumberToLocalizedStringTransformer(null, null, NumberToLocalizedStringTransformer::ROUND_DOWN); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/PercentToLocalizedStringTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/PercentToLocalizedStringTransformerTest.php index 104941c9b3..6c9ef6186c 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/PercentToLocalizedStringTransformerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/PercentToLocalizedStringTransformerTest.php @@ -53,7 +53,7 @@ class PercentToLocalizedStringTransformerTest extends \PHPUnit_Framework_TestCas $this->assertEquals('16', $transformer->transform(15.9)); } - public function testTransformWithPrecision() + public function testTransformWithScale() { $transformer = new PercentToLocalizedStringTransformer(2); @@ -87,7 +87,7 @@ class PercentToLocalizedStringTransformerTest extends \PHPUnit_Framework_TestCas $this->assertEquals(200, $transformer->reverseTransform('200')); } - public function testReverseTransformWithPrecision() + public function testReverseTransformWithScale() { $transformer = new PercentToLocalizedStringTransformer(2); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/NumberTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/NumberTypeTest.php index d4a88dd260..f21a7d1ec4 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/NumberTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/NumberTypeTest.php @@ -44,9 +44,9 @@ class NumberTypeTest extends TestCase $this->assertSame('12.345,679', $view->vars['value']); } - public function testDefaultFormattingWithPrecision() + public function testDefaultFormattingWithScale() { - $form = $this->factory->create('number', null, array('precision' => 2)); + $form = $this->factory->create('number', null, array('scale' => 2)); $form->setData('12345.67890'); $view = $form->createView(); @@ -55,7 +55,7 @@ class NumberTypeTest extends TestCase public function testDefaultFormattingWithRounding() { - $form = $this->factory->create('number', null, array('precision' => 0, 'rounding_mode' => \NumberFormatter::ROUND_UP)); + $form = $this->factory->create('number', null, array('scale' => 0, 'rounding_mode' => \NumberFormatter::ROUND_UP)); $form->setData('12345.54321'); $view = $form->createView(); From a4551f97f41a8cc230632350d039395a19a4a0fd Mon Sep 17 00:00:00 2001 From: WouterJ Date: Sun, 8 Feb 2015 20:47:12 +0100 Subject: [PATCH 48/60] Added feedback about the current symfony version --- .../views/Collector/config.html.twig | 13 ++- .../DataCollector/ConfigDataCollector.php | 86 +++++++++++++++++++ .../DataCollector/ConfigDataCollectorTest.php | 1 + 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig index c37b50d8c8..efa3fd5765 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig @@ -9,7 +9,18 @@ {% if collector.applicationname %} {{ collector.applicationname }} {{ collector.applicationversion }} {% else %} - {{ collector.symfonyversion }} + {% if 'unknown' == collector.symfonyState -%} + + {%- elseif 'eol' == collector.symfonyState -%} + + {%- elseif 'eom' == collector.symfonyState -%} + + {%- elseif 'dev' == collector.symfonyState -%} + + {%- else -%} + + {%- endif -%} + {{ collector.symfonyversion }} {% endif %} diff --git a/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php index f58027e218..3344283a0d 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php @@ -23,9 +23,13 @@ use Symfony\Component\HttpFoundation\Response; */ class ConfigDataCollector extends DataCollector { + /** + * @var KernelInterface + */ private $kernel; private $name; private $version; + private $cacheVersionInfo = true; /** * Constructor. @@ -59,6 +63,7 @@ class ConfigDataCollector extends DataCollector 'app_version' => $this->version, 'token' => $response->headers->get('X-Debug-Token'), 'symfony_version' => Kernel::VERSION, + 'symfony_state' => 'unknown', 'name' => isset($this->kernel) ? $this->kernel->getName() : 'n/a', 'env' => isset($this->kernel) ? $this->kernel->getEnvironment() : 'n/a', 'debug' => isset($this->kernel) ? $this->kernel->isDebug() : 'n/a', @@ -77,6 +82,8 @@ class ConfigDataCollector extends DataCollector foreach ($this->kernel->getBundles() as $name => $bundle) { $this->data['bundles'][$name] = $bundle->getPath(); } + + $this->data['symfony_state'] = $this->requestSymfonyState(); } } @@ -110,6 +117,21 @@ class ConfigDataCollector extends DataCollector return $this->data['symfony_version']; } + /** + * Returns the state of the current Symfony release. + * + * @return string One of: unknown, dev, stable, eom, eol + */ + public function getSymfonyState() + { + return $this->data['symfony_state']; + } + + public function setCacheVersionInfo($cacheVersionInfo) + { + $this->cacheVersionInfo = $cacheVersionInfo; + } + /** * Gets the PHP version. * @@ -242,4 +264,68 @@ class ConfigDataCollector extends DataCollector { return 'config'; } + + /** + * Tries to retrieve information about the current Symfony version. + * + * @return string One of: unknown, dev, stable, eom, eol + */ + private function requestSymfonyState() + { + $versionInfo = null; + + // get version information from cache or the roadmap + $versionCachePath = $this->kernel->getCacheDir().'/version_info.json'; + if (file_exists($versionCachePath)) { + $versionInfo = json_decode(file_get_contents($versionCachePath), true); + } else { + $versionResponse = @file_get_contents('http://symfony.com/roadmap.json?version='.preg_replace('/^(\d+\.\d+).*/', '\\1', $this->data['symfony_version'])); + + if (false !== $versionResponse) { + $versionInfo = json_decode($versionResponse, true); + + if (isset($versionInfo['error_message'])) { + // wrong version + $versionInfo = null; + } + } + } + + // get the version state + $versionState = 'unknown'; + if (null !== $versionInfo) { + $now = new \DateTime(); + $eom = \DateTime::createFromFormat('m/Y', $versionInfo['eom'])->modify('last day of this month'); + $eol = \DateTime::createFromFormat('m/Y', $versionInfo['eol'])->modify('last day of this month'); + + if ($now > $eom) { + $versionState = 'eom'; + } elseif ($now > $eol) { + $versionState = 'eol'; + } elseif ('DEV' === Kernel::EXTRA_VERSION) { + $versionState = 'dev'; + } else { + $versionState = 'stable'; + } + } + + // invalidate or create cache + if (null === $versionInfo) { + // nothing to cache + } elseif (isset($versionInfo['previous_state'])) { + if ($versionInfo['previous_state'] !== $versionState) { + // state changed => invalidate the cache + unlink($versionCachePath); + } + } elseif (substr(Kernel::VERSION, 0, 3) !== $versionInfo['version']) { + // version changed => invalidate the cache + unlink($versionCachePath); + } elseif ($this->cacheVersionInfo) { + // no cache yet + $versionInfo['previous_state'] = $versionState; + file_put_contents($versionCachePath, json_encode($versionInfo)); + } + + return $versionState; + } } diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/ConfigDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/ConfigDataCollectorTest.php index 0d672a1f0c..4aa01f0e7c 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/ConfigDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/ConfigDataCollectorTest.php @@ -23,6 +23,7 @@ class ConfigDataCollectorTest extends \PHPUnit_Framework_TestCase { $kernel = new KernelForTest('test', true); $c = new ConfigDataCollector(); + $c->setCacheVersionInfo(false); $c->setKernel($kernel); $c->collect(new Request(), new Response()); From 40ab7ef349751d3d7a9526780abf2ed8b227eb6e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 3 Apr 2015 07:27:47 +0200 Subject: [PATCH 49/60] [HttpKernel] Embed the original exception as previous to bounced exceptions --- .../HttpKernel/EventListener/ExceptionListener.php | 13 ++++++++++++- .../Tests/EventListener/ExceptionListenerTest.php | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php b/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php index a54fe45e9f..b1c1982f45 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php @@ -72,7 +72,18 @@ class ExceptionListener implements EventSubscriberInterface // set handling to false otherwise it wont be able to handle further more $handling = false; - // throwing $e, not $exception, is on purpose: fixing error handling code paths is the most important + $wrapper = $e; + + while ($prev = $wrapper->getPrevious()) { + if ($exception === $wrapper = $prev) { + throw $e; + } + } + + $prev = new \ReflectionProperty('Exception', 'previous'); + $prev->setAccessible(true); + $prev->setValue($wrapper, $exception); + throw $e; } diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php index 86244df0fc..8fb00f51c1 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php @@ -57,6 +57,7 @@ class ExceptionListenerTest extends \PHPUnit_Framework_TestCase $this->fail('RuntimeException expected'); } catch (\RuntimeException $e) { $this->assertSame('bar', $e->getMessage()); + $this->assertSame('foo', $e->getPrevious()->getMessage()); } } @@ -77,6 +78,7 @@ class ExceptionListenerTest extends \PHPUnit_Framework_TestCase $this->fail('RuntimeException expected'); } catch (\RuntimeException $e) { $this->assertSame('bar', $e->getMessage()); + $this->assertSame('foo', $e->getPrevious()->getMessage()); } $this->assertEquals(3, $logger->countErrors()); From 6863ba5bef687cd1b7843aebefa764ae2b405019 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 1 Apr 2015 13:17:00 +0200 Subject: [PATCH 50/60] [Form] Updated CHANGELOG and UPGRADE files --- UPGRADE-2.7.md | 409 +++++++++++++++++- src/Symfony/Bridge/Doctrine/CHANGELOG.md | 10 + .../Form/ChoiceList/DoctrineChoiceLoader.php | 8 +- src/Symfony/Component/Form/CHANGELOG.md | 17 + 4 files changed, 421 insertions(+), 23 deletions(-) diff --git a/UPGRADE-2.7.md b/UPGRADE-2.7.md index 60ad352fa6..cb00c115e7 100644 --- a/UPGRADE-2.7.md +++ b/UPGRADE-2.7.md @@ -29,36 +29,405 @@ Form Before: ```php - use Symfony\Component\OptionsResolver\OptionsResolverInterface; - - class TaskType extends AbstractType + use Symfony\Component\OptionsResolver\OptionsResolverInterface; + + class TaskType extends AbstractType + { + // ... + public function setDefaultOptions(OptionsResolverInterface $resolver) { - // ... - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults(array( - 'data_class' => 'AppBundle\Entity\Task', - )); - } + $resolver->setDefaults(array( + 'data_class' => 'AppBundle\Entity\Task', + )); } + } ``` After: ```php - use Symfony\Component\OptionsResolver\OptionsResolver; - - class TaskType extends AbstractType + use Symfony\Component\OptionsResolver\OptionsResolver; + + class TaskType extends AbstractType + { + // ... + public function configureOptions(OptionsResolver $resolver) { - // ... - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults(array( - 'data_class' => 'AppBundle\Entity\Task', - )); - } + $resolver->setDefaults(array( + 'data_class' => 'AppBundle\Entity\Task', + )); } + } ``` + + * The "choice_list" option of ChoiceType was deprecated. You should use + "choices_as_values" or "choice_loader" now. + + Before: + + ```php + $form->add('status', 'choice', array( + 'choice_list' => new ObjectChoiceList(array( + Status::getInstance(Status::ENABLED), + Status::getInstance(Status::DISABLED), + Status::getInstance(Status::IGNORED), + )), + )); + ``` + + After: + + ```php + $form->add('status', 'choice', array( + 'choices' => array( + Status::getInstance(Status::ENABLED), + Status::getInstance(Status::DISABLED), + Status::getInstance(Status::IGNORED), + ), + 'choices_as_values' => true, + )); + ``` + + * You should flip the keys and values of the "choices" option in ChoiceType + and set the "choices_as_values" option to `true`. The default value of that + option will be switched to `true` in Symfony 3.0. + + Before: + + ```php + $form->add('status', 'choice', array( + 'choices' => array( + Status::ENABLED => 'Enabled', + Status::DISABLED => 'Disabled', + Status::IGNORED => 'Ignored', + )), + )); + ``` + + After: + + ```php + $form->add('status', 'choice', array( + 'choices' => array( + 'Enabled' => Status::ENABLED, + 'Disabled' => Status::DISABLED, + 'Ignored' => Status::IGNORED, + ), + 'choices_as_values' => true, + )); + ``` + + * `Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface` was + deprecated and will be removed in Symfony 3.0. You should use + `Symfony\Component\Form\ChoiceList\ChoiceListInterface` instead. + + Before: + + ```php + use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; + + public function doSomething(ChoiceListInterface $choiceList) + { + // ... + } + ``` + + After: + + ```php + use Symfony\Component\Form\ChoiceList\ChoiceListInterface; + + public function doSomething(ChoiceListInterface $choiceList) + { + // ... + } + ``` + + * `Symfony\Component\Form\Extension\Core\ChoiceList\View\ChoiceView` was + deprecated and will be removed in Symfony 3.0. You should use + `Symfony\Component\Form\ChoiceList\View\ChoiceView` instead. + + Note that the order of the arguments passed to the constructor was inverted. + + Before: + + ```php + use Symfony\Component\Form\Extension\Core\ChoiceList\View\ChoiceView; + + $view = new ChoiceView($data, 'value', 'Label'); + ``` + + After: + + ```php + use Symfony\Component\Form\ChoiceList\View\ChoiceView; + + $view = new ChoiceView('Label', 'value', $data); + ``` + + * `Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList` was + deprecated and will be removed in Symfony 3.0. You should use + `Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory` instead. + + Before: + + ```php + use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; + + $choiceList = new ChoiceList( + array(Status::ENABLED, Status::DISABLED, Status::IGNORED), + array('Enabled', 'Disabled', 'Ignored'), + // Preferred choices + array(Status::ENABLED), + ); + ``` + + After: + + ```php + use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; + + $factory = new DefaultChoiceListFactory(); + + $choices = array(Status::ENABLED, Status::DISABLED, Status::IGNORED); + $labels = array('Enabled', 'Disabled', 'Ignored'); + + $choiceList = $factory->createListFromChoices($choices); + + $choiceListView = $factory->createView( + $choiceList, + // Preferred choices + array(Status::ENABLED), + // Labels + function ($choice, $key) use ($labels) { + return $labels[$key]; + } + ); + ``` + + * `Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList` was + deprecated and will be removed in Symfony 3.0. You should use + `Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory::createListFromLoader()` + together with an implementation of + `Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface` instead. + + Before: + + ```php + use Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList; + + class MyLazyChoiceList extends LazyChoiceList + { + public function loadChoiceList() + { + // load $choiceList + + return $choiceList; + } + } + + $choiceList = new MyLazyChoiceList(); + ``` + + After: + + ```php + use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; + use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; + + class MyChoiceLoader implements ChoiceLoaderInterface + { + // ... + } + + $factory = new DefaultChoiceListFactory(); + + $choiceList = $factory->createListFromLoader(new MyChoiceLoader()); + ``` + + * `Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList` was + deprecated and will be removed in Symfony 3.0. You should use + `Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory` instead. + + Before: + + ```php + use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; + + $choiceList = new ObjectChoiceList( + array(Status::getInstance(Status::ENABLED), Status::getInstance(Status::DISABLED)), + // Label property + 'name' + ); + ``` + + After: + + ```php + use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; + + $factory = new DefaultChoiceListFactory(); + + $choiceList = $factory->createListFromChoices(array( + Status::getInstance(Status::ENABLED), + Status::getInstance(Status::DISABLED), + )); + + $choiceListView = $factory->createView( + $choiceList, + // Preferred choices + array(), + // Label property + 'name' + ); + ``` + + * `Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList` was + deprecated and will be removed in Symfony 3.0. You should use + `Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory` instead. + + Before: + + ```php + use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; + + $choiceList = new SimpleChoiceList(array( + Status::ENABLED => 'Enabled', + Status::DISABLED => 'Disabled', + )); + ``` + + After: + + ```php + use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; + + $factory = new DefaultChoiceListFactory(); + + $choices = array(Status::ENABLED, Status::DISABLED); + $labels = array('Enabled', 'Disabled'); + + $choiceList = $factory->createListFromChoices($choices); + + $choiceListView = $factory->createView( + $choiceList, + // Preferred choices + array(), + // Label + function ($choice, $key) use ($labels) { + return $labels[$key]; + } + ); + ``` + + * The "property" option of `DoctrineType` was deprecated. You should use the + new inherited option "choice_label" instead, which has the same effect. + + Before: + + ```php + $form->add('tags', 'entity', array( + 'class' => 'Acme\Entity\MyTag', + 'property' => 'name', + )) + ``` + + After: + + ```php + $form->add('tags', 'entity', array( + 'class' => 'Acme\Entity\MyTag', + 'choice_label' => 'name', + )) + ``` + + * The "loader" option of `DoctrineType` was deprecated and will be removed in + Symfony 3.0. You should override the `getLoader()` method instead in a custom + type. + + Before: + + ```php + $form->add('tags', 'entity', array( + 'class' => 'Acme\Entity\MyTag', + 'loader' => new MyEntityLoader(), + )) + ``` + + After: + + class MyEntityType extends DoctrineType + { + // ... + + public function getLoader() + { + return new MyEntityLoader(); + } + } + + * `Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList` was + deprecated and will be removed in Symfony 3.0. You should use + `Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader` instead. + + Before: + + ```php + use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; + + $choiceList = new EntityChoiceList($em, 'Acme\Entity\MyEntity'); + ``` + + After: + + ```php + use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; + + $factory = new DefaultChoiceListFactory(); + + $choices = array(Status::ENABLED, Status::DISABLED); + $labels = array('Enabled', 'Disabled'); + + $choiceLoader = new DoctrineChoiceLoader($factory, $em, 'Acme\Entity\MyEntity'); + $choiceList = $factory->createListFromLoader($choiceLoader); + ``` + + * Passing a query builder closure to `ORMQueryBuilderLoader` was deprecated and + will not be supported anymore in Symfony 3.0. You should pass resolved query + builders only. + + Consequently, the arguments `$manager` and `$class` of `ORMQueryBuilderLoader` + have been deprecated as well. + + Note that the "query_builder" option of `DoctrineType` *does* support + closures, but the closure is now resolved in the type instead of in the + loader. + + Before: + + ``` + use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; + + $queryBuilder = function () { + // return QueryBuilder + }; + $loader = new ORMQueryBuilderLoader($queryBuilder); + ``` + + After: + + ``` + use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; + + // create $queryBuilder + $loader = new ORMQueryBuilderLoader($queryBuilder); + ``` + + * The classes `ChoiceToBooleanArrayTransformer`, + `ChoicesToBooleanArrayTransformer`, `FixRadioInputListener` and + `FixCheckboxInputListener` were deprecated and will be removed in Symfony 3.0. + Their functionality is covered by the new classes `RadioListMapper` and + `CheckboxListMapper`. Serializer ---------- diff --git a/src/Symfony/Bridge/Doctrine/CHANGELOG.md b/src/Symfony/Bridge/Doctrine/CHANGELOG.md index c00ac113d7..4d8c44701d 100644 --- a/src/Symfony/Bridge/Doctrine/CHANGELOG.md +++ b/src/Symfony/Bridge/Doctrine/CHANGELOG.md @@ -1,6 +1,16 @@ CHANGELOG ========= +2.7.0 +----- + + * added DoctrineChoiceLoader + * deprecated EntityChoiceList + * deprecated passing a query builder closure to ORMQueryBuilderLoader + * deprecated $manager and $em arguments of ORMQueryBuilderLoader + * added optional arguments $propertyAccessor and $choiceListFactory to DoctrineOrmExtension constructor + * deprecated "loader" and "property" options of DoctrineType + 2.4.0 ----- diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php index 5456c0eedb..2335af7131 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -69,12 +69,14 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface * IDs. * @param null|EntityLoaderInterface $objectLoader The objects loader */ - public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, IdReader $idReader, EntityLoaderInterface $objectLoader = null) + public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, IdReader $idReader = null, EntityLoaderInterface $objectLoader = null) { + $classMetadata = $manager->getClassMetadata($class); + $this->factory = $factory; $this->manager = $manager; - $this->class = $manager->getClassMetadata($class)->getName(); - $this->idReader = $idReader; + $this->class = $classMetadata->getName(); + $this->idReader = $idReader ?: new IdReader($manager, $classMetadata); $this->objectLoader = $objectLoader; } diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index ea959e1377..a5588aaf91 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -6,6 +6,23 @@ CHANGELOG * deprecated the overwriting of AbstractType::setDefaultOptions() in favor of overwriting AbstractType::configureOptions(). * deprecated the overwriting of AbstractTypeExtension::setDefaultOptions() in favor of overwriting AbstractTypeExtension::configureOptions(). + * added new ChoiceList interface and implementations in the Symfony\Component\Form\ChoiceList namespace + * added new ChoiceView in the Symfony\Component\Form\ChoiceList\View namespace + * choice groups are now represented by ChoiceGroupView objects in the view + * deprecated the old ChoiceList interface and implementations + * deprecated the old ChoiceView class + * added CheckboxListMapper and RadioListMapper + * deprecated ChoiceToBooleanArrayTransformer and ChoicesToBooleanArrayTransformer + * deprecated FixCheckboxInputListener and FixRadioInputListener + * deprecated the "choice_list" option of ChoiceType + * added new options to ChoiceType: + * "choices_as_values" + * "choice_loader" + * "choice_label" + * "choice_name" + * "choice_value" + * "choice_attr" + * "group_by" 2.6.2 ----- From eacdc626ae27481fafddd797fd58c1b25cba4fff Mon Sep 17 00:00:00 2001 From: Julien Pauli Date: Fri, 3 Oct 2014 10:29:03 +0200 Subject: [PATCH 51/60] [Debug] Add symfony_debug_backtrace() that works with fatal errors --- .../Component/Debug/Resources/ext/README.md | 133 ++++++++++++++++++ .../Component/Debug/Resources/ext/README.rst | 72 ---------- .../Debug/Resources/ext/php_symfony_debug.h | 13 +- .../Debug/Resources/ext/symfony_debug.c | 87 ++++++++++-- .../Debug/Resources/ext/tests/001.phpt | 6 +- .../Debug/Resources/ext/tests/002.phpt | 64 +++++++++ .../Debug/Resources/ext/tests/002_1.phpt | 47 +++++++ 7 files changed, 329 insertions(+), 93 deletions(-) create mode 100644 src/Symfony/Component/Debug/Resources/ext/README.md delete mode 100644 src/Symfony/Component/Debug/Resources/ext/README.rst create mode 100644 src/Symfony/Component/Debug/Resources/ext/tests/002.phpt create mode 100644 src/Symfony/Component/Debug/Resources/ext/tests/002_1.phpt diff --git a/src/Symfony/Component/Debug/Resources/ext/README.md b/src/Symfony/Component/Debug/Resources/ext/README.md new file mode 100644 index 0000000000..0672ef8f4a --- /dev/null +++ b/src/Symfony/Component/Debug/Resources/ext/README.md @@ -0,0 +1,133 @@ +Symfony Debug Extension +======================= + +This extension publishes several functions to help building powerful debugging tools. + +symfony_zval_info() +------------------- + +- exposes zval_hash/refcounts, allowing e.g. efficient exploration of arbitrary structures in PHP, +- does work with references, preventing memory copying. + +Its behavior is about the same as: + +```php + gettype($array[$key]), + 'zval_hash' => /* hashed memory address of $array[$key] */, + 'zval_refcount' => /* internal zval refcount of $array[$key] */, + 'zval_isref' => /* is_ref status of $array[$key] */, + ); + + switch ($info['type']) { + case 'object': + $info += array( + 'object_class' => get_class($array[$key]), + 'object_refcount' => /* internal object refcount of $array[$key] */, + 'object_hash' => spl_object_hash($array[$key]), + 'object_handle' => /* internal object handle $array[$key] */, + ); + break; + + case 'resource': + $info += array( + 'resource_handle' => (int) $array[$key], + 'resource_type' => get_resource_type($array[$key]), + 'resource_refcount' => /* internal resource refcount of $array[$key] */, + ); + break; + + case 'array': + $info += array( + 'array_count' => count($array[$key]), + ); + break; + + case 'string': + $info += array( + 'strlen' => strlen($array[$key]), + ); + break; + } + + return $info; +} +``` + +symfony_debug_backtrace() +------------------------- + +This function works like debug_backtrace(), except that it can fetch the full backtrace in case of fatal errors: + +```php +function foo() { fatal(); } +function bar() { foo(); } + +function sd() { var_dump(symfony_debug_backtrace()); } + +register_shutdown_function('sd'); + +bar(); + +/* Will output +Fatal error: Call to undefined function fatal() in foo.php on line 42 +array(3) { + [0]=> + array(2) { + ["function"]=> + string(2) "sd" + ["args"]=> + array(0) { + } + } + [1]=> + array(4) { + ["file"]=> + string(7) "foo.php" + ["line"]=> + int(1) + ["function"]=> + string(3) "foo" + ["args"]=> + array(0) { + } + } + [2]=> + array(4) { + ["file"]=> + string(102) "foo.php" + ["line"]=> + int(2) + ["function"]=> + string(3) "bar" + ["args"]=> + array(0) { + } + } +} +*/ +``` + +Usage +----- + +The extension is compatible with ZTS mode, and should be supported by PHP5.3, 5.4, 5.5 and 5.6. +To enable the extension from source, run: + +``` + phpize + ./configure + make + sudo make install +``` diff --git a/src/Symfony/Component/Debug/Resources/ext/README.rst b/src/Symfony/Component/Debug/Resources/ext/README.rst deleted file mode 100644 index b0d1c58959..0000000000 --- a/src/Symfony/Component/Debug/Resources/ext/README.rst +++ /dev/null @@ -1,72 +0,0 @@ -Symfony Debug Extension -======================= - -This extension adds a ``symfony_zval_info($key, $array, $options = 0)`` function that: - -- exposes zval_hash/refcounts, allowing e.g. efficient exploration of arbitrary structures in PHP, -- does work with references, preventing memory copying. - -Its behavior is about the same as: - -.. code-block:: php - - gettype($array[$key]), - 'zval_hash' => /* hashed memory address of $array[$key] */, - 'zval_refcount' => /* internal zval refcount of $array[$key] */, - 'zval_isref' => /* is_ref status of $array[$key] */, - ); - - switch ($info['type']) { - case 'object': - $info += array( - 'object_class' => get_class($array[$key]), - 'object_refcount' => /* internal object refcount of $array[$key] */, - 'object_hash' => spl_object_hash($array[$key]), - 'object_handle' => /* internal object handle $array[$key] */, - ); - break; - - case 'resource': - $info += array( - 'resource_handle' => (int) $array[$key], - 'resource_type' => get_resource_type($array[$key]), - 'resource_refcount' => /* internal resource refcount of $array[$key] */, - ); - break; - - case 'array': - $info += array( - 'array_count' => count($array[$key]), - ); - break; - - case 'string': - $info += array( - 'strlen' => strlen($array[$key]), - ); - break; - } - - return $info; - } - -To enable the extension from source, run: - -.. code-block:: sh - - phpize - ./configure - make - sudo make install - diff --git a/src/Symfony/Component/Debug/Resources/ext/php_symfony_debug.h b/src/Symfony/Component/Debug/Resources/ext/php_symfony_debug.h index c935f67019..26d0e8c012 100644 --- a/src/Symfony/Component/Debug/Resources/ext/php_symfony_debug.h +++ b/src/Symfony/Component/Debug/Resources/ext/php_symfony_debug.h @@ -13,7 +13,7 @@ extern zend_module_entry symfony_debug_module_entry; #define phpext_symfony_debug_ptr &symfony_debug_module_entry -#define PHP_SYMFONY_DEBUG_VERSION "1.0" +#define PHP_SYMFONY_DEBUG_VERSION "2.7" #ifdef PHP_WIN32 # define PHP_SYMFONY_DEBUG_API __declspec(dllexport) @@ -29,6 +29,8 @@ extern zend_module_entry symfony_debug_module_entry; ZEND_BEGIN_MODULE_GLOBALS(symfony_debug) intptr_t req_rand_init; + void (*old_error_cb)(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args); + zval *debug_bt; ZEND_END_MODULE_GLOBALS(symfony_debug) PHP_MINIT_FUNCTION(symfony_debug); @@ -40,11 +42,14 @@ PHP_GINIT_FUNCTION(symfony_debug); PHP_GSHUTDOWN_FUNCTION(symfony_debug); PHP_FUNCTION(symfony_zval_info); +PHP_FUNCTION(symfony_debug_backtrace); -static char *_symfony_debug_memory_address_hash(void *); +static char *_symfony_debug_memory_address_hash(void * TSRMLS_DC); static const char *_symfony_debug_zval_type(zval *); -static const char* _symfony_debug_get_resource_type(long); -static int _symfony_debug_get_resource_refcount(long); +static const char* _symfony_debug_get_resource_type(long TSRMLS_DC); +static int _symfony_debug_get_resource_refcount(long TSRMLS_DC); + +void symfony_debug_error_cb(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args); #ifdef ZTS #define SYMFONY_DEBUG_G(v) TSRMG(symfony_debug_globals_id, zend_symfony_debug_globals *, v) diff --git a/src/Symfony/Component/Debug/Resources/ext/symfony_debug.c b/src/Symfony/Component/Debug/Resources/ext/symfony_debug.c index 8dc5d4356a..0d7cb60232 100644 --- a/src/Symfony/Component/Debug/Resources/ext/symfony_debug.c +++ b/src/Symfony/Component/Debug/Resources/ext/symfony_debug.c @@ -12,6 +12,9 @@ #endif #include "php.h" +#ifdef ZTS +#include "TSRM.h" +#endif #include "php_ini.h" #include "ext/standard/info.h" #include "php_symfony_debug.h" @@ -19,6 +22,13 @@ #include "ext/standard/php_lcg.h" #include "ext/spl/php_spl.h" #include "Zend/zend_gc.h" +#include "Zend/zend_builtin_functions.h" +#include "Zend/zend_extensions.h" /* for ZEND_EXTENSION_API_NO */ +#include "ext/standard/php_array.h" +#include "Zend/zend_interfaces.h" +#include "SAPI.h" + +#define IS_PHP_53 ZEND_EXTENSION_API_NO == 220090626 ZEND_DECLARE_MODULE_GLOBALS(symfony_debug) @@ -30,9 +40,28 @@ ZEND_END_ARG_INFO() const zend_function_entry symfony_debug_functions[] = { PHP_FE(symfony_zval_info, symfony_zval_arginfo) + PHP_FE(symfony_debug_backtrace, NULL) PHP_FE_END }; +PHP_FUNCTION(symfony_debug_backtrace) +{ + if (zend_parse_parameters_none() == FAILURE) { + return; + } +#if IS_PHP_53 + zend_fetch_debug_backtrace(return_value, 1, 0 TSRMLS_CC); +#else + zend_fetch_debug_backtrace(return_value, 1, 0, 0 TSRMLS_CC); +#endif + + if (!SYMFONY_DEBUG_G(debug_bt)) { + return; + } + + php_array_merge(Z_ARRVAL_P(return_value), Z_ARRVAL_P(SYMFONY_DEBUG_G(debug_bt)), 0 TSRMLS_CC); +} + PHP_FUNCTION(symfony_zval_info) { zval *key = NULL, *arg = NULL; @@ -40,7 +69,7 @@ PHP_FUNCTION(symfony_zval_info) HashTable *array = NULL; long options = 0; - if (zend_parse_parameters(ZEND_NUM_ARGS(), "zh|l", &key, &array, &options) == FAILURE) { + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "zh|l", &key, &array, &options) == FAILURE) { return; } @@ -62,13 +91,14 @@ PHP_FUNCTION(symfony_zval_info) array_init(return_value); add_assoc_string(return_value, "type", (char *)_symfony_debug_zval_type(arg), 1); - add_assoc_stringl(return_value, "zval_hash", _symfony_debug_memory_address_hash((void *)arg), 16, 1); + add_assoc_stringl(return_value, "zval_hash", _symfony_debug_memory_address_hash((void *)arg TSRMLS_CC), 16, 0); add_assoc_long(return_value, "zval_refcount", Z_REFCOUNT_P(arg)); add_assoc_bool(return_value, "zval_isref", (zend_bool)Z_ISREF_P(arg)); if (Z_TYPE_P(arg) == IS_OBJECT) { - static char hash[33] = {0}; - php_spl_object_hash(arg, (char *)hash); + char hash[33] = {0}; + + php_spl_object_hash(arg, (char *)hash TSRMLS_CC); add_assoc_stringl(return_value, "object_class", (char *)Z_OBJCE_P(arg)->name, Z_OBJCE_P(arg)->name_length, 1); add_assoc_long(return_value, "object_refcount", EG(objects_store).object_buckets[Z_OBJ_HANDLE_P(arg)].bucket.obj.refcount); add_assoc_string(return_value, "object_hash", hash, 1); @@ -77,17 +107,41 @@ PHP_FUNCTION(symfony_zval_info) add_assoc_long(return_value, "array_count", zend_hash_num_elements(Z_ARRVAL_P(arg))); } else if(Z_TYPE_P(arg) == IS_RESOURCE) { add_assoc_long(return_value, "resource_handle", Z_LVAL_P(arg)); - add_assoc_string(return_value, "resource_type", (char *)_symfony_debug_get_resource_type(Z_LVAL_P(arg)), 1); - add_assoc_long(return_value, "resource_refcount", _symfony_debug_get_resource_refcount(Z_LVAL_P(arg))); + add_assoc_string(return_value, "resource_type", (char *)_symfony_debug_get_resource_type(Z_LVAL_P(arg) TSRMLS_CC), 1); + add_assoc_long(return_value, "resource_refcount", _symfony_debug_get_resource_refcount(Z_LVAL_P(arg) TSRMLS_CC)); } else if (Z_TYPE_P(arg) == IS_STRING) { add_assoc_long(return_value, "strlen", Z_STRLEN_P(arg)); } } -static const char* _symfony_debug_get_resource_type(long rsid) +void symfony_debug_error_cb(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args) +{ + TSRMLS_FETCH(); + zval *retval; + + switch (type) { + case E_ERROR: + case E_PARSE: + case E_CORE_ERROR: + case E_CORE_WARNING: + case E_COMPILE_ERROR: + case E_COMPILE_WARNING: + ALLOC_INIT_ZVAL(retval); +#if IS_PHP_53 + zend_fetch_debug_backtrace(retval, 1, 0 TSRMLS_CC); +#else + zend_fetch_debug_backtrace(retval, 1, 0, 0 TSRMLS_CC); +#endif + SYMFONY_DEBUG_G(debug_bt) = retval; + } + + SYMFONY_DEBUG_G(old_error_cb)(type, error_filename, error_lineno, format, args); +} + +static const char* _symfony_debug_get_resource_type(long rsid TSRMLS_DC) { const char *res_type; - res_type = zend_rsrc_list_get_rsrc_type(rsid); + res_type = zend_rsrc_list_get_rsrc_type(rsid TSRMLS_CC); if (!res_type) { return "Unknown"; @@ -96,7 +150,7 @@ static const char* _symfony_debug_get_resource_type(long rsid) return res_type; } -static int _symfony_debug_get_resource_refcount(long rsid) +static int _symfony_debug_get_resource_refcount(long rsid TSRMLS_DC) { zend_rsrc_list_entry *le; @@ -107,21 +161,21 @@ static int _symfony_debug_get_resource_refcount(long rsid) return 0; } -static char *_symfony_debug_memory_address_hash(void *address) +static char *_symfony_debug_memory_address_hash(void *address TSRMLS_DC) { - static char result[17] = {0}; + char *result = NULL; intptr_t address_rand; if (!SYMFONY_DEBUG_G(req_rand_init)) { if (!BG(mt_rand_is_seeded)) { php_mt_srand(GENERATE_SEED() TSRMLS_CC); } - SYMFONY_DEBUG_G(req_rand_init) = (intptr_t)php_mt_rand(); + SYMFONY_DEBUG_G(req_rand_init) = (intptr_t)php_mt_rand(TSRMLS_C); } address_rand = (intptr_t)address ^ SYMFONY_DEBUG_G(req_rand_init); - snprintf(result, 17, "%016zx", address_rand); + spprintf(&result, 17, "%016zx", address_rand); return result; } @@ -187,7 +241,7 @@ ZEND_GET_MODULE(symfony_debug) PHP_GINIT_FUNCTION(symfony_debug) { - symfony_debug_globals->req_rand_init = 0; + memset(symfony_debug_globals, 0 , sizeof(*symfony_debug_globals)); } PHP_GSHUTDOWN_FUNCTION(symfony_debug) @@ -197,11 +251,16 @@ PHP_GSHUTDOWN_FUNCTION(symfony_debug) PHP_MINIT_FUNCTION(symfony_debug) { + SYMFONY_DEBUG_G(old_error_cb) = zend_error_cb; + zend_error_cb = symfony_debug_error_cb; + return SUCCESS; } PHP_MSHUTDOWN_FUNCTION(symfony_debug) { + zend_error_cb = SYMFONY_DEBUG_G(old_error_cb); + return SUCCESS; } diff --git a/src/Symfony/Component/Debug/Resources/ext/tests/001.phpt b/src/Symfony/Component/Debug/Resources/ext/tests/001.phpt index 30b25a25e2..4d41417b43 100644 --- a/src/Symfony/Component/Debug/Resources/ext/tests/001.phpt +++ b/src/Symfony/Component/Debug/Resources/ext/tests/001.phpt @@ -3,7 +3,7 @@ Test symfony_zval_info API --SKIPIF-- --FILE-- - string(32) "%s" ["object_handle"]=> - int(1) + int(%d) } array(5) { ["type"]=> @@ -112,7 +112,7 @@ array(7) { ["zval_isref"]=> bool(false) ["resource_handle"]=> - int(4) + int(%d) ["resource_type"]=> string(6) "stream" ["resource_refcount"]=> diff --git a/src/Symfony/Component/Debug/Resources/ext/tests/002.phpt b/src/Symfony/Component/Debug/Resources/ext/tests/002.phpt new file mode 100644 index 0000000000..ebe2f32d8f --- /dev/null +++ b/src/Symfony/Component/Debug/Resources/ext/tests/002.phpt @@ -0,0 +1,64 @@ +--TEST-- +Test symfony_debug_backtrace in case of fatal error +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +Fatal error: Call to undefined function notexist() in %s on line %d +Array +( + [0] => Array + ( + [function] => bt + [args] => Array + ( + ) + + ) + + [1] => Array + ( + [file] => %s + [line] => %d + [function] => foo + [args] => Array + ( + ) + + ) + + [2] => Array + ( + [file] => %s + [line] => %d + [function] => bar + [args] => Array + ( + ) + + ) + +) diff --git a/src/Symfony/Component/Debug/Resources/ext/tests/002_1.phpt b/src/Symfony/Component/Debug/Resources/ext/tests/002_1.phpt new file mode 100644 index 0000000000..4d52dbf457 --- /dev/null +++ b/src/Symfony/Component/Debug/Resources/ext/tests/002_1.phpt @@ -0,0 +1,47 @@ +--TEST-- +Test symfony_debug_backtrace in case of non fatal error +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +Array +( + [0] => Array + ( + [file] => %s + [line] => %d + [function] => bt + [args] => Array + ( + ) + + ) + + [1] => Array + ( + [file] => %s + [line] => %d + [function] => bar + [args] => Array + ( + ) + + ) + +) From 6487b6e07b0cdea332999a7378ffd5809949d164 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 1 Apr 2015 10:07:27 +0200 Subject: [PATCH 52/60] [Debug] Add debug extension to the test suite --- .travis.yml | 1 + src/Symfony/Component/Debug/phpunit.xml.dist | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 746b228fe0..6c03aaf46a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,6 +31,7 @@ before_install: - if [[ "$TRAVIS_PHP_VERSION" != *"nightly" ]]; then echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi; - if [[ "$TRAVIS_PHP_VERSION" != *"nightly" ]] && [ $(php -r "echo PHP_MINOR_VERSION;") -le 4 ]; then echo "extension = apc.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi; - if [[ "$TRAVIS_PHP_VERSION" != *"nightly" ]]; then (pecl install -f memcached-2.1.0 && echo "extension = memcache.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini) || echo "Let's continue without memcache extension"; fi; + - if [[ "$TRAVIS_PHP_VERSION" != *"nightly" ]]; then (cd src/Symfony/Component/Debug/Resources/ext && phpize && ./configure && make && echo "extension = $(pwd)/modules/symfony_debug.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini); fi; - if [[ "$TRAVIS_PHP_VERSION" != *"nightly" ]]; then php -i; fi; - sudo locale-gen fr_FR.UTF-8 && sudo update-locale # Set the COMPOSER_ROOT_VERSION to the right version according to the branch being built diff --git a/src/Symfony/Component/Debug/phpunit.xml.dist b/src/Symfony/Component/Debug/phpunit.xml.dist index 20b0313f0c..e99c4ddf44 100644 --- a/src/Symfony/Component/Debug/phpunit.xml.dist +++ b/src/Symfony/Component/Debug/phpunit.xml.dist @@ -14,6 +14,9 @@ ./Tests/ + + ./Resources/ext/tests/ + From beb88f759014e5e44a36c2da3f3157119cf93590 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 1 Apr 2015 11:34:18 +0200 Subject: [PATCH 53/60] [Debug] Use symfony_debug_backtrace() in FatalErrorException when available --- .../Debug/Exception/FatalErrorException.php | 5 ++ .../Debug/Resources/ext/tests/003.phpt | 85 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 src/Symfony/Component/Debug/Resources/ext/tests/003.phpt diff --git a/src/Symfony/Component/Debug/Exception/FatalErrorException.php b/src/Symfony/Component/Debug/Exception/FatalErrorException.php index d142051ba9..41bb97c6a9 100644 --- a/src/Symfony/Component/Debug/Exception/FatalErrorException.php +++ b/src/Symfony/Component/Debug/Exception/FatalErrorException.php @@ -69,6 +69,11 @@ class FatalErrorException extends LegacyFatalErrorException unset($frame); $trace = array_reverse($trace); + } elseif (function_exists('symfony_debug_backtrace')) { + $trace = symfony_debug_backtrace(); + if (0 < $traceOffset) { + array_splice($trace, 0, $traceOffset); + } } else { $trace = array(); } diff --git a/src/Symfony/Component/Debug/Resources/ext/tests/003.phpt b/src/Symfony/Component/Debug/Resources/ext/tests/003.phpt new file mode 100644 index 0000000000..1d464720b7 --- /dev/null +++ b/src/Symfony/Component/Debug/Resources/ext/tests/003.phpt @@ -0,0 +1,85 @@ +--TEST-- +Test ErrorHandler in case of fatal error +--SKIPIF-- + +--FILE-- +setExceptionHandler('print_r'); + +if (function_exists('xdebug_disable')) { + xdebug_disable(); +} + +bar(); +?> +--EXPECTF-- +Fatal error: Call to undefined function Symfony\Component\Debug\notexist() in %s on line %d +Symfony\Component\Debug\Exception\UndefinedFunctionException Object +( + [message:protected] => Attempted to call function "notexist" from namespace "Symfony\Component\Debug". + [string:Exception:private] => + [code:protected] => 0 + [file:protected] => - + [line:protected] => %d + [trace:Exception:private] => Array + ( + [0] => Array + ( +%A [function] => Symfony\Component\Debug\foo +%A [args] => Array + ( + ) + + ) + + [1] => Array + ( +%A [function] => Symfony\Component\Debug\bar +%A [args] => Array + ( + ) + + ) +%A + ) + + [previous:Exception:private] => + [severity:protected] => 1 +) From 5cbd9fe207f39ee1d75e00ba8b00e3a8247fd4c4 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 2 Apr 2015 10:03:58 +0200 Subject: [PATCH 54/60] [Debug] Updated CHANGELOG --- src/Symfony/Component/Debug/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Symfony/Component/Debug/CHANGELOG.md b/src/Symfony/Component/Debug/CHANGELOG.md index ff7c6af2b7..31f0de9c23 100644 --- a/src/Symfony/Component/Debug/CHANGELOG.md +++ b/src/Symfony/Component/Debug/CHANGELOG.md @@ -1,6 +1,14 @@ CHANGELOG ========= +2.7.0 +----- + +* added deprecations checking for parent interfaces/classes to DebugClassLoader +* added ZTS support to symfony_debug extension +* added symfony_debug_backtrace() to symfony_debug extension + to track the backtrace of fatal errors + 2.6.0 ----- From d851a0550a11798e0389b13f5ac94a9e0d7bb0e1 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 3 Apr 2015 17:48:01 +0200 Subject: [PATCH 55/60] [DoctrineBridge] Removed useless code --- src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php index 7b48005408..43ca6a2fe7 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php @@ -58,7 +58,7 @@ class IdReader $this->om = $om; $this->classMetadata = $classMetadata; $this->singleId = 1 === count($ids); - $this->intId = $this->singleId && 1 === count($ids) && in_array($idType, array('integer', 'smallint', 'bigint')); + $this->intId = $this->singleId && in_array($idType, array('integer', 'smallint', 'bigint')); $this->idField = current($ids); } From b470a5bfb6f9b7e484f4848c40a2f9d954400369 Mon Sep 17 00:00:00 2001 From: Abdellatif Ait boudad Date: Thu, 2 Apr 2015 19:54:16 +0100 Subject: [PATCH 56/60] [Translation][Profiler] added the number of times a translation has been used. --- .../views/Collector/translation.html.twig | 28 ++++-- .../TranslationDataCollector.php | 24 +++-- .../TranslationDataCollectorTest.php | 99 +++++++++++++++++++ 3 files changed, 138 insertions(+), 13 deletions(-) create mode 100644 src/Symfony/Component/Translation/Tests/DataCollector/TranslationDataCollectorTest.php diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig index 2a86e4648a..d446854717 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig @@ -57,12 +57,23 @@ {% endblock %} {% block panelContent %} -

    Called Translations

    -
      -
    • Defined messages: {{ collector.countdefines }}
    • -
    • Fallback messages: {{ collector.countFallbacks }}
    • -
    • Missing messages: {{ collector.countMissings }}
    • -
    +

    Translation Stats

    + + + + + + + + + + + + + + + +
    Defined messages
    {{ collector.countdefines }}
    Fallback messages
    {{ collector.countFallbacks }}
    Missing messages
    {{ collector.countMissings }}
    @@ -77,7 +88,10 @@ - + {% endfor %} diff --git a/src/Symfony/Component/Translation/DataCollector/TranslationDataCollector.php b/src/Symfony/Component/Translation/DataCollector/TranslationDataCollector.php index 928e2fe8ac..4d9e3db193 100644 --- a/src/Symfony/Component/Translation/DataCollector/TranslationDataCollector.php +++ b/src/Symfony/Component/Translation/DataCollector/TranslationDataCollector.php @@ -40,8 +40,10 @@ class TranslationDataCollector extends DataCollector implements LateDataCollecto */ public function lateCollect() { - $this->data = $this->computeCount(); - $this->data['messages'] = $this->sanitizeCollectedMessages($this->translator->getCollectedMessages()); + $messages = $this->sanitizeCollectedMessages($this->translator->getCollectedMessages()); + + $this->data = $this->computeCount($messages); + $this->data['messages'] = $messages; } /** @@ -94,13 +96,23 @@ class TranslationDataCollector extends DataCollector implements LateDataCollecto private function sanitizeCollectedMessages($messages) { foreach ($messages as $key => $message) { - $messages[$key]['translation'] = $this->sanitizeString($messages[$key]['translation']); + $messages[$key]['translation'] = $this->sanitizeString($message['translation']); } - return $messages; + return array_reduce($messages, function ($result, $message) { + $messageId = $message['locale'].$message['domain'].$message['id']; + if (!isset($result[$messageId])) { + $message['count'] = 1; + $result[$messageId] = $message; + } else { + $result[$messageId]['count']++; + } + + return $result; + }); } - private function computeCount() + private function computeCount($messages) { $count = array( DataCollectorTranslator::MESSAGE_DEFINED => 0, @@ -108,7 +120,7 @@ class TranslationDataCollector extends DataCollector implements LateDataCollecto DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK => 0, ); - foreach ($this->translator->getCollectedMessages() as $message) { + foreach ($messages as $message) { ++$count[$message['state']]; } diff --git a/src/Symfony/Component/Translation/Tests/DataCollector/TranslationDataCollectorTest.php b/src/Symfony/Component/Translation/Tests/DataCollector/TranslationDataCollectorTest.php new file mode 100644 index 0000000000..4f186086e2 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/DataCollector/TranslationDataCollectorTest.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Tests\DataCollector; + +use Symfony\Component\Translation\DataCollectorTranslator; +use Symfony\Component\Translation\DataCollector\TranslationDataCollector; + +class TranslationDataCollectorTest extends \PHPUnit_Framework_TestCase +{ + protected function setUp() + { + if (!class_exists('Symfony\Component\HttpKernel\DataCollector\DataCollector')) { + $this->markTestSkipped('The "DataCollector" is not available'); + } + } + public function testCollect() + { + $collectedMessages = array( + array( + 'id' => 'foo', + 'translation' => 'foo (en)', + 'locale' => 'en', + 'domain' => 'messages', + 'state' => DataCollectorTranslator::MESSAGE_DEFINED, + ), + array( + 'id' => 'bar', + 'translation' => 'bar (fr)', + 'locale' => 'fr', + 'domain' => 'messages', + 'state' => DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK, + ), + array( + 'id' => 'choice', + 'translation' => 'choice', + 'locale' => 'en', + 'domain' => 'messages', + 'state' => DataCollectorTranslator::MESSAGE_MISSING, + ), + array( + 'id' => 'choice', + 'translation' => 'choice', + 'locale' => 'en', + 'domain' => 'messages', + 'state' => DataCollectorTranslator::MESSAGE_MISSING, + ), + ); + $expectedMessages = array( + array( + 'id' => 'foo', + 'translation' => 'foo (en)', + 'locale' => 'en', + 'domain' => 'messages', + 'state' => DataCollectorTranslator::MESSAGE_DEFINED, + 'count' => 1, + ), + array( + 'id' => 'bar', + 'translation' => 'bar (fr)', + 'locale' => 'fr', + 'domain' => 'messages', + 'state' => DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK, + 'count' => 1, + ), + array( + 'id' => 'choice', + 'translation' => 'choice', + 'locale' => 'en', + 'domain' => 'messages', + 'state' => DataCollectorTranslator::MESSAGE_MISSING, + 'count' => 2, + ), + ); + + $translator = $this + ->getMockBuilder('Symfony\Component\Translation\DataCollectorTranslator') + ->disableOriginalConstructor() + ->getMock() + ; + $translator->expects($this->any())->method('getCollectedMessages')->will($this->returnValue($collectedMessages)); + + $dataCollector = new TranslationDataCollector($translator); + $dataCollector->lateCollect(); + + $this->assertEquals(1, $dataCollector->getCountMissings()); + $this->assertEquals(1, $dataCollector->getCountFallbacks()); + $this->assertEquals(1, $dataCollector->getCountDefines()); + $this->assertEquals($expectedMessages, array_values($dataCollector->getMessages())); + } +} From 5a33c2ca2ec64fdff79e86834009134eda7db760 Mon Sep 17 00:00:00 2001 From: Abdellatif Ait boudad Date: Wed, 1 Apr 2015 12:46:44 +0100 Subject: [PATCH 57/60] [Form][choice] added choice_translation_domain to avoid trans options. --- UPGRADE-2.7.md | 246 ++++++++++-------- .../Doctrine/Form/Type/DoctrineType.php | 1 + .../views/Form/form_div_layout.html.twig | 4 +- .../views/Form/choice_widget_options.html.php | 6 +- src/Symfony/Component/Form/CHANGELOG.md | 3 +- .../Form/Extension/Core/Type/ChoiceType.php | 17 ++ .../Form/Extension/Core/Type/CountryType.php | 1 + .../Form/Extension/Core/Type/CurrencyType.php | 1 + .../Form/Extension/Core/Type/LanguageType.php | 1 + .../Form/Extension/Core/Type/LocaleType.php | 1 + .../Form/Extension/Core/Type/TimezoneType.php | 1 + .../Tests/AbstractBootstrap3LayoutTest.php | 12 +- .../Form/Tests/AbstractLayoutTest.php | 12 +- .../Extension/Core/Type/ChoiceTypeTest.php | 45 ++++ 14 files changed, 228 insertions(+), 123 deletions(-) diff --git a/UPGRADE-2.7.md b/UPGRADE-2.7.md index cb00c115e7..c7a6590fd2 100644 --- a/UPGRADE-2.7.md +++ b/UPGRADE-2.7.md @@ -15,7 +15,7 @@ Router `foo%bar%2` which would be compiled to `$foo % $bar % 2` in 2.6 but in 2.7 you would get an error if `bar` parameter doesn't exist or unexpected result otherwise. - + Form ---- @@ -23,14 +23,14 @@ Form AbstractType or AbstractExtensionType has been deprecated in favor of overriding the new "configureOptions" method. - The method "setDefaultOptions(OptionsResolverInterface $resolver)" will + The method "setDefaultOptions(OptionsResolverInterface $resolver)" will be renamed in Symfony 3.0 to "configureOptions(OptionsResolver $resolver)". Before: ```php use Symfony\Component\OptionsResolver\OptionsResolverInterface; - + class TaskType extends AbstractType { // ... @@ -47,7 +47,7 @@ Form ```php use Symfony\Component\OptionsResolver\OptionsResolver; - + class TaskType extends AbstractType { // ... @@ -59,12 +59,12 @@ Form } } ``` - + * The "choice_list" option of ChoiceType was deprecated. You should use "choices_as_values" or "choice_loader" now. - + Before: - + ```php $form->add('status', 'choice', array( 'choice_list' => new ObjectChoiceList(array( @@ -74,9 +74,9 @@ Form )), )); ``` - + After: - + ```php $form->add('status', 'choice', array( 'choices' => array( @@ -87,13 +87,13 @@ Form 'choices_as_values' => true, )); ``` - + * You should flip the keys and values of the "choices" option in ChoiceType and set the "choices_as_values" option to `true`. The default value of that option will be switched to `true` in Symfony 3.0. - + Before: - + ```php $form->add('status', 'choice', array( 'choices' => array( @@ -103,9 +103,9 @@ Form )), )); ``` - + After: - + ```php $form->add('status', 'choice', array( 'choices' => array( @@ -116,64 +116,64 @@ Form 'choices_as_values' => true, )); ``` - + * `Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface` was deprecated and will be removed in Symfony 3.0. You should use `Symfony\Component\Form\ChoiceList\ChoiceListInterface` instead. - + Before: - + ```php use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; - + public function doSomething(ChoiceListInterface $choiceList) { // ... } ``` - + After: - + ```php use Symfony\Component\Form\ChoiceList\ChoiceListInterface; - + public function doSomething(ChoiceListInterface $choiceList) { // ... } ``` - + * `Symfony\Component\Form\Extension\Core\ChoiceList\View\ChoiceView` was deprecated and will be removed in Symfony 3.0. You should use `Symfony\Component\Form\ChoiceList\View\ChoiceView` instead. - + Note that the order of the arguments passed to the constructor was inverted. - + Before: - + ```php use Symfony\Component\Form\Extension\Core\ChoiceList\View\ChoiceView; - + $view = new ChoiceView($data, 'value', 'Label'); ``` - + After: - + ```php use Symfony\Component\Form\ChoiceList\View\ChoiceView; - + $view = new ChoiceView('Label', 'value', $data); ``` - + * `Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList` was deprecated and will be removed in Symfony 3.0. You should use `Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory` instead. - + Before: - + ```php use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; - + $choiceList = new ChoiceList( array(Status::ENABLED, Status::DISABLED, Status::IGNORED), array('Enabled', 'Disabled', 'Ignored'), @@ -181,19 +181,19 @@ Form array(Status::ENABLED), ); ``` - + After: - + ```php use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; - + $factory = new DefaultChoiceListFactory(); - + $choices = array(Status::ENABLED, Status::DISABLED, Status::IGNORED); $labels = array('Enabled', 'Disabled', 'Ignored'); - + $choiceList = $factory->createListFromChoices($choices); - + $choiceListView = $factory->createView( $choiceList, // Preferred choices @@ -204,75 +204,75 @@ Form } ); ``` - + * `Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList` was deprecated and will be removed in Symfony 3.0. You should use `Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory::createListFromLoader()` - together with an implementation of + together with an implementation of `Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface` instead. - + Before: - + ```php use Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList; - + class MyLazyChoiceList extends LazyChoiceList { public function loadChoiceList() { // load $choiceList - + return $choiceList; } } - + $choiceList = new MyLazyChoiceList(); ``` - + After: - + ```php use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; - + class MyChoiceLoader implements ChoiceLoaderInterface { // ... } - + $factory = new DefaultChoiceListFactory(); - + $choiceList = $factory->createListFromLoader(new MyChoiceLoader()); ``` - + * `Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList` was deprecated and will be removed in Symfony 3.0. You should use `Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory` instead. - + Before: - + ```php use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; - + $choiceList = new ObjectChoiceList( array(Status::getInstance(Status::ENABLED), Status::getInstance(Status::DISABLED)), // Label property 'name' ); ``` - + After: - + ```php use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; - + $factory = new DefaultChoiceListFactory(); - + $choiceList = $factory->createListFromChoices(array( - Status::getInstance(Status::ENABLED), - Status::getInstance(Status::DISABLED), + Status::getInstance(Status::ENABLED), + Status::getInstance(Status::DISABLED), )); - + $choiceListView = $factory->createView( $choiceList, // Preferred choices @@ -281,34 +281,34 @@ Form 'name' ); ``` - + * `Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList` was deprecated and will be removed in Symfony 3.0. You should use `Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory` instead. - + Before: - + ```php use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; - + $choiceList = new SimpleChoiceList(array( Status::ENABLED => 'Enabled', Status::DISABLED => 'Disabled', )); ``` - + After: - + ```php use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; - + $factory = new DefaultChoiceListFactory(); - + $choices = array(Status::ENABLED, Status::DISABLED); $labels = array('Enabled', 'Disabled'); - + $choiceList = $factory->createListFromChoices($choices); - + $choiceListView = $factory->createView( $choiceList, // Preferred choices @@ -319,116 +319,152 @@ Form } ); ``` - + * The "property" option of `DoctrineType` was deprecated. You should use the new inherited option "choice_label" instead, which has the same effect. - + Before: - + ```php $form->add('tags', 'entity', array( 'class' => 'Acme\Entity\MyTag', 'property' => 'name', )) ``` - + After: - + ```php $form->add('tags', 'entity', array( 'class' => 'Acme\Entity\MyTag', 'choice_label' => 'name', )) ``` - + * The "loader" option of `DoctrineType` was deprecated and will be removed in Symfony 3.0. You should override the `getLoader()` method instead in a custom type. - + Before: - + ```php $form->add('tags', 'entity', array( 'class' => 'Acme\Entity\MyTag', 'loader' => new MyEntityLoader(), )) ``` - + After: - + class MyEntityType extends DoctrineType { // ... - + public function getLoader() { return new MyEntityLoader(); } } - + * `Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList` was deprecated and will be removed in Symfony 3.0. You should use `Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader` instead. - + Before: - + ```php use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; - + $choiceList = new EntityChoiceList($em, 'Acme\Entity\MyEntity'); ``` - + After: - + ```php use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; - + $factory = new DefaultChoiceListFactory(); - + $choices = array(Status::ENABLED, Status::DISABLED); $labels = array('Enabled', 'Disabled'); - + $choiceLoader = new DoctrineChoiceLoader($factory, $em, 'Acme\Entity\MyEntity'); $choiceList = $factory->createListFromLoader($choiceLoader); ``` - + * Passing a query builder closure to `ORMQueryBuilderLoader` was deprecated and will not be supported anymore in Symfony 3.0. You should pass resolved query builders only. - + Consequently, the arguments `$manager` and `$class` of `ORMQueryBuilderLoader` have been deprecated as well. - + Note that the "query_builder" option of `DoctrineType` *does* support closures, but the closure is now resolved in the type instead of in the loader. - + Before: - + ``` use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; - + $queryBuilder = function () { // return QueryBuilder }; $loader = new ORMQueryBuilderLoader($queryBuilder); ``` - + After: - + ``` use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; - + // create $queryBuilder $loader = new ORMQueryBuilderLoader($queryBuilder); ``` - - * The classes `ChoiceToBooleanArrayTransformer`, + + * The classes `ChoiceToBooleanArrayTransformer`, `ChoicesToBooleanArrayTransformer`, `FixRadioInputListener` and - `FixCheckboxInputListener` were deprecated and will be removed in Symfony 3.0. - Their functionality is covered by the new classes `RadioListMapper` and + `FixCheckboxInputListener` were deprecated and will be removed in Symfony 3.0. + Their functionality is covered by the new classes `RadioListMapper` and `CheckboxListMapper`. + * The ability to translate Doctrine type entries by the translator component + is now disabled by default and to enable it you must explicitly set the option + "choice_translation_domain" to true + + Before: + + ``` + $form->add('products', 'entity', array( + 'class' => 'AppBundle/Entity/Product', + )); + ``` + + After: + + ``` + $form->add('products', 'entity', array( + 'class' => 'AppBundle/Entity/Product', + 'choice_translation_domain' => true, + )); + ``` + + * In the block `choice_widget_options` the `translation_domain` has been replaced + with the `choice_translation_domain` option. + + Before: + + ```jinja + {{ choice.label|trans({}, translation_domain) }} + ``` + + After: + + ```jinja + {{ choice_translation_domain is sameas(false) ? choice.label : choice.label|trans({}, choice_translation_domain) }} + ``` + Serializer ---------- diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index a478574190..acc6d52a92 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -292,6 +292,7 @@ abstract class DoctrineType extends AbstractType 'choice_name' => $choiceName, 'choice_value' => $choiceValue, 'id_reader' => null, // internal + 'choice_translation_domain' => false, )); $resolver->setRequired(array('class')); diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index df0e571602..de54d49c30 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig @@ -74,13 +74,13 @@ {%- block choice_widget_options -%} {% for group_label, choice in options %} {%- if choice is iterable -%} - + {% set options = choice %} {{- block('choice_widget_options') -}} {%- else -%} {% set attr = choice.attr %} - + {%- endif -%} {% endfor %} {%- endblock choice_widget_options -%} diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php index 81402efffb..a0363cc5a4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php @@ -2,12 +2,12 @@ $translatorHelper = $view['translator']; // outside of the loop for performance reasons! ?> - $choice): ?> + $choice): ?> - + block($form, 'choice_widget_options', array('choices' => $choice)) ?> - + diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 0ddc660b21..b1387bb2e6 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 2.7.0 ----- + * added option "choice_translation_domain" to ChoiceType. * deprecated option "precision" in favor of "scale" * deprecated the overwriting of AbstractType::setDefaultOptions() in favor of overwriting AbstractType::configureOptions(). * deprecated the overwriting of AbstractTypeExtension::setDefaultOptions() in favor of overwriting AbstractTypeExtension::configureOptions(). @@ -16,7 +17,7 @@ CHANGELOG * deprecated ChoiceToBooleanArrayTransformer and ChoicesToBooleanArrayTransformer * deprecated FixCheckboxInputListener and FixRadioInputListener * deprecated the "choice_list" option of ChoiceType - * added new options to ChoiceType: + * added new options to ChoiceType: * "choices_as_values" * "choice_loader" * "choice_label" diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index a597e1d4f4..09d81ee7af 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -158,6 +158,11 @@ class ChoiceType extends AbstractType */ public function buildView(FormView $view, FormInterface $form, array $options) { + $choiceTranslationDomain = $options['choice_translation_domain']; + if ($view->parent && null === $choiceTranslationDomain) { + $choiceTranslationDomain = $view->vars['translation_domain']; + } + /** @var ChoiceListView $choiceListView */ $choiceListView = $form->getConfig()->hasAttribute('choice_list_view') ? $form->getConfig()->getAttribute('choice_list_view') @@ -170,6 +175,7 @@ class ChoiceType extends AbstractType 'choices' => $choiceListView->choices, 'separator' => '-------------------', 'placeholder' => null, + 'choice_translation_domain' => $choiceTranslationDomain, )); // The decision, whether a choice is selected, is potentially done @@ -295,6 +301,14 @@ class ChoiceType extends AbstractType return $options['expanded']; }; + $choiceTranslationDomainNormalizer = function (Options $options, $choiceTranslationDomain) { + if (true === $choiceTranslationDomain) { + return $options['translation_domain']; + } + + return $choiceTranslationDomain; + }; + $resolver->setDefaults(array( 'multiple' => false, 'expanded' => false, @@ -317,14 +331,17 @@ class ChoiceType extends AbstractType // is manually set to an object. // See https://github.com/symfony/symfony/pull/5582 'data_class' => null, + 'choice_translation_domain' => true, )); $resolver->setNormalizer('choice_list', $choiceListNormalizer); $resolver->setNormalizer('empty_value', $placeholderNormalizer); $resolver->setNormalizer('placeholder', $placeholderNormalizer); + $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer); $resolver->setAllowedTypes('choice_list', array('null', 'Symfony\Component\Form\ChoiceList\ChoiceListInterface')); $resolver->setAllowedTypes('choices', array('null', 'array', '\Traversable')); + $resolver->setAllowedTypes('choice_translation_domain', array('null', 'bool', 'string')); $resolver->setAllowedTypes('choices_as_values', 'bool'); $resolver->setAllowedTypes('choice_loader', array('null', 'Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface')); $resolver->setAllowedTypes('choice_label', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath')); diff --git a/src/Symfony/Component/Form/Extension/Core/Type/CountryType.php b/src/Symfony/Component/Form/Extension/Core/Type/CountryType.php index e6231c596b..30ee0a0f9e 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/CountryType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/CountryType.php @@ -24,6 +24,7 @@ class CountryType extends AbstractType { $resolver->setDefaults(array( 'choices' => Intl::getRegionBundle()->getCountryNames(), + 'choice_translation_domain' => false, )); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/CurrencyType.php b/src/Symfony/Component/Form/Extension/Core/Type/CurrencyType.php index 9d77b76381..b473d139e6 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/CurrencyType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/CurrencyType.php @@ -24,6 +24,7 @@ class CurrencyType extends AbstractType { $resolver->setDefaults(array( 'choices' => Intl::getCurrencyBundle()->getCurrencyNames(), + 'choice_translation_domain' => false, )); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/LanguageType.php b/src/Symfony/Component/Form/Extension/Core/Type/LanguageType.php index 4bd09d26e9..9d071eb8b0 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/LanguageType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/LanguageType.php @@ -24,6 +24,7 @@ class LanguageType extends AbstractType { $resolver->setDefaults(array( 'choices' => Intl::getLanguageBundle()->getLanguageNames(), + 'choice_translation_domain' => false, )); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/LocaleType.php b/src/Symfony/Component/Form/Extension/Core/Type/LocaleType.php index 92a41080e4..f09f5a62f1 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/LocaleType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/LocaleType.php @@ -24,6 +24,7 @@ class LocaleType extends AbstractType { $resolver->setDefaults(array( 'choices' => Intl::getLocaleBundle()->getLocaleNames(), + 'choice_translation_domain' => false, )); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php b/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php index d19fb52fdc..82c07e2f12 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php @@ -30,6 +30,7 @@ class TimezoneType extends AbstractType { $resolver->setDefaults(array( 'choices' => self::getTimezones(), + 'choice_translation_domain' => false, )); } diff --git a/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php index 6cc7edd7a3..28f576890b 100644 --- a/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php @@ -840,7 +840,7 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest '/select [@name="name"] [@class="my&class form-control"] - [./option[@value="AT"][@selected="selected"][.="[trans]Austria[/trans]"]] + [./option[@value="AT"][@selected="selected"][.="Austria"]] [count(./option)>200] ' ); @@ -858,7 +858,7 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest [@name="name"] [@class="my&class form-control"] [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Select&Country[/trans]"]] - [./option[@value="AT"][@selected="selected"][.="[trans]Austria[/trans]"]] + [./option[@value="AT"][@selected="selected"][.="Austria"]] [count(./option)>201] ' ); @@ -1388,7 +1388,7 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest '/select [@name="name"] [@class="my&class form-control"] - [./option[@value="de"][@selected="selected"][.="[trans]German[/trans]"]] + [./option[@value="de"][@selected="selected"][.="German"]] [count(./option)>200] ' ); @@ -1402,7 +1402,7 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest '/select [@name="name"] [@class="my&class form-control"] - [./option[@value="de_AT"][@selected="selected"][.="[trans]German (Austria)[/trans]"]] + [./option[@value="de_AT"][@selected="selected"][.="German (Austria)"]] [count(./option)>200] ' ); @@ -1826,8 +1826,8 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest [@class="my&class form-control"] [not(@required)] [./optgroup - [@label="[trans]Europe[/trans]"] - [./option[@value="Europe/Vienna"][@selected="selected"][.="[trans]Vienna[/trans]"]] + [@label="Europe"] + [./option[@value="Europe/Vienna"][@selected="selected"][.="Vienna"]] ] [count(./optgroup)>10] [count(.//option)>200] diff --git a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php index 3bf84d71c8..5660d2769d 100644 --- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php @@ -1016,7 +1016,7 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg $this->assertWidgetMatchesXpath($form->createView(), array(), '/select [@name="name"] - [./option[@value="AT"][@selected="selected"][.="[trans]Austria[/trans]"]] + [./option[@value="AT"][@selected="selected"][.="Austria"]] [count(./option)>200] ' ); @@ -1033,7 +1033,7 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg '/select [@name="name"] [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Select&Country[/trans]"]] - [./option[@value="AT"][@selected="selected"][.="[trans]Austria[/trans]"]] + [./option[@value="AT"][@selected="selected"][.="Austria"]] [count(./option)>201] ' ); @@ -1557,7 +1557,7 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg $this->assertWidgetMatchesXpath($form->createView(), array(), '/select [@name="name"] - [./option[@value="de"][@selected="selected"][.="[trans]German[/trans]"]] + [./option[@value="de"][@selected="selected"][.="German"]] [count(./option)>200] ' ); @@ -1570,7 +1570,7 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg $this->assertWidgetMatchesXpath($form->createView(), array(), '/select [@name="name"] - [./option[@value="de_AT"][@selected="selected"][.="[trans]German (Austria)[/trans]"]] + [./option[@value="de_AT"][@selected="selected"][.="German (Austria)"]] [count(./option)>200] ' ); @@ -1936,8 +1936,8 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg [@name="name"] [not(@required)] [./optgroup - [@label="[trans]Europe[/trans]"] - [./option[@value="Europe/Vienna"][@selected="selected"][.="[trans]Vienna[/trans]"]] + [@label="Europe"] + [./option[@value="Europe/Vienna"][@selected="selected"][.="Vienna"]] ] [count(./optgroup)>10] [count(.//option)>200] diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php index d34d5b2184..791df12d85 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -1331,6 +1331,51 @@ class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertTrue($view->vars['expanded']); } + public function testPassChoiceTranslationDomainToView() + { + $form = $this->factory->create('choice', null, array( + 'choices' => $this->choices, + )); + $view = $form->createView(); + + $this->assertNull($view->vars['choice_translation_domain']); + } + + public function testChoiceTranslationDomainWithTrueValueToView() + { + $form = $this->factory->create('choice', null, array( + 'choices' => $this->choices, + 'choice_translation_domain' => true, + )); + $view = $form->createView(); + + $this->assertNull($view->vars['choice_translation_domain']); + } + + public function testDefaulChoiceTranslationDomainIsSameAsTranslationDomainToView() + { + $form = $this->factory->create('choice', null, array( + 'choices' => $this->choices, + 'translation_domain' => 'foo', + )); + $view = $form->createView(); + + $this->assertEquals('foo', $view->vars['choice_translation_domain']); + } + + public function testInheritChoiceTranslationDomainFromParent() + { + $view = $this->factory + ->createNamedBuilder('parent', 'form', null, array( + 'translation_domain' => 'domain', + )) + ->add('child', 'choice') + ->getForm() + ->createView(); + + $this->assertEquals('domain', $view['child']->vars['choice_translation_domain']); + } + public function testPlaceholderIsNullByDefaultIfRequired() { $form = $this->factory->create('choice', null, array( From 1122160d9ed4c75fa490c54c7707166da771a42e Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Fri, 3 Apr 2015 22:48:09 +0200 Subject: [PATCH 58/60] Fix currently broken tests --- src/Symfony/Component/Console/Tester/CommandTester.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Console/Tester/CommandTester.php b/src/Symfony/Component/Console/Tester/CommandTester.php index a6d5359452..8d6486e11c 100644 --- a/src/Symfony/Component/Console/Tester/CommandTester.php +++ b/src/Symfony/Component/Console/Tester/CommandTester.php @@ -61,7 +61,7 @@ class CommandTester && (null !== $application = $this->command->getApplication()) && $application->getDefinition()->hasArgument('command') ) { - $input['command'] = $this->command->getName(); + $input = array_merge(array('command' => $this->command->getName()), $input); } $this->input = new ArrayInput($input); From df484dadfcdca1927869f9566fe23829d002f318 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 3 Apr 2015 19:35:00 +0200 Subject: [PATCH 59/60] [VarDumper] Towards PHP7 support --- .../VarDumper/Caster/ExceptionCaster.php | 32 +++++++++---------- .../Component/VarDumper/Cloner/VarCloner.php | 6 ++-- .../Exception/ThrowingCasterException.php | 2 +- .../VarDumper/Tests/CliDumperTest.php | 6 ++-- .../VarDumper/Tests/HtmlDumperTest.php | 3 +- 5 files changed, 22 insertions(+), 27 deletions(-) diff --git a/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php b/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php index 7938da63e5..7597c2d7fc 100644 --- a/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php @@ -42,18 +42,19 @@ class ExceptionCaster public static function castException(\Exception $e, array $a, Stub $stub, $isNested) { - $trace = $a["\0Exception\0trace"]; - unset($a["\0Exception\0trace"]); // Ensures the trace is always last + $xPrefix = PHP_VERSION_ID >= 70000 ? "\0BaseException\0" : "\0Exception\0"; + $trace = $a[$xPrefix.'trace']; + unset($a[$xPrefix.'trace']); // Ensures the trace is always last static::filterTrace($trace, static::$traceArgs); if (null !== $trace) { - $a["\0Exception\0trace"] = $trace; + $a[$xPrefix.'trace'] = $trace; } - if (empty($a["\0Exception\0previous"])) { - unset($a["\0Exception\0previous"]); + if (empty($a[$xPrefix.'previous'])) { + unset($a[$xPrefix.'previous']); } - unset($a["\0Exception\0string"], $a["\0+\0xdebug_message"], $a["\0+\0__destructorException"]); + unset($a[$xPrefix.'string'], $a["\0+\0xdebug_message"], $a["\0+\0__destructorException"]); return $a; } @@ -69,23 +70,20 @@ class ExceptionCaster public static function castThrowingCasterException(ThrowingCasterException $e, array $a, Stub $stub, $isNested) { - $b = (array) $a["\0Exception\0previous"]; + $xPrefix = PHP_VERSION_ID >= 70000 ? "\0BaseException\0" : "\0Exception\0"; + $b = (array) $a[$xPrefix.'previous']; - if (isset($b["\0*\0message"])) { - $a["\0~\0message"] = $b["\0*\0message"]; - } - - if (isset($a["\0Exception\0trace"])) { - $b["\0Exception\0trace"][0] += array( + if (isset($a[$xPrefix.'trace'][0])) { + $b[$xPrefix.'trace'][0] += array( 'file' => $b["\0*\0file"], 'line' => $b["\0*\0line"], ); - array_splice($b["\0Exception\0trace"], -1 - count($a["\0Exception\0trace"])); - static::filterTrace($b["\0Exception\0trace"], false); - $a["\0~\0trace"] = $b["\0Exception\0trace"]; + array_splice($b[$xPrefix.'trace'], -1 - count($a[$xPrefix.'trace'])); + static::filterTrace($b[$xPrefix.'trace'], false); + $a["\0~\0trace"] = $b[$xPrefix.'trace']; } - unset($a["\0Exception\0trace"], $a["\0Exception\0previous"], $a["\0*\0code"], $a["\0*\0file"], $a["\0*\0line"]); + unset($a[$xPrefix.'trace'], $a[$xPrefix.'previous'], $a["\0*\0code"], $a["\0*\0file"], $a["\0*\0line"]); return $a; } diff --git a/src/Symfony/Component/VarDumper/Cloner/VarCloner.php b/src/Symfony/Component/VarDumper/Cloner/VarCloner.php index dd40512819..4699e19182 100644 --- a/src/Symfony/Component/VarDumper/Cloner/VarCloner.php +++ b/src/Symfony/Component/VarDumper/Cloner/VarCloner.php @@ -119,12 +119,10 @@ class VarCloner extends AbstractCloner $stub = $arrayRefs[$len] = new Stub(); $stub->type = Stub::TYPE_ARRAY; $stub->class = Stub::ARRAY_ASSOC; - $stub->value = $zval['array_count'] ?: count($v); - - $a = $v; // Copies of $GLOBALS have very strange behavior, // let's detect them with some black magic + $a = $v; $a[$gid] = true; // Happens with copies of $GLOBALS @@ -137,6 +135,8 @@ class VarCloner extends AbstractCloner } else { $a = $v; } + + $stub->value = $zval['array_count'] ?: count($a); } break; diff --git a/src/Symfony/Component/VarDumper/Exception/ThrowingCasterException.php b/src/Symfony/Component/VarDumper/Exception/ThrowingCasterException.php index 1ab2f26ed7..ce2ab4d4b4 100644 --- a/src/Symfony/Component/VarDumper/Exception/ThrowingCasterException.php +++ b/src/Symfony/Component/VarDumper/Exception/ThrowingCasterException.php @@ -22,6 +22,6 @@ class ThrowingCasterException extends \Exception */ public function __construct($caster, \Exception $prev) { - parent::__construct('Unexpected exception thrown from a caster: '.get_class($prev), 0, $prev); + parent::__construct('Unexpected '.get_class($prev).' thrown from a caster: '.$prev->getMessage(), 0, $prev); } } diff --git a/src/Symfony/Component/VarDumper/Tests/CliDumperTest.php b/src/Symfony/Component/VarDumper/Tests/CliDumperTest.php index 05cff61088..62d2a99612 100644 --- a/src/Symfony/Component/VarDumper/Tests/CliDumperTest.php +++ b/src/Symfony/Component/VarDumper/Tests/CliDumperTest.php @@ -38,7 +38,6 @@ class CliDumperTest extends \PHPUnit_Framework_TestCase ob_start(); $dumper->dump($data); $out = ob_get_clean(); - $closureLabel = PHP_VERSION_ID >= 50400 ? 'public method' : 'function'; $out = preg_replace('/[ \t]+$/m', '', $out); $intMax = PHP_INT_MAX; $res1 = (int) $var['res']; @@ -77,7 +76,7 @@ array:25 [ } "closure" => Closure {#%d reflection: """ - Closure [ {$closureLabel} Symfony\Component\VarDumper\Tests\Fixture\{closure} ] { + Closure [ %s Symfony\Component\VarDumper\Tests\Fixture\{closure} ] { @@ {$var['file']} {$var['line']} - {$var['line']} - Parameters [2] { @@ -143,8 +142,7 @@ EOTXT eof: false options: [] ⚠: Symfony\Component\VarDumper\Exception\ThrowingCasterException {#%d - #message: "Unexpected exception thrown from a caster: Exception" - message: "Foobar" + #message: "Unexpected Exception thrown from a caster: Foobar" trace: array:1 [ 0 => array:2 [ "call" => "%s{closure}()" diff --git a/src/Symfony/Component/VarDumper/Tests/HtmlDumperTest.php b/src/Symfony/Component/VarDumper/Tests/HtmlDumperTest.php index 22083b5161..0cc9d106b8 100644 --- a/src/Symfony/Component/VarDumper/Tests/HtmlDumperTest.php +++ b/src/Symfony/Component/VarDumper/Tests/HtmlDumperTest.php @@ -39,7 +39,6 @@ class HtmlDumperTest extends \PHPUnit_Framework_TestCase ob_start(); $dumper->dump($data); $out = ob_get_clean(); - $closureLabel = PHP_VERSION_ID >= 50400 ? 'public method' : 'function'; $out = preg_replace('/[ \t]+$/m', '', $out); $var['file'] = htmlspecialchars($var['file'], ENT_QUOTES, 'UTF-8'); $intMax = PHP_INT_MAX; @@ -81,7 +80,7 @@ class HtmlDumperTest extends \PHPUnit_Framework_TestCase } "closure" => Closure {#%d reflection: """ - Closure [ <user> {$closureLabel} Symfony\Component\VarDumper\Tests\Fixture\{closure} ] { + Closure [ <user%S> %s Symfony\Component\VarDumper\Tests\Fixture\{closure} ] { @@ {$var['file']} {$var['line']} - {$var['line']} - Parameters [2] { From e965e5992060561b9279314417fc519343a1b4a2 Mon Sep 17 00:00:00 2001 From: Abdellatif Ait boudad Date: Sat, 4 Apr 2015 17:28:09 +0100 Subject: [PATCH 60/60] [Translation][Profiler] fixed Collect empty Messages. --- .../TranslationDataCollector.php | 2 +- .../TranslationDataCollectorTest.php | 32 ++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Translation/DataCollector/TranslationDataCollector.php b/src/Symfony/Component/Translation/DataCollector/TranslationDataCollector.php index 4d9e3db193..4e3f54b608 100644 --- a/src/Symfony/Component/Translation/DataCollector/TranslationDataCollector.php +++ b/src/Symfony/Component/Translation/DataCollector/TranslationDataCollector.php @@ -109,7 +109,7 @@ class TranslationDataCollector extends DataCollector implements LateDataCollecto } return $result; - }); + }, array()); } private function computeCount($messages) diff --git a/src/Symfony/Component/Translation/Tests/DataCollector/TranslationDataCollectorTest.php b/src/Symfony/Component/Translation/Tests/DataCollector/TranslationDataCollectorTest.php index 4f186086e2..085d31267b 100644 --- a/src/Symfony/Component/Translation/Tests/DataCollector/TranslationDataCollectorTest.php +++ b/src/Symfony/Component/Translation/Tests/DataCollector/TranslationDataCollectorTest.php @@ -22,6 +22,21 @@ class TranslationDataCollectorTest extends \PHPUnit_Framework_TestCase $this->markTestSkipped('The "DataCollector" is not available'); } } + + public function testCollectEmptyMessages() + { + $translator = $this->getTranslator(); + $translator->expects($this->any())->method('getCollectedMessages')->will($this->returnValue(array())); + + $dataCollector = new TranslationDataCollector($translator); + $dataCollector->lateCollect(); + + $this->assertEquals(0, $dataCollector->getCountMissings()); + $this->assertEquals(0, $dataCollector->getCountFallbacks()); + $this->assertEquals(0, $dataCollector->getCountDefines()); + $this->assertEquals(array(), $dataCollector->getMessages()); + } + public function testCollect() { $collectedMessages = array( @@ -81,11 +96,7 @@ class TranslationDataCollectorTest extends \PHPUnit_Framework_TestCase ), ); - $translator = $this - ->getMockBuilder('Symfony\Component\Translation\DataCollectorTranslator') - ->disableOriginalConstructor() - ->getMock() - ; + $translator = $this->getTranslator(); $translator->expects($this->any())->method('getCollectedMessages')->will($this->returnValue($collectedMessages)); $dataCollector = new TranslationDataCollector($translator); @@ -96,4 +107,15 @@ class TranslationDataCollectorTest extends \PHPUnit_Framework_TestCase $this->assertEquals(1, $dataCollector->getCountDefines()); $this->assertEquals($expectedMessages, array_values($dataCollector->getMessages())); } + + private function getTranslator() + { + $translator = $this + ->getMockBuilder('Symfony\Component\Translation\DataCollectorTranslator') + ->disableOriginalConstructor() + ->getMock() + ; + + return $translator; + } }
    {{ translator.state(message) }} {{ message.locale }} {{ message.domain }}{{ message.id }} + {{ message.id }} + {% if message.count > 1 %}
    (used {{ message.count }} times){% endif %} +
    {{ message.translation }}