Inline.php (19828B)
1 <?php 2 3 /* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12 namespace Symfony\Component\Yaml; 13 14 use Symfony\Component\Yaml\Exception\ParseException; 15 use Symfony\Component\Yaml\Exception\DumpException; 16 17 /** 18 * Inline implements a YAML parser/dumper for the YAML inline syntax. 19 * 20 * @author Fabien Potencier <fabien@symfony.com> 21 */ 22 class Inline 23 { 24 const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\']*(?:\'\'[^\']*)*)\')'; 25 26 private static $exceptionOnInvalidType = false; 27 private static $objectSupport = false; 28 private static $objectForMap = false; 29 30 /** 31 * Converts a YAML string to a PHP array. 32 * 33 * @param string $value A YAML string 34 * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise 35 * @param bool $objectSupport true if object support is enabled, false otherwise 36 * @param bool $objectForMap true if maps should return a stdClass instead of array() 37 * @param array $references Mapping of variable names to values 38 * 39 * @return array A PHP array representing the YAML string 40 * 41 * @throws ParseException 42 */ 43 public static function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false, $references = array()) 44 { 45 self::$exceptionOnInvalidType = $exceptionOnInvalidType; 46 self::$objectSupport = $objectSupport; 47 self::$objectForMap = $objectForMap; 48 49 $value = trim($value); 50 51 if ('' === $value) { 52 return ''; 53 } 54 55 if (function_exists('mb_internal_encoding') && ((int) ini_get('mbstring.func_overload')) & 2) { 56 $mbEncoding = mb_internal_encoding(); 57 mb_internal_encoding('ASCII'); 58 } 59 60 $i = 0; 61 switch ($value[0]) { 62 case '[': 63 $result = self::parseSequence($value, $i, $references); 64 ++$i; 65 break; 66 case '{': 67 $result = self::parseMapping($value, $i, $references); 68 ++$i; 69 break; 70 default: 71 $result = self::parseScalar($value, null, array('"', "'"), $i, true, $references); 72 } 73 74 // some comments are allowed at the end 75 if (preg_replace('/\s+#.*$/A', '', substr($value, $i))) { 76 throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i))); 77 } 78 79 if (isset($mbEncoding)) { 80 mb_internal_encoding($mbEncoding); 81 } 82 83 return $result; 84 } 85 86 /** 87 * Dumps a given PHP variable to a YAML string. 88 * 89 * @param mixed $value The PHP variable to convert 90 * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise 91 * @param bool $objectSupport true if object support is enabled, false otherwise 92 * 93 * @return string The YAML string representing the PHP array 94 * 95 * @throws DumpException When trying to dump PHP resource 96 */ 97 public static function dump($value, $exceptionOnInvalidType = false, $objectSupport = false) 98 { 99 switch (true) { 100 case is_resource($value): 101 if ($exceptionOnInvalidType) { 102 throw new DumpException(sprintf('Unable to dump PHP resources in a YAML file ("%s").', get_resource_type($value))); 103 } 104 105 return 'null'; 106 case is_object($value): 107 if ($objectSupport) { 108 return '!!php/object:'.serialize($value); 109 } 110 111 if ($exceptionOnInvalidType) { 112 throw new DumpException('Object support when dumping a YAML file has been disabled.'); 113 } 114 115 return 'null'; 116 case is_array($value): 117 return self::dumpArray($value, $exceptionOnInvalidType, $objectSupport); 118 case null === $value: 119 return 'null'; 120 case true === $value: 121 return 'true'; 122 case false === $value: 123 return 'false'; 124 case ctype_digit($value): 125 return is_string($value) ? "'$value'" : (int) $value; 126 case is_numeric($value): 127 $locale = setlocale(LC_NUMERIC, 0); 128 if (false !== $locale) { 129 setlocale(LC_NUMERIC, 'C'); 130 } 131 if (is_float($value)) { 132 $repr = (string) $value; 133 if (is_infinite($value)) { 134 $repr = str_ireplace('INF', '.Inf', $repr); 135 } elseif (floor($value) == $value && $repr == $value) { 136 // Preserve float data type since storing a whole number will result in integer value. 137 $repr = '!!float '.$repr; 138 } 139 } else { 140 $repr = is_string($value) ? "'$value'" : (string) $value; 141 } 142 if (false !== $locale) { 143 setlocale(LC_NUMERIC, $locale); 144 } 145 146 return $repr; 147 case '' == $value: 148 return "''"; 149 case Escaper::requiresDoubleQuoting($value): 150 return Escaper::escapeWithDoubleQuotes($value); 151 case Escaper::requiresSingleQuoting($value): 152 case preg_match(self::getHexRegex(), $value): 153 case preg_match(self::getTimestampRegex(), $value): 154 return Escaper::escapeWithSingleQuotes($value); 155 default: 156 return $value; 157 } 158 } 159 160 /** 161 * Dumps a PHP array to a YAML string. 162 * 163 * @param array $value The PHP array to dump 164 * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise 165 * @param bool $objectSupport true if object support is enabled, false otherwise 166 * 167 * @return string The YAML string representing the PHP array 168 */ 169 private static function dumpArray($value, $exceptionOnInvalidType, $objectSupport) 170 { 171 // array 172 $keys = array_keys($value); 173 $keysCount = count($keys); 174 if ((1 === $keysCount && '0' == $keys[0]) 175 || ($keysCount > 1 && array_reduce($keys, function ($v, $w) { return (int) $v + $w; }, 0) === $keysCount * ($keysCount - 1) / 2) 176 ) { 177 $output = array(); 178 foreach ($value as $val) { 179 $output[] = self::dump($val, $exceptionOnInvalidType, $objectSupport); 180 } 181 182 return sprintf('[%s]', implode(', ', $output)); 183 } 184 185 // mapping 186 $output = array(); 187 foreach ($value as $key => $val) { 188 $output[] = sprintf('%s: %s', self::dump($key, $exceptionOnInvalidType, $objectSupport), self::dump($val, $exceptionOnInvalidType, $objectSupport)); 189 } 190 191 return sprintf('{ %s }', implode(', ', $output)); 192 } 193 194 /** 195 * Parses a scalar to a YAML string. 196 * 197 * @param string $scalar 198 * @param string $delimiters 199 * @param array $stringDelimiters 200 * @param int &$i 201 * @param bool $evaluate 202 * @param array $references 203 * 204 * @return string A YAML string 205 * 206 * @throws ParseException When malformed inline YAML string is parsed 207 */ 208 public static function parseScalar($scalar, $delimiters = null, $stringDelimiters = array('"', "'"), &$i = 0, $evaluate = true, $references = array()) 209 { 210 if (in_array($scalar[$i], $stringDelimiters)) { 211 // quoted scalar 212 $output = self::parseQuotedScalar($scalar, $i); 213 214 if (null !== $delimiters) { 215 $tmp = ltrim(substr($scalar, $i), ' '); 216 if (!in_array($tmp[0], $delimiters)) { 217 throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i))); 218 } 219 } 220 } else { 221 // "normal" string 222 if (!$delimiters) { 223 $output = substr($scalar, $i); 224 $i += strlen($output); 225 226 // remove comments 227 if (preg_match('/[ \t]+#/', $output, $match, PREG_OFFSET_CAPTURE)) { 228 $output = substr($output, 0, $match[0][1]); 229 } 230 } elseif (preg_match('/^(.+?)('.implode('|', $delimiters).')/', substr($scalar, $i), $match)) { 231 $output = $match[1]; 232 $i += strlen($output); 233 } else { 234 throw new ParseException(sprintf('Malformed inline YAML string (%s).', $scalar)); 235 } 236 237 if ($evaluate) { 238 $output = self::evaluateScalar($output, $references); 239 } 240 } 241 242 return $output; 243 } 244 245 /** 246 * Parses a quoted scalar to YAML. 247 * 248 * @param string $scalar 249 * @param int &$i 250 * 251 * @return string A YAML string 252 * 253 * @throws ParseException When malformed inline YAML string is parsed 254 */ 255 private static function parseQuotedScalar($scalar, &$i) 256 { 257 if (!preg_match('/'.self::REGEX_QUOTED_STRING.'/Au', substr($scalar, $i), $match)) { 258 throw new ParseException(sprintf('Malformed inline YAML string (%s).', substr($scalar, $i))); 259 } 260 261 $output = substr($match[0], 1, strlen($match[0]) - 2); 262 263 $unescaper = new Unescaper(); 264 if ('"' == $scalar[$i]) { 265 $output = $unescaper->unescapeDoubleQuotedString($output); 266 } else { 267 $output = $unescaper->unescapeSingleQuotedString($output); 268 } 269 270 $i += strlen($match[0]); 271 272 return $output; 273 } 274 275 /** 276 * Parses a sequence to a YAML string. 277 * 278 * @param string $sequence 279 * @param int &$i 280 * @param array $references 281 * 282 * @return string A YAML string 283 * 284 * @throws ParseException When malformed inline YAML string is parsed 285 */ 286 private static function parseSequence($sequence, &$i = 0, $references = array()) 287 { 288 $output = array(); 289 $len = strlen($sequence); 290 ++$i; 291 292 // [foo, bar, ...] 293 while ($i < $len) { 294 switch ($sequence[$i]) { 295 case '[': 296 // nested sequence 297 $output[] = self::parseSequence($sequence, $i, $references); 298 break; 299 case '{': 300 // nested mapping 301 $output[] = self::parseMapping($sequence, $i, $references); 302 break; 303 case ']': 304 return $output; 305 case ',': 306 case ' ': 307 break; 308 default: 309 $isQuoted = in_array($sequence[$i], array('"', "'")); 310 $value = self::parseScalar($sequence, array(',', ']'), array('"', "'"), $i, true, $references); 311 312 // the value can be an array if a reference has been resolved to an array var 313 if (!is_array($value) && !$isQuoted && false !== strpos($value, ': ')) { 314 // embedded mapping? 315 try { 316 $pos = 0; 317 $value = self::parseMapping('{'.$value.'}', $pos, $references); 318 } catch (\InvalidArgumentException $e) { 319 // no, it's not 320 } 321 } 322 323 $output[] = $value; 324 325 --$i; 326 } 327 328 ++$i; 329 } 330 331 throw new ParseException(sprintf('Malformed inline YAML string %s', $sequence)); 332 } 333 334 /** 335 * Parses a mapping to a YAML string. 336 * 337 * @param string $mapping 338 * @param int &$i 339 * @param array $references 340 * 341 * @return string A YAML string 342 * 343 * @throws ParseException When malformed inline YAML string is parsed 344 */ 345 private static function parseMapping($mapping, &$i = 0, $references = array()) 346 { 347 $output = array(); 348 $len = strlen($mapping); 349 ++$i; 350 351 // {foo: bar, bar:foo, ...} 352 while ($i < $len) { 353 switch ($mapping[$i]) { 354 case ' ': 355 case ',': 356 ++$i; 357 continue 2; 358 case '}': 359 if (self::$objectForMap) { 360 return (object) $output; 361 } 362 363 return $output; 364 } 365 366 // key 367 $key = self::parseScalar($mapping, array(':', ' '), array('"', "'"), $i, false); 368 369 // value 370 $done = false; 371 372 while ($i < $len) { 373 switch ($mapping[$i]) { 374 case '[': 375 // nested sequence 376 $value = self::parseSequence($mapping, $i, $references); 377 // Spec: Keys MUST be unique; first one wins. 378 // Parser cannot abort this mapping earlier, since lines 379 // are processed sequentially. 380 if (!isset($output[$key])) { 381 $output[$key] = $value; 382 } 383 $done = true; 384 break; 385 case '{': 386 // nested mapping 387 $value = self::parseMapping($mapping, $i, $references); 388 // Spec: Keys MUST be unique; first one wins. 389 // Parser cannot abort this mapping earlier, since lines 390 // are processed sequentially. 391 if (!isset($output[$key])) { 392 $output[$key] = $value; 393 } 394 $done = true; 395 break; 396 case ':': 397 case ' ': 398 break; 399 default: 400 $value = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i, true, $references); 401 // Spec: Keys MUST be unique; first one wins. 402 // Parser cannot abort this mapping earlier, since lines 403 // are processed sequentially. 404 if (!isset($output[$key])) { 405 $output[$key] = $value; 406 } 407 $done = true; 408 --$i; 409 } 410 411 ++$i; 412 413 if ($done) { 414 continue 2; 415 } 416 } 417 } 418 419 throw new ParseException(sprintf('Malformed inline YAML string %s', $mapping)); 420 } 421 422 /** 423 * Evaluates scalars and replaces magic values. 424 * 425 * @param string $scalar 426 * @param array $references 427 * 428 * @return string A YAML string 429 * 430 * @throws ParseException when object parsing support was disabled and the parser detected a PHP object or when a reference could not be resolved 431 */ 432 private static function evaluateScalar($scalar, $references = array()) 433 { 434 $scalar = trim($scalar); 435 $scalarLower = strtolower($scalar); 436 437 if (0 === strpos($scalar, '*')) { 438 if (false !== $pos = strpos($scalar, '#')) { 439 $value = substr($scalar, 1, $pos - 2); 440 } else { 441 $value = substr($scalar, 1); 442 } 443 444 // an unquoted * 445 if (false === $value || '' === $value) { 446 throw new ParseException('A reference must contain at least one character.'); 447 } 448 449 if (!array_key_exists($value, $references)) { 450 throw new ParseException(sprintf('Reference "%s" does not exist.', $value)); 451 } 452 453 return $references[$value]; 454 } 455 456 switch (true) { 457 case 'null' === $scalarLower: 458 case '' === $scalar: 459 case '~' === $scalar: 460 return; 461 case 'true' === $scalarLower: 462 return true; 463 case 'false' === $scalarLower: 464 return false; 465 // Optimise for returning strings. 466 case $scalar[0] === '+' || $scalar[0] === '-' || $scalar[0] === '.' || $scalar[0] === '!' || is_numeric($scalar[0]): 467 switch (true) { 468 case 0 === strpos($scalar, '!str'): 469 return (string) substr($scalar, 5); 470 case 0 === strpos($scalar, '! '): 471 return (int) self::parseScalar(substr($scalar, 2)); 472 case 0 === strpos($scalar, '!!php/object:'): 473 if (self::$objectSupport) { 474 return unserialize(substr($scalar, 13)); 475 } 476 477 if (self::$exceptionOnInvalidType) { 478 throw new ParseException('Object support when parsing a YAML file has been disabled.'); 479 } 480 481 return; 482 case 0 === strpos($scalar, '!!float '): 483 return (float) substr($scalar, 8); 484 case ctype_digit($scalar): 485 $raw = $scalar; 486 $cast = (int) $scalar; 487 488 return '0' == $scalar[0] ? octdec($scalar) : (((string) $raw == (string) $cast) ? $cast : $raw); 489 case '-' === $scalar[0] && ctype_digit(substr($scalar, 1)): 490 $raw = $scalar; 491 $cast = (int) $scalar; 492 493 return '0' == $scalar[1] ? octdec($scalar) : (((string) $raw === (string) $cast) ? $cast : $raw); 494 case is_numeric($scalar): 495 case preg_match(self::getHexRegex(), $scalar): 496 return '0x' === $scalar[0].$scalar[1] ? hexdec($scalar) : (float) $scalar; 497 case '.inf' === $scalarLower: 498 case '.nan' === $scalarLower: 499 return -log(0); 500 case '-.inf' === $scalarLower: 501 return log(0); 502 case preg_match('/^(-|\+)?[0-9,]+(\.[0-9]+)?$/', $scalar): 503 return (float) str_replace(',', '', $scalar); 504 case preg_match(self::getTimestampRegex(), $scalar): 505 return strtotime($scalar); 506 } 507 default: 508 return (string) $scalar; 509 } 510 } 511 512 /** 513 * Gets a regex that matches a YAML date. 514 * 515 * @return string The regular expression 516 * 517 * @see http://www.yaml.org/spec/1.2/spec.html#id2761573 518 */ 519 private static function getTimestampRegex() 520 { 521 return <<<EOF 522 ~^ 523 (?P<year>[0-9][0-9][0-9][0-9]) 524 -(?P<month>[0-9][0-9]?) 525 -(?P<day>[0-9][0-9]?) 526 (?:(?:[Tt]|[ \t]+) 527 (?P<hour>[0-9][0-9]?) 528 :(?P<minute>[0-9][0-9]) 529 :(?P<second>[0-9][0-9]) 530 (?:\.(?P<fraction>[0-9]*))? 531 (?:[ \t]*(?P<tz>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?) 532 (?::(?P<tz_minute>[0-9][0-9]))?))?)? 533 $~x 534 EOF; 535 } 536 537 /** 538 * Gets a regex that matches a YAML number in hexadecimal notation. 539 * 540 * @return string 541 */ 542 private static function getHexRegex() 543 { 544 return '~^0x[0-9a-f]++$~i'; 545 } 546 }