diff --git a/.travis.yml b/.travis.yml index 778d0e9c04..b5a28f71fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ env: matrix: include: # Use the newer stack for HHVM as HHVM does not support Precise anymore since a long time and so Precise has an outdated version - - php: hhvm + - php: hhvm-3.12 sudo: required dist: trusty group: edge diff --git a/src/Symfony/Bridge/Doctrine/HttpFoundation/DbalSessionHandler.php b/src/Symfony/Bridge/Doctrine/HttpFoundation/DbalSessionHandler.php index fd7dcff62c..c3570da7da 100644 --- a/src/Symfony/Bridge/Doctrine/HttpFoundation/DbalSessionHandler.php +++ b/src/Symfony/Bridge/Doctrine/HttpFoundation/DbalSessionHandler.php @@ -180,7 +180,7 @@ class DbalSessionHandler implements \SessionHandlerInterface $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); $updateStmt->execute(); - // When MERGE is not supported, like in Postgres, we have to use this approach that can result in + // When MERGE is not supported, like in Postgres < 9.5, we have to use this approach that can result in // duplicate key errors when the same session is written simultaneously. We can just catch such an // error and re-execute the update. This is similar to a serializable transaction with retry logic // on serialization failures but without the overhead and without possible false positives due to @@ -224,11 +224,11 @@ class DbalSessionHandler implements \SessionHandlerInterface { $platform = $this->con->getDatabasePlatform()->getName(); - switch ($platform) { - case 'mysql': + switch (true) { + case 'mysql' === $platform: return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ". "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->timeCol = VALUES($this->timeCol)"; - case 'oracle': + case 'oracle' === $platform: // DUAL is Oracle specific dummy table return "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) ". "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ". @@ -239,8 +239,11 @@ class DbalSessionHandler implements \SessionHandlerInterface return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = :id) ". "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ". "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time;"; - case 'sqlite': + case 'sqlite' === $platform: return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)"; + case 'postgresql' === $platform && version_compare($this->con->getServerVersion(), '9.5', '>='): + return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ". + "ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->timeCol) = (:data, :time) WHERE $this->idCol = :id"; } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_attributes.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_attributes.html.php new file mode 100644 index 0000000000..8d2d6d7fd6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_attributes.html.php @@ -0,0 +1,9 @@ +id="escape($id) ?>" name="escape($full_name) ?>" +disabled="disabled" + $v): ?> + +escape($k), $view->escape($k)) ?> + +escape($k), $view->escape($v)) ?> + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php index a0363cc5a4..f1c6ad3b56 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php @@ -8,6 +8,6 @@ $translatorHelper = $view['translator']; // outside of the loop for performance block($form, 'choice_widget_options', array('choices' => $choice)) ?> - + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig index 03617dfa95..cb4fbeed7e 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig @@ -211,7 +211,7 @@ var addEventListener; var el = document.createElement('div'); - if (!'addEventListener' in el) { + if (!('addEventListener' in el)) { addEventListener = function (element, eventName, callback) { element.attachEvent('on' + eventName, callback); }; diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index e7717a528f..0d4798cd48 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -42,7 +42,7 @@ class ProgressBar private $stepWidth; private $percent = 0.0; private $formatLineCount; - private $messages; + private $messages = array(); private $overwrite = true; private static $formatters; @@ -140,6 +140,16 @@ class ProgressBar return isset(self::$formats[$name]) ? self::$formats[$name] : null; } + /** + * Associates a text with a named placeholder. + * + * The text is displayed when the progress bar is rendered but only + * when the corresponding placeholder is part of the custom format line + * (by wrapping the name with %). + * + * @param string $message The text to associate with the placeholder + * @param string $name The name of the placeholder + */ public function setMessage($message, $name = 'message') { $this->messages[$name] = $message; diff --git a/src/Symfony/Component/Console/Helper/TableSeparator.php b/src/Symfony/Component/Console/Helper/TableSeparator.php index 8cbbc6613b..8cc73e69a2 100644 --- a/src/Symfony/Component/Console/Helper/TableSeparator.php +++ b/src/Symfony/Component/Console/Helper/TableSeparator.php @@ -19,8 +19,7 @@ namespace Symfony\Component\Console\Helper; class TableSeparator extends TableCell { /** - * @param string $value - * @param array $options + * @param array $options */ public function __construct(array $options = array()) { diff --git a/src/Symfony/Component/Console/Output/NullOutput.php b/src/Symfony/Component/Console/Output/NullOutput.php index 682f9a4d43..218f285bfe 100644 --- a/src/Symfony/Component/Console/Output/NullOutput.php +++ b/src/Symfony/Component/Console/Output/NullOutput.php @@ -73,21 +73,33 @@ class NullOutput implements OutputInterface return self::VERBOSITY_QUIET; } + /** + * {@inheritdoc} + */ public function isQuiet() { return true; } + /** + * {@inheritdoc} + */ public function isVerbose() { return false; } + /** + * {@inheritdoc} + */ public function isVeryVerbose() { return false; } + /** + * {@inheritdoc} + */ public function isDebug() { return false; diff --git a/src/Symfony/Component/Console/Output/Output.php b/src/Symfony/Component/Console/Output/Output.php index 4476ffb590..c12015cc8f 100644 --- a/src/Symfony/Component/Console/Output/Output.php +++ b/src/Symfony/Component/Console/Output/Output.php @@ -94,21 +94,33 @@ abstract class Output implements OutputInterface return $this->verbosity; } + /** + * {@inheritdoc} + */ public function isQuiet() { return self::VERBOSITY_QUIET === $this->verbosity; } + /** + * {@inheritdoc} + */ public function isVerbose() { return self::VERBOSITY_VERBOSE <= $this->verbosity; } + /** + * {@inheritdoc} + */ public function isVeryVerbose() { return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity; } + /** + * {@inheritdoc} + */ public function isDebug() { return self::VERBOSITY_DEBUG <= $this->verbosity; diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index 300a19ffcf..96b0b59705 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -338,7 +338,6 @@ class XmlFileLoader extends FileLoader foreach ($definitions as $id => $def) { list($domElement, $file, $wild) = $def; - if (null !== $definition = $this->parseDefinition($domElement, $file)) { $this->container->setDefinition($id, $definition); } @@ -508,7 +507,9 @@ $imports EOF ; + $disableEntities = libxml_disable_entity_loader(false); $valid = @$dom->schemaValidateSource($source); + libxml_disable_entity_loader($disableEntities); foreach ($tmpfiles as $tmpfile) { @unlink($tmpfile); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index 5d7b3f0fac..7322298a79 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -84,6 +84,19 @@ class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase $this->assertInstanceOf('DOMDocument', $xml, '->parseFileToDOM() returns an SimpleXMLElement object'); } + public function testLoadWithExternalEntitiesDisabled() + { + $disableEntities = libxml_disable_entity_loader(true); + + $containerBuilder = new ContainerBuilder(); + $loader = new XmlFileLoader($containerBuilder, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services2.xml'); + + libxml_disable_entity_loader($disableEntities); + + $this->assertTrue(count($containerBuilder->getParameterBag()->all()) > 0, 'Parameters can be read from the config file.'); + } + public function testLoadParameters() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php index bc235a5c42..7a283ea041 100644 --- a/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractBootstrap3LayoutTest.php @@ -232,6 +232,68 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest ); } + public function testSingleChoiceAttributesWithMainAttributes() + { + $form = $this->factory->createNamed('name', 'choice', '&a', array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), + 'choices_as_values' => true, + 'multiple' => false, + 'expanded' => false, + 'attr' => array('class' => 'bar&baz'), + )); + + $this->assertWidgetMatchesXpath($form->createView(), array('attr' => array('class' => 'bar&baz')), +'/select + [@name="name"] + [@class="bar&baz form-control"] + [not(@required)] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + + public function testSingleExpandedChoiceAttributesWithMainAttributes() + { + $form = $this->factory->createNamed('name', 'choice', '&a', array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), + 'choices_as_values' => true, + 'multiple' => false, + 'expanded' => true, + 'attr' => array('class' => 'bar&baz'), + )); + + $this->assertWidgetMatchesXpath($form->createView(), array('attr' => array('class' => 'bar&baz')), +'/div + [@class="bar&baz"] + [ + ./div + [@class="radio"] + [ + ./label + [.=" [trans]Choice&A[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] + ] + ] + /following-sibling::div + [@class="radio"] + [ + ./label + [.=" [trans]Choice&B[/trans]"] + [ + ./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)] + ] + ] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] +' + ); + } + public function testSelectWithSizeBiggerThanOneCanBeRequired() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', null, array( diff --git a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php index 953555724a..b3cbfd7b4c 100644 --- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php @@ -641,6 +641,55 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormInteg ); } + public function testSingleChoiceAttributesWithMainAttributes() + { + $form = $this->factory->createNamed('name', 'choice', '&a', array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), + 'choices_as_values' => true, + 'multiple' => false, + 'expanded' => false, + 'attr' => array('class' => 'bar&baz'), + )); + + $this->assertWidgetMatchesXpath($form->createView(), array('attr' => array('class' => 'bar&baz')), +'/select + [@name="name"] + [@class="bar&baz"] + [not(@required)] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][not(@class)][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + + public function testSingleExpandedChoiceAttributesWithMainAttributes() + { + $form = $this->factory->createNamed('name', 'choice', '&a', array( + 'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'), + 'choices_as_values' => true, + 'multiple' => false, + 'expanded' => true, + 'attr' => array('class' => 'bar&baz'), + )); + + $this->assertWidgetMatchesXpath($form->createView(), array('attr' => array('class' => 'bar&baz')), +'/div + [@class="bar&baz"] + [ + ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked] + /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"] + /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)] + /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"] + /following-sibling::input[@type="hidden"][@id="name__token"] + ] + [count(./input)=3] +' + ); + } + public function testSingleChoiceWithPreferred() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array( diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php index 48e81ee0f1..1aad67960a 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php @@ -347,7 +347,7 @@ class PdoSessionHandler implements \SessionHandlerInterface $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); $updateStmt->execute(); - // When MERGE is not supported, like in Postgres, we have to use this approach that can result in + // When MERGE is not supported, like in Postgres < 9.5, we have to use this approach that can result in // duplicate key errors when the same session is written simultaneously (given the LOCK_NONE behavior). // We can just catch such an error and re-execute the update. This is similar to a serializable // transaction with retry logic on serialization failures but without the overhead and without possible @@ -659,11 +659,11 @@ class PdoSessionHandler implements \SessionHandlerInterface */ private function getMergeSql() { - switch ($this->driver) { - case 'mysql': + switch (true) { + case 'mysql' === $this->driver: return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ". "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)"; - case 'oci': + case 'oci' === $this->driver: // DUAL is Oracle specific dummy table return "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) ". "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ". @@ -674,8 +674,11 @@ class PdoSessionHandler implements \SessionHandlerInterface return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = :id) ". "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ". "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time;"; - case 'sqlite': + case 'sqlite' === $this->driver: return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"; + case 'pgsql' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '9.5', '>='): + return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ". + "ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (:data, :lifetime, :time) WHERE $this->idCol = :id"; } } diff --git a/src/Symfony/Component/Translation/Loader/XliffFileLoader.php b/src/Symfony/Component/Translation/Loader/XliffFileLoader.php index a02f544a91..4e2b7c182b 100644 --- a/src/Symfony/Component/Translation/Loader/XliffFileLoader.php +++ b/src/Symfony/Component/Translation/Loader/XliffFileLoader.php @@ -173,10 +173,16 @@ class XliffFileLoader implements LoaderInterface { $internalErrors = libxml_use_internal_errors(true); + $disableEntities = libxml_disable_entity_loader(false); + if (!@$dom->schemaValidateSource($schema)) { + libxml_disable_entity_loader($disableEntities); + throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: %s', $file, implode("\n", $this->getXmlErrors($internalErrors)))); } + libxml_disable_entity_loader($disableEntities); + $dom->normalizeDocument(); libxml_clear_errors(); diff --git a/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php index 9aafa4b161..2f466d879c 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php @@ -46,6 +46,20 @@ class XliffFileLoaderTest extends \PHPUnit_Framework_TestCase libxml_use_internal_errors($internalErrors); } + public function testLoadWithExternalEntitiesDisabled() + { + $disableEntities = libxml_disable_entity_loader(true); + + $loader = new XliffFileLoader(); + $resource = __DIR__.'/../fixtures/resources.xlf'; + $catalogue = $loader->load($resource, 'en', 'domain1'); + + libxml_disable_entity_loader($disableEntities); + + $this->assertEquals('en', $catalogue->getLocale()); + $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources()); + } + public function testLoadWithResname() { $loader = new XliffFileLoader(); diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index 5d719a33bd..7cdb964393 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -30,17 +30,21 @@ class Parser private $currentLineNb = -1; private $currentLine = ''; private $refs = array(); + private $skippedLineNumbers = array(); + private $locallySkippedLineNumbers = array(); /** * Constructor. * * @param int $offset The offset of YAML document (used for line numbers in error messages) * @param int|null $totalNumberOfLines The overall number of lines being parsed + * @param int[] $skippedLineNumbers Number of comment lines that have been skipped by the parser */ - public function __construct($offset = 0, $totalNumberOfLines = null) + public function __construct($offset = 0, $totalNumberOfLines = null, array $skippedLineNumbers = array()) { $this->offset = $offset; $this->totalNumberOfLines = $totalNumberOfLines; + $this->skippedLineNumbers = $skippedLineNumbers; } /** @@ -101,25 +105,18 @@ class Parser // array if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) { - $c = $this->getRealCurrentLineNb() + 1; - $parser = new self($c, $this->totalNumberOfLines); - $parser->refs = &$this->refs; - $data[] = $parser->parse($this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap); + $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap); } else { if (isset($values['leadspaces']) && preg_match('#^(?P'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P.+?))?\s*$#u', $values['value'], $matches) ) { // this is a compact notation element, add to next block and parse - $c = $this->getRealCurrentLineNb(); - $parser = new self($c, $this->totalNumberOfLines); - $parser->refs = &$this->refs; - $block = $values['value']; if ($this->isNextLineIndented()) { $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1); } - $data[] = $parser->parse($block, $exceptionOnInvalidType, $objectSupport, $objectForMap); + $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $exceptionOnInvalidType, $objectSupport, $objectForMap); } else { $data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context); } @@ -175,10 +172,7 @@ class Parser } else { $value = $this->getNextEmbedBlock(); } - $c = $this->getRealCurrentLineNb() + 1; - $parser = new self($c, $this->totalNumberOfLines); - $parser->refs = &$this->refs; - $parsed = $parser->parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap); + $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $exceptionOnInvalidType, $objectSupport, $objectForMap); if (!is_array($parsed)) { throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine); @@ -226,10 +220,7 @@ class Parser $data[$key] = null; } } else { - $c = $this->getRealCurrentLineNb() + 1; - $parser = new self($c, $this->totalNumberOfLines); - $parser->refs = &$this->refs; - $value = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap); + $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap); // Spec: Keys MUST be unique; first one wins. // But overwriting is allowed when a merge node is used in current block. if ($allowOverwrite || !isset($data[$key])) { @@ -323,6 +314,24 @@ class Parser return empty($data) ? null : $data; } + private function parseBlock($offset, $yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap) + { + $skippedLineNumbers = $this->skippedLineNumbers; + + foreach ($this->locallySkippedLineNumbers as $lineNumber) { + if ($lineNumber < $offset) { + continue; + } + + $skippedLineNumbers[] = $lineNumber; + } + + $parser = new self($offset, $this->totalNumberOfLines, $skippedLineNumbers); + $parser->refs = &$this->refs; + + return $parser->parse($yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap); + } + /** * Returns the current line number (takes the offset into account). * @@ -330,7 +339,17 @@ class Parser */ private function getRealCurrentLineNb() { - return $this->currentLineNb + $this->offset; + $realCurrentLineNumber = $this->currentLineNb + $this->offset; + + foreach ($this->skippedLineNumbers as $skippedLineNumber) { + if ($skippedLineNumber > $realCurrentLineNumber) { + break; + } + + ++$realCurrentLineNumber; + } + + return $realCurrentLineNumber; } /** @@ -432,7 +451,15 @@ class Parser } // we ignore "comment" lines only when we are not inside a scalar block - if (empty($blockScalarIndentations) && $this->isCurrentLineComment() && false === $this->checkIfPreviousNonCommentLineIsCollectionItem()) { + if (empty($blockScalarIndentations) && $this->isCurrentLineComment()) { + // remember ignored comment lines (they are used later in nested + // parser calls to determine real line numbers) + // + // CAUTION: beware to not populate the global property here as it + // will otherwise influence the getRealCurrentLineNb() call here + // for consecutive comment lines and subsequent embedded blocks + $this->locallySkippedLineNumbers[] = $this->getRealCurrentLineNb(); + continue; } @@ -802,44 +829,4 @@ class Parser { return (bool) preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine); } - - /** - * Returns true if the current line is a collection item. - * - * @return bool - */ - private function isCurrentLineCollectionItem() - { - $ltrimmedLine = ltrim($this->currentLine, ' '); - - return '' !== $ltrimmedLine && '-' === $ltrimmedLine[0]; - } - - /** - * Tests whether the current comment line is in a collection. - * - * @return bool - */ - private function checkIfPreviousNonCommentLineIsCollectionItem() - { - $isCollectionItem = false; - $moves = 0; - while ($this->moveToPreviousLine()) { - ++$moves; - // If previous line is a comment, move back again. - if ($this->isCurrentLineComment()) { - continue; - } - $isCollectionItem = $this->isCurrentLineCollectionItem(); - break; - } - - // Move parser back to previous line. - while ($moves > 0) { - $this->moveToNextLine(); - --$moves; - } - - return $isCollectionItem; - } } diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php index 396c51b777..0152e07ffe 100644 --- a/src/Symfony/Component/Yaml/Tests/ParserTest.php +++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php @@ -656,6 +656,25 @@ EOT; $this->assertSame($expected, $this->parser->parse($yaml)); } + public function testSequenceFollowedByCommentEmbeddedInMapping() + { + $yaml = << array( + 'b' => array('c'), + 'd' => 'e', + ), + ); + + $this->assertSame($expected, $this->parser->parse($yaml)); + } + /** * @expectedException \Symfony\Component\Yaml\Exception\ParseException */