1: | <?php declare(strict_types=1); |
2: | |
3: | namespace PhpParser; |
4: | |
5: | class JsonDecoder { |
6: | |
7: | private array $reflectionClassCache; |
8: | |
9: | |
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: | |
21: | |
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: | |
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: | |
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: | |