vendor/symfony/yaml/Parser.php line 401

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