TreeInterpreter.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. <?php
  2. namespace JmesPath;
  3. /**
  4. * Tree visitor used to evaluates JMESPath AST expressions.
  5. */
  6. class TreeInterpreter
  7. {
  8. /** @var callable */
  9. private $fnDispatcher;
  10. /**
  11. * @param callable $fnDispatcher Function dispatching function that accepts
  12. * a function name argument and an array of
  13. * function arguments and returns the result.
  14. */
  15. public function __construct(callable $fnDispatcher = null)
  16. {
  17. $this->fnDispatcher = $fnDispatcher ?: FnDispatcher::getInstance();
  18. }
  19. /**
  20. * Visits each node in a JMESPath AST and returns the evaluated result.
  21. *
  22. * @param array $node JMESPath AST node
  23. * @param mixed $data Data to evaluate
  24. *
  25. * @return mixed
  26. */
  27. public function visit(array $node, $data)
  28. {
  29. return $this->dispatch($node, $data);
  30. }
  31. /**
  32. * Recursively traverses an AST using depth-first, pre-order traversal.
  33. * The evaluation logic for each node type is embedded into a large switch
  34. * statement to avoid the cost of "double dispatch".
  35. * @return mixed
  36. */
  37. private function dispatch(array $node, $value)
  38. {
  39. $dispatcher = $this->fnDispatcher;
  40. switch ($node['type']) {
  41. case 'field':
  42. if (is_array($value) || $value instanceof \ArrayAccess) {
  43. return isset($value[$node['value']]) ? $value[$node['value']] : null;
  44. } elseif ($value instanceof \stdClass) {
  45. return isset($value->{$node['value']}) ? $value->{$node['value']} : null;
  46. }
  47. return null;
  48. case 'subexpression':
  49. return $this->dispatch(
  50. $node['children'][1],
  51. $this->dispatch($node['children'][0], $value)
  52. );
  53. case 'index':
  54. if (!Utils::isArray($value)) {
  55. return null;
  56. }
  57. $idx = $node['value'] >= 0
  58. ? $node['value']
  59. : $node['value'] + count($value);
  60. return isset($value[$idx]) ? $value[$idx] : null;
  61. case 'projection':
  62. $left = $this->dispatch($node['children'][0], $value);
  63. switch ($node['from']) {
  64. case 'object':
  65. if (!Utils::isObject($left)) {
  66. return null;
  67. }
  68. break;
  69. case 'array':
  70. if (!Utils::isArray($left)) {
  71. return null;
  72. }
  73. break;
  74. default:
  75. if (!is_array($left) || !($left instanceof \stdClass)) {
  76. return null;
  77. }
  78. }
  79. $collected = [];
  80. foreach ((array) $left as $val) {
  81. $result = $this->dispatch($node['children'][1], $val);
  82. if ($result !== null) {
  83. $collected[] = $result;
  84. }
  85. }
  86. return $collected;
  87. case 'flatten':
  88. static $skipElement = [];
  89. $value = $this->dispatch($node['children'][0], $value);
  90. if (!Utils::isArray($value)) {
  91. return null;
  92. }
  93. $merged = [];
  94. foreach ($value as $values) {
  95. // Only merge up arrays lists and not hashes
  96. if (is_array($values) && isset($values[0])) {
  97. $merged = array_merge($merged, $values);
  98. } elseif ($values !== $skipElement) {
  99. $merged[] = $values;
  100. }
  101. }
  102. return $merged;
  103. case 'literal':
  104. return $node['value'];
  105. case 'current':
  106. return $value;
  107. case 'or':
  108. $result = $this->dispatch($node['children'][0], $value);
  109. return Utils::isTruthy($result)
  110. ? $result
  111. : $this->dispatch($node['children'][1], $value);
  112. case 'and':
  113. $result = $this->dispatch($node['children'][0], $value);
  114. return Utils::isTruthy($result)
  115. ? $this->dispatch($node['children'][1], $value)
  116. : $result;
  117. case 'not':
  118. return !Utils::isTruthy(
  119. $this->dispatch($node['children'][0], $value)
  120. );
  121. case 'pipe':
  122. return $this->dispatch(
  123. $node['children'][1],
  124. $this->dispatch($node['children'][0], $value)
  125. );
  126. case 'multi_select_list':
  127. if ($value === null) {
  128. return null;
  129. }
  130. $collected = [];
  131. foreach ($node['children'] as $node) {
  132. $collected[] = $this->dispatch($node, $value);
  133. }
  134. return $collected;
  135. case 'multi_select_hash':
  136. if ($value === null) {
  137. return null;
  138. }
  139. $collected = [];
  140. foreach ($node['children'] as $node) {
  141. $collected[$node['value']] = $this->dispatch(
  142. $node['children'][0],
  143. $value
  144. );
  145. }
  146. return $collected;
  147. case 'comparator':
  148. $left = $this->dispatch($node['children'][0], $value);
  149. $right = $this->dispatch($node['children'][1], $value);
  150. if ($node['value'] == '==') {
  151. return Utils::isEqual($left, $right);
  152. } elseif ($node['value'] == '!=') {
  153. return !Utils::isEqual($left, $right);
  154. } else {
  155. return self::relativeCmp($left, $right, $node['value']);
  156. }
  157. case 'condition':
  158. return Utils::isTruthy($this->dispatch($node['children'][0], $value))
  159. ? $this->dispatch($node['children'][1], $value)
  160. : null;
  161. case 'function':
  162. $args = [];
  163. foreach ($node['children'] as $arg) {
  164. $args[] = $this->dispatch($arg, $value);
  165. }
  166. return $dispatcher($node['value'], $args);
  167. case 'slice':
  168. return is_string($value) || Utils::isArray($value)
  169. ? Utils::slice(
  170. $value,
  171. $node['value'][0],
  172. $node['value'][1],
  173. $node['value'][2]
  174. ) : null;
  175. case 'expref':
  176. $apply = $node['children'][0];
  177. return function ($value) use ($apply) {
  178. return $this->visit($apply, $value);
  179. };
  180. default:
  181. throw new \RuntimeException("Unknown node type: {$node['type']}");
  182. }
  183. }
  184. /**
  185. * @return bool
  186. */
  187. private static function relativeCmp($left, $right, $cmp)
  188. {
  189. if (!(is_int($left) || is_float($left)) || !(is_int($right) || is_float($right))) {
  190. return false;
  191. }
  192. switch ($cmp) {
  193. case '>': return $left > $right;
  194. case '>=': return $left >= $right;
  195. case '<': return $left < $right;
  196. case '<=': return $left <= $right;
  197. default: throw new \RuntimeException("Invalid comparison: $cmp");
  198. }
  199. }
  200. }