Merge branch '2.8' into 3.2

* 2.8:
  [DI] Autowiring and factories are incompatible with each others
  [DI] Don't use auto-registered services to populate type-candidates
  Lighten tests output by removing composer suggestions
  support nullable array or collection
  Complete the injection of the expression in all syntax errors
  CS: Remove invisible chars
  Disable resource tracking if the config component is missing
  [EventDispatcher] Remove unneded count()
  Fix tests expecting a valid date
  Avoid forcing to define the choices_as_values option when using choice_loader
  add expression text to SyntaxError
  [Console] Fix table cell styling
  [Console] Revised exception rendering
  [WebProfilerBundle] Normalize whitespace in exceptions passed in headers
  Disable color support detection for tests
  [Form] Improve the exceptions when trying to get the data in a PRE_SET_DATA listener and the data has not already been set
This commit is contained in:
Nicolas Grekas 2017-04-04 09:26:27 +02:00
commit ccbbff2328
30 changed files with 301 additions and 94 deletions

View File

@ -86,7 +86,7 @@ install:
- export COMPOSER_ROOT_VERSION=$SYMFONY_VERSION.x-dev - export COMPOSER_ROOT_VERSION=$SYMFONY_VERSION.x-dev
- if [[ ! $skip && $deps ]]; then export SYMFONY_DEPRECATIONS_HELPER=weak; fi - if [[ ! $skip && $deps ]]; then export SYMFONY_DEPRECATIONS_HELPER=weak; fi
- if [[ ! $skip && $deps ]]; then mv composer.json.phpunit composer.json; 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 ]]; then ./phpunit install; fi
- if [[ ! $skip && ! $PHP = hhvm* ]]; then php -i; else hhvm --php -r 'print_r($_SERVER);print_r(ini_get_all());'; fi - if [[ ! $skip && ! $PHP = hhvm* ]]; then php -i; else hhvm --php -r 'print_r($_SERVER);print_r(ini_get_all());'; fi
@ -97,8 +97,8 @@ script:
- if [[ ! $deps && ! $PHP = hhvm* ]]; then echo -e "\\nRunning tests requiring tty"; $PHPUNIT --group tty; fi - 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 = 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={}" && ENHANCE_SIGCHLD={} php-$MIN_PHP/sapi/cli/php .phpunit/phpunit-4.8/phpunit --colors=always src/Symfony/Component/Process/'; fi - if [[ ! $deps && $PHP = ${MIN_PHP%.*} ]]; then echo -e "1\\n0" | xargs -I{} sh -c 'echo "\\nPHP --enable-sigchild enhanced={}" && 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 = 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 --ansi --prefer-lowest --prefer-stable; $PHPUNIT --exclude-group tty,benchmark,intl-data'"$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 # 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 && 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 - if [[ $deps = low ]]; then (cd src/Symfony/Bridge/PhpUnit && phpenv global 5.3 && php --version && composer update && php phpunit-4.8.phar); fi

View File

@ -51,7 +51,7 @@ install:
- copy /Y .composer\* %APPDATA%\Composer\ - copy /Y .composer\* %APPDATA%\Composer\
- php .github/build-packages.php "HEAD^" src\Symfony\Bridge\PhpUnit - 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) - 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 - php phpunit install
test_script: test_script:

View File

@ -71,7 +71,7 @@ class WebDebugToolbarListener implements EventSubscriberInterface
$this->urlGenerator->generate('_profiler', array('token' => $response->headers->get('X-Debug-Token')), UrlGeneratorInterface::ABSOLUTE_URL) $this->urlGenerator->generate('_profiler', array('token' => $response->headers->get('X-Debug-Token')), UrlGeneratorInterface::ABSOLUTE_URL)
); );
} catch (\Exception $e) { } 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()));
} }
} }

View File

