vendor/symfony/yaml/Parser.php line 393

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.         if (!$this->moveToNextLine()) {
  467.             return;
  468.         }
  469.         if (null === $indentation) {
  470.             $newIndent null;
  471.             $movements 0;
  472.             do {
  473.                 $EOF false;
  474.                 // empty and comment-like lines do not influence the indentation depth
  475.                 if ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
  476.                     $EOF = !$this->moveToNextLine();
  477.                     if (!$EOF) {
  478.                         ++$movements;
  479.                     }
  480.                 } else {
  481.                     $newIndent $this->getCurrentLineIndentation();
  482.                 }
  483.             } while (!$EOF && null === $newIndent);
  484.             for ($i 0$i $movements; ++$i) {
  485.                 $this->moveToPreviousLine();
  486.             }
  487.             $unindentedEmbedBlock $this->isStringUnIndentedCollectionItem();
  488.             if (!$this->isCurrentLineEmpty() && === $newIndent && !$unindentedEmbedBlock) {
  489.                 throw new ParseException('Indentation problem.'$this->getRealCurrentLineNb() + 1$this->currentLine$this->filename);
  490.             }
  491.         } else {
  492.             $newIndent $indentation;
  493.         }
  494.         $data = array();
  495.         if ($this->getCurrentLineIndentation() >= $newIndent) {
  496.             $data[] = substr($this->currentLine$newIndent);
  497.         } elseif ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
  498.             $data[] = $this->currentLine;
  499.         } else {
  500.             $this->moveToPreviousLine();
  501.             return;
  502.         }
  503.         if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
  504.             // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
  505.             // and therefore no nested list or mapping
  506.             $this->moveToPreviousLine();
  507.             return;
  508.         }
  509.         $isItUnindentedCollection $this->isStringUnIndentedCollectionItem();
  510.         while ($this->moveToNextLine()) {
  511.             $indent $this->getCurrentLineIndentation();
  512.             if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
  513.                 $this->moveToPreviousLine();
  514.                 break;
  515.             }
  516.             if ($this->isCurrentLineBlank()) {
  517.                 $data[] = substr($this->currentLine$newIndent);
  518.                 continue;
  519.             }
  520.             if ($indent >= $newIndent) {
  521.                 $data[] = substr($this->currentLine$newIndent);
  522.             } elseif ($this->isCurrentLineComment()) {
  523.                 $data[] = $this->currentLine;
  524.             } elseif (== $indent) {
  525.                 $this->moveToPreviousLine();
  526.                 break;
  527.             } else {
  528.                 throw new ParseException('Indentation problem.'$this->getRealCurrentLineNb() + 1$this->currentLine$this->filename);
  529.             }
  530.         }
  531.         return implode("\n"$data);
  532.     }
  533.     /**
  534.      * Moves the parser to the next line.
  535.      *
  536.      * @return bool
  537.      */
  538.     private function moveToNextLine()
  539.     {
  540.         if ($this->currentLineNb >= \count($this->lines) - 1) {
  541.             return false;
  542.         }
  543.         $this->currentLine $this->lines[++$this->currentLineNb];
  544.         return true;
  545.     }
  546.     /**
  547.      * Moves the parser to the previous line.
  548.      *
  549.      * @return bool
  550.      */
  551.     private function moveToPreviousLine()
  552.     {
  553.         if ($this->currentLineNb 1) {
  554.             return false;
  555.         }
  556.         $this->currentLine $this->lines[--$this->currentLineNb];
  557.         return true;
  558.     }
  559.     /**
  560.      * Parses a YAML value.
  561.      *
  562.      * @param string $value   A YAML value
  563.      * @param int    $flags   A bit field of PARSE_* constants to customize the YAML parser behavior
  564.      * @param string $context The parser context (either sequence or mapping)
  565.      *
  566.      * @return mixed A PHP value
  567.      *
  568.      * @throws ParseException When reference does not exist
  569.      */
  570.     private function parseValue($value$flags$context)
  571.     {
  572.         if (=== strpos($value'*')) {
  573.             if (false !== $pos strpos($value'#')) {
  574.                 $value substr($value1$pos 2);
  575.             } else {
  576.                 $value substr($value1);
  577.             }
  578.             if (!array_key_exists($value$this->refs)) {
  579.                 throw new ParseException(sprintf('Reference "%s" does not exist.'$value), $this->currentLineNb 1$this->currentLine$this->filename);
  580.             }
  581.             return $this->refs[$value];
  582.         }
  583.         if (self::preg_match('/^(?:'.self::TAG_PATTERN.' +)?'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/'$value$matches)) {
  584.             $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
  585.             $data $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#'''$modifiers), (int) abs($modifiers));
  586.             if ('' !== $matches['tag']) {
  587.                 if ('!!binary' === $matches['tag']) {
  588.                     return Inline::evaluateBinaryScalar($data);
  589.                 } elseif ('tagged' === $matches['tag']) {
  590.                     return new TaggedValue(substr($matches['tag'], 1), $data);
  591.                 } elseif ('!' !== $matches['tag']) {
  592.                     @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);
  593.                 }
  594.             }
  595.             return $data;
  596.         }
  597.         try {
  598.             $quotation '' !== $value && ('"' === $value[0] || "'" === $value[0]) ? $value[0] : null;
  599.             // do not take following lines into account when the current line is a quoted single line value
  600.             if (null !== $quotation && self::preg_match('/^'.$quotation.'.*'.$quotation.'(\s*#.*)?$/'$value)) {
  601.                 return Inline::parse($value$flags$this->refs);
  602.             }
  603.             $lines = array();
  604.             while ($this->moveToNextLine()) {
  605.                 // unquoted strings end before the first unindented line
  606.                 if (null === $quotation && === $this->getCurrentLineIndentation()) {
  607.                     $this->moveToPreviousLine();
  608.                     break;
  609.                 }
  610.                 $lines[] = trim($this->currentLine);
  611.                 // quoted string values end with a line that is terminated with the quotation character
  612.                 if ('' !== $this->currentLine && substr($this->currentLine, -1) === $quotation) {
  613.                     break;
  614.                 }
  615.             }
  616.             for ($i 0$linesCount = \count($lines), $previousLineBlank false$i $linesCount; ++$i) {
  617.                 if ('' === $lines[$i]) {
  618.                     $value .= "\n";
  619.                     $previousLineBlank true;
  620.                 } elseif ($previousLineBlank) {
  621.                     $value .= $lines[$i];
  622.                     $previousLineBlank false;
  623.                 } else {
  624.                     $value .= ' '.$lines[$i];
  625.                     $previousLineBlank false;
  626.                 }
  627.             }
  628.             Inline::$parsedLineNumber $this->getRealCurrentLineNb();
  629.             $parsedValue Inline::parse($value$flags$this->refs);
  630.             if ('mapping' === $context && \is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue': ')) {
  631.                 throw new ParseException('A colon cannot be used in an unquoted mapping value.'$this->getRealCurrentLineNb() + 1$value$this->filename);
  632.             }
  633.             return $parsedValue;
  634.         } catch (ParseException $e) {
  635.             $e->setParsedLine($this->getRealCurrentLineNb() + 1);
  636.             $e->setSnippet($this->currentLine);
  637.             throw $e;
  638.         }
  639.     }
  640.     /**
  641.      * Parses a block scalar.
  642.      *
  643.      * @param string $style       The style indicator that was used to begin this block scalar (| or >)
  644.      * @param string $chomping    The chomping indicator that was used to begin this block scalar (+ or -)
  645.      * @param int    $indentation The indentation indicator that was used to begin this block scalar
  646.      *
  647.      * @return string The text value
  648.      */
  649.     private function parseBlockScalar($style$chomping ''$indentation 0)
  650.     {
  651.         $notEOF $this->moveToNextLine();
  652.         if (!$notEOF) {
  653.             return '';
  654.         }
  655.         $isCurrentLineBlank $this->isCurrentLineBlank();
  656.         $blockLines = array();
  657.         // leading blank lines are consumed before determining indentation
  658.         while ($notEOF && $isCurrentLineBlank) {
  659.             // newline only if not EOF
  660.             if ($notEOF $this->moveToNextLine()) {
  661.                 $blockLines[] = '';
  662.                 $isCurrentLineBlank $this->isCurrentLineBlank();
  663.             }
  664.         }
  665.         // determine indentation if not specified
  666.         if (=== $indentation) {
  667.             if (self::preg_match('/^ +/'$this->currentLine$matches)) {
  668.                 $indentation = \strlen($matches[0]);
  669.             }
  670.         }
  671.         if ($indentation 0) {
  672.             $pattern sprintf('/^ {%d}(.*)$/'$indentation);
  673.             while (
  674.                 $notEOF && (
  675.                     $isCurrentLineBlank ||
  676.                     self::preg_match($pattern$this->currentLine$matches)
  677.                 )
  678.             ) {
  679.                 if ($isCurrentLineBlank && \strlen($this->currentLine) > $indentation) {
  680.                     $blockLines[] = substr($this->currentLine$indentation);
  681.                 } elseif ($isCurrentLineBlank) {
  682.                     $blockLines[] = '';
  683.                 } else {
  684.                     $blockLines[] = $matches[1];
  685.                 }
  686.                 // newline only if not EOF
  687.                 if ($notEOF $this->moveToNextLine()) {
  688.                     $isCurrentLineBlank $this->isCurrentLineBlank();
  689.                 }
  690.             }
  691.         } elseif ($notEOF) {
  692.             $blockLines[] = '';
  693.         }
  694.         if ($notEOF) {
  695.             $blockLines[] = '';
  696.             $this->moveToPreviousLine();
  697.         } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
  698.             $blockLines[] = '';
  699.         }
  700.         // folded style
  701.         if ('>' === $style) {
  702.             $text '';
  703.             $previousLineIndented false;
  704.             $previousLineBlank false;
  705.             for ($i 0$blockLinesCount = \count($blockLines); $i $blockLinesCount; ++$i) {
  706.                 if ('' === $blockLines[$i]) {
  707.                     $text .= "\n";
  708.                     $previousLineIndented false;
  709.                     $previousLineBlank true;
  710.                 } elseif (' ' === $blockLines[$i][0]) {
  711.                     $text .= "\n".$blockLines[$i];
  712.                     $previousLineIndented true;
  713.                     $previousLineBlank false;
  714.                 } elseif ($previousLineIndented) {
  715.                     $text .= "\n".$blockLines[$i];
  716.                     $previousLineIndented false;
  717.                     $previousLineBlank false;
  718.                 } elseif ($previousLineBlank || === $i) {
  719.                     $text .= $blockLines[$i];
  720.                     $previousLineIndented false;
  721.                     $previousLineBlank false;
  722.                 } else {
  723.                     $text .= ' '.$blockLines[$i];
  724.                     $previousLineIndented false;
  725.                     $previousLineBlank false;
  726.                 }
  727.             }
  728.         } else {
  729.             $text implode("\n"$blockLines);
  730.         }
  731.         // deal with trailing newlines
  732.         if ('' === $chomping) {
  733.             $text preg_replace('/\n+$/'"\n"$text);
  734.         } elseif ('-' === $chomping) {
  735.             $text preg_replace('/\n+$/'''$text);
  736.         }
  737.         return $text;
  738.     }
  739.     /**
  740.      * Returns true if the next line is indented.
  741.      *
  742.      * @return bool Returns true if the next line is indented, false otherwise
  743.      */
  744.     private function isNextLineIndented()
  745.     {
  746.         $currentIndentation $this->getCurrentLineIndentation();
  747.         $movements 0;
  748.         do {
  749.             $EOF = !$this->moveToNextLine();
  750.             if (!$EOF) {
  751.                 ++$movements;
  752.             }
  753.         } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));
  754.         if ($EOF) {
  755.             return false;
  756.         }
  757.         $ret $this->getCurrentLineIndentation() > $currentIndentation;
  758.         for ($i 0$i $movements; ++$i) {
  759.             $this->moveToPreviousLine();
  760.         }
  761.         return $ret;
  762.     }
  763.     /**
  764.      * Returns true if the current line is blank or if it is a comment line.
  765.      *
  766.      * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
  767.      */
  768.     private function isCurrentLineEmpty()
  769.     {
  770.         return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
  771.     }
  772.     /**
  773.      * Returns true if the current line is blank.
  774.      *
  775.      * @return bool Returns true if the current line is blank, false otherwise
  776.      */
  777.     private function isCurrentLineBlank()
  778.     {
  779.         return '' == trim($this->currentLine' ');
  780.     }
  781.     /**
  782.      * Returns true if the current line is a comment line.
  783.      *
  784.      * @return bool Returns true if the current line is a comment line, false otherwise
  785.      */
  786.     private function isCurrentLineComment()
  787.     {
  788.         //checking explicitly the first char of the trim is faster than loops or strpos
  789.         $ltrimmedLine ltrim($this->currentLine' ');
  790.         return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0];
  791.     }
  792.     private function isCurrentLineLastLineInDocument()
  793.     {
  794.         return ($this->offset $this->currentLineNb) >= ($this->totalNumberOfLines 1);
  795.     }
  796.     /**
  797.      * Cleanups a YAML string to be parsed.
  798.      *
  799.      * @param string $value The input YAML string
  800.      *
  801.      * @return string A cleaned up YAML string
  802.      */
  803.     private function cleanup($value)
  804.     {
  805.         $value str_replace(array("\r\n""\r"), "\n"$value);
  806.         // strip YAML header
  807.         $count 0;
  808.         $value preg_replace('#^\%YAML[: ][\d\.]+.*\n#u'''$value, -1$count);
  809.         $this->offset += $count;
  810.         // remove leading comments
  811.         $trimmedValue preg_replace('#^(\#.*?\n)+#s'''$value, -1$count);
  812.         if (=== $count) {
  813.             // items have been removed, update the offset
  814.             $this->offset += substr_count($value"\n") - substr_count($trimmedValue"\n");
  815.             $value $trimmedValue;
  816.         }
  817.         // remove start of the document marker (---)
  818.         $trimmedValue preg_replace('#^\-\-\-.*?\n#s'''$value, -1$count);
  819.         if (=== $count) {
  820.             // items have been removed, update the offset
  821.             $this->offset += substr_count($value"\n") - substr_count($trimmedValue"\n");
  822.             $value $trimmedValue;
  823.             // remove end of the document marker (...)
  824.             $value preg_replace('#\.\.\.\s*$#'''$value);
  825.         }
  826.         return $value;
  827.     }
  828.     /**
  829.      * Returns true if the next line starts unindented collection.
  830.      *
  831.      * @return bool Returns true if the next line starts unindented collection, false otherwise
  832.      */
  833.     private function isNextLineUnIndentedCollection()
  834.     {
  835.         $currentIndentation $this->getCurrentLineIndentation();
  836.         $movements 0;
  837.         do {
  838.             $EOF = !$this->moveToNextLine();
  839.             if (!$EOF) {
  840.                 ++$movements;
  841.             }
  842.         } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));
  843.         if ($EOF) {
  844.             return false;
  845.         }
  846.         $ret $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
  847.         for ($i 0$i $movements; ++$i) {
  848.             $this->moveToPreviousLine();
  849.         }
  850.         return $ret;
  851.     }
  852.     /**
  853.      * Returns true if the string is un-indented collection item.
  854.      *
  855.      * @return bool Returns true if the string is un-indented collection item, false otherwise
  856.      */
  857.     private function isStringUnIndentedCollectionItem()
  858.     {
  859.         return '-' === rtrim($this->currentLine) || === strpos($this->currentLine'- ');
  860.     }
  861.     /**
  862.      * A local wrapper for `preg_match` which will throw a ParseException if there
  863.      * is an internal error in the PCRE engine.
  864.      *
  865.      * This avoids us needing to check for "false" every time PCRE is used
  866.      * in the YAML engine
  867.      *
  868.      * @throws ParseException on a PCRE internal error
  869.      *
  870.      * @see preg_last_error()
  871.      *
  872.      * @internal
  873.      */
  874.     public static function preg_match($pattern$subject, &$matches null$flags 0$offset 0)
  875.     {
  876.         if (false === $ret preg_match($pattern$subject$matches$flags$offset)) {
  877.             switch (preg_last_error()) {
  878.                 case PREG_INTERNAL_ERROR:
  879.                     $error 'Internal PCRE error.';
  880.                     break;
  881.                 case PREG_BACKTRACK_LIMIT_ERROR:
  882.                     $error 'pcre.backtrack_limit reached.';
  883.                     break;
  884.                 case PREG_RECURSION_LIMIT_ERROR:
  885.                     $error 'pcre.recursion_limit reached.';
  886.                     break;
  887.                 case PREG_BAD_UTF8_ERROR:
  888.                     $error 'Malformed UTF-8 data.';
  889.                     break;
  890.                 case PREG_BAD_UTF8_OFFSET_ERROR:
  891.                     $error 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
  892.                     break;
  893.                 default:
  894.                     $error 'Error.';
  895.             }
  896.             throw new ParseException($error);
  897.         }
  898.         return $ret;
  899.     }
  900.     /**
  901.      * Trim the tag on top of the value.
  902.      *
  903.      * Prevent values such as `!foo {quz: bar}` to be considered as
  904.      * a mapping block.
  905.      */
  906.     private function trimTag($value)
  907.     {
  908.         if ('!' === $value[0]) {
  909.             return ltrim(substr($value1strcspn($value" \r\n"1)), ' ');
  910.         }
  911.         return $value;
  912.     }
  913.     private function getLineTag($value$flags$nextLineCheck true)
  914.     {
  915.         if ('' === $value || '!' !== $value[0] || !== self::preg_match('/^'.self::TAG_PATTERN.' *( +#.*)?$/'$value$matches)) {
  916.             return;
  917.         }
  918.         if ($nextLineCheck && !$this->isNextLineIndented()) {
  919.             return;
  920.         }
  921.         $tag substr($matches['tag'], 1);
  922.         // Built-in tags
  923.         if ($tag && '!' === $tag[0]) {
  924.             throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.'$tag), $this->getRealCurrentLineNb() + 1$value$this->filename);
  925.         }
  926.         if (Yaml::PARSE_CUSTOM_TAGS $flags) {
  927.             return $tag;
  928.         }
  929.         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);
  930.     }
  931.     private function getDeprecationMessage($message)
  932.     {
  933.         $message rtrim($message'.');
  934.         if (null !== $this->filename) {
  935.             $message .= ' in '.$this->filename;
  936.         }
  937.         $message .= ' on line '.($this->getRealCurrentLineNb() + 1);
  938.         return $message.'.';
  939.     }
  940. }