vendor/symfony/symfony/src/Symfony/Component/Yaml/Parser.php line 383

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Yaml;
  11. use Symfony\Component\Yaml\Exception\ParseException;
  12. use Symfony\Component\Yaml\Tag\TaggedValue;
  13. /**
  14.  * Parser parses YAML strings to convert them to PHP arrays.
  15.  *
  16.  * @author Fabien Potencier <fabien@symfony.com>
  17.  *
  18.  * @final since version 3.4
  19.  */
  20. class Parser
  21. {
  22.     const TAG_PATTERN '(?P<tag>![\w!.\/:-]+)';
  23.     const BLOCK_SCALAR_HEADER_PATTERN '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
  24.     private $filename;
  25.     private $offset 0;
  26.     private $totalNumberOfLines;
  27.     private $lines = array();
  28.     private $currentLineNb = -1;
  29.     private $currentLine '';
  30.     private $refs = array();
  31.     private $skippedLineNumbers = array();
  32.     private $locallySkippedLineNumbers = array();
  33.     public function __construct()
  34.     {
  35.         if (func_num_args() > 0) {
  36.             @trigger_error(sprintf('The constructor arguments $offset, $totalNumberOfLines, $skippedLineNumbers of %s are deprecated and will be removed in 4.0'self::class), E_USER_DEPRECATED);
  37.             $this->offset func_get_arg(0);
  38.             if (func_num_args() > 1) {
  39.                 $this->totalNumberOfLines func_get_arg(1);
  40.             }
  41.             if (func_num_args() > 2) {
  42.                 $this->skippedLineNumbers func_get_arg(2);
  43.             }
  44.         }
  45.     }
  46.     /**
  47.      * Parses a YAML file into a PHP value.
  48.      *
  49.      * @param string $filename The path to the YAML file to be parsed
  50.      * @param int    $flags    A bit field of PARSE_* constants to customize the YAML parser behavior
  51.      *
  52.      * @return mixed The YAML converted to a PHP value
  53.      *
  54.      * @throws ParseException If the file could not be read or the YAML is not valid
  55.      */
  56.     public function parseFile($filename$flags 0)
  57.     {
  58.         if (!is_file($filename)) {
  59.             throw new ParseException(sprintf('File "%s" does not exist.'$filename));
  60.         }
  61.         if (!is_readable($filename)) {
  62.             throw new ParseException(sprintf('File "%s" cannot be read.'$filename));
  63.         }
  64.         $this->filename $filename;
  65.         try {
  66.             return $this->parse(file_get_contents($filename), $flags);
  67.         } finally {
  68.             $this->filename null;
  69.         }
  70.     }
  71.     /**
  72.      * Parses a YAML string to a PHP value.
  73.      *
  74.      * @param string $value A YAML string
  75.      * @param int    $flags A bit field of PARSE_* constants to customize the YAML parser behavior
  76.      *
  77.      * @return mixed A PHP value
  78.      *
  79.      * @throws ParseException If the YAML is not valid
  80.      */
  81.     public function parse($value$flags 0)
  82.     {
  83.         if (is_bool($flags)) {
  84.             @trigger_error('Passing a boolean flag to toggle exception handling is deprecated since Symfony 3.1 and will be removed in 4.0. Use the Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE flag instead.'E_USER_DEPRECATED);
  85.             if ($flags) {
  86.                 $flags Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE;
  87.             } else {
  88.                 $flags 0;
  89.             }
  90.         }
  91.         if (func_num_args() >= 3) {
  92.             @trigger_error('Passing a boolean flag to toggle object support is deprecated since Symfony 3.1 and will be removed in 4.0. Use the Yaml::PARSE_OBJECT flag instead.'E_USER_DEPRECATED);
  93.             if (func_get_arg(2)) {
  94.                 $flags |= Yaml::PARSE_OBJECT;
  95.             }
  96.         }
  97.         if (func_num_args() >= 4) {
  98.             @trigger_error('Passing a boolean flag to toggle object for map support is deprecated since Symfony 3.1 and will be removed in 4.0. Use the Yaml::PARSE_OBJECT_FOR_MAP flag instead.'E_USER_DEPRECATED);
  99.             if (func_get_arg(3)) {
  100.                 $flags |= Yaml::PARSE_OBJECT_FOR_MAP;
  101.             }
  102.         }
  103.         if (Yaml::PARSE_KEYS_AS_STRINGS $flags) {
  104.             @trigger_error('Using the Yaml::PARSE_KEYS_AS_STRINGS flag is deprecated since Symfony 3.4 as it will be removed in 4.0. Quote your keys when they are evaluable instead.'E_USER_DEPRECATED);
  105.         }
  106.         if (false === preg_match('//u'$value)) {
  107.             throw new ParseException('The YAML value does not appear to be valid UTF-8.', -1null$this->filename);
  108.         }
  109.         $this->refs = array();
  110.         $mbEncoding null;
  111.         $e null;
  112.         $data null;
  113.         if (/* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
  114.             $mbEncoding mb_internal_encoding();
  115.             mb_internal_encoding('UTF-8');
  116.         }
  117.         try {
  118.             $data $this->doParse($value$flags);
  119.         } catch (\Exception $e) {
  120.         } catch (\Throwable $e) {
  121.         }
  122.         if (null !== $mbEncoding) {
  123.             mb_internal_encoding($mbEncoding);
  124.         }
  125.         $this->lines = array();
  126.         $this->currentLine '';
  127.         $this->refs = array();
  128.         $this->skippedLineNumbers = array();
  129.         $this->locallySkippedLineNumbers = array();
  130.         if (null !== $e) {
  131.             throw $e;
  132.         }
  133.         return $data;
  134.     }
  135.     private function doParse($value$flags)
  136.     {
  137.         $this->currentLineNb = -1;
  138.         $this->currentLine '';
  139.         $value $this->cleanup($value);
  140.         $this->lines explode("\n"$value);
  141.         $this->locallySkippedLineNumbers = array();
  142.         if (null === $this->totalNumberOfLines) {
  143.             $this->totalNumberOfLines count($this->lines);
  144.         }
  145.         if (!$this->moveToNextLine()) {
  146.             return null;
  147.         }
  148.         $data = array();
  149.         $context null;
  150.         $allowOverwrite false;
  151.         while ($this->isCurrentLineEmpty()) {
  152.             if (!$this->moveToNextLine()) {
  153.                 return null;
  154.             }
  155.         }
  156.         // Resolves the tag and returns if end of the document
  157.         if (null !== ($tag $this->getLineTag($this->currentLine$flagsfalse)) && !$this->moveToNextLine()) {
  158.             return new TaggedValue($tag'');
  159.         }
  160.         do {
  161.             if ($this->isCurrentLineEmpty()) {
  162.                 continue;
  163.             }
  164.             // tab?
  165.             if ("\t" === $this->currentLine[0]) {
  166.                 throw new ParseException('A YAML file cannot contain tabs as indentation.'$this->getRealCurrentLineNb() + 1$this->currentLine$this->filename);
  167.             }
  168.             Inline::initialize($flags$this->getRealCurrentLineNb(), $this->filename);
  169.             $isRef $mergeNode false;
  170.             if (self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u'rtrim($this->currentLine), $values)) {
  171.                 if ($context && 'mapping' == $context) {
  172.                     throw new ParseException('You cannot define a sequence item when in a mapping'$this->getRealCurrentLineNb() + 1$this->currentLine$this->filename);
  173.                 }
  174.                 $context 'sequence';
  175.                 if (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u'$values['value'], $matches)) {
  176.                     $isRef $matches['ref'];
  177.                     $values['value'] = $matches['value'];
  178.                 }
  179.                 if (isset($values['value'][1]) && '?' === $values['value'][0] && ' ' === $values['value'][1]) {
  180.                     @trigger_error($this->getDeprecationMessage('Starting an unquoted string with a question mark followed by a space is deprecated since Symfony 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.'), E_USER_DEPRECATED);
  181.                 }
  182.                 // array
  183.                 if (!isset($values['value']) || '' == trim($values['value'], ' ') || === strpos(ltrim($values['value'], ' '), '#')) {
  184.                     $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1$this->getNextEmbedBlock(nulltrue), $flags);
  185.                 } elseif (null !== $subTag $this->getLineTag(ltrim($values['value'], ' '), $flags)) {
  186.                     $data[] = new TaggedValue(
  187.                         $subTag,
  188.                         $this->parseBlock($this->getRealCurrentLineNb() + 1$this->getNextEmbedBlock(nulltrue), $flags)
  189.                     );
  190.                 } else {
  191.                     if (isset($values['leadspaces'])
  192.                         && self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u'$this->trimTag($values['value']), $matches)
  193.                     ) {
  194.                         // this is a compact notation element, add to next block and parse
  195.                         $block $values['value'];
  196.                         if ($this->isNextLineIndented()) {
  197.                             $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1);
  198.                         }
  199.                         $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block$flags);
  200.                     } else {
  201.                         $data[] = $this->parseValue($values['value'], $flags$context);
  202.                     }
  203.                 }
  204.                 if ($isRef) {
  205.                     $this->refs[$isRef] = end($data);
  206.                 }
  207.             } elseif (
  208.                 self::preg_match('#^(?P<key>(?:![^\s]++\s++)?(?:'.Inline::REGEX_QUOTED_STRING.'|(?:!?!php/const:)?[^ \'"\[\{!].*?)) *\:(\s++(?P<value>.+))?$#u'rtrim($this->currentLine), $values)
  209.                 && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"'"'")))
  210.             ) {
  211.                 if ($context && 'sequence' == $context) {
  212.                     throw new ParseException('You cannot define a mapping item when in a sequence'$this->currentLineNb 1$this->currentLine$this->filename);
  213.                 }
  214.                 $context 'mapping';
  215.                 try {
  216.                     $i 0;
  217.                     $evaluateKey = !(Yaml::PARSE_KEYS_AS_STRINGS $flags);
  218.                     // constants in key will be evaluated anyway
  219.                     if (isset($values['key'][0]) && '!' === $values['key'][0] && Yaml::PARSE_CONSTANT $flags) {
  220.                         $evaluateKey true;
  221.                     }
  222.                     $key Inline::parseScalar($values['key'], 0null$i$evaluateKey);
  223.                 } catch (ParseException $e) {
  224.                     $e->setParsedLine($this->getRealCurrentLineNb() + 1);
  225.                     $e->setSnippet($this->currentLine);
  226.                     throw $e;
  227.                 }
  228.                 if (!is_string($key) && !is_int($key)) {
  229.                     $keyType is_numeric($key) ? 'numeric key' 'non-string key';
  230.                     @trigger_error($this->getDeprecationMessage(sprintf('Implicit casting of %s to string is deprecated since Symfony 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Quote your evaluable mapping keys instead.'$keyType)), E_USER_DEPRECATED);
  231.                 }
  232.                 // Convert float keys to strings, to avoid being converted to integers by PHP
  233.                 if (is_float($key)) {
  234.                     $key = (string) $key;
  235.                 }
  236.                 if ('<<' === $key && (!isset($values['value']) || !self::preg_match('#^&(?P<ref>[^ ]+)#u'$values['value'], $refMatches))) {
  237.                     $mergeNode true;
  238.                     $allowOverwrite true;
  239.                     if (isset($values['value'][0]) && '*' === $values['value'][0]) {
  240.                         $refName substr(rtrim($values['value']), 1);
  241.                         if (!array_key_exists($refName$this->refs)) {
  242.                             throw new ParseException(sprintf('Reference "%s" does not exist.'$refName), $this->getRealCurrentLineNb() + 1$this->currentLine$this->filename);
  243.                         }
  244.                         $refValue $this->refs[$refName];
  245.                         if (Yaml::PARSE_OBJECT_FOR_MAP $flags && $refValue instanceof \stdClass) {
  246.                             $refValue = (array) $refValue;
  247.                         }
  248.                         if (!is_array($refValue)) {
  249.                             throw new ParseException('YAML merge keys used with a scalar value instead of an array.'$this->getRealCurrentLineNb() + 1$this->currentLine$this->filename);
  250.                         }
  251.                         $data += $refValue// array union
  252.                     } else {
  253.                         if (isset($values['value']) && '' !== $values['value']) {
  254.                             $value $values['value'];
  255.                         } else {
  256.                             $value $this->getNextEmbedBlock();
  257.                         }
  258.                         $parsed $this->parseBlock($this->getRealCurrentLineNb() + 1$value$flags);
  259.                         if (Yaml::PARSE_OBJECT_FOR_MAP $flags && $parsed instanceof \stdClass) {
  260.                             $parsed = (array) $parsed;
  261.                         }
  262.                         if (!is_array($parsed)) {
  263.                             throw new ParseException('YAML merge keys used with a scalar value instead of an array.'$this->getRealCurrentLineNb() + 1$this->currentLine$this->filename);
  264.                         }
  265.                         if (isset($parsed[0])) {
  266.                             // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
  267.                             // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
  268.                             // in the sequence override keys specified in later mapping nodes.
  269.                             foreach ($parsed as $parsedItem) {
  270.                                 if (Yaml::PARSE_OBJECT_FOR_MAP $flags && $parsedItem instanceof \stdClass) {
  271.                                     $parsedItem = (array) $parsedItem;
  272.                                 }
  273.                                 if (!is_array($parsedItem)) {
  274.                                     throw new ParseException('Merge items must be arrays.'$this->getRealCurrentLineNb() + 1$parsedItem$this->filename);
  275.                                 }
  276.                                 $data += $parsedItem// array union
  277.                             }
  278.                         } else {
  279.                             // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
  280.                             // current mapping, unless the key already exists in it.
  281.                             $data += $parsed// array union
  282.                         }
  283.                     }
  284.                 } elseif ('<<' !== $key && isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]++) *+(?P<value>.*)#u'$values['value'], $matches)) {
  285.                     $isRef $matches['ref'];
  286.                     $values['value'] = $matches['value'];
  287.                 }
  288.                 $subTag null;
  289.                 if ($mergeNode) {
  290.                     // Merge keys
  291.                 } elseif (!isset($values['value']) || '' === $values['value'] || === strpos($values['value'], '#') || (null !== $subTag $this->getLineTag($values['value'], $flags)) || '<<' === $key) {
  292.                     // hash
  293.                     // if next line is less indented or equal, then it means that the current value is null
  294.                     if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
  295.                         // Spec: Keys MUST be unique; first one wins.
  296.                         // But overwriting is allowed when a merge node is used in current block.
  297.                         if ($allowOverwrite || !isset($data[$key])) {
  298.                             if (null !== $subTag) {
  299.                                 $data[$key] = new TaggedValue($subTag'');
  300.                             } else {
  301.                                 $data[$key] = null;
  302.                             }
  303.                         } else {
  304.                             @trigger_error($this->getDeprecationMessage(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since Symfony 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.'$key)), E_USER_DEPRECATED);
  305.                         }
  306.                     } else {
  307.                         $value $this->parseBlock($this->getRealCurrentLineNb() + 1$this->getNextEmbedBlock(), $flags);
  308.                         if ('<<' === $key) {
  309.                             $this->refs[$refMatches['ref']] = $value;
  310.                             if (Yaml::PARSE_OBJECT_FOR_MAP $flags && $value instanceof \stdClass) {
  311.                                 $value = (array) $value;
  312.                             }
  313.                             $data += $value;
  314.                         } elseif ($allowOverwrite || !isset($data[$key])) {
  315.                             // Spec: Keys MUST be unique; first one wins.
  316.                             // But overwriting is allowed when a merge node is used in current block.
  317.                             if (null !== $subTag) {
  318.                                 $data[$key] = new TaggedValue($subTag$value);
  319.                             } else {
  320.                                 $data[$key] = $value;
  321.                             }
  322.                         } else {
  323.                             @trigger_error($this->getDeprecationMessage(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since Symfony 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.'$key)), E_USER_DEPRECATED);
  324.                         }
  325.                     }
  326.                 } else {
  327.                     $value $this->parseValue(rtrim($values['value']), $flags$context);
  328.                     // Spec: Keys MUST be unique; first one wins.
  329.                     // But overwriting is allowed when a merge node is used in current block.
  330.                     if ($allowOverwrite || !isset($data[$key])) {
  331.                         $data[$key] = $value;
  332.                     } else {
  333.                         @trigger_error($this->getDeprecationMessage(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since Symfony 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.'$key)), E_USER_DEPRECATED);
  334.                     }
  335.                 }
  336.                 if ($isRef) {
  337.                     $this->refs[$isRef] = $data[$key];
  338.                 }
  339.             } else {
  340.                 // multiple documents are not supported
  341.                 if ('---' === $this->currentLine) {
  342.                     throw new ParseException('Multiple documents are not supported.'$this->currentLineNb 1$this->currentLine$this->filename);
  343.                 }
  344.                 if ($deprecatedUsage = (isset($this->currentLine[1]) && '?' === $this->currentLine[0] && ' ' === $this->currentLine[1])) {
  345.                     @trigger_error($this->getDeprecationMessage('Starting an unquoted string with a question mark followed by a space is deprecated since Symfony 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.'), E_USER_DEPRECATED);
  346.                 }
  347.                 // 1-liner optionally followed by newline(s)
  348.                 if (is_string($value) && $this->lines[0] === trim($value)) {
  349.                     try {
  350.                         $value Inline::parse($this->lines[0], $flags$this->refs);
  351.                     } catch (ParseException $e) {
  352.                         $e->setParsedLine($this->getRealCurrentLineNb() + 1);
  353.                         $e->setSnippet($this->currentLine);
  354.                         throw $e;
  355.                     }
  356.                     return $value;
  357.                 }
  358.                 // try to parse the value as a multi-line string as a last resort
  359.                 if (=== $this->currentLineNb) {
  360.                     $previousLineWasNewline false;
  361.                     $previousLineWasTerminatedWithBackslash false;
  362.                     $value '';
  363.                     foreach ($this->lines as $line) {
  364.                         // If the indentation is not consistent at offset 0, it is to be considered as a ParseError
  365.                         if (=== $this->offset && !$deprecatedUsage && isset($line[0]) && ' ' === $line[0]) {
  366.                             throw new ParseException('Unable to parse.'$this->getRealCurrentLineNb() + 1$this->currentLine$this->filename);
  367.                         }
  368.                         if ('' === trim($line)) {
  369.                             $value .= "\n";
  370.                         } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
  371.                             $value .= ' ';
  372.                         }
  373.                         if ('' !== trim($line) && '\\' === substr($line, -1)) {
  374.                             $value .= ltrim(substr($line0, -1));
  375.                         } elseif ('' !== trim($line)) {
  376.                             $value .= trim($line);
  377.                         }
  378.                         if ('' === trim($line)) {
  379.                             $previousLineWasNewline true;
  380.                             $previousLineWasTerminatedWithBackslash false;
  381.                         } elseif ('\\' === substr($line, -1)) {
  382.                             $previousLineWasNewline false;
  383.                             $previousLineWasTerminatedWithBackslash true;
  384.                         } else {
  385.                             $previousLineWasNewline false;
  386.                             $previousLineWasTerminatedWithBackslash false;
  387.                         }
  388.                     }
  389.                     try {
  390.                         return Inline::parse(trim($value));
  391.                     } catch (ParseException $e) {
  392.                         // fall-through to the ParseException thrown below
  393.                     }
  394.                 }
  395.                 throw new ParseException('Unable to parse.'$this->getRealCurrentLineNb() + 1$this->currentLine$this->filename);
  396.             }
  397.         } while ($this->moveToNextLine());
  398.         if (null !== $tag) {
  399.             $data = new TaggedValue($tag$data);
  400.         }
  401.         if (Yaml::PARSE_OBJECT_FOR_MAP $flags && !is_object($data) && 'mapping' === $context) {
  402.             $object = new \stdClass();
  403.             foreach ($data as $key => $value) {
  404.                 $object->$key $value;
  405.             }
  406.             $data $object;
  407.         }
  408.         return empty($data) ? null $data;
  409.     }
  410.     private function parseBlock($offset$yaml$flags)
  411.     {
  412.         $skippedLineNumbers $this->skippedLineNumbers;
  413.         foreach ($this->locallySkippedLineNumbers as $lineNumber) {
  414.             if ($lineNumber $offset) {
  415.                 continue;
  416.             }
  417.             $skippedLineNumbers[] = $lineNumber;
  418.         }
  419.         $parser = new self();
  420.         $parser->offset $offset;
  421.         $parser->totalNumberOfLines $this->totalNumberOfLines;
  422.         $parser->skippedLineNumbers $skippedLineNumbers;
  423.         $parser->refs = &$this->refs;
  424.         return $parser->doParse($yaml$flags);
  425.     }
  426.     /**
  427.      * Returns the current line number (takes the offset into account).
  428.      *
  429.      * @internal
  430.      *
  431.      * @return int The current line number
  432.      */
  433.     public function getRealCurrentLineNb()
  434.     {
  435.         $realCurrentLineNumber $this->currentLineNb $this->offset;
  436.         foreach ($this->skippedLineNumbers as $skippedLineNumber) {
  437.             if ($skippedLineNumber $realCurrentLineNumber) {
  438.                 break;
  439.             }
  440.             ++$realCurrentLineNumber;
  441.         }
  442.         return $realCurrentLineNumber;
  443.     }
  444.     /**
  445.      * Returns the current line indentation.
  446.      *
  447.      * @return int The current line indentation
  448.      */
  449.     private function getCurrentLineIndentation()
  450.     {
  451.         return strlen($this->currentLine) - strlen(ltrim($this->currentLine' '));
  452.     }
  453.     /**
  454.      * Returns the next embed block of YAML.
  455.      *
  456.      * @param int  $indentation The indent level at which the block is to be read, or null for default
  457.      * @param bool $inSequence  True if the enclosing data structure is a sequence
  458.      *
  459.      * @return string A YAML string
  460.      *
  461.      * @throws ParseException When indentation problem are detected
  462.      */
  463.     private function getNextEmbedBlock($indentation null$inSequence false)
  464.     {
  465.         $oldLineIndentation $this->getCurrentLineIndentation();
  466.         $blockScalarIndentations = array();
  467.         if ($this->isBlockScalarHeader()) {
  468.             $blockScalarIndentations[] = $oldLineIndentation;
  469.         }
  470.         if (!$this->moveToNextLine()) {
  471.             return;
  472.         }
  473.         if (null === $indentation) {
  474.             $newIndent null;
  475.             $movements 0;
  476.             do {
  477.                 $EOF false;
  478.                 // empty and comment-like lines do not influence the indentation depth
  479.                 if ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
  480.                     $EOF = !$this->moveToNextLine();
  481.                     if (!$EOF) {
  482.                         ++$movements;
  483.                     }
  484.                 } else {
  485.                     $newIndent $this->getCurrentLineIndentation();
  486.                 }
  487.             } while (!$EOF && null === $newIndent);
  488.             for ($i 0$i $movements; ++$i) {
  489.                 $this->moveToPreviousLine();
  490.             }
  491.             $unindentedEmbedBlock $this->isStringUnIndentedCollectionItem();
  492.             if (!$this->isCurrentLineEmpty() && === $newIndent && !$unindentedEmbedBlock) {
  493.                 throw new ParseException('Indentation problem.'$this->getRealCurrentLineNb() + 1$this->currentLine$this->filename);
  494.             }
  495.         } else {
  496.             $newIndent $indentation;
  497.         }
  498.         $data = array();
  499.         if ($this->getCurrentLineIndentation() >= $newIndent) {
  500.             $data[] = substr($this->currentLine$newIndent);
  501.         } elseif ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
  502.             $data[] = $this->currentLine;
  503.         } else {
  504.             $this->moveToPreviousLine();
  505.             return;
  506.         }
  507.         if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
  508.             // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
  509.             // and therefore no nested list or mapping
  510.             $this->moveToPreviousLine();
  511.             return;
  512.         }
  513.         $isItUnindentedCollection $this->isStringUnIndentedCollectionItem();
  514.         if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) {
  515.             $blockScalarIndentations[] = $this->getCurrentLineIndentation();
  516.         }
  517.         $previousLineIndentation $this->getCurrentLineIndentation();
  518.         while ($this->moveToNextLine()) {
  519.             $indent $this->getCurrentLineIndentation();
  520.             // terminate all block scalars that are more indented than the current line
  521.             if (!empty($blockScalarIndentations) && $indent $previousLineIndentation && '' !== trim($this->currentLine)) {
  522.                 foreach ($blockScalarIndentations as $key => $blockScalarIndentation) {
  523.                     if ($blockScalarIndentation >= $indent) {
  524.                         unset($blockScalarIndentations[$key]);
  525.                     }
  526.                 }
  527.             }
  528.             if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) {
  529.                 $blockScalarIndentations[] = $indent;
  530.             }
  531.             $previousLineIndentation $indent;
  532.             if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
  533.                 $this->moveToPreviousLine();
  534.                 break;
  535.             }
  536.             if ($this->isCurrentLineBlank()) {
  537.                 $data[] = substr($this->currentLine$newIndent);
  538.                 continue;
  539.             }
  540.             if ($indent >= $newIndent) {
  541.                 $data[] = substr($this->currentLine$newIndent);
  542.             } elseif ($this->isCurrentLineComment()) {
  543.                 $data[] = $this->currentLine;
  544.             } elseif (== $indent) {
  545.                 $this->moveToPreviousLine();
  546.                 break;
  547.             } else {
  548.                 throw new ParseException('Indentation problem.'$this->getRealCurrentLineNb() + 1$this->currentLine$this->filename);
  549.             }
  550.         }
  551.         return implode("\n"$data);
  552.     }
  553.     /**
  554.      * Moves the parser to the next line.
  555.      *
  556.      * @return bool
  557.      */
  558.     private function moveToNextLine()
  559.     {
  560.         if ($this->currentLineNb >= count($this->lines) - 1) {
  561.             return false;
  562.         }
  563.         $this->currentLine $this->lines[++$this->currentLineNb];
  564.         return true;
  565.     }
  566.     /**
  567.      * Moves the parser to the previous line.
  568.      *
  569.      * @return bool
  570.      */
  571.     private function moveToPreviousLine()
  572.     {
  573.         if ($this->currentLineNb 1) {
  574.             return false;
  575.         }
  576.         $this->currentLine $this->lines[--$this->currentLineNb];
  577.         return true;
  578.     }
  579.     /**
  580.      * Parses a YAML value.
  581.      *
  582.      * @param string $value   A YAML value
  583.      * @param int    $flags   A bit field of PARSE_* constants to customize the YAML parser behavior
  584.      * @param string $context The parser context (either sequence or mapping)
  585.      *
  586.      * @return mixed A PHP value
  587.      *
  588.      * @throws ParseException When reference does not exist
  589.      */
  590.     private function parseValue($value$flags$context)
  591.     {
  592.         if (=== strpos($value'*')) {
  593.             if (false !== $pos strpos($value'#')) {
  594.                 $value substr($value1$pos 2);
  595.             } else {
  596.                 $value substr($value1);
  597.             }
  598.             if (!array_key_exists($value$this->refs)) {
  599.                 throw new ParseException(sprintf('Reference "%s" does not exist.'$value), $this->currentLineNb 1$this->currentLine$this->filename);
  600.             }
  601.             return $this->refs[$value];
  602.         }
  603.         if (self::preg_match('/^(?:'.self::TAG_PATTERN.' +)?'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/'$value$matches)) {
  604.             $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
  605.             $data $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#'''$modifiers), (int) abs($modifiers));
  606.             if ('' !== $matches['tag']) {
  607.                 if ('!!binary' === $matches['tag']) {
  608.                     return Inline::evaluateBinaryScalar($data);
  609.                 } elseif ('tagged' === $matches['tag']) {
  610.                     return new TaggedValue(substr($matches['tag'], 1), $data);
  611.                 } elseif ('!' !== $matches['tag']) {
  612.                     @trigger_error($this->getDeprecationMessage(sprintf('Using the custom tag "%s" for the value "%s" is deprecated since Symfony 3.3. It will be replaced by an instance of %s in 4.0.'$matches['tag'], $dataTaggedValue::class)), E_USER_DEPRECATED);
  613.                 }
  614.             }
  615.             return $data;
  616.         }
  617.         try {
  618.             $quotation '' !== $value && ('"' === $value[0] || "'" === $value[0]) ? $value[0] : null;
  619.             // do not take following lines into account when the current line is a quoted single line value
  620.             if (null !== $quotation && self::preg_match('/^'.$quotation.'.*'.$quotation.'(\s*#.*)?$/'$value)) {
  621.                 return Inline::parse($value$flags$this->refs);
  622.             }
  623.             $lines = array();
  624.             while ($this->moveToNextLine()) {
  625.                 // unquoted strings end before the first unindented line
  626.                 if (null === $quotation && === $this->getCurrentLineIndentation()) {
  627.                     $this->moveToPreviousLine();
  628.                     break;
  629.                 }
  630.                 $lines[] = trim($this->currentLine);
  631.                 // quoted string values end with a line that is terminated with the quotation character
  632.                 if ('' !== $this->currentLine && substr($this->currentLine, -1) === $quotation) {
  633.                     break;
  634.                 }
  635.             }
  636.             for ($i 0$linesCount count($lines), $previousLineBlank false$i $linesCount; ++$i) {
  637.                 if ('' === $lines[$i]) {
  638.                     $value .= "\n";
  639.                     $previousLineBlank true;
  640.                 } elseif ($previousLineBlank) {
  641.                     $value .= $lines[$i];
  642.                     $previousLineBlank false;
  643.                 } else {
  644.                     $value .= ' '.$lines[$i];
  645.                     $previousLineBlank false;
  646.                 }
  647.             }
  648.             Inline::$parsedLineNumber $this->getRealCurrentLineNb();
  649.             $parsedValue Inline::parse($value$flags$this->refs);
  650.             if ('mapping' === $context && is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue': ')) {
  651.                 throw new ParseException('A colon cannot be used in an unquoted mapping value.'$this->getRealCurrentLineNb() + 1$value$this->filename);
  652.             }
  653.             return $parsedValue;
  654.         } catch (ParseException $e) {
  655.             $e->setParsedLine($this->getRealCurrentLineNb() + 1);
  656.             $e->setSnippet($this->currentLine);
  657.             throw $e;
  658.         }
  659.     }
  660.     /**
  661.      * Parses a block scalar.
  662.      *
  663.      * @param string $style       The style indicator that was used to begin this block scalar (| or >)
  664.      * @param string $chomping    The chomping indicator that was used to begin this block scalar (+ or -)
  665.      * @param int    $indentation The indentation indicator that was used to begin this block scalar
  666.      *
  667.      * @return string The text value
  668.      */
  669.     private function parseBlockScalar($style$chomping ''$indentation 0)
  670.     {
  671.         $notEOF $this->moveToNextLine();
  672.         if (!$notEOF) {
  673.             return '';
  674.         }
  675.         $isCurrentLineBlank $this->isCurrentLineBlank();
  676.         $blockLines = array();
  677.         // leading blank lines are consumed before determining indentation
  678.         while ($notEOF && $isCurrentLineBlank) {
  679.             // newline only if not EOF
  680.             if ($notEOF $this->moveToNextLine()) {
  681.                 $blockLines[] = '';
  682.                 $isCurrentLineBlank $this->isCurrentLineBlank();
  683.             }
  684.         }
  685.         // determine indentation if not specified
  686.         if (=== $indentation) {
  687.             if (self::preg_match('/^ +/'$this->currentLine$matches)) {
  688.                 $indentation strlen($matches[0]);
  689.             }
  690.         }
  691.         if ($indentation 0) {
  692.             $pattern sprintf('/^ {%d}(.*)$/'$indentation);
  693.             while (
  694.                 $notEOF && (
  695.                     $isCurrentLineBlank ||
  696.                     self::preg_match($pattern$this->currentLine$matches)
  697.                 )
  698.             ) {
  699.                 if ($isCurrentLineBlank && strlen($this->currentLine) > $indentation) {
  700.                     $blockLines[] = substr($this->currentLine$indentation);
  701.                 } elseif ($isCurrentLineBlank) {
  702.                     $blockLines[] = '';
  703.                 } else {
  704.                     $blockLines[] = $matches[1];
  705.                 }
  706.                 // newline only if not EOF
  707.                 if ($notEOF $this->moveToNextLine()) {
  708.                     $isCurrentLineBlank $this->isCurrentLineBlank();
  709.                 }
  710.             }
  711.         } elseif ($notEOF) {
  712.             $blockLines[] = '';
  713.         }
  714.         if ($notEOF) {
  715.             $blockLines[] = '';
  716.             $this->moveToPreviousLine();
  717.         } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
  718.             $blockLines[] = '';
  719.         }
  720.         // folded style
  721.         if ('>' === $style) {
  722.             $text '';
  723.             $previousLineIndented false;
  724.             $previousLineBlank false;
  725.             for ($i 0$blockLinesCount count($blockLines); $i $blockLinesCount; ++$i) {
  726.                 if ('' === $blockLines[$i]) {
  727.                     $text .= "\n";
  728.                     $previousLineIndented false;
  729.                     $previousLineBlank true;
  730.                 } elseif (' ' === $blockLines[$i][0]) {
  731.                     $text .= "\n".$blockLines[$i];
  732.                     $previousLineIndented true;
  733.                     $previousLineBlank false;
  734.                 } elseif ($previousLineIndented) {
  735.                     $text .= "\n".$blockLines[$i];
  736.                     $previousLineIndented false;
  737.                     $previousLineBlank false;
  738.                 } elseif ($previousLineBlank || === $i) {
  739.                     $text .= $blockLines[$i];
  740.                     $previousLineIndented false;
  741.                     $previousLineBlank false;
  742.                 } else {
  743.                     $text .= ' '.$blockLines[$i];
  744.                     $previousLineIndented false;
  745.                     $previousLineBlank false;
  746.                 }
  747.             }
  748.         } else {
  749.             $text implode("\n"$blockLines);
  750.         }
  751.         // deal with trailing newlines
  752.         if ('' === $chomping) {
  753.             $text preg_replace('/\n+$/'"\n"$text);
  754.         } elseif ('-' === $chomping) {
  755.             $text preg_replace('/\n+$/'''$text);
  756.         }
  757.         return $text;
  758.     }
  759.     /**
  760.      * Returns true if the next line is indented.
  761.      *
  762.      * @return bool Returns true if the next line is indented, false otherwise
  763.      */
  764.     private function isNextLineIndented()
  765.     {
  766.         $currentIndentation $this->getCurrentLineIndentation();
  767.         $movements 0;
  768.         do {
  769.             $EOF = !$this->moveToNextLine();
  770.             if (!$EOF) {
  771.                 ++$movements;
  772.             }
  773.         } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));
  774.         if ($EOF) {
  775.             return false;
  776.         }
  777.         $ret $this->getCurrentLineIndentation() > $currentIndentation;
  778.         for ($i 0$i $movements; ++$i) {
  779.             $this->moveToPreviousLine();
  780.         }
  781.         return $ret;
  782.     }
  783.     /**
  784.      * Returns true if the current line is blank or if it is a comment line.
  785.      *
  786.      * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
  787.      */
  788.     private function isCurrentLineEmpty()
  789.     {
  790.         return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
  791.     }
  792.     /**
  793.      * Returns true if the current line is blank.
  794.      *
  795.      * @return bool Returns true if the current line is blank, false otherwise
  796.      */
  797.     private function isCurrentLineBlank()
  798.     {
  799.         return '' == trim($this->currentLine' ');
  800.     }
  801.     /**
  802.      * Returns true if the current line is a comment line.
  803.      *
  804.      * @return bool Returns true if the current line is a comment line, false otherwise
  805.      */
  806.     private function isCurrentLineComment()
  807.     {
  808.         //checking explicitly the first char of the trim is faster than loops or strpos
  809.         $ltrimmedLine ltrim($this->currentLine' ');
  810.         return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0];
  811.     }
  812.     private function isCurrentLineLastLineInDocument()
  813.     {
  814.         return ($this->offset $this->currentLineNb) >= ($this->totalNumberOfLines 1);
  815.     }
  816.     /**
  817.      * Cleanups a YAML string to be parsed.
  818.      *
  819.      * @param string $value The input YAML string
  820.      *
  821.      * @return string A cleaned up YAML string
  822.      */
  823.     private function cleanup($value)
  824.     {
  825.         $value str_replace(array("\r\n""\r"), "\n"$value);
  826.         // strip YAML header
  827.         $count 0;
  828.         $value preg_replace('#^\%YAML[: ][\d\.]+.*\n#u'''$value, -1$count);
  829.         $this->offset += $count;
  830.         // remove leading comments
  831.         $trimmedValue preg_replace('#^(\#.*?\n)+#s'''$value, -1$count);
  832.         if (=== $count) {
  833.             // items have been removed, update the offset
  834.             $this->offset += substr_count($value"\n") - substr_count($trimmedValue"\n");
  835.             $value $trimmedValue;
  836.         }
  837.         // remove start of the document marker (---)
  838.         $trimmedValue preg_replace('#^\-\-\-.*?\n#s'''$value, -1$count);
  839.         if (=== $count) {
  840.             // items have been removed, update the offset
  841.             $this->offset += substr_count($value"\n") - substr_count($trimmedValue"\n");
  842.             $value $trimmedValue;
  843.             // remove end of the document marker (...)
  844.             $value preg_replace('#\.\.\.\s*$#'''$value);
  845.         }
  846.         return $value;
  847.     }
  848.     /**
  849.      * Returns true if the next line starts unindented collection.
  850.      *
  851.      * @return bool Returns true if the next line starts unindented collection, false otherwise
  852.      */
  853.     private function isNextLineUnIndentedCollection()
  854.     {
  855.         $currentIndentation $this->getCurrentLineIndentation();
  856.         $movements 0;
  857.         do {
  858.             $EOF = !$this->moveToNextLine();
  859.             if (!$EOF) {
  860.                 ++$movements;
  861.             }
  862.         } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));
  863.         if ($EOF) {
  864.             return false;
  865.         }
  866.         $ret $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
  867.         for ($i 0$i $movements; ++$i) {
  868.             $this->moveToPreviousLine();
  869.         }
  870.         return $ret;
  871.     }
  872.     /**
  873.      * Returns true if the string is un-indented collection item.
  874.      *
  875.      * @return bool Returns true if the string is un-indented collection item, false otherwise
  876.      */
  877.     private function isStringUnIndentedCollectionItem()
  878.     {
  879.         return '-' === rtrim($this->currentLine) || === strpos($this->currentLine'- ');
  880.     }
  881.     /**
  882.      * Tests whether or not the current line is the header of a block scalar.
  883.      *
  884.      * @return bool
  885.      */
  886.     private function isBlockScalarHeader()
  887.     {
  888.         return (bool) self::preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~'$this->currentLine);
  889.     }
  890.     /**
  891.      * A local wrapper for `preg_match` which will throw a ParseException if there
  892.      * is an internal error in the PCRE engine.
  893.      *
  894.      * This avoids us needing to check for "false" every time PCRE is used
  895.      * in the YAML engine
  896.      *
  897.      * @throws ParseException on a PCRE internal error
  898.      *
  899.      * @see preg_last_error()
  900.      *
  901.      * @internal
  902.      */
  903.     public static function preg_match($pattern$subject, &$matches null$flags 0$offset 0)
  904.     {
  905.         if (false === $ret preg_match($pattern$subject$matches$flags$offset)) {
  906.             switch (preg_last_error()) {
  907.                 case PREG_INTERNAL_ERROR:
  908.                     $error 'Internal PCRE error.';
  909.                     break;
  910.                 case PREG_BACKTRACK_LIMIT_ERROR:
  911.                     $error 'pcre.backtrack_limit reached.';
  912.                     break;
  913.                 case PREG_RECURSION_LIMIT_ERROR:
  914.                     $error 'pcre.recursion_limit reached.';
  915.                     break;
  916.                 case PREG_BAD_UTF8_ERROR:
  917.                     $error 'Malformed UTF-8 data.';
  918.                     break;
  919.                 case PREG_BAD_UTF8_OFFSET_ERROR:
  920.                     $error 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
  921.                     break;
  922.                 default:
  923.                     $error 'Error.';
  924.             }
  925.             throw new ParseException($error);
  926.         }
  927.         return $ret;
  928.     }
  929.     /**
  930.      * Trim the tag on top of the value.
  931.      *
  932.      * Prevent values such as `!foo {quz: bar}` to be considered as
  933.      * a mapping block.
  934.      */
  935.     private function trimTag($value)
  936.     {
  937.         if ('!' === $value[0]) {
  938.             return ltrim(substr($value1strcspn($value" \r\n"1)), ' ');
  939.         }
  940.         return $value;
  941.     }
  942.     private function getLineTag($value$flags$nextLineCheck true)
  943.     {
  944.         if ('' === $value || '!' !== $value[0] || !== self::preg_match('/^'.self::TAG_PATTERN.' *( +#.*)?$/'$value$matches)) {
  945.             return;
  946.         }
  947.         if ($nextLineCheck && !$this->isNextLineIndented()) {
  948.             return;
  949.         }
  950.         $tag substr($matches['tag'], 1);
  951.         // Built-in tags
  952.         if ($tag && '!' === $tag[0]) {
  953.             throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.'$tag), $this->getRealCurrentLineNb() + 1$value$this->filename);
  954.         }
  955.         if (Yaml::PARSE_CUSTOM_TAGS $flags) {
  956.             return $tag;
  957.         }
  958.         throw new ParseException(sprintf('Tags support is not enabled. You must use the flag `Yaml::PARSE_CUSTOM_TAGS` to use "%s".'$matches['tag']), $this->getRealCurrentLineNb() + 1$value$this->filename);
  959.     }
  960.     private function getDeprecationMessage($message)
  961.     {
  962.         $message rtrim($message'.');
  963.         if (null !== $this->filename) {
  964.             $message .= ' in '.$this->filename;
  965.         }
  966.         $message .= ' on line '.($this->getRealCurrentLineNb() + 1);
  967.         return $message.'.';
  968.     }
  969. }