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
*/