diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php index 8746998468..d09de2d615 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\TwigBundle\DependencyInjection\Compiler; +use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -73,7 +74,7 @@ class ExtensionPass implements CompilerPassInterface $loader->addTag('twig.loader'); $loader->setMethodCalls($container->getDefinition('twig.loader.filesystem')->getMethodCalls()); - $container->setDefinition('twig.loader.filesystem', $loader); + $container->setAlias('twig.loader.filesystem', new Alias('twig.loader.native_filesystem', false)); } if ($container->has('assets.packages')) { diff --git a/src/Symfony/Component/Console/Descriptor/TextDescriptor.php b/src/Symfony/Component/Console/Descriptor/TextDescriptor.php index 0d722ed5aa..eb02c89dfa 100644 --- a/src/Symfony/Component/Console/Descriptor/TextDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/TextDescriptor.php @@ -38,13 +38,13 @@ class TextDescriptor extends Descriptor } $totalWidth = isset($options['total_width']) ? $options['total_width'] : strlen($argument->getName()); - $spacingWidth = $totalWidth - strlen($argument->getName()) + 2; + $spacingWidth = $totalWidth - strlen($argument->getName()); - $this->writeText(sprintf(' %s%s%s%s', + $this->writeText(sprintf(' %s %s%s%s', $argument->getName(), str_repeat(' ', $spacingWidth), - // + 17 = 2 spaces + + + 2 spaces - preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 17), $argument->getDescription()), + // + 4 = 2 spaces before , 2 spaces after + preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $argument->getDescription()), $default ), $options); } @@ -75,13 +75,13 @@ class TextDescriptor extends Descriptor sprintf('--%s%s', $option->getName(), $value) ); - $spacingWidth = $totalWidth - strlen($synopsis) + 2; + $spacingWidth = $totalWidth - strlen($synopsis); - $this->writeText(sprintf(' %s%s%s%s%s', + $this->writeText(sprintf(' %s %s%s%s%s', $synopsis, str_repeat(' ', $spacingWidth), - // + 17 = 2 spaces + + + 2 spaces - preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 17), $option->getDescription()), + // + 4 = 2 spaces before , 2 spaces after + preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $option->getDescription()), $default, $option->isArray() ? ' (multiple values allowed)' : '' ), $options); diff --git a/src/Symfony/Component/Console/Tests/Fixtures/input_argument_4.txt b/src/Symfony/Component/Console/Tests/Fixtures/input_argument_4.txt index aa74e8ceb2..fc7d669a11 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/input_argument_4.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/input_argument_4.txt @@ -1,2 +1,2 @@ argument_name multiline - argument description + argument description diff --git a/src/Symfony/Component/Console/Tests/Fixtures/input_option_5.txt b/src/Symfony/Component/Console/Tests/Fixtures/input_option_5.txt index 4368883cc7..9563b4cab1 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/input_option_5.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/input_option_5.txt @@ -1,2 +1,2 @@ -o, --option_name=OPTION_NAME multiline - option description + option description diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckCircularReferencesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckCircularReferencesPass.php index d7570ddc2c..156bcc0c3a 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckCircularReferencesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckCircularReferencesPass.php @@ -60,15 +60,19 @@ class CheckCircularReferencesPass implements CompilerPassInterface $id = $node->getId(); if (empty($this->checkedNodes[$id])) { - $searchKey = array_search($id, $this->currentPath); - $this->currentPath[] = $id; - if (false !== $searchKey) { - throw new ServiceCircularReferenceException($id, array_slice($this->currentPath, $searchKey)); + // don't check circular dependencies for lazy services + if (!$node->getValue() || !$node->getValue()->isLazy()) { + $searchKey = array_search($id, $this->currentPath); + $this->currentPath[] = $id; + + if (false !== $searchKey) { + throw new ServiceCircularReferenceException($id, array_slice($this->currentPath, $searchKey)); + } + + $this->checkOutEdges($node->getOutEdges()); } - $this->checkOutEdges($node->getOutEdges()); - $this->checkedNodes[$id] = true; array_pop($this->currentPath); } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 96d5b4dfd3..2ef29aa3cf 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -1313,6 +1313,13 @@ EOF; $visited[$argumentId] = true; $service = $this->container->getDefinition($argumentId); + + // if the proxy manager is enabled, disable searching for references in lazy services, + // as these services will be instantiated lazily and don't have direct related references. + if ($service->isLazy() && !$this->getProxyDumper() instanceof NullDumper) { + continue; + } + $arguments = array_merge($service->getMethodCalls(), $service->getArguments(), $service->getProperties()); if ($this->hasReference($id, $arguments, $deep, $visited)) { diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 5c49f1755f..18fd740a9a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Dumper; +use DummyProxyDumper; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Dumper\PhpDumper; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; @@ -19,6 +20,8 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Variable; use Symfony\Component\ExpressionLanguage\Expression; +require_once __DIR__.'/../Fixtures/includes/classes.php'; + class PhpDumperTest extends \PHPUnit_Framework_TestCase { protected static $fixturesPath; @@ -294,4 +297,52 @@ class PhpDumperTest extends \PHPUnit_Framework_TestCase $container = new \Symfony_DI_PhpDumper_Test_Properties_Before_Method_Calls(); $this->assertTrue($container->get('bar')->callPassed(), '->dump() initializes properties before method calls'); } + + public function testCircularReferenceAllowanceForLazyServices() + { + $container = new ContainerBuilder(); + $container->register('foo', 'stdClass')->addArgument(new Reference('bar')); + $container->register('bar', 'stdClass')->setLazy(true)->addArgument(new Reference('foo')); + $container->compile(); + + $dumper = new PhpDumper($container); + $dumper->dump(); + } + + public function testCircularReferenceAllowanceForInlinedDefinitionsForLazyServices() + { + /* + * test graph: + * [connection] -> [event_manager] --> [entity_manager](lazy) + * | + * --(call)- addEventListener ("@lazy_service") + * + * [lazy_service](lazy) -> [entity_manager](lazy) + * + */ + + $container = new ContainerBuilder(); + + $eventManagerDefinition = new Definition('stdClass'); + + $connectionDefinition = $container->register('connection', 'stdClass'); + $connectionDefinition->addArgument($eventManagerDefinition); + + $container->register('entity_manager', 'stdClass') + ->setLazy(true) + ->addArgument(new Reference('connection')); + + $lazyServiceDefinition = $container->register('lazy_service', 'stdClass'); + $lazyServiceDefinition->setLazy(true); + $lazyServiceDefinition->addArgument(new Reference('entity_manager')); + + $eventManagerDefinition->addMethodCall('addEventListener', array(new Reference('lazy_service'))); + + $container->compile(); + + $dumper = new PhpDumper($container); + + $dumper->setProxyDumper(new DummyProxyDumper()); + $dumper->dump(); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php index 48b687c1f4..0ecdea3fbf 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php @@ -1,5 +1,8 @@ configure(); @@ -76,3 +79,21 @@ class MethodCallClass return $this->callPassed; } } + +class DummyProxyDumper implements ProxyDumper +{ + public function isProxyCandidate(Definition $definition) + { + return false; + } + + public function getProxyFactoryCode(Definition $definition, $id) + { + return ''; + } + + public function getProxyCode(Definition $definition) + { + return ''; + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index 25ebbd35d8..526bfdb6b9 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -39,6 +39,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class ChoiceType extends AbstractType { + /** + * @internal To be removed in 3.0 + */ + const DEPRECATED_EMPTY_VALUE = '__deprecated_empty_value__'; + /** * Caches created choice lists. * @@ -344,7 +349,7 @@ class ChoiceType extends AbstractType }; $placeholderNormalizer = function (Options $options, $placeholder) use ($that) { - if (!is_object($options['empty_value']) || !$options['empty_value'] instanceof \Exception) { + if ($that::DEPRECATED_EMPTY_VALUE !== $options['empty_value']) { @trigger_error(sprintf('The form option "empty_value" of the "%s" form type (%s) is deprecated since version 2.6 and will be removed in 3.0. Use "placeholder" instead.', $that->getName(), __CLASS__), E_USER_DEPRECATED); if (null === $placeholder || '' === $placeholder) { @@ -396,7 +401,7 @@ class ChoiceType extends AbstractType 'preferred_choices' => array(), 'group_by' => null, 'empty_data' => $emptyData, - 'empty_value' => new \Exception(), // deprecated + 'empty_value' => self::DEPRECATED_EMPTY_VALUE, 'placeholder' => $placeholder, 'error_bubbling' => false, 'compound' => $compound, diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php index c48fde3510..209ece5cae 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php @@ -191,7 +191,7 @@ class DateType extends AbstractType }; $placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) { - if (!is_object($options['empty_value']) || !$options['empty_value'] instanceof \Exception) { + if (ChoiceType::DEPRECATED_EMPTY_VALUE !== $options['empty_value']) { @trigger_error('The form option "empty_value" is deprecated since version 2.6 and will be removed in 3.0. Use "placeholder" instead.', E_USER_DEPRECATED); $placeholder = $options['empty_value']; @@ -243,7 +243,7 @@ class DateType extends AbstractType 'format' => $format, 'model_timezone' => null, 'view_timezone' => null, - 'empty_value' => new \Exception(), // deprecated + 'empty_value' => ChoiceType::DEPRECATED_EMPTY_VALUE, 'placeholder' => $placeholder, 'html5' => true, // Don't modify \DateTime classes by reference, we treat diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FileType.php b/src/Symfony/Component/Form/Extension/Core/Type/FileType.php index 6c67b8dc4b..40da512b30 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FileType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FileType.php @@ -12,12 +12,37 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; +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\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class FileType extends AbstractType { + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + if ($options['multiple']) { + $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { + $form = $event->getForm(); + $data = $event->getData(); + + // submitted data for an input file (not required) without choosing any file + if (array(null) === $data) { + $emptyData = $form->getConfig()->getEmptyData(); + + $data = is_callable($emptyData) ? call_user_func($emptyData, $form, $data) : $emptyData; + $event->setData($data); + } + }); + } + } + /** * {@inheritdoc} */ @@ -39,9 +64,7 @@ class FileType extends AbstractType */ public function finishView(FormView $view, FormInterface $form, array $options) { - $view - ->vars['multipart'] = true - ; + $view->vars['multipart'] = true; } /** @@ -49,10 +72,18 @@ class FileType extends AbstractType */ public function configureOptions(OptionsResolver $resolver) { + $dataClass = function (Options $options) { + return $options['multiple'] ? null : 'Symfony\Component\HttpFoundation\File\File'; + }; + + $emptyData = function (Options $options) { + return $options['multiple'] ? array() : null; + }; + $resolver->setDefaults(array( 'compound' => false, - 'data_class' => 'Symfony\Component\HttpFoundation\File\File', - 'empty_data' => null, + 'data_class' => $dataClass, + 'empty_data' => $emptyData, 'multiple' => false, )); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php index 81edfd6c42..8aae516a5e 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php @@ -192,7 +192,7 @@ class TimeType extends AbstractType }; $placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) { - if (!is_object($options['empty_value']) || !$options['empty_value'] instanceof \Exception) { + if (ChoiceType::DEPRECATED_EMPTY_VALUE !== $options['empty_value']) { @trigger_error('The form option "empty_value" is deprecated since version 2.6 and will be removed in 3.0. Use "placeholder" instead.', E_USER_DEPRECATED); $placeholder = $options['empty_value']; @@ -241,7 +241,7 @@ class TimeType extends AbstractType 'with_seconds' => false, 'model_timezone' => null, 'view_timezone' => null, - 'empty_value' => new \Exception(), // deprecated + 'empty_value' => ChoiceType::DEPRECATED_EMPTY_VALUE, 'placeholder' => $placeholder, 'html5' => true, // Don't modify \DateTime classes by reference, we treat diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FileTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FileTypeTest.php index eebb6ad583..cbeeb468c1 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FileTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FileTypeTest.php @@ -54,6 +54,33 @@ class FileTypeTest extends \Symfony\Component\Form\Test\TypeTestCase $this->assertNull($form->getData()); } + public function testSubmitEmptyMultiple() + { + $form = $this->factory->createBuilder('file', null, array( + 'multiple' => true, + ))->getForm(); + + // submitted data when an input file is uploaded without choosing any file + $form->submit(array(null)); + + $this->assertSame(array(), $form->getData()); + } + + public function testSetDataMultiple() + { + $form = $this->factory->createBuilder('file', null, array( + 'multiple' => true, + ))->getForm(); + + $data = array( + $this->createUploadedFileMock('abcdef', 'first.jpg', true), + $this->createUploadedFileMock('zyxwvu', 'second.jpg', true), + ); + + $form->setData($data); + $this->assertSame($data, $form->getData()); + } + public function testSubmitMultiple() { $form = $this->factory->createBuilder('Symfony\Component\Form\Extension\Core\Type\FileType', null, array( diff --git a/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php b/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php index c76b57bb6a..a1cff53535 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php @@ -36,6 +36,13 @@ class ControllerResolver implements ControllerResolverInterface */ private $supportsVariadic; + /** + * If scalar types exists. + * + * @var bool + */ + private $supportsScalarTypes; + /** * Constructor. * @@ -46,6 +53,7 @@ class ControllerResolver implements ControllerResolverInterface $this->logger = $logger; $this->supportsVariadic = method_exists('ReflectionParameter', 'isVariadic'); + $this->supportsScalarTypes = method_exists('ReflectionParameter', 'getType'); } /** @@ -132,7 +140,7 @@ class ControllerResolver implements ControllerResolverInterface $arguments[] = $request; } elseif ($param->isDefaultValueAvailable()) { $arguments[] = $param->getDefaultValue(); - } elseif ($param->allowsNull()) { + } elseif ($this->supportsScalarTypes && $param->hasType() && $param->allowsNull()) { $arguments[] = null; } else { if (is_array($controller)) { diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ControllerResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ControllerResolverTest.php index 9aa7e20a9a..f3b0ac025d 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ControllerResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ControllerResolverTest.php @@ -223,6 +223,19 @@ class ControllerResolverTest extends \PHPUnit_Framework_TestCase $mock->getController($request); } + /** + * @expectedException \RuntimeException + */ + public function testIfExceptionIsThrownWhenMissingAnArgument() + { + $resolver = new ControllerResolver(); + $request = Request::create('/'); + + $controller = array($this, 'controllerMethod1'); + + $resolver->getArguments($request, $controller); + } + /** * @requires PHP 7.1 */