@ -246,6 +246,27 @@ class WebDebugToolbarListenerTest extends TestCase
$this->assertEquals('Exception: foo', $response->headers->get('X-Debug-Error')); $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) protected function getRequestMock($isXmlHttpRequest = false, $requestFormat = 'html', $hasSession = true)
{ {
$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->setMethods(array('getSession', 'isXmlHttpRequest', 'getRequestFormat'))->disableOriginalConstructor()->getMock(); $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->setMethods(array('getSession', 'isXmlHttpRequest', 'getRequestFormat'))->disableOriginalConstructor()->getMock();

View File

@ -635,12 +635,11 @@ class Application
if (defined('HHVM_VERSION') && $width > 1 << 31) { if (defined('HHVM_VERSION') && $width > 1 << 31) {
$width = 1 << 31; $width = 1 << 31;
} }
$formatter = $output->getFormatter();
$lines = array(); $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) { foreach ($this->splitStringByWidth($line, $width - 4) as $line) {
// pre-format lines to get the right string length // 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); $lines[] = array($line, $lineLength);
$len = max($lineLength, $len); $len = max($lineLength, $len);
@ -648,15 +647,15 @@ class Application
} }
$messages = array(); $messages = array();
$messages[] = $emptyLine = $formatter->format(sprintf('<error>%s</error>', str_repeat(' ', $len))); $messages[] = $emptyLine = sprintf('<error>%s</error>', str_repeat(' ', $len));
$messages[] = $formatter->format(sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title))))); $messages[] = sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title))));
foreach ($lines as $line) { foreach ($lines as $line) {
$messages[] = $formatter->format(sprintf('<error> %s %s</error>', $line[0], str_repeat(' ', $len - $line[1]))); $messages[] = sprintf('<error> %s %s</error>', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1]));
} }
$messages[] = $emptyLine; $messages[] = $emptyLine;
$messages[] = ''; $messages[] = '';
$output->writeln($messages, OutputInterface::OUTPUT_RAW | OutputInterface::VERBOSITY_QUIET); $output->writeln($messages, OutputInterface::VERBOSITY_QUIET);
if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
$output->writeln('<comment>Exception trace:</comment>', OutputInterface::VERBOSITY_QUIET); $output->writeln('<comment>Exception trace:</comment>', OutputInterface::VERBOSITY_QUIET);

View File

@ -105,6 +105,11 @@ abstract class Helper implements HelperInterface
} }
public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, $string) public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, $string)
{
return self::strlen(self::removeDecoration($formatter, $string));
}
public static function removeDecoration(OutputFormatterInterface $formatter, $string)
{ {
$isDecorated = $formatter->isDecorated(); $isDecorated = $formatter->isDecorated();
$formatter->setDecorated(false); $formatter->setDecorated(false);
@ -114,6 +119,6 @@ abstract class Helper implements HelperInterface
$string = preg_replace("/\033\[[^m]*m/", '', $string); $string = preg_replace("/\033\[[^m]*m/", '', $string);
$formatter->setDecorated($isDecorated); $formatter->setDecorated($isDecorated);
return self::strlen($string); return $string;
} }
} }

View File

