ParserXsdAttributeAssert.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. <?php
  2. class ParserXsdAttributeAssert {
  3. public static function buildFromAssertCode($assertCode, $xsdType) { // @param string $assertCode
  4. $fieldNamesUpperCase = array_map('strtoupper', (!empty($xsdType['struct'])) ? array_keys($xsdType['struct']) : []);
  5. $instanceName = $xsdType['name'];
  6. $xsdAssertParser = new ParserXsdAttributeAssert();
  7. $xsdAssertParser->parseAssertCode($assertCode, [
  8. 'instanceName' => $instanceName,
  9. 'fields' => $fieldNamesUpperCase,
  10. ]);
  11. return $xsdAssertParser;
  12. }
  13. public static function buildFromString($str, $xsdType) { // @param string $str
  14. $xsdAssertParser = new ParserXsdAttributeAssert();
  15. return $xsdAssertParser;
  16. }
  17. public function __construct() {
  18. $this->_state = [];
  19. }
  20. public function parseAssertCode($assertCode, $params = []) {
  21. if (empty($params['instanceName'])) throw new Exception("Missing instanceName");
  22. $instanceName = $params['instanceName'];
  23. $fieldNamesUpperCase = V::get('fields', [], $params, 'array');
  24. $this->_state = [
  25. 'instanceName' => $instanceName,
  26. 'fieldNamesUpperCase' => $fieldNamesUpperCase,
  27. 'token_tree' => [],
  28. ];
  29. $tokens = token_get_all("<?php " . $assertCode);
  30. $parsedTokens = array_map(function ($token) {
  31. if (is_string($token)) return [
  32. 'type' => 'string',
  33. 'value' => $token,
  34. ];
  35. return [
  36. 'type' => token_name($token[0]),
  37. 'value' => $token[1]
  38. ];
  39. }, $tokens);
  40. array_shift($parsedTokens); // remove first token: `<?php`
  41. $parsedTokens = array_map(function ($token) {
  42. if ('string' === $token['type'] && '(' === $token['value']) return array_merge($token, [ 'type' => 'opening parenthesis' ]);
  43. if ('string' === $token['type'] && ')' === $token['value']) return array_merge($token, [ 'type' => 'closing parenthesis' ]);
  44. if ('string' === $token['type'] && ',' === $token['value']) return array_merge($token, [ 'type' => 'comma' ]);
  45. return $token;
  46. }, $parsedTokens);
  47. $parsedTokens = array_filter($parsedTokens, function ($token) {
  48. return ('T_WHITESPACE' !== $token['type']);
  49. });
  50. // opening parenthesis
  51. // closing parenthesis
  52. // function array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] ) // return mixed
  53. UI::table(['rows'=>$parsedTokens, 'caption'=>"parseAttributeWithAssertCode({$attrName})"]);
  54. $parser = array_reduce($parsedTokens, function ($parser, $token) use ($fieldNamesUpperCase, $instanceName) {
  55. if (!$parser) $parser = [ 'deep' => 0, 'cur' => '', 'tokens' => [] ];
  56. // 1. `concat(ID, ' ', DESC, ' ', DESC_PL)`
  57. // 1.1 $parser = { deep: 0, cur: 'concat', tokens: []}
  58. // 1.2 $parser = { deep: 1, cur: '', tokens: [ {'function', 'concat'}, {'('} ]}
  59. if (!empty($parser['cur'])) {
  60. switch ($parser['cur']) {
  61. case 'substring':
  62. case 'concat':
  63. {
  64. if ('opening parenthesis' === $token['type']) {
  65. $parser['deep'] = $parser['deep'] + 1;
  66. $parser['tokens'][] = [ 'type' => "function", 'value' => $parser['cur'] ];
  67. $parser['tokens'][] = [ 'type' => "opening parenthesis", 'value' => '(' ];
  68. $parser['cur'] = '';
  69. return $parser;
  70. }
  71. } break;
  72. case 'name': return array_merge($parser, [ 'cur' => 'name(' ]);
  73. case 'name(': {
  74. if ('closing parenthesis' === $token['type']) {
  75. $parser['tokens'][] = [ 'type' => "instanceName", 'value' => $instanceName ];
  76. $parser['cur'] = '';
  77. return $parser;
  78. }
  79. } break;
  80. }
  81. throw new Exception("Parse attribute assert code failed. Not implemented function '{$parser['cur']}'.");
  82. }
  83. if ('string' === $token['type']) throw new Exception("Parse attribute assert code failed. Not implemented string token '{$token['value']}'.");
  84. if ('comma' === $token['type']) {
  85. $parser['tokens'][] = $token;
  86. return $parser;
  87. }
  88. if ('T_CONSTANT_ENCAPSED_STRING' === $token['type']) {
  89. $parser['tokens'][] = array_merge($token, [ 'type' => 'encapsed string' ]);
  90. return $parser;
  91. }
  92. if ('T_LNUMBER' === $token['type']) {
  93. $parser['tokens'][] = array_merge($token, [ 'type' => 'number' ]);
  94. return $parser;
  95. }
  96. if ('closing parenthesis' === $token['type']) {
  97. if ($parser['deep'] <= 0) throw new Exception("Parse attribute assert code failed. Parenthesis not match.");
  98. $parser['tokens'][] = $token;
  99. $parser['deep'] = $parser['deep'] - 1;
  100. return $parser;
  101. }
  102. if ('T_STRING' === $token['type']) {
  103. if ('concat' === $token['value']) return array_merge($parser, [ 'cur' => 'concat' ]);
  104. if ('substring' === $token['value']) return array_merge($parser, [ 'cur' => 'substring' ]);
  105. if ('name' === $token['value']) return array_merge($parser, [ 'cur' => 'name' ]);
  106. if (in_array(strtoupper($token['value']), $fieldNamesUpperCase)) {
  107. $parser['tokens'][] = [ 'type' => 'field', 'value' => strtoupper($token['value']) ];
  108. return $parser;
  109. }
  110. throw new Exception("Parse attribute assert code failed. Not implemented T_STRING token '{$token['value']}'.");
  111. }
  112. throw new Exception("Parse attribute assert code failed. Not implemented {$token['type']} token '{$token['value']}'.");
  113. // return $parser;
  114. }, []);
  115. DBG::nicePrint($parser, "parseAttributeWithAssertCode({$attrName}) parsed tokens");
  116. $checkParenthesisDepth = array_reduce($parser['tokens'], function ($ret, $token) {
  117. switch ($token['type']) {
  118. case 'opening parenthesis': $ret['deep'] += 1; break;
  119. case 'closing parenthesis': $ret['deep'] -= 1; break;
  120. }
  121. switch ($token['type']) {
  122. case 'opening parenthesis': break;
  123. case 'closing parenthesis': break;
  124. default: $ret['tokens'][] = array_merge($token, [ 'deep' => $ret['deep'] ]);
  125. }
  126. return $ret;
  127. }, [ 'deep' => 1, 'tokens' => [] ]);
  128. if (1 !== $checkParenthesisDepth['deep']) throw new Exception("Parse attribute assert code failed. Parentheses do not match.");
  129. $parser['tokens'] = $checkParenthesisDepth['tokens'];
  130. UI::table(['rows'=>$parser['tokens'], 'caption'=>"parseAttributeWithAssertCode({$attrName}) parsed tokens"]);
  131. $tokenTree = array_reduce($parser['tokens'], function ($ret, $token) {
  132. static $counter = 0;
  133. $counter += 1;
  134. DBG::log($ret, 'array', "token_tree loop({$counter}) >>> \$ret");
  135. DBG::log($token, 'array', "token_tree loop({$counter}) >>> \$token");
  136. $funcIdx = $ret['funcIdx'];
  137. if ($ret['lastDeep'] < $token['deep']) { // 'starting parenthesis' was before => func arg
  138. $ret['lastDeep'] = $token['deep'];
  139. }
  140. else if ($ret['lastDeep'] > $token['deep']) { // 'closing parenthesis' was before =>
  141. $ret['lastDeep'] = $token['deep'];
  142. for ($idx = $funcIdx; $idx >= 0; $idx--) {
  143. if ($token['deep'] - 1 === $ret['token_tree'][$idx]['deep']) {
  144. $funcIdx = $idx;
  145. $ret['funcIdx'] = $funcIdx;
  146. break;
  147. }
  148. }
  149. }
  150. if ('function' === $token['type']) {
  151. $ret['token_tree'][] = array_merge($token, [ 'funcIdx' => $funcIdx, 'args' => [] ]);
  152. $ret['funcIdx'] = count($ret['token_tree']) - 1;
  153. if ($funcIdx >= 0) $ret['token_tree'][$funcIdx]['args'][] = [ 'type' => 'funcIdx', 'value' => $ret['funcIdx'] ];
  154. return $ret;
  155. } else {
  156. $ret['token_tree'][$funcIdx]['args'][] = $token;
  157. }
  158. return $ret;
  159. }, [ 'funcIdx' => -1, 'lastDeep' => 1, 'token_tree' => [] ]); // $token_tree = { 'type', 'value', 'deep', 'args' }
  160. DBG::nicePrint($parser['token_tree'], "parseAttributeWithAssertCode({$attrName}) token_tree");
  161. // TODO: validate $tokenTree
  162. $parser['token_tree'] = $tokenTree['token_tree'];
  163. $this->_state['token_tree'] = $parser['token_tree'];
  164. }
  165. public function toMysqlSelect($rootTableName, $primaryKeyField) {
  166. $sqlAssertCode = '';
  167. $sqlPrefix = 't';
  168. $tokenTree = array_map(function ($val) { return $val; }, $this->_state['token_tree']);
  169. for ($idx = count($tokenTree) - 1; $idx >= 0; $idx--) {
  170. $func = $tokenTree[$idx];
  171. switch ($func['value']) {
  172. case 'concat': $value = $this->_toMysqlConcat($func, $sqlPrefix); break;
  173. case 'substring': $value = $this->_toMysqlSubstring($func, $sqlPrefix); break;
  174. default: throw new Exception("Not implemented asser code function '{$func['value']}'");
  175. }
  176. for ($subIdx = $idx - 1; $subIdx >= 0; $subIdx--) {
  177. $tokenTree[$subIdx]['args'] = array_map(function ($arg) use ($value, $idx) {
  178. if ('funcIdx' === $arg['type'] && $arg['value'] == $idx) {
  179. return [
  180. 'type' => 'raw string',
  181. 'value' => $value,
  182. ];
  183. }
  184. return $arg;
  185. }, $tokenTree[$subIdx]['args']);
  186. }
  187. }
  188. $func = $tokenTree[0];
  189. $sqlAssertCode .= "{$func['value']}(";
  190. $sqlAssertCode .= implode("", array_map(function ($arg) use ($sqlPrefix) {
  191. switch ($arg['type']) {
  192. case 'field': return "{$sqlPrefix}.{$arg['value']}";
  193. case 'number': return $arg['value'];
  194. }
  195. return $arg['value'];
  196. }, $func['args']));
  197. $sqlAssertCode .= ")";
  198. $sql = "
  199. select {$sqlPrefix}.{$primaryKeyField} as primary_key
  200. , {$sqlAssertCode} as label
  201. from `{$rootTableName}` {$sqlPrefix}
  202. ";
  203. return $sql;
  204. }
  205. function _toMysqlConcat($func, $sqlPrefix) {
  206. return "concat(" . implode("", array_map(function ($arg) use ($sqlPrefix) {
  207. if ('field' == $arg['type']) return "{$sqlPrefix}.{$arg['value']}";
  208. return $arg['value'];
  209. }, $func['args'])) . ")";
  210. }
  211. function _toMysqlSubstring($func, $sqlPrefix) {
  212. return "substring(" . implode("", array_map(function ($arg) use ($sqlPrefix) {
  213. if ('field' == $arg['type']) return "{$sqlPrefix}.{$arg['value']}";
  214. return $arg['value'];
  215. }, $func['args'])) . ")";
  216. }
  217. public function evaluate($item) {
  218. $value = '';
  219. $uppserCaseItem = array_combine(
  220. array_map('strtoupper', array_keys($item)),
  221. array_values($item)
  222. );
  223. $tokenTree = array_map(function ($val) { return $val; }, $this->_state['token_tree']);
  224. for ($idx = count($tokenTree) - 1; $idx >= 0; $idx--) {
  225. $func = $tokenTree[$idx];
  226. switch ($func['value']) {
  227. case 'concat': $value = $this->_evaluateConcat($func, $uppserCaseItem); break;
  228. case 'substring': $value = $this->_evaluateSubstring($func, $uppserCaseItem); break;
  229. default: throw new Exception("Not implemented asser code function '{$func['value']}'");
  230. }
  231. for ($subIdx = $idx - 1; $subIdx >= 0; $subIdx--) {
  232. $tokenTree[$subIdx]['args'] = array_map(function ($arg) use ($value, $idx) {
  233. if ('funcIdx' === $arg['type'] && $arg['value'] == $idx) {
  234. return [
  235. 'type' => 'raw string',
  236. 'value' => $value,
  237. ];
  238. }
  239. return $arg;
  240. }, $tokenTree[$subIdx]['args']);
  241. }
  242. }
  243. return $value;
  244. }
  245. public function _evaluateConcat($func, $item) {
  246. return implode('', array_map(function ($arg) use ($item) {
  247. switch ($arg['type']) {
  248. case 'field': return V::get($arg['value'], '', $item);
  249. case 'comma': return '';
  250. case 'encapsed string': return substr($arg['value'], 1, -1);
  251. case 'raw string': return $arg['value'];
  252. }
  253. }, $func['args']));
  254. }
  255. public function _evaluateSubstring($func, $item) {
  256. // PHP: substr( $str, $start [ , $length ] ) // NOTE $start with 0
  257. // XPath: substr( $str, $start [ , $length ] ) // NOTE $start with 1
  258. // SQL: substr( $str, $start [ , $length ] ) // NOTE $start with 1
  259. $str = '';
  260. $start = 0;
  261. $length = null;
  262. // $func ['args'] = [ string, comma, number, comma, number ]
  263. // or = [ string, comma, number ]
  264. if (!in_array(count($func['args']), [3, 5])) throw new Exception("Sytax error for substring function in assert code");
  265. $arg = $func['args'][0];
  266. switch ($arg['type']) {
  267. case 'field': $str = V::get($arg['value'], '', $item); break;
  268. case 'encapsed string': $str = substr($arg['value'], 1, -1); break;
  269. case 'number': $str = (string)$arg['value']; break;
  270. case 'raw string': $str = $arg['value']; break;
  271. }
  272. if ('number' !== $func['args'][2]['type']) throw new Exception("Sytax error for substring function in assert code - expected second arguemnt type as number but '{$func['args'][2]['type']}' given");
  273. $start = (int)$func['args'][2]['value'] - 1;
  274. $start = ($start < 0) ? 0 : $start; // BUG in assert code
  275. if (5 === count($func['args'])) {
  276. if ('number' !== $func['args'][4]['type']) throw new Exception("Sytax error for substring function in assert code - expected third arguemnt type as number but '{$func['args'][2]['type']}' given");
  277. $length = (int)$func['args'][4]['value'];
  278. }
  279. return (null === $length)
  280. ? substr($str, $start)
  281. : substr($str, $start, $length)
  282. ;
  283. }
  284. public function toString() {
  285. throw new Exception("TODO: ParserXsdAttributeAssert->toString()");
  286. }
  287. }