4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\Yaml;
14 use Symfony\Component\Yaml\Exception\ParseException;
17 * Parser parses YAML strings to convert them to PHP arrays.
19 * @author Fabien Potencier <fabien@symfony.com>
23 const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
25 const FOLDED_SCALAR_PATTERN = self::BLOCK_SCALAR_HEADER_PATTERN;
28 private $totalNumberOfLines;
29 private $lines = array();
30 private $currentLineNb = -1;
31 private $currentLine = '';
32 private $refs = array();
33 private $skippedLineNumbers = array();
34 private $locallySkippedLineNumbers = array();
39 * @param int $offset The offset of YAML document (used for line numbers in error messages)
40 * @param int|null $totalNumberOfLines The overall number of lines being parsed
41 * @param int[] $skippedLineNumbers Number of comment lines that have been skipped by the parser
43 public function __construct($offset = 0, $totalNumberOfLines = null, array $skippedLineNumbers = array())
45 $this->offset = $offset;
46 $this->totalNumberOfLines = $totalNumberOfLines;
47 $this->skippedLineNumbers = $skippedLineNumbers;
51 * Parses a YAML string to a PHP value.
53 * @param string $value A YAML string
54 * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
55 * @param bool $objectSupport true if object support is enabled, false otherwise
56 * @param bool $objectForMap true if maps should return a stdClass instead of array()
58 * @return mixed A PHP value
60 * @throws ParseException If the YAML is not valid
62 public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
64 if (false === preg_match('//u', $value)) {
65 throw new ParseException('The YAML value does not appear to be valid UTF-8.');
68 $this->refs = array();
74 if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
75 $mbEncoding = mb_internal_encoding();
76 mb_internal_encoding('UTF-8');
80 $data = $this->doParse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
81 } catch (\Exception $e) {
82 } catch (\Throwable $e) {
85 if (null !== $mbEncoding) {
86 mb_internal_encoding($mbEncoding);
89 $this->lines = array();
90 $this->currentLine = '';
91 $this->refs = array();
92 $this->skippedLineNumbers = array();
93 $this->locallySkippedLineNumbers = array();
102 private function doParse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
104 $this->currentLineNb = -1;
105 $this->currentLine = '';
106 $value = $this->cleanup($value);
107 $this->lines = explode("\n", $value);
108 $this->locallySkippedLineNumbers = array();
110 if (null === $this->totalNumberOfLines) {
111 $this->totalNumberOfLines = count($this->lines);
116 $allowOverwrite = false;
118 while ($this->moveToNextLine()) {
119 if ($this->isCurrentLineEmpty()) {
124 if ("\t" === $this->currentLine[0]) {
125 throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
128 $isRef = $mergeNode = false;
129 if (self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) {
130 if ($context && 'mapping' == $context) {
131 throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine);
133 $context = 'sequence';
135 if (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
136 $isRef = $matches['ref'];
137 $values['value'] = $matches['value'];
141 if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
142 $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap);
144 if (isset($values['leadspaces'])
145 && self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($values['value']), $matches)
147 // this is a compact notation element, add to next block and parse
148 $block = $values['value'];
149 if ($this->isNextLineIndented()) {
150 $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1);
153 $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $exceptionOnInvalidType, $objectSupport, $objectForMap);
155 $data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
159 $this->refs[$isRef] = end($data);
162 self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
163 && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))
165 if ($context && 'sequence' == $context) {
166 throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine);
168 $context = 'mapping';
170 // force correct settings
171 Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
173 $key = Inline::parseScalar($values['key']);
174 } catch (ParseException $e) {
175 $e->setParsedLine($this->getRealCurrentLineNb() + 1);
176 $e->setSnippet($this->currentLine);
181 // Convert float keys to strings, to avoid being converted to integers by PHP
182 if (is_float($key)) {
183 $key = (string) $key;
188 $allowOverwrite = true;
189 if (isset($values['value']) && 0 === strpos($values['value'], '*')) {
190 $refName = substr($values['value'], 1);
191 if (!array_key_exists($refName, $this->refs)) {
192 throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
195 $refValue = $this->refs[$refName];
197 if (!is_array($refValue)) {
198 throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
201 $data += $refValue; // array union
203 if (isset($values['value']) && $values['value'] !== '') {
204 $value = $values['value'];
206 $value = $this->getNextEmbedBlock();
208 $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
210 if (!is_array($parsed)) {
211 throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
214 if (isset($parsed[0])) {
215 // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
216 // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
217 // in the sequence override keys specified in later mapping nodes.
218 foreach ($parsed as $parsedItem) {
219 if (!is_array($parsedItem)) {
220 throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
223 $data += $parsedItem; // array union
226 // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
227 // current mapping, unless the key already exists in it.
228 $data += $parsed; // array union
231 } elseif (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
232 $isRef = $matches['ref'];
233 $values['value'] = $matches['value'];
238 } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
240 // if next line is less indented or equal, then it means that the current value is null
241 if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
242 // Spec: Keys MUST be unique; first one wins.
243 // But overwriting is allowed when a merge node is used in current block.
244 if ($allowOverwrite || !isset($data[$key])) {
248 $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap);
249 // Spec: Keys MUST be unique; first one wins.
250 // But overwriting is allowed when a merge node is used in current block.
251 if ($allowOverwrite || !isset($data[$key])) {
252 $data[$key] = $value;
256 $value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
257 // Spec: Keys MUST be unique; first one wins.
258 // But overwriting is allowed when a merge node is used in current block.
259 if ($allowOverwrite || !isset($data[$key])) {
260 $data[$key] = $value;
264 $this->refs[$isRef] = $data[$key];
267 // multiple documents are not supported
268 if ('---' === $this->currentLine) {
269 throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine);
272 // 1-liner optionally followed by newline(s)
273 if (is_string($value) && $this->lines[0] === trim($value)) {
275 $value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
276 } catch (ParseException $e) {
277 $e->setParsedLine($this->getRealCurrentLineNb() + 1);
278 $e->setSnippet($this->currentLine);
286 throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
290 if ($objectForMap && !is_object($data) && 'mapping' === $context) {
291 $object = new \stdClass();
293 foreach ($data as $key => $value) {
294 $object->$key = $value;
300 return empty($data) ? null : $data;
303 private function parseBlock($offset, $yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap)
305 $skippedLineNumbers = $this->skippedLineNumbers;
307 foreach ($this->locallySkippedLineNumbers as $lineNumber) {
308 if ($lineNumber < $offset) {
312 $skippedLineNumbers[] = $lineNumber;
315 $parser = new self($offset, $this->totalNumberOfLines, $skippedLineNumbers);
316 $parser->refs = &$this->refs;
318 return $parser->doParse($yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap);
322 * Returns the current line number (takes the offset into account).
324 * @return int The current line number
326 private function getRealCurrentLineNb()
328 $realCurrentLineNumber = $this->currentLineNb + $this->offset;
330 foreach ($this->skippedLineNumbers as $skippedLineNumber) {
331 if ($skippedLineNumber > $realCurrentLineNumber) {
335 ++$realCurrentLineNumber;
338 return $realCurrentLineNumber;
342 * Returns the current line indentation.
344 * @return int The current line indentation
346 private function getCurrentLineIndentation()
348 return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
352 * Returns the next embed block of YAML.
354 * @param int $indentation The indent level at which the block is to be read, or null for default
355 * @param bool $inSequence True if the enclosing data structure is a sequence
357 * @return string A YAML string
359 * @throws ParseException When indentation problem are detected
361 private function getNextEmbedBlock($indentation = null, $inSequence = false)
363 $oldLineIndentation = $this->getCurrentLineIndentation();
364 $blockScalarIndentations = array();
366 if ($this->isBlockScalarHeader()) {
367 $blockScalarIndentations[] = $this->getCurrentLineIndentation();
370 if (!$this->moveToNextLine()) {
374 if (null === $indentation) {
375 $newIndent = $this->getCurrentLineIndentation();
377 $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
379 if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
380 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
383 $newIndent = $indentation;
387 if ($this->getCurrentLineIndentation() >= $newIndent) {
388 $data[] = substr($this->currentLine, $newIndent);
390 $this->moveToPreviousLine();
395 if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
396 // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
397 // and therefore no nested list or mapping
398 $this->moveToPreviousLine();
403 $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
405 if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) {
406 $blockScalarIndentations[] = $this->getCurrentLineIndentation();
409 $previousLineIndentation = $this->getCurrentLineIndentation();
411 while ($this->moveToNextLine()) {
412 $indent = $this->getCurrentLineIndentation();
414 // terminate all block scalars that are more indented than the current line
415 if (!empty($blockScalarIndentations) && $indent < $previousLineIndentation && trim($this->currentLine) !== '') {
416 foreach ($blockScalarIndentations as $key => $blockScalarIndentation) {
417 if ($blockScalarIndentation >= $this->getCurrentLineIndentation()) {
418 unset($blockScalarIndentations[$key]);
423 if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) {
424 $blockScalarIndentations[] = $this->getCurrentLineIndentation();
427 $previousLineIndentation = $indent;
429 if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
430 $this->moveToPreviousLine();
434 if ($this->isCurrentLineBlank()) {
435 $data[] = substr($this->currentLine, $newIndent);
439 // we ignore "comment" lines only when we are not inside a scalar block
440 if (empty($blockScalarIndentations) && $this->isCurrentLineComment()) {
441 // remember ignored comment lines (they are used later in nested
442 // parser calls to determine real line numbers)
444 // CAUTION: beware to not populate the global property here as it
445 // will otherwise influence the getRealCurrentLineNb() call here
446 // for consecutive comment lines and subsequent embedded blocks
447 $this->locallySkippedLineNumbers[] = $this->getRealCurrentLineNb();
452 if ($indent >= $newIndent) {
453 $data[] = substr($this->currentLine, $newIndent);
454 } elseif (0 == $indent) {
455 $this->moveToPreviousLine();
459 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
463 return implode("\n", $data);
467 * Moves the parser to the next line.
471 private function moveToNextLine()
473 if ($this->currentLineNb >= count($this->lines) - 1) {
477 $this->currentLine = $this->lines[++$this->currentLineNb];
483 * Moves the parser to the previous line.
487 private function moveToPreviousLine()
489 if ($this->currentLineNb < 1) {
493 $this->currentLine = $this->lines[--$this->currentLineNb];
499 * Parses a YAML value.
501 * @param string $value A YAML value
502 * @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise
503 * @param bool $objectSupport True if object support is enabled, false otherwise
504 * @param bool $objectForMap true if maps should return a stdClass instead of array()
505 * @param string $context The parser context (either sequence or mapping)
507 * @return mixed A PHP value
509 * @throws ParseException When reference does not exist
511 private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $context)
513 if (0 === strpos($value, '*')) {
514 if (false !== $pos = strpos($value, '#')) {
515 $value = substr($value, 1, $pos - 2);
517 $value = substr($value, 1);
520 if (!array_key_exists($value, $this->refs)) {
521 throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine);
524 return $this->refs[$value];
527 if (self::preg_match('/^'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
528 $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
530 return $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers));
534 $parsedValue = Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
536 if ('mapping' === $context && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
537 @trigger_error(sprintf('Using a colon in the unquoted mapping value "%s" in line %d is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.', $value, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
539 // to be thrown in 3.0
540 // throw new ParseException('A colon cannot be used in an unquoted mapping value.');
544 } catch (ParseException $e) {
545 $e->setParsedLine($this->getRealCurrentLineNb() + 1);
546 $e->setSnippet($this->currentLine);
553 * Parses a block scalar.
555 * @param string $style The style indicator that was used to begin this block scalar (| or >)
556 * @param string $chomping The chomping indicator that was used to begin this block scalar (+ or -)
557 * @param int $indentation The indentation indicator that was used to begin this block scalar
559 * @return string The text value
561 private function parseBlockScalar($style, $chomping = '', $indentation = 0)
563 $notEOF = $this->moveToNextLine();
568 $isCurrentLineBlank = $this->isCurrentLineBlank();
569 $blockLines = array();
571 // leading blank lines are consumed before determining indentation
572 while ($notEOF && $isCurrentLineBlank) {
573 // newline only if not EOF
574 if ($notEOF = $this->moveToNextLine()) {
576 $isCurrentLineBlank = $this->isCurrentLineBlank();
580 // determine indentation if not specified
581 if (0 === $indentation) {
582 if (self::preg_match('/^ +/', $this->currentLine, $matches)) {
583 $indentation = strlen($matches[0]);
587 if ($indentation > 0) {
588 $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
592 $isCurrentLineBlank ||
593 self::preg_match($pattern, $this->currentLine, $matches)
596 if ($isCurrentLineBlank && strlen($this->currentLine) > $indentation) {
597 $blockLines[] = substr($this->currentLine, $indentation);
598 } elseif ($isCurrentLineBlank) {
601 $blockLines[] = $matches[1];
604 // newline only if not EOF
605 if ($notEOF = $this->moveToNextLine()) {
606 $isCurrentLineBlank = $this->isCurrentLineBlank();
615 $this->moveToPreviousLine();
616 } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
621 if ('>' === $style) {
623 $previousLineIndented = false;
624 $previousLineBlank = false;
626 for ($i = 0, $blockLinesCount = count($blockLines); $i < $blockLinesCount; ++$i) {
627 if ('' === $blockLines[$i]) {
629 $previousLineIndented = false;
630 $previousLineBlank = true;
631 } elseif (' ' === $blockLines[$i][0]) {
632 $text .= "\n".$blockLines[$i];
633 $previousLineIndented = true;
634 $previousLineBlank = false;
635 } elseif ($previousLineIndented) {
636 $text .= "\n".$blockLines[$i];
637 $previousLineIndented = false;
638 $previousLineBlank = false;
639 } elseif ($previousLineBlank || 0 === $i) {
640 $text .= $blockLines[$i];
641 $previousLineIndented = false;
642 $previousLineBlank = false;
644 $text .= ' '.$blockLines[$i];
645 $previousLineIndented = false;
646 $previousLineBlank = false;
650 $text = implode("\n", $blockLines);
653 // deal with trailing newlines
654 if ('' === $chomping) {
655 $text = preg_replace('/\n+$/', "\n", $text);
656 } elseif ('-' === $chomping) {
657 $text = preg_replace('/\n+$/', '', $text);
664 * Returns true if the next line is indented.
666 * @return bool Returns true if the next line is indented, false otherwise
668 private function isNextLineIndented()
670 $currentIndentation = $this->getCurrentLineIndentation();
671 $EOF = !$this->moveToNextLine();
673 while (!$EOF && $this->isCurrentLineEmpty()) {
674 $EOF = !$this->moveToNextLine();
681 $ret = $this->getCurrentLineIndentation() > $currentIndentation;
683 $this->moveToPreviousLine();
689 * Returns true if the current line is blank or if it is a comment line.
691 * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
693 private function isCurrentLineEmpty()
695 return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
699 * Returns true if the current line is blank.
701 * @return bool Returns true if the current line is blank, false otherwise
703 private function isCurrentLineBlank()
705 return '' == trim($this->currentLine, ' ');
709 * Returns true if the current line is a comment line.
711 * @return bool Returns true if the current line is a comment line, false otherwise
713 private function isCurrentLineComment()
715 //checking explicitly the first char of the trim is faster than loops or strpos
716 $ltrimmedLine = ltrim($this->currentLine, ' ');
718 return '' !== $ltrimmedLine && $ltrimmedLine[0] === '#';
721 private function isCurrentLineLastLineInDocument()
723 return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1);
727 * Cleanups a YAML string to be parsed.
729 * @param string $value The input YAML string
731 * @return string A cleaned up YAML string
733 private function cleanup($value)
735 $value = str_replace(array("\r\n", "\r"), "\n", $value);
739 $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
740 $this->offset += $count;
742 // remove leading comments
743 $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
745 // items have been removed, update the offset
746 $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
747 $value = $trimmedValue;
750 // remove start of the document marker (---)
751 $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
753 // items have been removed, update the offset
754 $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
755 $value = $trimmedValue;
757 // remove end of the document marker (...)
758 $value = preg_replace('#\.\.\.\s*$#', '', $value);
765 * Returns true if the next line starts unindented collection.
767 * @return bool Returns true if the next line starts unindented collection, false otherwise
769 private function isNextLineUnIndentedCollection()
771 $currentIndentation = $this->getCurrentLineIndentation();
772 $notEOF = $this->moveToNextLine();
774 while ($notEOF && $this->isCurrentLineEmpty()) {
775 $notEOF = $this->moveToNextLine();
778 if (false === $notEOF) {
782 $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
784 $this->moveToPreviousLine();
790 * Returns true if the string is un-indented collection item.
792 * @return bool Returns true if the string is un-indented collection item, false otherwise
794 private function isStringUnIndentedCollectionItem()
796 return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- ');
800 * Tests whether or not the current line is the header of a block scalar.
804 private function isBlockScalarHeader()
806 return (bool) self::preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine);
810 * A local wrapper for `preg_match` which will throw a ParseException if there
811 * is an internal error in the PCRE engine.
813 * This avoids us needing to check for "false" every time PCRE is used
816 * @throws ParseException on a PCRE internal error
818 * @see preg_last_error()
822 public static function preg_match($pattern, $subject, &$matches = null, $flags = 0, $offset = 0)
824 if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) {
825 switch (preg_last_error()) {
826 case PREG_INTERNAL_ERROR:
827 $error = 'Internal PCRE error.';
829 case PREG_BACKTRACK_LIMIT_ERROR:
830 $error = 'pcre.backtrack_limit reached.';
832 case PREG_RECURSION_LIMIT_ERROR:
833 $error = 'pcre.recursion_limit reached.';
835 case PREG_BAD_UTF8_ERROR:
836 $error = 'Malformed UTF-8 data.';
838 case PREG_BAD_UTF8_OFFSET_ERROR:
839 $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
845 throw new ParseException($error);