@ -426,7 +426,7 @@ class Table
if (!strstr($cell, "\n")) { if (!strstr($cell, "\n")) {
continue; continue;
} }
$lines = explode("\n", $cell); $lines = explode("\n", str_replace("\n", "<fg=default;bg=default>\n</>", $cell));
foreach ($lines as $lineKey => $line) { foreach ($lines as $lineKey => $line) {
if ($cell instanceof TableCell) { if ($cell instanceof TableCell) {
$line = new TableCell($line, array('colspan' => $cell->getColspan())); $line = new TableCell($line, array('colspan' => $cell->getColspan()));
@ -467,7 +467,7 @@ class Table
$nbLines = $cell->getRowspan() - 1; $nbLines = $cell->getRowspan() - 1;
$lines = array($cell); $lines = array($cell);
if (strstr($cell, "\n")) { if (strstr($cell, "\n")) {
$lines = explode("\n", $cell); $lines = explode("\n", str_replace("\n", "<fg=default;bg=default>\n</>", $cell));
$nbLines = count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines; $nbLines = count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines;
$rows[$line][$column] = new TableCell($lines[0], array('colspan' => $cell->getColspan())); $rows[$line][$column] = new TableCell($lines[0], array('colspan' => $cell->getColspan()));
@ -602,9 +602,10 @@ class Table
foreach ($row as $i => $cell) { foreach ($row as $i => $cell) {
if ($cell instanceof TableCell) { 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) { if ($textLength > 0) {
$contentColumns = str_split($cell, ceil($textLength / $cell->getColspan())); $contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan()));
foreach ($contentColumns as $position => $content) { foreach ($contentColumns as $position => $content) {
$row[$i + $position] = $content; $row[$i + $position] = $content;
} }

View File

@ -76,9 +76,7 @@ class CommandTester
} }
$this->output = new StreamOutput(fopen('php://memory', 'w', false)); $this->output = new StreamOutput(fopen('php://memory', 'w', false));
if (isset($options['decorated'])) { $this->output->setDecorated(isset($options['decorated']) ? $options['decorated'] : false);
$this->output->setDecorated($options['decorated']);
}
if (isset($options['verbosity'])) { if (isset($options['verbosity'])) {
$this->output->setVerbosity($options['verbosity']); $this->output->setVerbosity($options['verbosity']);
} }

View File

@ -586,6 +586,22 @@ class ApplicationTest extends TestCase
putenv('COLUMNS=120'); 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 <info>!</info>');
});
$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() public function testRun()
{ {
$application = new Application(); $application = new Application();

View File

@ -0,0 +1,9 @@
[Exception]
dont break here <
info>!</info>
foo

View File

@ -511,6 +511,35 @@ TABLE
| Dante Alighieri | J. R. R. Tolkien | J. R. R | | Dante Alighieri | J. R. R. Tolkien | J. R. R |
+-----------------+------------------+---------+ +-----------------+------------------+---------+
TABLE
,
true,
),
'Row with formatted cells containing a newline' => array(
array(),
array(
array(
new TableCell('<error>Dont break'."\n".'here</error>', array('colspan' => 2)),
),
new TableSeparator(),
array(
'foo',
new TableCell('<error>Dont break'."\n".'here</error>', array('rowspan' => 2)),
),
array(
'bar',
),
),
'default',
<<<'TABLE'
+-------+------------+
| Dont break |
| here |
+-------+------------+
| foo | Dont break |
| bar | here |
+-------+------------+
TABLE TABLE
, ,
true, true,

View File

@ -29,7 +29,7 @@ class AutowirePass implements CompilerPassInterface
private $definedTypes = array(); private $definedTypes = array();
private $types; private $types;
private $ambiguousServiceTypes = array(); private $ambiguousServiceTypes = array();
private $usedTypes = array(); private $autowired = array();
/** /**
* {@inheritdoc} * {@inheritdoc}
@ -46,25 +46,15 @@ class AutowirePass implements CompilerPassInterface
$this->completeDefinition($id, $definition); $this->completeDefinition($id, $definition);
} }
} }
foreach ($this->usedTypes as $type => $id) {
if (isset($this->usedTypes[$type]) && isset($this->ambiguousServiceTypes[$type])) {
$classOrInterface = class_exists($type) ? 'class' : 'interface';
$matchingServices = implode(', ', $this->ambiguousServiceTypes[$type]);
throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". Multiple services exist for this %s (%s).', $type, $id, $classOrInterface, $matchingServices));
}
}
} finally { } finally {
spl_autoload_unregister($throwingAutoloader); spl_autoload_unregister($throwingAutoloader);
// Free memory and remove circular reference to container // Free memory and remove circular reference to container
$this->container = null;
$this->reflectionClasses = array(); $this->reflectionClasses = array();
$this->definedTypes = array(); $this->definedTypes = array();
$this->types = null; $this->types = null;
$this->ambiguousServiceTypes = array(); $this->ambiguousServiceTypes = array();
$this->usedTypes = array(); $this->autowired = array();
} }
} }
@ -100,6 +90,10 @@ class AutowirePass implements CompilerPassInterface
*/ */
private function completeDefinition($id, Definition $definition) private function completeDefinition($id, Definition $definition)
{ {
if ($definition->getFactory()) {
throw new RuntimeException(sprintf('Service "%s" can use either autowiring or a factory, not both.', $id));
}
if (!$reflectionClass = $this->getReflectionClass($id, $definition)) { if (!$reflectionClass = $this->getReflectionClass($id, $definition)) {
return; return;
} }
@ -111,47 +105,56 @@ class AutowirePass implements CompilerPassInterface
if (!$constructor = $reflectionClass->getConstructor()) { if (!$constructor = $reflectionClass->getConstructor()) {
return; return;
} }
$parameters = $constructor->getParameters();
if (method_exists('ReflectionMethod', 'isVariadic') && $constructor->isVariadic()) {
array_pop($parameters);
}
$arguments = $definition->getArguments(); $arguments = $definition->getArguments();
foreach ($constructor->getParameters() as $index => $parameter) { foreach ($parameters as $index => $parameter) {
if (array_key_exists($index, $arguments) && '' !== $arguments[$index]) { if (array_key_exists($index, $arguments) && '' !== $arguments[$index]) {
continue; continue;
} }
try { try {
if (!$typeHint = $parameter->getClass()) { if (!$typeHint = $parameter->getClass()) {
if (isset($arguments[$index])) {
continue;
}
// no default value? Then fail // no default value? Then fail
if (!$parameter->isOptional()) { if (!$parameter->isOptional()) {
throw new RuntimeException(sprintf('Unable to autowire argument index %d ($%s) for the service "%s". If this is an object, give it a type-hint. Otherwise, specify this argument\'s value explicitly.', $index, $parameter->name, $id)); throw new RuntimeException(sprintf('Unable to autowire argument index %d ($%s) for the service "%s". If this is an object, give it a type-hint. Otherwise, specify this argument\'s value explicitly.', $index, $parameter->name, $id));
} }
if (!array_key_exists($index, $arguments)) { // specifically pass the default value
// specifically pass the default value $arguments[$index] = $parameter->getDefaultValue();
$arguments[$index] = $parameter->getDefaultValue();
}
continue; continue;
} }
if (isset($this->autowired[$typeHint->name])) {
return $this->autowired[$typeHint->name] ? new Reference($this->autowired[$typeHint->name]) : null;
}
if (null === $this->types) { if (null === $this->types) {
$this->populateAvailableTypes(); $this->populateAvailableTypes();
} }
if (isset($this->types[$typeHint->name])) { if (isset($this->types[$typeHint->name])) {
$value = new Reference($this->types[$typeHint->name]); $value = new Reference($this->types[$typeHint->name]);
$this->usedTypes[$typeHint->name] = $id;
} else { } else {
try { try {
$value = $this->createAutowiredDefinition($typeHint, $id); $value = $this->createAutowiredDefinition($typeHint, $id);
$this->usedTypes[$typeHint->name] = $id;
} catch (RuntimeException $e) { } catch (RuntimeException $e) {
if ($parameter->allowsNull()) { if ($parameter->isDefaultValueAvailable()) {
$value = null;
} elseif ($parameter->isDefaultValueAvailable()) {
$value = $parameter->getDefaultValue(); $value = $parameter->getDefaultValue();
} elseif ($parameter->allowsNull()) {
$value = null;
} else { } else {
throw $e; throw $e;
} }
$this->autowired[$typeHint->name] = false;
} }
} }
} catch (\ReflectionException $e) { } catch (\ReflectionException $e) {
@ -167,6 +170,16 @@ class AutowirePass implements CompilerPassInterface
$arguments[$index] = $value; $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 // 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 // make sure that we re-order so they're injected as expected
ksort($arguments); ksort($arguments);
@ -275,13 +288,11 @@ class AutowirePass implements CompilerPassInterface
throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". No services were found matching this %s and it cannot be auto-registered.', $typeHint->name, $id, $classOrInterface)); throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". No services were found matching this %s and it cannot be auto-registered.', $typeHint->name, $id, $classOrInterface));
} }
$argumentId = sprintf('autowired.%s', $typeHint->name); $this->autowired[$typeHint->name] = $argumentId = sprintf('autowired.%s', $typeHint->name);
$argumentDefinition = $this->container->register($argumentId, $typeHint->name); $argumentDefinition = $this->container->register($argumentId, $typeHint->name);
$argumentDefinition->setPublic(false); $argumentDefinition->setPublic(false);
$this->populateAvailableType($argumentId, $argumentDefinition);
try { try {
$this->completeDefinition($argumentId, $argumentDefinition); $this->completeDefinition($argumentId, $argumentDefinition);
} catch (RuntimeException $e) { } catch (RuntimeException $e) {

View File

@ -26,6 +26,7 @@ use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Resource\ResourceInterface; use Symfony\Component\Config\Resource\ResourceInterface;
use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInterface; use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInterface;
use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\RealServiceInstantiator; use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\RealServiceInstantiator;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
@ -68,7 +69,7 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
*/ */
private $compiler; private $compiler;
private $trackResources = true; private $trackResources;
/** /**
* @var InstantiatorInterface|null * @var InstantiatorInterface|null
@ -85,6 +86,13 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
*/ */
private $expressionLanguageProviders = array(); private $expressionLanguageProviders = array();
public function __construct(ParameterBagInterface $parameterBag = null)
{
parent::__construct($parameterBag);
$this->trackResources = interface_exists('Symfony\Component\Config\Resource\ResourceInterface');
}
/** /**
* @var string[] with tag names used by findTaggedServiceIds * @var string[] with tag names used by findTaggedServiceIds
*/ */

View File

@ -440,10 +440,6 @@ class AutowirePassTest extends TestCase
array( array(
new Reference('a'), new Reference('a'),
new Reference('lille'), 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() $definition->getArguments()
); );
@ -513,23 +509,6 @@ class AutowirePassTest extends TestCase
$this->assertEquals(array(new Reference('a'), '', new Reference('lille')), $container->getDefinition('foo')->getArguments()); $this->assertEquals(array(new Reference('a'), '', new Reference('lille')), $container->getDefinition('foo')->getArguments());
} }
/**
* @dataProvider provideAutodiscoveredAutowiringOrder
*
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
* @expectedExceptionMEssage Unable to autowire argument of type "Symfony\Component\DependencyInjection\Tests\Compiler\CollisionInterface" for the service "a". Multiple services exist for this interface (autowired.Symfony\Component\DependencyInjection\Tests\Compiler\CollisionA, autowired.Symfony\Component\DependencyInjection\Tests\Compiler\CollisionB).
*/
public function testAutodiscoveredAutowiringOrder($class)
{
$container = new ContainerBuilder();
$container->register('a', __NAMESPACE__.'\\'.$class)
->setAutowired(true);
$pass = new AutowirePass();
$pass->process($container);
}
public function provideAutodiscoveredAutowiringOrder() public function provideAutodiscoveredAutowiringOrder()
{ {
return array( return array(
@ -537,6 +516,22 @@ class AutowirePassTest extends TestCase
array('CannotBeAutowiredReverseOrder'), array('CannotBeAutowiredReverseOrder'),
); );
} }
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
* @expectedExceptionMessage Service "a" can use either autowiring or a factory, not both.
*/
public function testWithFactory()
{
$container = new ContainerBuilder();
$container->register('a', __NAMESPACE__.'\A')
->setFactory('foo')
->setAutowired(true);
$pass = new AutowirePass();
$pass->process($container);
}
} }
class Foo class Foo

View File

@ -93,7 +93,7 @@ class EventDispatcher implements EventDispatcherInterface
*/ */
public function hasListeners($eventName = null) public function hasListeners($eventName = null)
{ {
return (bool) count($this->getListeners($eventName)); return (bool) $this->getListeners($eventName);
} }
/** /**

View File

@ -59,12 +59,12 @@ class Lexer
} elseif (false !== strpos(')]}', $expression[$cursor])) { } elseif (false !== strpos(')]}', $expression[$cursor])) {
// closing bracket // closing bracket
if (empty($brackets)) { 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); list($expect, $cur) = array_pop($brackets);
if ($expression[$cursor] != strtr($expect, '([{', ')]}')) { 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); $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1);
@ -87,7 +87,7 @@ class Lexer
$cursor += strlen($match[0]); $cursor += strlen($match[0]);
} else { } else {
// unlexable // 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)) { if (!empty($brackets)) {
list($expect, $cur) = array_pop($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);
} }
} }

View File

@ -99,7 +99,7 @@ class Parser
$node = $this->parseExpression(); $node = $this->parseExpression();
if (!$stream->isEOF()) { 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; return $node;
@ -195,13 +195,13 @@ class Parser
default: default:
if ('(' === $this->stream->current->value) { if ('(' === $this->stream->current->value) {
if (false === isset($this->functions[$token->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()); $node = new Node\FunctionNode($token->value, $this->parseArguments());
} else { } else {
if (!in_array($token->value, $this->names, true)) { 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 // is the name used in the compiled code different
@ -227,7 +227,7 @@ class Parser
} elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) { } elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) {
$node = $this->parseHashExpression(); $node = $this->parseHashExpression();
} else { } 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 { } else {
$current = $this->stream->current; $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 (:)'); $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. // 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)) ($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); $arg = new Node\ConstantNode($token->value, true);

View File

@ -13,8 +13,14 @@ namespace Symfony\Component\ExpressionLanguage;
class SyntaxError extends \LogicException 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);
} }
} }

View File

@ -18,14 +18,43 @@ use Symfony\Component\ExpressionLanguage\TokenStream;
class LexerTest extends TestCase class LexerTest extends TestCase
{ {
/**
* @var Lexer
*/
private $lexer;
protected function setUp()
{
$this->lexer = new Lexer();
}
/** /**
* @dataProvider getTokenizeData * @dataProvider getTokenizeData
*/ */
public function testTokenize($tokens, $expression) public function testTokenize($tokens, $expression)
{ {
$tokens[] = new Token('end of expression', null, strlen($expression) + 1); $tokens[] = new Token('end of expression', null, strlen($expression) + 1);
$lexer = new Lexer(); $this->assertEquals(new TokenStream($tokens, $expression), $this->lexer->tokenize($expression));
$this->assertEquals(new TokenStream($tokens), $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() public function getTokenizeData()

View File

@ -20,7 +20,7 @@ class ParserTest extends TestCase
{ {
/** /**
* @expectedException \Symfony\Component\ExpressionLanguage\SyntaxError * @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() public function testParseWithInvalidName()
{ {
@ -31,7 +31,7 @@ class ParserTest extends TestCase
/** /**
* @expectedException \Symfony\Component\ExpressionLanguage\SyntaxError * @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() public function testParseWithZeroInNames()
{ {

View File

@ -22,16 +22,19 @@ class TokenStream
private $tokens; private $tokens;
private $position = 0; private $position = 0;
private $expression;
/** /**
* Constructor. * 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->tokens = $tokens;
$this->current = $tokens[0]; $this->current = $tokens[0];
$this->expression = $expression;
} }
/** /**
@ -50,7 +53,7 @@ class TokenStream
public function next() public function next()
{ {
if (!isset($this->tokens[$this->position])) { 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; ++$this->position;
@ -69,7 +72,7 @@ class TokenStream
{ {
$token = $this->current; $token = $this->current;
if (!$token->test($type, $value)) { 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(); $this->next();
} }
@ -83,4 +86,14 @@ class TokenStream
{ {
return $this->current->type === Token::EOF_TYPE; return $this->current->type === Token::EOF_TYPE;
} }
/**
* @internal
*
* @return string
*/
public function getExpression()
{
return $this->expression;
}
} }

View File

@ -120,7 +120,7 @@ class PercentToLocalizedStringTransformer implements DataTransformerInterface
$formatter = $this->getNumberFormatter(); $formatter = $this->getNumberFormatter();
// replace normal spaces so that the formatter can read them // 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())) { if (intl_is_failure($formatter->getErrorCode())) {
throw new TransformationFailedException($formatter->getErrorMessage()); throw new TransformationFailedException($formatter->getErrorMessage());

View File

@ -407,6 +407,10 @@ class Form implements \IteratorAggregate, FormInterface
} }
if (!$this->defaultDataSet) { 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()); $this->setData($this->config->getData());
} }
@ -427,6 +431,10 @@ class Form implements \IteratorAggregate, FormInterface
} }
if (!$this->defaultDataSet) { 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()); $this->setData($this->config->getData());
} }
@ -447,6 +455,10 @@ class Form implements \IteratorAggregate, FormInterface
} }
if (!$this->defaultDataSet) { 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()); $this->setData($this->config->getData());
} }

