[Yaml] support parsing files

This commit is contained in:
Christian Flothmann 2017-09-19 10:04:34 +01:00
parent aad62c427c
commit 9becb8ae34
7 changed files with 177 additions and 50 deletions

View File

@ -4,6 +4,8 @@ CHANGELOG
3.4.0
-----
* added support for parsing YAML files using the `Yaml::parseFile()` or `Parser::parseFile()` method
* the `Dumper`, `Parser`, and `Yaml` classes are marked as final
* Deprecated the `!php/object:` tag which will be replaced by the

View File

@ -26,7 +26,8 @@ class Inline
{
const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+)"|\'([^\']*+(?:\'\'[^\']*+)*+)\')';
public static $parsedLineNumber;
public static $parsedLineNumber = -1;
public static $parsedFilename;
private static $exceptionOnInvalidType = false;
private static $objectSupport = false;
@ -34,19 +35,18 @@ class Inline
private static $constantSupport = false;
/**
* @param int $flags
* @param int|null $parsedLineNumber
* @param int $flags
* @param int|null $parsedLineNumber
* @param string|null $parsedFilename
*/
public static function initialize($flags, $parsedLineNumber = null)
public static function initialize($flags, $parsedLineNumber = null, $parsedFilename = null)
{
self::$exceptionOnInvalidType = (bool) (Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE & $flags);
self::$objectSupport = (bool) (Yaml::PARSE_OBJECT & $flags);
self::$objectForMap = (bool) (Yaml::PARSE_OBJECT_FOR_MAP & $flags);
self::$constantSupport = (bool) (Yaml::PARSE_CONSTANT & $flags);
if (null !== $parsedLineNumber) {
self::$parsedLineNumber = $parsedLineNumber;
}
self::$parsedFilename = $parsedFilename;
self::$parsedLineNumber = null !== $parsedLineNumber ? $parsedLineNumber : -1;
}
/**
@ -128,7 +128,7 @@ class Inline
// some comments are allowed at the end
if (preg_replace('/\s+#.*$/A', '', substr($value, $i))) {
throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i)));
throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i)), self::$parsedLineNumber, $value, self::$parsedFilename);
}
if (isset($mbEncoding)) {
@ -322,7 +322,7 @@ class Inline
if (null !== $delimiters) {
$tmp = ltrim(substr($scalar, $i), ' ');
if (!in_array($tmp[0], $delimiters)) {
throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i)));
throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i)), self::$parsedLineNumber, $scalar, self::$parsedFilename);
}
}
} else {
@ -339,12 +339,12 @@ class Inline
$output = $match[1];
$i += strlen($output);
} else {
throw new ParseException(sprintf('Malformed inline YAML string: %s.', $scalar));
throw new ParseException(sprintf('Malformed inline YAML string: %s.', $scalar), self::$parsedLineNumber, null, self::$parsedFilename);
}
// a non-quoted string cannot start with @ or ` (reserved) nor with a scalar indicator (| or >)
if ($output && ('@' === $output[0] || '`' === $output[0] || '|' === $output[0] || '>' === $output[0])) {
throw new ParseException(sprintf('The reserved indicator "%s" cannot start a plain scalar; you need to quote the scalar.', $output[0]));
throw new ParseException(sprintf('The reserved indicator "%s" cannot start a plain scalar; you need to quote the scalar.', $output[0]), self::$parsedLineNumber, $output, self::$parsedFilename);
}
if ($output && '%' === $output[0]) {
@ -372,7 +372,7 @@ class Inline
private static function parseQuotedScalar($scalar, &$i)
{
if (!Parser::preg_match('/'.self::REGEX_QUOTED_STRING.'/Au', substr($scalar, $i), $match)) {
throw new ParseException(sprintf('Malformed inline YAML string: %s.', substr($scalar, $i)));
throw new ParseException(sprintf('Malformed inline YAML string: %s.', substr($scalar, $i)), self::$parsedLineNumber, $scalar, self::$parsedFilename);
}
$output = substr($match[0], 1, strlen($match[0]) - 2);
@ -455,7 +455,7 @@ class Inline
++$i;
}
throw new ParseException(sprintf('Malformed inline YAML string: %s.', $sequence));
throw new ParseException(sprintf('Malformed inline YAML string: %s.', $sequence), self::$parsedLineNumber, null, self::$parsedFilename);
}
/**
@ -572,7 +572,7 @@ class Inline
}
}
throw new ParseException(sprintf('Malformed inline YAML string: %s.', $mapping));
throw new ParseException(sprintf('Malformed inline YAML string: %s.', $mapping), self::$parsedLineNumber, null, self::$parsedFilename);
}
/**
@ -600,11 +600,11 @@ class Inline
// an unquoted *
if (false === $value || '' === $value) {
throw new ParseException('A reference must contain at least one character.');
throw new ParseException('A reference must contain at least one character.', self::$parsedLineNumber, $value, self::$parsedFilename);
}
if (!array_key_exists($value, $references)) {
throw new ParseException(sprintf('Reference "%s" does not exist.', $value));
throw new ParseException(sprintf('Reference "%s" does not exist.', $value), self::$parsedLineNumber, $value, self::$parsedFilename);
}
return $references[$value];
@ -639,7 +639,7 @@ class Inline
}
if (self::$exceptionOnInvalidType) {
throw new ParseException('Object support when parsing a YAML file has been disabled.');
throw new ParseException('Object support when parsing a YAML file has been disabled.', self::$parsedLineNumber, $scalar, self::$parsedFilename);
}
return;
@ -651,7 +651,7 @@ class Inline
}
if (self::$exceptionOnInvalidType) {
throw new ParseException('Object support when parsing a YAML file has been disabled.');
throw new ParseException('Object support when parsing a YAML file has been disabled.', self::$parsedLineNumber, $scalar, self::$parsedFilename);
}
return;
@ -661,7 +661,7 @@ class Inline
}
if (self::$exceptionOnInvalidType) {
throw new ParseException('Object support when parsing a YAML file has been disabled.');
throw new ParseException('Object support when parsing a YAML file has been disabled.', self::$parsedLineNumber, $scalar, self::$parsedFilename);
}
return;
@ -673,10 +673,10 @@ class Inline
return constant($const);
}
throw new ParseException(sprintf('The constant "%s" is not defined.', $const));
throw new ParseException(sprintf('The constant "%s" is not defined.', $const), self::$parsedLineNumber, $scalar, self::$parsedFilename);
}
if (self::$exceptionOnInvalidType) {
throw new ParseException(sprintf('The string "%s" could not be parsed as a constant. Have you forgotten to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar));
throw new ParseException(sprintf('The string "%s" could not be parsed as a constant. Have you forgotten to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar), self::$parsedLineNumber, $scalar, self::$parsedFilename);
}
return;
@ -686,10 +686,10 @@ class Inline
return constant($const);
}
throw new ParseException(sprintf('The constant "%s" is not defined.', $const));
throw new ParseException(sprintf('The constant "%s" is not defined.', $const), self::$parsedLineNumber, $scalar, self::$parsedFilename);
}
if (self::$exceptionOnInvalidType) {
throw new ParseException(sprintf('The string "%s" could not be parsed as a constant. Have you forgotten to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar));
throw new ParseException(sprintf('The string "%s" could not be parsed as a constant. Have you forgotten to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar), self::$parsedLineNumber, $scalar, self::$parsedFilename);
}
return;
@ -781,7 +781,7 @@ class Inline
// Built-in tags
if ($tag && '!' === $tag[0]) {
throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag));
throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag), self::$parsedLineNumber, $value, self::$parsedFilename);
}
if (Yaml::PARSE_CUSTOM_TAGS & $flags) {
@ -790,7 +790,7 @@ class Inline
return $tag;
}
throw new ParseException(sprintf('Tags support is not enabled. Enable the `Yaml::PARSE_CUSTOM_TAGS` flag to use "!%s".', $tag));
throw new ParseException(sprintf('Tags support is not enabled. Enable the `Yaml::PARSE_CUSTOM_TAGS` flag to use "!%s".', $tag), self::$parsedLineNumber, $value, self::$parsedFilename);
}
/**
@ -805,11 +805,11 @@ class Inline
$parsedBinaryData = self::parseScalar(preg_replace('/\s/', '', $scalar));
if (0 !== (strlen($parsedBinaryData) % 4)) {
throw new ParseException(sprintf('The normalized base64 encoded data (data without whitespace characters) length must be a multiple of four (%d bytes given).', strlen($parsedBinaryData)));
throw new ParseException(sprintf('The normalized base64 encoded data (data without whitespace characters) length must be a multiple of four (%d bytes given).', strlen($parsedBinaryData)), self::$parsedLineNumber, $scalar, self::$parsedFilename);
}
if (!Parser::preg_match('#^[A-Z0-9+/]+={0,2}$#i', $parsedBinaryData)) {
throw new ParseException(sprintf('The base64 encoded data (%s) contains invalid characters.', $parsedBinaryData));
throw new ParseException(sprintf('The base64 encoded data (%s) contains invalid characters.', $parsedBinaryData), self::$parsedLineNumber, $scalar, self::$parsedFilename);
}
return base64_decode($parsedBinaryData, true);

View File

@ -26,6 +26,7 @@ class Parser
const TAG_PATTERN = '(?P<tag>![\w!.\/:-]+)';
const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
private $filename;
private $offset = 0;
private $totalNumberOfLines;
private $lines = array();
@ -50,6 +51,35 @@ class Parser
}
}
/**
* Parses a YAML file into a PHP value.
*
* @param string $filename The path to the YAML file to be parsed
* @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior
*
* @return mixed The YAML converted to a PHP value
*
* @throws ParseException If the file could not be read or the YAML is not valid
*/
public function parseFile($filename, $flags = 0)
{
if (!is_file($filename)) {
throw new ParseException(sprintf('File "%s" does not exist.', $filename));
}
if (!is_readable($filename)) {
throw new ParseException(sprintf('File "%s" cannot be read.', $filename));
}
$this->filename = $filename;
try {
return $this->parse(file_get_contents($filename), $flags);
} finally {
$this->filename = null;
}
}
/**
* Parses a YAML string to a PHP value.
*
@ -93,7 +123,7 @@ class Parser
}
if (false === preg_match('//u', $value)) {
throw new ParseException('The YAML value does not appear to be valid UTF-8.');
throw new ParseException('The YAML value does not appear to be valid UTF-8.', -1, null, $this->filename);
}
$this->refs = array();
@ -168,13 +198,13 @@ class Parser
// tab?
if ("\t" === $this->currentLine[0]) {
throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
}
$isRef = $mergeNode = false;
if (self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) {
if ($context && 'mapping' == $context) {
throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine);
throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
}
$context = 'sequence';
@ -218,11 +248,11 @@ class Parser
&& (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))
) {
if ($context && 'sequence' == $context) {
throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine);
throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine, $this->filename);
}
$context = 'mapping';
Inline::initialize($flags, $this->getRealCurrentLineNb());
Inline::initialize($flags, $this->getRealCurrentLineNb(), $this->filename);
try {
$i = 0;
$evaluateKey = !(Yaml::PARSE_KEYS_AS_STRINGS & $flags);
@ -256,13 +286,13 @@ class Parser
if (isset($values['value']) && 0 === strpos($values['value'], '*')) {
$refName = substr(rtrim($values['value']), 1);
if (!array_key_exists($refName, $this->refs)) {
throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
}
$refValue = $this->refs[$refName];
if (!is_array($refValue)) {
throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
}
$data += $refValue; // array union
@ -275,7 +305,7 @@ class Parser
$parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $flags);
if (!is_array($parsed)) {
throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
}
if (isset($parsed[0])) {
@ -284,7 +314,7 @@ class Parser
// in the sequence override keys specified in later mapping nodes.
foreach ($parsed as $parsedItem) {
if (!is_array($parsedItem)) {
throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem, $this->filename);
}
$data += $parsedItem; // array union
@ -350,7 +380,7 @@ class Parser
} else {
// multiple documents are not supported
if ('---' === $this->currentLine) {
throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine);
throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine, $this->filename);
}
if (isset($this->currentLine[1]) && '?' === $this->currentLine[0] && ' ' === $this->currentLine[1]) {
@ -412,7 +442,7 @@ class Parser
}
}
throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
}
} while ($this->moveToNextLine());
@ -515,7 +545,7 @@ class Parser
$unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
}
} else {
$newIndent = $indentation;
@ -594,7 +624,7 @@ class Parser
break;
} else {
throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
}
}
@ -654,7 +684,7 @@ class Parser
}
if (!array_key_exists($value, $this->refs)) {
throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine);
throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine, $this->filename);
}
return $this->refs[$value];
@ -704,7 +734,7 @@ class Parser
$parsedValue = Inline::parse($value, $flags, $this->refs);
if ('mapping' === $context && is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
throw new ParseException('A colon cannot be used in an unquoted mapping value.');
throw new ParseException('A colon cannot be used in an unquoted mapping value.', $this->getRealCurrentLineNb() + 1, $value, $this->filename);
}
return $parsedValue;
@ -1044,13 +1074,13 @@ class Parser
// Built-in tags
if ($tag && '!' === $tag[0]) {
throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag));
throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag), $this->getRealCurrentLineNb() + 1, $value, $this->filename);
}
if (Yaml::PARSE_CUSTOM_TAGS & $flags) {
return $tag;
}
throw new ParseException(sprintf('Tags support is not enabled. You must use the flag `Yaml::PARSE_CUSTOM_TAGS` to use "%s".', $matches['tag']));
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);
}
}

View File

@ -0,0 +1,18 @@
- escapedCharacters
- sfComments
- sfCompact
- sfTests
- sfObjects
- sfMergeKey
- sfQuotes
- YtsAnchorAlias
- YtsBasicTests
- YtsBlockMapping
- YtsDocumentSeparator
- YtsErrorTests
- YtsFlowCollections
- YtsFoldedScalars
- YtsNullsAndEmpties
- YtsSpecificationExamples
- YtsTypeTransfers
- unindentedCollections

View File

@ -18,6 +18,11 @@ use Symfony\Component\Yaml\Yaml;
class InlineTest extends TestCase
{
protected function setUp()
{
Inline::initialize(0);
}
/**
* @dataProvider getTestsForParse
*/
@ -283,11 +288,16 @@ class InlineTest extends TestCase
/**
* @dataProvider getReservedIndicators
* @expectedException \Symfony\Component\Yaml\Exception\ParseException
* @expectedExceptionMessage cannot start a plain scalar; you need to quote the scalar.
*/
public function testParseUnquotedScalarStartingWithReservedIndicator($indicator)
{
if (method_exists($this, 'expectExceptionMessage')) {
$this->expectException(ParseException::class);
$this->expectExceptionMessage(sprintf('cannot start a plain scalar; you need to quote the scalar (near "%sfoo ").', $indicator));
} else {
$this->setExpectedException(ParseException::class, sprintf('cannot start a plain scalar; you need to quote the scalar (near "%sfoo ").', $indicator));
}
Inline::parse(sprintf('{ foo: %sfoo }', $indicator));
}
@ -298,11 +308,16 @@ class InlineTest extends TestCase
/**
* @dataProvider getScalarIndicators
* @expectedException \Symfony\Component\Yaml\Exception\ParseException
* @expectedExceptionMessage cannot start a plain scalar; you need to quote the scalar.
*/
public function testParseUnquotedScalarStartingWithScalarIndicator($indicator)
{
if (method_exists($this, 'expectExceptionMessage')) {
$this->expectException(ParseException::class);
$this->expectExceptionMessage(sprintf('cannot start a plain scalar; you need to quote the scalar (near "%sfoo ").', $indicator));
} else {
$this->setExpectedException(ParseException::class, sprintf('cannot start a plain scalar; you need to quote the scalar (near "%sfoo ").', $indicator));
}
Inline::parse(sprintf('{ foo: %sfoo }', $indicator));
}

