diff --git a/.travis.yml b/.travis.yml index a527f770d5..e8f3245d37 100644 --- a/.travis.yml +++ b/.travis.yml @@ -87,7 +87,7 @@ install: - export COMPOSER_ROOT_VERSION=$SYMFONY_VERSION.x-dev - if [[ ! $skip && $deps ]]; then export SYMFONY_DEPRECATIONS_HELPER=weak; fi - if [[ ! $skip && $deps ]]; then mv composer.json.phpunit composer.json; fi - - if [[ ! $skip ]]; then composer update; fi + - if [[ ! $skip ]]; then composer update --no-suggest; fi - if [[ ! $skip ]]; then ./phpunit install; fi - if [[ ! $skip && ! $PHP = hhvm* ]]; then php -i; else hhvm --php -r 'print_r($_SERVER);print_r(ini_get_all());'; fi @@ -98,8 +98,8 @@ script: - if [[ ! $deps && ! $PHP = hhvm* ]]; then echo -e "\\nRunning tests requiring tty"; $PHPUNIT --group tty; fi - if [[ ! $deps && $PHP = hhvm* ]]; then $PHPUNIT --exclude-group benchmark,intl-data; fi - if [[ ! $deps && $PHP = ${MIN_PHP%.*} ]]; then echo -e "1\\n0" | xargs -I{} sh -c 'echo "\\nPHP --enable-sigchild enhanced={}" && SYMFONY_DEPRECATIONS_HELPER=weak ENHANCE_SIGCHLD={} php-$MIN_PHP/sapi/cli/php .phpunit/phpunit-4.8/phpunit --colors=always src/Symfony/Component/Process/'; fi - - if [[ $deps = high ]]; then echo "$COMPONENTS" | parallel --gnu -j10% 'cd {}; composer update --no-progress --ansi; $PHPUNIT --exclude-group tty,benchmark,intl-data'$LEGACY"$REPORT"; fi - - if [[ $deps = low ]]; then echo "$COMPONENTS" | parallel --gnu -j10% 'cd {}; composer update --no-progress --ansi --prefer-lowest --prefer-stable; $PHPUNIT --exclude-group tty,benchmark,intl-data'"$REPORT"; fi + - if [[ $deps = high ]]; then echo "$COMPONENTS" | parallel --gnu -j10% 'cd {}; composer update --no-progress --no-suggest --ansi; $PHPUNIT --exclude-group tty,benchmark,intl-data'$LEGACY"$REPORT"; fi + - if [[ $deps = low ]]; then echo "$COMPONENTS" | parallel --gnu -j10% 'cd {}; composer update --no-progress --no-suggest --ansi --prefer-lowest --prefer-stable; $PHPUNIT --exclude-group tty,benchmark,intl-data'"$REPORT"; fi # Test the PhpUnit bridge using the original phpunit script - if [[ $deps = low ]]; then (cd src/Symfony/Bridge/PhpUnit && wget https://phar.phpunit.de/phpunit-4.8.phar); fi - if [[ $deps = low ]]; then (cd src/Symfony/Bridge/PhpUnit && phpenv global 5.3 && php --version && composer update && php phpunit-4.8.phar); fi diff --git a/appveyor.yml b/appveyor.yml index d5c663d5a0..79be565e0d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -51,7 +51,7 @@ install: - copy /Y .composer\* %APPDATA%\Composer\ - php .github/build-packages.php "HEAD^" src\Symfony\Bridge\PhpUnit - IF %APPVEYOR_REPO_BRANCH%==master (SET COMPOSER_ROOT_VERSION=dev-master) ELSE (SET COMPOSER_ROOT_VERSION=%APPVEYOR_REPO_BRANCH%.x-dev) - - php composer.phar update --no-progress --ansi + - php composer.phar update --no-progress --no-suggest --ansi - php phpunit install test_script: diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php index 9477d82655..f199f16265 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -61,9 +61,9 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface * loaded objects * @param IdReader $idReader The reader for the object * IDs. + * @param null|EntityLoaderInterface $objectLoader The objects loader * @param ChoiceListFactoryInterface $factory The factory for creating * the loaded choice list - * @param null|EntityLoaderInterface $objectLoader The objects loader */ public function __construct($manager, $class, $idReader = null, $objectLoader = null, $factory = null) { diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php index d811421b00..33092d3646 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php @@ -40,12 +40,13 @@ class ProfilerController /** * Constructor. * - * @param UrlGeneratorInterface $generator The URL Generator - * @param Profiler $profiler The profiler - * @param \Twig_Environment $twig The twig environment - * @param array $templates The templates - * @param string $toolbarPosition The toolbar position (top, bottom, normal, or null -- use the configuration) - * @param string $baseDir The project root directory + * @param UrlGeneratorInterface $generator The URL Generator + * @param Profiler $profiler The profiler + * @param \Twig_Environment $twig The twig environment + * @param array $templates The templates + * @param string $toolbarPosition The toolbar position (top, bottom, normal, or null -- use the configuration) + * @param ContentSecurityPolicyHandler $cspHandler The Content-Security-Policy handler + * @param string $baseDir The project root directory */ public function __construct(UrlGeneratorInterface $generator, Profiler $profiler = null, \Twig_Environment $twig, array $templates, $toolbarPosition = 'bottom', ContentSecurityPolicyHandler $cspHandler = null, $baseDir = null) { diff --git a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php index 760cb8365e..a40c42c88e 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php +++ b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php @@ -71,7 +71,7 @@ class WebDebugToolbarListener implements EventSubscriberInterface $this->urlGenerator->generate('_profiler', array('token' => $response->headers->get('X-Debug-Token')), UrlGeneratorInterface::ABSOLUTE_URL) ); } catch (\Exception $e) { - $response->headers->set('X-Debug-Error', get_class($e).': '.$e->getMessage()); + $response->headers->set('X-Debug-Error', get_class($e).': '.preg_replace('/\s+/', ' ', $e->getMessage())); } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php index da7495c8c4..dff05c182c 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php @@ -246,6 +246,27 @@ class WebDebugToolbarListenerTest extends TestCase $this->assertEquals('Exception: foo', $response->headers->get('X-Debug-Error')); } + public function testThrowingErrorCleanup() + { + $response = new Response(); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + + $urlGenerator = $this->getUrlGeneratorMock(); + $urlGenerator + ->expects($this->once()) + ->method('generate') + ->with('_profiler', array('token' => 'xxxxxxxx')) + ->will($this->throwException(new \Exception("This\nmultiline\r\ntabbed text should\tcome out\r on\n \ta single plain\r\nline"))) + ; + + $event = new FilterResponseEvent($this->getKernelMock(), $this->getRequestMock(), HttpKernelInterface::MASTER_REQUEST, $response); + + $listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, 'bottom', $urlGenerator); + $listener->onKernelResponse($event); + + $this->assertEquals('Exception: This multiline tabbed text should come out on a single plain line', $response->headers->get('X-Debug-Error')); + } + protected function getRequestMock($isXmlHttpRequest = false, $requestFormat = 'html', $hasSession = true) { $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->setMethods(array('getSession', 'isXmlHttpRequest', 'getRequestFormat'))->disableOriginalConstructor()->getMock(); diff --git a/src/Symfony/Component/Asset/PathPackage.php b/src/Symfony/Component/Asset/PathPackage.php index 906879f8b0..a8785c53f4 100644 --- a/src/Symfony/Component/Asset/PathPackage.php +++ b/src/Symfony/Component/Asset/PathPackage.php @@ -31,6 +31,7 @@ class PathPackage extends Package /** * @param string $basePath The base path to be prepended to relative paths * @param VersionStrategyInterface $versionStrategy The version strategy + * @param ContextInterface|null $context The context */ public function __construct($basePath, VersionStrategyInterface $versionStrategy, ContextInterface $context = null) { diff --git a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php index f6b523a74b..8ddfa27f38 100644 --- a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php @@ -57,7 +57,8 @@ class PhpArrayAdapter implements AdapterInterface * stores arrays in its latest versions. This factory method decorates the given * fallback pool with this adapter only if the current PHP version is supported. * - * @param string $file The PHP file were values are cached + * @param string $file The PHP file were values are cached + * @param CacheItemPoolInterface $fallbackPool Fallback for old PHP versions or opcache disabled * * @return CacheItemPoolInterface */ diff --git a/src/Symfony/Component/Cache/Adapter/RedisAdapter.php b/src/Symfony/Component/Cache/Adapter/RedisAdapter.php index 75cb764f40..ae718ca954 100644 --- a/src/Symfony/Component/Cache/Adapter/RedisAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/RedisAdapter.php @@ -18,7 +18,9 @@ class RedisAdapter extends AbstractAdapter use RedisTrait; /** - * @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient + * @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient The redis client + * @param string $namespace The default namespace + * @param integer $defaultLifetime The default lifetime */ public function __construct($redisClient, $namespace = '', $defaultLifetime = 0) { diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 73bea791a9..0ab3c3e0e3 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -672,12 +672,11 @@ class Application if (defined('HHVM_VERSION') && $width > 1 << 31) { $width = 1 << 31; } - $formatter = $output->getFormatter(); $lines = array(); - foreach (preg_split('/\r?\n/', OutputFormatter::escape($e->getMessage())) as $line) { + foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) { foreach ($this->splitStringByWidth($line, $width - 4) as $line) { // pre-format lines to get the right string length - $lineLength = $this->stringWidth(preg_replace('/\[[^m]*m/', '', $formatter->format($line))) + 4; + $lineLength = $this->stringWidth($line) + 4; $lines[] = array($line, $lineLength); $len = max($lineLength, $len); @@ -685,15 +684,15 @@ class Application } $messages = array(); - $messages[] = $emptyLine = $formatter->format(sprintf('%s', str_repeat(' ', $len))); - $messages[] = $formatter->format(sprintf('%s%s', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title))))); + $messages[] = $emptyLine = sprintf('%s', str_repeat(' ', $len)); + $messages[] = sprintf('%s%s', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title)))); foreach ($lines as $line) { - $messages[] = $formatter->format(sprintf(' %s %s', $line[0], str_repeat(' ', $len - $line[1]))); + $messages[] = sprintf(' %s %s', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1])); } $messages[] = $emptyLine; $messages[] = ''; - $output->writeln($messages, OutputInterface::OUTPUT_RAW | OutputInterface::VERBOSITY_QUIET); + $output->writeln($messages, OutputInterface::VERBOSITY_QUIET); if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { $output->writeln('Exception trace:', OutputInterface::VERBOSITY_QUIET); diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php index 1124f80ef3..44bc2da239 100644 --- a/src/Symfony/Component/Console/Helper/Helper.php +++ b/src/Symfony/Component/Console/Helper/Helper.php @@ -123,6 +123,11 @@ abstract class Helper implements HelperInterface } public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, $string) + { + return self::strlen(self::removeDecoration($formatter, $string)); + } + + public static function removeDecoration(OutputFormatterInterface $formatter, $string) { $isDecorated = $formatter->isDecorated(); $formatter->setDecorated(false); @@ -132,6 +137,6 @@ abstract class Helper implements HelperInterface $string = preg_replace("/\033\[[^m]*m/", '', $string); $formatter->setDecorated($isDecorated); - return self::strlen($string); + return $string; } } diff --git a/src/Symfony/Component/Console/Helper/Table.php b/src/Symfony/Component/Console/Helper/Table.php index 36f3301817..66962c94d8 100644 --- a/src/Symfony/Component/Console/Helper/Table.php +++ b/src/Symfony/Component/Console/Helper/Table.php @@ -426,7 +426,7 @@ class Table if (!strstr($cell, "\n")) { continue; } - $lines = explode("\n", $cell); + $lines = explode("\n", str_replace("\n", "\n", $cell)); foreach ($lines as $lineKey => $line) { if ($cell instanceof TableCell) { $line = new TableCell($line, array('colspan' => $cell->getColspan())); @@ -467,7 +467,7 @@ class Table $nbLines = $cell->getRowspan() - 1; $lines = array($cell); if (strstr($cell, "\n")) { - $lines = explode("\n", $cell); + $lines = explode("\n", str_replace("\n", "\n", $cell)); $nbLines = count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines; $rows[$line][$column] = new TableCell($lines[0], array('colspan' => $cell->getColspan())); @@ -602,9 +602,10 @@ class Table foreach ($row as $i => $cell) { if ($cell instanceof TableCell) { - $textLength = Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell); + $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell); + $textLength = Helper::strlen($textContent); if ($textLength > 0) { - $contentColumns = str_split($cell, ceil($textLength / $cell->getColspan())); + $contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan())); foreach ($contentColumns as $position => $content) { $row[$i + $position] = $content; } diff --git a/src/Symfony/Component/Console/Tester/CommandTester.php b/src/Symfony/Component/Console/Tester/CommandTester.php index 080ace5c95..0bb1603c33 100644 --- a/src/Symfony/Component/Console/Tester/CommandTester.php +++ b/src/Symfony/Component/Console/Tester/CommandTester.php @@ -76,9 +76,7 @@ class CommandTester } $this->output = new StreamOutput(fopen('php://memory', 'w', false)); - if (isset($options['decorated'])) { - $this->output->setDecorated($options['decorated']); - } + $this->output->setDecorated(isset($options['decorated']) ? $options['decorated'] : false); if (isset($options['verbosity'])) { $this->output->setVerbosity($options['verbosity']); } diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index fa583d99af..314165a195 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -636,6 +636,22 @@ class ApplicationTest extends TestCase putenv('COLUMNS=120'); } + public function testRenderExceptionEscapesLines() + { + $application = $this->getMockBuilder('Symfony\Component\Console\Application')->setMethods(array('getTerminalWidth'))->getMock(); + $application->setAutoExit(false); + $application->expects($this->any()) + ->method('getTerminalWidth') + ->will($this->returnValue(22)); + $application->register('foo')->setCode(function () { + throw new \Exception('dont break here !'); + }); + $tester = new ApplicationTester($application); + + $tester->run(array('command' => 'foo'), array('decorated' => false)); + $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_escapeslines.txt', $tester->getDisplay(true), '->renderException() escapes lines containing formatting'); + } + public function testRun() { $application = new Application(); diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_escapeslines.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_escapeslines.txt new file mode 100644 index 0000000000..cf79b37a92 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_escapeslines.txt @@ -0,0 +1,9 @@ + + + [Exception] + dont break here < + info>! + + +foo + diff --git a/src/Symfony/Component/Console/Tests/Helper/TableTest.php b/src/Symfony/Component/Console/Tests/Helper/TableTest.php index 89b8efffb6..d8a8ff0087 100644 --- a/src/Symfony/Component/Console/Tests/Helper/TableTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/TableTest.php @@ -511,6 +511,35 @@ TABLE | Dante Alighieri | J. R. R. Tolkien | J. R. R | +-----------------+------------------+---------+ +TABLE + , + true, + ), + 'Row with formatted cells containing a newline' => array( + array(), + array( + array( + new TableCell('Dont break'."\n".'here', array('colspan' => 2)), + ), + new TableSeparator(), + array( + 'foo', + new TableCell('Dont break'."\n".'here', array('rowspan' => 2)), + ), + array( + 'bar', + ), + ), + 'default', + <<<'TABLE' ++-------+------------+ +| Dont break | +| here | ++-------+------------+ +| foo | Dont break | +| bar | here | ++-------+------------+ + TABLE , true, diff --git a/src/Symfony/Component/Debug/ExceptionHandler.php b/src/Symfony/Component/Debug/ExceptionHandler.php index 2fdd8456fd..f2ae4b102e 100644 --- a/src/Symfony/Component/Debug/ExceptionHandler.php +++ b/src/Symfony/Component/Debug/ExceptionHandler.php @@ -83,7 +83,7 @@ class ExceptionHandler /** * Sets the format for links to source files. * - * @param string|FileLinkFormatter $format The format for links to source files + * @param string|FileLinkFormatter $fileLinkFormat The format for links to source files * * @return string The previous file link format */ diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index 59019f634b..d35d38569c 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -30,7 +30,7 @@ class AutowirePass extends AbstractRecursivePass private $definedTypes = array(); private $types; private $ambiguousServiceTypes = array(); - private $usedTypes = array(); + private $autowired = array(); private $currentDefinition; /** @@ -40,27 +40,11 @@ class AutowirePass extends AbstractRecursivePass { try { parent::process($container); - - foreach ($this->usedTypes as $type => $id) { - if (!isset($this->usedTypes[$type]) || !isset($this->ambiguousServiceTypes[$type])) { - continue; - } - - if ($container->has($type) && !$container->findDefinition($type)->isAbstract()) { - continue; - } - - $this->container = $container; - $classOrInterface = class_exists($type, false) ? 'class' : 'interface'; - - throw new RuntimeException(sprintf('Cannot autowire service "%s": multiple candidate services exist for %s "%s".%s', $id, $classOrInterface, $type, $this->createTypeAlternatives($type))); - } } finally { - $this->container = null; $this->definedTypes = array(); $this->types = null; $this->ambiguousServiceTypes = array(); - $this->usedTypes = array(); + $this->autowired = array(); } } @@ -111,6 +95,9 @@ class AutowirePass extends AbstractRecursivePass if (!$value->isAutowired() || $value->isAbstract() || !$value->getClass()) { return parent::processValue($value, $isRoot); } + if ($value->getFactory()) { + throw new RuntimeException(sprintf('Service "%s" can use either autowiring or a factory, not both.', $this->currentId)); + } if (!$reflectionClass = $this->container->getReflectionClass($value->getClass())) { $this->container->log($this, sprintf('Skipping service "%s": Class or interface "%s" does not exist.', $this->currentId, $value->getClass())); @@ -250,37 +237,38 @@ class AutowirePass extends AbstractRecursivePass if (!$isConstructor && !$arguments && !$reflectionMethod->getNumberOfRequiredParameters()) { throw new RuntimeException(sprintf('Cannot autowire service "%s": method %s() has only optional arguments, thus must be wired explicitly.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method)); } + $parameters = $reflectionMethod->getParameters(); + if (method_exists('ReflectionMethod', 'isVariadic') && $reflectionMethod->isVariadic()) { + array_pop($parameters); + } - foreach ($reflectionMethod->getParameters() as $index => $parameter) { + foreach ($parameters as $index => $parameter) { if (array_key_exists($index, $arguments) && '' !== $arguments[$index]) { continue; } if (!$isConstructor && $parameter->isOptional() && !array_key_exists($index, $arguments)) { break; } - if (method_exists($parameter, 'isVariadic') && $parameter->isVariadic()) { - continue; - } $type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, true); if (!$type) { + if (isset($arguments[$index])) { + continue; + } + // no default value? Then fail if (!$parameter->isOptional()) { throw new RuntimeException(sprintf('Cannot autowire service "%s": argument $%s of method %s() must have a type-hint or be given a value explicitly.', $this->currentId, $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method)); } - if (!array_key_exists($index, $arguments)) { - // specifically pass the default value - $arguments[$index] = $parameter->getDefaultValue(); - } + // specifically pass the default value + $arguments[$index] = $parameter->getDefaultValue(); continue; } - if ($value = $this->getAutowiredReference($type)) { - $this->usedTypes[$type] = $this->currentId; - } else { + if (!$value = $this->getAutowiredReference($type)) { $failureMessage = $this->createTypeNotFoundMessage($type, sprintf('argument $%s of method %s()', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method)); if ($parameter->isDefaultValueAvailable()) { @@ -296,6 +284,16 @@ class AutowirePass extends AbstractRecursivePass $arguments[$index] = $value; } + if ($parameters && !isset($arguments[++$index])) { + while (0 <= --$index) { + $parameter = $parameters[$index]; + if (!$parameter->isDefaultValueAvailable() || $parameter->getDefaultValue() !== $arguments[$index]) { + break; + } + unset($arguments[$index]); + } + } + // it's possible index 1 was set, then index 0, then 2, etc // make sure that we re-order so they're injected as expected ksort($arguments); @@ -305,6 +303,8 @@ class AutowirePass extends AbstractRecursivePass /** * @return Reference|null A reference to the service matching the given type, if any + * + * @throws RuntimeException */ private function getAutowiredReference($type, $autoRegister = true) { @@ -316,6 +316,10 @@ class AutowirePass extends AbstractRecursivePass return; } + if (isset($this->autowired[$type])) { + return $this->autowired[$type] ? new Reference($this->autowired[$type]) : null; + } + if (null === $this->types) { $this->populateAvailableTypes(); } @@ -326,8 +330,14 @@ class AutowirePass extends AbstractRecursivePass return new Reference($this->types[$type]); } - if ($autoRegister && $class = $this->container->getReflectionClass($type, true)) { - return $this->createAutowiredDefinition($class); + if (isset($this->ambiguousServiceTypes[$type])) { + $classOrInterface = class_exists($type, false) ? 'class' : 'interface'; + + throw new RuntimeException(sprintf('Cannot autowire service "%s": multiple candidate services exist for %s "%s".%s', $this->currentId, $classOrInterface, $type, $this->createTypeAlternatives($type))); + } + + if ($autoRegister) { + return $this->createAutowiredDefinition($type); } } @@ -412,45 +422,28 @@ class AutowirePass extends AbstractRecursivePass /** * Registers a definition for the type if possible or throws an exception. * - * @param \ReflectionClass $typeHint + * @param string $type * * @return Reference|null A reference to the registered definition - * - * @throws RuntimeException */ - private function createAutowiredDefinition(\ReflectionClass $typeHint) + private function createAutowiredDefinition($type) { - if (isset($this->ambiguousServiceTypes[$type = $typeHint->name])) { - $classOrInterface = class_exists($type) ? 'class' : 'interface'; - - throw new RuntimeException(sprintf('Cannot autowire service "%s": multiple candidate services exist for %s "%s".%s', $this->currentId, $classOrInterface, $type, $this->createTypeAlternatives($type))); - } - - if (!$typeHint->isInstantiable()) { - $this->container->log($this, sprintf('Type "%s" is not instantiable thus cannot be auto-registered for service "%s".', $type, $this->currentId)); - + if (!($typeHint = $this->container->getReflectionClass($type, true)) || !$typeHint->isInstantiable()) { return; } - $ambiguousServiceTypes = $this->ambiguousServiceTypes; $currentDefinition = $this->currentDefinition; - $definitions = $this->container->getDefinitions(); $currentId = $this->currentId; - $this->currentId = $argumentId = sprintf('autowired.%s', $type); + $this->currentId = $this->autowired[$type] = $argumentId = sprintf('autowired.%s', $type); $this->currentDefinition = $argumentDefinition = new Definition($type); $argumentDefinition->setPublic(false); $argumentDefinition->setAutowired(true); - $this->populateAvailableType($argumentId, $argumentDefinition); - try { $this->processValue($argumentDefinition, true); $this->container->setDefinition($argumentId, $argumentDefinition); } catch (RuntimeException $e) { - // revert any changes done to our internal state - unset($this->types[$type]); - $this->ambiguousServiceTypes = $ambiguousServiceTypes; - $this->container->setDefinitions($definitions); + $this->autowired[$type] = false; $this->container->log($this, $e->getMessage()); return; diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 9d2876a595..63b2bc8662 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -81,7 +81,7 @@ class ContainerBuilder extends Container implements TaggedContainerInterface */ private $compiler; - private $trackResources = true; + private $trackResources; /** * @var InstantiatorInterface|null @@ -122,6 +122,7 @@ class ContainerBuilder extends Container implements TaggedContainerInterface { parent::__construct($parameterBag); + $this->trackResources = interface_exists('Symfony\Component\Config\Resource\ResourceInterface'); $this->setDefinition('service_container', (new Definition(ContainerInterface::class))->setSynthetic(true)); $this->setAlias(PsrContainerInterface::class, new Alias('service_container', false)); $this->setAlias(ContainerInterface::class, new Alias('service_container', false)); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index c5dbf101ec..6dbc7c87e5 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -444,10 +444,6 @@ class AutowirePassTest extends TestCase array( new Reference('a'), new Reference('lille'), - // third arg shouldn't *need* to be passed - // but that's hard to "pull of" with autowiring, so - // this assumes passing the default val is ok - 'some_val', ), $definition->getArguments() ); @@ -618,30 +614,21 @@ class AutowirePassTest extends TestCase } /** - * @dataProvider provideAutodiscoveredAutowiringOrder - * * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @expectedExceptionMEssage Unable to autowire argument of type "Symfony\Component\DependencyInjection\Tests\Compiler\CollisionInterface" for service "a". Multiple services exist for this interface: autowired.Symfony\Component\DependencyInjection\Tests\Compiler\CollisionA, autowired.Symfony\Component\DependencyInjection\Tests\Compiler\CollisionB. + * @expectedExceptionMessage Service "a" can use either autowiring or a factory, not both. */ - public function testAutodiscoveredAutowiringOrder($class) + public function testWithFactory() { $container = new ContainerBuilder(); - $container->register('a', __NAMESPACE__.'\\'.$class) + $container->register('a', __NAMESPACE__.'\A') + ->setFactory('foo') ->setAutowired(true); $pass = new AutowirePass(); $pass->process($container); } - public function provideAutodiscoveredAutowiringOrder() - { - return array( - array('CannotBeAutowiredForwardOrder'), - array('CannotBeAutowiredReverseOrder'), - ); - } - /** * @dataProvider provideNotWireableCalls * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException @@ -680,19 +667,6 @@ class AutowirePassTest extends TestCase ); } - public function testAutoregisterRestoresStateOnFailure() - { - $container = new ContainerBuilder(); - - $container->register('e', E::class) - ->setAutowired(true); - - $pass = new AutowirePass(); - $pass->process($container); - - $this->assertSame(array('service_container', 'e'), array_keys($container->getDefinitions())); - } - /** * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException * @expectedExceptionMessage Cannot autowire service "j": multiple candidate services exist for class "Symfony\Component\DependencyInjection\Tests\Compiler\I". This type-hint could be aliased to one of these existing services: "f", "i"; or be updated to "Symfony\Component\DependencyInjection\Tests\Compiler\IInterface". diff --git a/src/Symfony/Component/EventDispatcher/EventDispatcher.php b/src/Symfony/Component/EventDispatcher/EventDispatcher.php index 2c977f4731..01bfc0066e 100644 --- a/src/Symfony/Component/EventDispatcher/EventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/EventDispatcher.php @@ -93,7 +93,7 @@ class EventDispatcher implements EventDispatcherInterface */ public function hasListeners($eventName = null) { - return (bool) count($this->getListeners($eventName)); + return (bool) $this->getListeners($eventName); } /** diff --git a/src/Symfony/Component/ExpressionLanguage/Lexer.php b/src/Symfony/Component/ExpressionLanguage/Lexer.php index 26bb51d42e..8c10b72d86 100644 --- a/src/Symfony/Component/ExpressionLanguage/Lexer.php +++ b/src/Symfony/Component/ExpressionLanguage/Lexer.php @@ -59,12 +59,12 @@ class Lexer } elseif (false !== strpos(')]}', $expression[$cursor])) { // closing bracket if (empty($brackets)) { - throw new SyntaxError(sprintf('Unexpected "%s"', $expression[$cursor]), $cursor); + throw new SyntaxError(sprintf('Unexpected "%s"', $expression[$cursor]), $cursor, $expression); } list($expect, $cur) = array_pop($brackets); if ($expression[$cursor] != strtr($expect, '([{', ')]}')) { - throw new SyntaxError(sprintf('Unclosed "%s"', $expect), $cur); + throw new SyntaxError(sprintf('Unclosed "%s"', $expect), $cur, $expression); } $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1); @@ -87,7 +87,7 @@ class Lexer $cursor += strlen($match[0]); } else { // unlexable - throw new SyntaxError(sprintf('Unexpected character "%s"', $expression[$cursor]), $cursor); + throw new SyntaxError(sprintf('Unexpected character "%s"', $expression[$cursor]), $cursor, $expression); } } @@ -95,9 +95,9 @@ class Lexer if (!empty($brackets)) { list($expect, $cur) = array_pop($brackets); - throw new SyntaxError(sprintf('Unclosed "%s"', $expect), $cur); + throw new SyntaxError(sprintf('Unclosed "%s"', $expect), $cur, $expression); } - return new TokenStream($tokens); + return new TokenStream($tokens, $expression); } } diff --git a/src/Symfony/Component/ExpressionLanguage/Parser.php b/src/Symfony/Component/ExpressionLanguage/Parser.php index f8900bfb12..c75972447d 100644 --- a/src/Symfony/Component/ExpressionLanguage/Parser.php +++ b/src/Symfony/Component/ExpressionLanguage/Parser.php @@ -99,7 +99,7 @@ class Parser $node = $this->parseExpression(); if (!$stream->isEOF()) { - throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s"', $stream->current->type, $stream->current->value), $stream->current->cursor); + throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s"', $stream->current->type, $stream->current->value), $stream->current->cursor, $stream->getExpression()); } return $node; @@ -195,13 +195,13 @@ class Parser default: if ('(' === $this->stream->current->value) { if (false === isset($this->functions[$token->value])) { - throw new SyntaxError(sprintf('The function "%s" does not exist', $token->value), $token->cursor); + throw new SyntaxError(sprintf('The function "%s" does not exist', $token->value), $token->cursor, $this->stream->getExpression()); } $node = new Node\FunctionNode($token->value, $this->parseArguments()); } else { if (!in_array($token->value, $this->names, true)) { - throw new SyntaxError(sprintf('Variable "%s" is not valid', $token->value), $token->cursor); + throw new SyntaxError(sprintf('Variable "%s" is not valid', $token->value), $token->cursor, $this->stream->getExpression()); } // is the name used in the compiled code different @@ -227,7 +227,7 @@ class Parser } elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) { $node = $this->parseHashExpression(); } else { - throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s"', $token->type, $token->value), $token->cursor); + throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s"', $token->type, $token->value), $token->cursor, $this->stream->getExpression()); } } @@ -289,7 +289,7 @@ class Parser } else { $current = $this->stream->current; - throw new SyntaxError(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s"', $current->type, $current->value), $current->cursor); + throw new SyntaxError(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s"', $current->type, $current->value), $current->cursor, $this->stream->getExpression()); } $this->stream->expect(Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)'); @@ -327,7 +327,7 @@ class Parser // As a result, if $token is NOT an operator OR $token->value is NOT a valid property or method name, an exception shall be thrown. ($token->type !== Token::OPERATOR_TYPE || !preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A', $token->value)) ) { - throw new SyntaxError('Expected name', $token->cursor); + throw new SyntaxError('Expected name', $token->cursor, $this->stream->getExpression()); } $arg = new Node\ConstantNode($token->value, true); diff --git a/src/Symfony/Component/ExpressionLanguage/SyntaxError.php b/src/Symfony/Component/ExpressionLanguage/SyntaxError.php index d149c00768..9373e9980b 100644 --- a/src/Symfony/Component/ExpressionLanguage/SyntaxError.php +++ b/src/Symfony/Component/ExpressionLanguage/SyntaxError.php @@ -13,8 +13,14 @@ namespace Symfony\Component\ExpressionLanguage; class SyntaxError extends \LogicException { - public function __construct($message, $cursor = 0) + public function __construct($message, $cursor = 0, $expression = '') { - parent::__construct(sprintf('%s around position %d.', $message, $cursor)); + $message = sprintf('%s around position %d', $message, $cursor); + if ($expression) { + $message = sprintf('%s for expression `%s`', $message, $expression); + } + $message .= '.'; + + parent::__construct($message); } } diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/LexerTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/LexerTest.php index 4292c22359..87c16f707b 100644 --- a/src/Symfony/Component/ExpressionLanguage/Tests/LexerTest.php +++ b/src/Symfony/Component/ExpressionLanguage/Tests/LexerTest.php @@ -18,14 +18,43 @@ use Symfony\Component\ExpressionLanguage\TokenStream; class LexerTest extends TestCase { + /** + * @var Lexer + */ + private $lexer; + + protected function setUp() + { + $this->lexer = new Lexer(); + } + /** * @dataProvider getTokenizeData */ public function testTokenize($tokens, $expression) { $tokens[] = new Token('end of expression', null, strlen($expression) + 1); - $lexer = new Lexer(); - $this->assertEquals(new TokenStream($tokens), $lexer->tokenize($expression)); + $this->assertEquals(new TokenStream($tokens, $expression), $this->lexer->tokenize($expression)); + } + + /** + * @expectedException \Symfony\Component\ExpressionLanguage\SyntaxError + * @expectedExceptionMessage Unexpected character "'" around position 33 for expression `service(faulty.expression.example').dummyMethod()`. + */ + public function testTokenizeThrowsErrorWithMessage() + { + $expression = "service(faulty.expression.example').dummyMethod()"; + $this->lexer->tokenize($expression); + } + + /** + * @expectedException \Symfony\Component\ExpressionLanguage\SyntaxError + * @expectedExceptionMessage Unclosed "(" around position 7 for expression `service(unclosed.expression.dummyMethod()`. + */ + public function testTokenizeThrowsErrorOnUnclosedBrace() + { + $expression = 'service(unclosed.expression.dummyMethod()'; + $this->lexer->tokenize($expression); } public function getTokenizeData() diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/ParserTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/ParserTest.php index b8b954afe0..13b80cd64f 100644 --- a/src/Symfony/Component/ExpressionLanguage/Tests/ParserTest.php +++ b/src/Symfony/Component/ExpressionLanguage/Tests/ParserTest.php @@ -20,7 +20,7 @@ class ParserTest extends TestCase { /** * @expectedException \Symfony\Component\ExpressionLanguage\SyntaxError - * @expectedExceptionMessage Variable "foo" is not valid around position 1. + * @expectedExceptionMessage Variable "foo" is not valid around position 1 for expression `foo`. */ public function testParseWithInvalidName() { @@ -31,7 +31,7 @@ class ParserTest extends TestCase /** * @expectedException \Symfony\Component\ExpressionLanguage\SyntaxError - * @expectedExceptionMessage Variable "foo" is not valid around position 1. + * @expectedExceptionMessage Variable "foo" is not valid around position 1 for expression `foo`. */ public function testParseWithZeroInNames() { diff --git a/src/Symfony/Component/ExpressionLanguage/TokenStream.php b/src/Symfony/Component/ExpressionLanguage/TokenStream.php index 6c4af745b2..3c22fc1d46 100644 --- a/src/Symfony/Component/ExpressionLanguage/TokenStream.php +++ b/src/Symfony/Component/ExpressionLanguage/TokenStream.php @@ -22,16 +22,19 @@ class TokenStream private $tokens; private $position = 0; + private $expression; /** * Constructor. * - * @param array $tokens An array of tokens + * @param array $tokens An array of tokens + * @param string $expression */ - public function __construct(array $tokens) + public function __construct(array $tokens, $expression = '') { $this->tokens = $tokens; $this->current = $tokens[0]; + $this->expression = $expression; } /** @@ -50,7 +53,7 @@ class TokenStream public function next() { if (!isset($this->tokens[$this->position])) { - throw new SyntaxError('Unexpected end of expression', $this->current->cursor); + throw new SyntaxError('Unexpected end of expression', $this->current->cursor, $this->expression); } ++$this->position; @@ -69,7 +72,7 @@ class TokenStream { $token = $this->current; if (!$token->test($type, $value)) { - throw new SyntaxError(sprintf('%sUnexpected token "%s" of value "%s" ("%s" expected%s)', $message ? $message.'. ' : '', $token->type, $token->value, $type, $value ? sprintf(' with value "%s"', $value) : ''), $token->cursor); + throw new SyntaxError(sprintf('%sUnexpected token "%s" of value "%s" ("%s" expected%s)', $message ? $message.'. ' : '', $token->type, $token->value, $type, $value ? sprintf(' with value "%s"', $value) : ''), $token->cursor, $this->expression); } $this->next(); } @@ -83,4 +86,14 @@ class TokenStream { return $this->current->type === Token::EOF_TYPE; } + + /** + * @internal + * + * @return string + */ + public function getExpression() + { + return $this->expression; + } } diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php index 88531feaa2..7fc191a054 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php @@ -120,7 +120,7 @@ class PercentToLocalizedStringTransformer implements DataTransformerInterface $formatter = $this->getNumberFormatter(); // replace normal spaces so that the formatter can read them - $value = $formatter->parse(str_replace(' ', ' ', $value)); + $value = $formatter->parse(str_replace(' ', "\xc2\xa0", $value)); if (intl_is_failure($formatter->getErrorCode())) { throw new TransformationFailedException($formatter->getErrorMessage()); diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index a04a17c908..cdbb0fe171 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -407,6 +407,10 @@ class Form implements \IteratorAggregate, FormInterface } if (!$this->defaultDataSet) { + if ($this->lockSetData) { + throw new RuntimeException('A cycle was detected. Listeners to the PRE_SET_DATA event must not call getData() if the form data has not already been set. You should call getData() on the FormEvent object instead.'); + } + $this->setData($this->config->getData()); } @@ -427,6 +431,10 @@ class Form implements \IteratorAggregate, FormInterface } if (!$this->defaultDataSet) { + if ($this->lockSetData) { + throw new RuntimeException('A cycle was detected. Listeners to the PRE_SET_DATA event must not call getNormData() if the form data has not already been set.'); + } + $this->setData($this->config->getData()); } @@ -447,6 +455,10 @@ class Form implements \IteratorAggregate, FormInterface } if (!$this->defaultDataSet) { + if ($this->lockSetData) { + throw new RuntimeException('A cycle was detected. Listeners to the PRE_SET_DATA event must not call getViewData() if the form data has not already been set.'); + } + $this->setData($this->config->getData()); } 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 c1f9509678..5a47594154 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTypeTest.php @@ -598,7 +598,7 @@ class DateTypeTest extends BaseTypeTest )); $form->submit(array( - 'day' => '0', + 'day' => '1', 'month' => '6', 'year' => '2010', )); diff --git a/src/Symfony/Component/Form/Tests/SimpleFormTest.php b/src/Symfony/Component/Form/Tests/SimpleFormTest.php index 689f2e8f77..d80ec68b8e 100644 --- a/src/Symfony/Component/Form/Tests/SimpleFormTest.php +++ b/src/Symfony/Component/Form/Tests/SimpleFormTest.php @@ -896,6 +896,7 @@ class SimpleFormTest extends AbstractFormTest /** * @expectedException \Symfony\Component\Form\Exception\RuntimeException + * @expectedExceptionMessage A cycle was detected. Listeners to the PRE_SET_DATA event must not call setData(). You should call setData() on the FormEvent object instead. */ public function testSetDataCannotInvokeItself() { @@ -1062,6 +1063,51 @@ class SimpleFormTest extends AbstractFormTest $child->initialize(); } + /** + * @expectedException \Symfony\Component\Form\Exception\RuntimeException + * @expectedExceptionMessage A cycle was detected. Listeners to the PRE_SET_DATA event must not call getData() if the form data has not already been set. You should call getData() on the FormEvent object instead. + */ + public function testCannotCallGetDataInPreSetDataListenerIfDataHasNotAlreadyBeenSet() + { + $config = new FormConfigBuilder('name', 'stdClass', $this->dispatcher); + $config->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { + $event->getForm()->getData(); + }); + $form = new Form($config); + + $form->setData('foo'); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\RuntimeException + * @expectedExceptionMessage A cycle was detected. Listeners to the PRE_SET_DATA event must not call getNormData() if the form data has not already been set. + */ + public function testCannotCallGetNormDataInPreSetDataListener() + { + $config = new FormConfigBuilder('name', 'stdClass', $this->dispatcher); + $config->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { + $event->getForm()->getNormData(); + }); + $form = new Form($config); + + $form->setData('foo'); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\RuntimeException + * @expectedExceptionMessage A cycle was detected. Listeners to the PRE_SET_DATA event must not call getViewData() if the form data has not already been set. + */ + public function testCannotCallGetViewDataInPreSetDataListener() + { + $config = new FormConfigBuilder('name', 'stdClass', $this->dispatcher); + $config->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { + $event->getForm()->getViewData(); + }); + $form = new Form($config); + + $form->setData('foo'); + } + protected function createForm() { return $this->getBuilder()->getForm(); diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php index a059d3c507..e90579efbb 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php @@ -213,7 +213,7 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property * @param string $ucFirstProperty * @param int $type * - * @return array + * @return array|null */ private function getDocBlockFromMethod($class, $ucFirstProperty, $type) { diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractors/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractors/PhpDocExtractorTest.php index 8aa508e3c2..d0eb3eed06 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractors/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractors/PhpDocExtractorTest.php @@ -68,6 +68,7 @@ class PhpDocExtractorTest extends TestCase array('d', array(new Type(Type::BUILTIN_TYPE_BOOL)), null, null), array('e', array(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_RESOURCE))), null, null), array('f', array(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTime'))), null, null), + array('g', array(new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)), 'Nullable array.', null), array('donotexist', null, null, null), array('staticGetter', null, null, null), array('staticSetter', null, null, null), diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractors/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractors/ReflectionExtractorTest.php index f8a5e45360..77230fba05 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractors/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractors/ReflectionExtractorTest.php @@ -39,6 +39,7 @@ class ReflectionExtractorTest extends TestCase 'parent', 'collection', 'B', + 'g', 'foo', 'foo2', 'foo3', diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php index 96cb87db4a..12065b18b6 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php @@ -51,6 +51,13 @@ class Dummy extends ParentDummy */ public $B; + /** + * Nullable array. + * + * @var array|null + */ + public $g; + public static function getStatic() { } diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php index b374a5dcba..bc14cd8b69 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php @@ -101,10 +101,10 @@ final class PhpDocTypeHelper $collectionValueType = null; } else { $collectionKeyType = new Type(Type::BUILTIN_TYPE_INT); - $collectionValueType = new Type($phpType, false, $class); + $collectionValueType = new Type($phpType, $nullable, $class); } - return new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, $collectionKeyType, $collectionValueType); + return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyType, $collectionValueType); } return new Type($phpType, $nullable, $class);