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

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