View File

@ -30,6 +30,8 @@ class ParserTest extends TestCase
protected function tearDown()
{
$this->parser = null;
chmod(__DIR__.'/Fixtures/not_readable.yml', 0644);
}
/**
@ -1696,7 +1698,7 @@ YAML
/**
* @expectedException \Symfony\Component\Yaml\Exception\ParseException
* @expectedExceptionMessage The built-in tag "!!foo" is not implemented.
* @expectedExceptionMessage The built-in tag "!!foo" is not implemented at line 1 (near "!!foo").
*/
public function testExceptionWhenUsingUnsuportedBuiltInTags()
{
@ -1903,6 +1905,43 @@ YAML;
$this->assertSame($expected, $this->parser->parse($yaml, Yaml::PARSE_CONSTANT | Yaml::PARSE_KEYS_AS_STRINGS));
}
public function testFilenamesAreParsedAsStringsWithoutFlag()
{
$file = __DIR__.'/Fixtures/index.yml';
$this->assertSame($file, $this->parser->parse($file));
}
public function testParseFile()
{
$this->assertInternalType('array', $this->parser->parseFile(__DIR__.'/Fixtures/index.yml'));
}
/**
* @expectedException \Symfony\Component\Yaml\Exception\ParseException
* @expectedExceptionMessageRegExp #^File ".+/Fixtures/nonexistent.yml" does not exist\.$#
*/
public function testParsingNonExistentFilesThrowsException()
{
$this->parser->parseFile(__DIR__.'/Fixtures/nonexistent.yml');
}
/**
* @expectedException \Symfony\Component\Yaml\Exception\ParseException
* @expectedExceptionMessageRegExp #^File ".+/Fixtures/not_readable.yml" cannot be read\.$#
*/
public function testParsingNotReadableFilesThrowsException()
{
if ('\\' === DIRECTORY_SEPARATOR) {
$this->markTestSkipped('chmod is not supported on Windows');
}
$file = __DIR__.'/Fixtures/not_readable.yml';
chmod($file, 0200);
$this->parser->parseFile($file);
}
}
class B

View File

@ -39,6 +39,29 @@ class Yaml
*/
const PARSE_KEYS_AS_STRINGS = 2048;
/**
* Parses a YAML file into a PHP value.
*
* Usage:
* <code>
* $array = Yaml::parseFile('config.yml');
* print_r($array);
* </code>
*
* @param string $filename The path to the YAML file to be parsed
* @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior
*
* @return mixed The YAML converted to a PHP value
*
* @throws ParseException If the file could not be read or the YAML is not valid
*/
public static function parseFile($filename, $flags = 0)
{
$yaml = new Parser();
return $yaml->parseFile($filename, $flags);
}
/**
* Parses YAML into a PHP value.
*