JsonDecoder.php 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. <?php declare(strict_types=1);
  2. namespace PhpParser;
  3. class JsonDecoder {
  4. /** @var \ReflectionClass<Node>[] Node type to reflection class map */
  5. private array $reflectionClassCache;
  6. /** @return mixed */
  7. public function decode(string $json) {
  8. $value = json_decode($json, true);
  9. if (json_last_error()) {
  10. throw new \RuntimeException('JSON decoding error: ' . json_last_error_msg());
  11. }
  12. return $this->decodeRecursive($value);
  13. }
  14. /**
  15. * @param mixed $value
  16. * @return mixed
  17. */
  18. private function decodeRecursive($value) {
  19. if (\is_array($value)) {
  20. if (isset($value['nodeType'])) {
  21. if ($value['nodeType'] === 'Comment' || $value['nodeType'] === 'Comment_Doc') {
  22. return $this->decodeComment($value);
  23. }
  24. return $this->decodeNode($value);
  25. }
  26. return $this->decodeArray($value);
  27. }
  28. return $value;
  29. }
  30. private function decodeArray(array $array): array {
  31. $decodedArray = [];
  32. foreach ($array as $key => $value) {
  33. $decodedArray[$key] = $this->decodeRecursive($value);
  34. }
  35. return $decodedArray;
  36. }
  37. private function decodeNode(array $value): Node {
  38. $nodeType = $value['nodeType'];
  39. if (!\is_string($nodeType)) {
  40. throw new \RuntimeException('Node type must be a string');
  41. }
  42. $reflectionClass = $this->reflectionClassFromNodeType($nodeType);
  43. $node = $reflectionClass->newInstanceWithoutConstructor();
  44. if (isset($value['attributes'])) {
  45. if (!\is_array($value['attributes'])) {
  46. throw new \RuntimeException('Attributes must be an array');
  47. }
  48. $node->setAttributes($this->decodeArray($value['attributes']));
  49. }
  50. foreach ($value as $name => $subNode) {
  51. if ($name === 'nodeType' || $name === 'attributes') {
  52. continue;
  53. }
  54. $node->$name = $this->decodeRecursive($subNode);
  55. }
  56. return $node;
  57. }
  58. private function decodeComment(array $value): Comment {
  59. $className = $value['nodeType'] === 'Comment' ? Comment::class : Comment\Doc::class;
  60. if (!isset($value['text'])) {
  61. throw new \RuntimeException('Comment must have text');
  62. }
  63. return new $className(
  64. $value['text'],
  65. $value['line'] ?? -1, $value['filePos'] ?? -1, $value['tokenPos'] ?? -1,
  66. $value['endLine'] ?? -1, $value['endFilePos'] ?? -1, $value['endTokenPos'] ?? -1
  67. );
  68. }
  69. /** @return \ReflectionClass<Node> */
  70. private function reflectionClassFromNodeType(string $nodeType): \ReflectionClass {
  71. if (!isset($this->reflectionClassCache[$nodeType])) {
  72. $className = $this->classNameFromNodeType($nodeType);
  73. $this->reflectionClassCache[$nodeType] = new \ReflectionClass($className);
  74. }
  75. return $this->reflectionClassCache[$nodeType];
  76. }
  77. /** @return class-string<Node> */
  78. private function classNameFromNodeType(string $nodeType): string {
  79. $className = 'PhpParser\\Node\\' . strtr($nodeType, '_', '\\');
  80. if (class_exists($className)) {
  81. return $className;
  82. }
  83. $className .= '_';
  84. if (class_exists($className)) {
  85. return $className;
  86. }
  87. throw new \RuntimeException("Unknown node type \"$nodeType\"");
  88. }
  89. }