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