View File

@ -598,7 +598,7 @@ class DateTypeTest extends BaseTypeTest
)); ));
$form->submit(array( $form->submit(array(
'day' => '0', 'day' => '1',
'month' => '6', 'month' => '6',
'year' => '2010', 'year' => '2010',
)); ));

View File

@ -896,6 +896,7 @@ class SimpleFormTest extends AbstractFormTest
/** /**
* @expectedException \Symfony\Component\Form\Exception\RuntimeException * @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() public function testSetDataCannotInvokeItself()
{ {
@ -1062,6 +1063,51 @@ class SimpleFormTest extends AbstractFormTest
$child->initialize(); $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() protected function createForm()
{ {
return $this->getBuilder()->getForm(); return $this->getBuilder()->getForm();

View File

@ -211,7 +211,7 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property
* @param string $ucFirstProperty * @param string $ucFirstProperty
* @param int $type * @param int $type
* *
* @return array * @return array|null
*/ */
private function getDocBlockFromMethod($class, $ucFirstProperty, $type) private function getDocBlockFromMethod($class, $ucFirstProperty, $type)
{ {

View File

@ -68,6 +68,7 @@ class PhpDocExtractorTest extends TestCase
array('d', array(new Type(Type::BUILTIN_TYPE_BOOL)), null, null), 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('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('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('donotexist', null, null, null),
array('staticGetter', null, null, null), array('staticGetter', null, null, null),
array('staticSetter', null, null, null), array('staticSetter', null, null, null),

View File

@ -39,6 +39,7 @@ class ReflectionExtractorTest extends TestCase
'parent', 'parent',
'collection', 'collection',
'B', 'B',
'g',
'foo', 'foo',
'foo2', 'foo2',
'foo3', 'foo3',

View File

@ -51,6 +51,13 @@ class Dummy extends ParentDummy
*/ */
public $B; public $B;
/**
* Nullable array.
*
* @var array|null
*/
public $g;
public static function getStatic() public static function getStatic()
{ {
} }

View File

@ -101,10 +101,10 @@ final class PhpDocTypeHelper
$collectionValueType = null; $collectionValueType = null;
} else { } else {
$collectionKeyType = new Type(Type::BUILTIN_TYPE_INT); $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); return new Type($phpType, $nullable, $class);