国内流行的内容管理系统(CMS)多端全媒体解决方案 https://www.dedebiz.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

645 lines
14KB

  1. <?php
  2. if (!defined('DEDEINC')) exit('dedebiz');
  3. require_once(dirname(__FILE__)."/Exceptions/ConditionNotAllowedException.php");
  4. require_once(dirname(__FILE__)."/Exceptions/FileNotFoundException.php");
  5. require_once(dirname(__FILE__)."/Exceptions/InvalidJsonException.php");
  6. require_once(dirname(__FILE__)."/Results/ValueNotFound.php");
  7. require_once(dirname(__FILE__)."/Condition.php");
  8. trait JsonQueriable
  9. {
  10. /**
  11. * store node path
  12. * @var string|array
  13. */
  14. protected $_node = '';
  15. /**
  16. * contain prepared data for process
  17. * @var mixed
  18. */
  19. protected $_map;
  20. /**
  21. * contains column names
  22. * @var array
  23. */
  24. protected $_select = [];
  25. /**
  26. * contains column names for except
  27. * @var array
  28. */
  29. protected $_except = [];
  30. /**
  31. * Stores base contents.
  32. *
  33. * @var array
  34. */
  35. protected $_baseContents = [];
  36. /**
  37. * Stores all conditions.
  38. *
  39. * @var array
  40. */
  41. protected $_conditions = [];
  42. /**
  43. * @var bool
  44. */
  45. protected $_isProcessed = false;
  46. /**
  47. * map all conditions with methods
  48. * @var array
  49. */
  50. protected static $_rulesMap = [
  51. '=' => 'equal',
  52. 'eq' => 'equal',
  53. '==' => 'strictEqual',
  54. 'seq' => 'strictEqual',
  55. '!=' => 'notEqual',
  56. 'neq' => 'notEqual',
  57. '!==' => 'strictNotEqual',
  58. 'sneq' => 'strictNotEqual',
  59. '>' => 'greaterThan',
  60. 'gt' => 'greaterThan',
  61. '<' => 'lessThan',
  62. 'lt' => 'lessThan',
  63. '>=' => 'greaterThanOrEqual',
  64. 'gte' => 'greaterThanOrEqual',
  65. '<=' => 'lessThanOrEqual',
  66. 'lte' => 'lessThanOrEqual',
  67. 'in' => 'in',
  68. 'notin' => 'notIn',
  69. 'null' => 'isNull',
  70. 'notnull' => 'isNotNull',
  71. 'startswith' => 'startWith',
  72. 'endswith' => 'endWith',
  73. 'match' => 'match',
  74. 'contains' => 'contains',
  75. 'dates' => 'dateEqual',
  76. 'month' => 'monthEqual',
  77. 'year' => 'yearEqual',
  78. ];
  79. /**
  80. * import data from file
  81. *
  82. * @param string|null $file
  83. * @return bool
  84. * @throws FileNotFoundException
  85. * @throws InvalidJsonException
  86. */
  87. public function import($file = null)
  88. {
  89. if (!is_null($file)) {
  90. if (is_string($file)) {
  91. $this->_map = $this->getDataFromFile($file);
  92. $this->_baseContents = $this->_map;
  93. return true;
  94. }
  95. }
  96. throw new FileNotFoundException();
  97. }
  98. /**
  99. * Prepare data from desire conditions
  100. *
  101. * @return $this
  102. * @throws ConditionNotAllowedException
  103. */
  104. protected function prepare()
  105. {
  106. if ($this->_isProcessed) {
  107. return $this;
  108. }
  109. if (count($this->_conditions) > 0) {
  110. $calculatedData = $this->processConditions();
  111. $this->_map = $this->objectToArray($calculatedData);
  112. $this->_conditions = [];
  113. $this->_node = '';
  114. $this->_isProcessed = true;
  115. return $this;
  116. }
  117. $this->_isProcessed = true;
  118. $this->_map = $this->objectToArray($this->getData());
  119. return $this;
  120. }
  121. /**
  122. * Our system will cache processed data and prevend multiple time processing. If
  123. * you want to reprocess this method can help you
  124. *
  125. * @return $this
  126. */
  127. public function reProcess()
  128. {
  129. $this->_isProcessed = false;
  130. return $this;
  131. }
  132. /**
  133. * Parse object to array
  134. *
  135. * @param object $obj
  136. * @return array|mixed
  137. */
  138. protected function objectToArray($obj)
  139. {
  140. if (!is_array($obj) && !is_object($obj)) {
  141. return $obj;
  142. }
  143. if (is_array($obj)) {
  144. return $obj;
  145. }
  146. if (is_object($obj)) {
  147. $obj = get_object_vars($obj);
  148. }
  149. return array_map([$this, 'objectToArray'], $obj);
  150. }
  151. /**
  152. * Check given value is multidimensional array
  153. *
  154. * @param array $arr
  155. * @return bool
  156. */
  157. protected function isMultiArray($arr)
  158. {
  159. if (!is_array($arr)) {
  160. return false;
  161. }
  162. rsort($arr);
  163. return isset($arr[0]) && is_array($arr[0]);
  164. }
  165. /**
  166. * Check given value is valid JSON
  167. *
  168. * @param string $value
  169. * @param bool $isReturnMap
  170. *
  171. * @return bool|array
  172. */
  173. public function isJson($value, $isReturnMap = false)
  174. {
  175. if (is_array($value) || is_object($value)) {
  176. return false;
  177. }
  178. $data = json_decode($value, true);
  179. if (json_last_error() !== JSON_ERROR_NONE) {
  180. return false;
  181. }
  182. return $isReturnMap ? $data : true;
  183. }
  184. public function takeColumn($array)
  185. {
  186. return $this->selectColumn($this->exceptColumn($array));
  187. }
  188. /**
  189. * selecting specific column
  190. *
  191. * @param $array
  192. * @return array
  193. */
  194. protected function selectColumn($array)
  195. {
  196. $keys = $this->_select;
  197. if (count($keys) == 0) {
  198. return $array;
  199. }
  200. return array_intersect_key($array, array_flip((array) $keys));
  201. }
  202. /**
  203. * selecting specific column
  204. *
  205. * @param $array
  206. * @return array
  207. */
  208. protected function exceptColumn($array)
  209. {
  210. $keys = $this->_except;
  211. if (count($keys) == 0) {
  212. return $array;
  213. }
  214. return array_diff_key($array, array_flip((array) $keys));
  215. }
  216. /**
  217. * Prepare data for result
  218. *
  219. * @param mixed $data
  220. * @param bool $isObject
  221. * @return array|mixed
  222. */
  223. protected function prepareResult($data, $isObject)
  224. {
  225. $output = [];
  226. if (is_null($data) || is_scalar($data)) {
  227. return $data;
  228. }
  229. if ($this->isMultiArray($data)) {
  230. foreach ($data as $key => $val) {
  231. $val = $this->takeColumn($val);
  232. $output[$key] = $isObject ? (object) $val : $val;
  233. }
  234. } else {
  235. $output = json_decode(json_encode($this->takeColumn($data)), $isObject);
  236. }
  237. return $output;
  238. }
  239. /**
  240. * Read JSON data from file
  241. *
  242. * @param string $file
  243. * @param string $type
  244. * @return bool|string|array
  245. * @throws FileNotFoundException
  246. * @throws InvalidJsonException
  247. */
  248. protected function getDataFromFile($file, $type = 'application/json')
  249. {
  250. $opts = [
  251. 'http' => [
  252. 'header' => 'Content-Type: '.$type.'; charset=utf-8',
  253. ],
  254. ];
  255. $context = stream_context_create($opts);
  256. $data = file_get_contents($file, 0, $context);
  257. $json = $this->isJson($data, true);
  258. if (!$json) {
  259. throw new InvalidJsonException();
  260. }
  261. return $json;
  262. }
  263. /**
  264. * Get data from nested array
  265. *
  266. * @param $map array
  267. * @param $node string
  268. * @return bool|array|mixed
  269. */
  270. protected function getFromNested($map, $node)
  271. {
  272. if (empty($node) || $node == '.') {
  273. return $map;
  274. }
  275. if ($node) {
  276. $terminate = false;
  277. $path = explode('.', $node);
  278. foreach ($path as $val) {
  279. if (!is_array($map)) return $map;
  280. if (!array_key_exists($val, $map)) {
  281. $terminate = true;
  282. break;
  283. }
  284. $map = &$map[$val];
  285. }
  286. if ($terminate) {
  287. return new ValueNotFound();
  288. }
  289. return $map;
  290. }
  291. return new ValueNotFound();
  292. }
  293. /**
  294. * get data from node path
  295. *
  296. * @return mixed
  297. */
  298. protected function getData()
  299. {
  300. return $this->getFromNested($this->_map, $this->_node);
  301. }
  302. /**
  303. * process AND and OR conditions
  304. *
  305. * @return array|string|object
  306. * @throws ConditionNotAllowedException
  307. */
  308. protected function processConditions()
  309. {
  310. $data = $this->getData();
  311. $conditions = $this->_conditions;
  312. $result = array_filter($data, function ($val) use ($conditions) {
  313. $res = false;
  314. foreach ($conditions as $cond) {
  315. $tmp = true;
  316. foreach ($cond as $rule) {
  317. $function = self::$_rulesMap[$rule['condition']];
  318. if (!is_callable($function)) {
  319. if (!method_exists(Condition::class, $function)) {
  320. throw new ConditionNotAllowedException("Exception: $function condition not allowed");
  321. }
  322. $function = [Condition::class, $function];
  323. }
  324. $value = $this->getFromNested($val, $rule['key']);
  325. $return = $value instanceof ValueNotFound ? false : call_user_func_array($function, [$value, $rule['value']]);
  326. $tmp &= $return;
  327. }
  328. $res |= $tmp;
  329. }
  330. return $res;
  331. });
  332. return $result;
  333. }
  334. /**
  335. * make WHERE clause
  336. *
  337. * @param string $key
  338. * @param string $condition
  339. * @param mixed $value
  340. * @return $this
  341. */
  342. public function where($key, $condition = null, $value = null)
  343. {
  344. if (!is_null($condition) && is_null($value)) {
  345. $value = $condition;
  346. $condition = '=';
  347. }
  348. if (count($this->_conditions) < 1) {
  349. array_push($this->_conditions, []);
  350. }
  351. return $this->makeWhere($key, $condition, $value);
  352. }
  353. /**
  354. * make WHERE clause with OR
  355. *
  356. * @param string $key
  357. * @param string $condition
  358. * @param mixed $value
  359. * @return $this
  360. */
  361. public function orWhere($key = null, $condition = null, $value = null)
  362. {
  363. if (!is_null($condition) && is_null($value)) {
  364. $value = $condition;
  365. $condition = '=';
  366. }
  367. array_push($this->_conditions, []);
  368. return $this->makeWhere($key, $condition, $value);
  369. }
  370. /**
  371. * generator for AND and OR where
  372. *
  373. * @param string $key
  374. * @param string $condition
  375. * @param mixed $value
  376. * @return $this
  377. */
  378. protected function makeWhere($key, $condition = null, $value = null)
  379. {
  380. $current = end($this->_conditions);
  381. $index = key($this->_conditions);
  382. if (is_callable($key)) {
  383. $key($this);
  384. return $this;
  385. }
  386. array_push($current, [
  387. 'key' => $key,
  388. 'condition' => $condition,
  389. 'value' => $value,
  390. ]);
  391. $this->_conditions[$index] = $current;
  392. return $this;
  393. }
  394. /**
  395. * make WHERE IN clause
  396. *
  397. * @param string $key
  398. * @param array $value
  399. * @return $this
  400. */
  401. public function whereIn($key = null, $value = [])
  402. {
  403. $this->where($key, 'in', $value);
  404. return $this;
  405. }
  406. /**
  407. * make WHERE NOT IN clause
  408. *
  409. * @param string $key
  410. * @param mixed $value
  411. * @return $this
  412. */
  413. public function whereNotIn($key = null, $value = [])
  414. {
  415. $this->where($key, 'notin', $value);
  416. return $this;
  417. }
  418. /**
  419. * make WHERE NULL clause
  420. *
  421. * @param string $key
  422. * @return $this
  423. */
  424. public function whereNull($key = null)
  425. {
  426. $this->where($key, 'null', 'null');
  427. return $this;
  428. }
  429. /**
  430. * make WHERE Boolean clause
  431. *
  432. * @param string $key
  433. * @return $this
  434. */
  435. public function whereBool($key, $value)
  436. {
  437. if (is_bool($value)) {
  438. $this->where($key, '==', $value);
  439. }
  440. return $this;
  441. }
  442. /**
  443. * make WHERE NOT NULL clause
  444. *
  445. * @param string $key
  446. * @return $this
  447. */
  448. public function whereNotNull($key = null)
  449. {
  450. $this->where($key, 'notnull', 'null');
  451. return $this;
  452. }
  453. /**
  454. * make WHERE START WITH clause
  455. *
  456. * @param string $key
  457. * @param string $value
  458. * @return $this
  459. */
  460. public function whereStartsWith($key, $value)
  461. {
  462. $this->where($key, 'startswith', $value);
  463. return $this;
  464. }
  465. /**
  466. * make WHERE ENDS WITH clause
  467. *
  468. * @param string $key
  469. * @param string $value
  470. * @return $this
  471. */
  472. public function whereEndsWith($key, $value)
  473. {
  474. $this->where($key, 'endswith', $value);
  475. return $this;
  476. }
  477. /**
  478. * make WHERE MATCH clause
  479. *
  480. * @param string $key
  481. * @param string $value
  482. * @return $this
  483. */
  484. public function whereMatch($key, $value)
  485. {
  486. $this->where($key, 'match', $value);
  487. return $this;
  488. }
  489. /**
  490. * make WHERE CONTAINS clause
  491. *
  492. * @param string $key
  493. * @param string $value
  494. * @return $this
  495. */
  496. public function whereContains($key, $value)
  497. {
  498. $this->where($key, 'contains', $value);
  499. return $this;
  500. }
  501. /**
  502. * make WHERE DATE clause
  503. *
  504. * @param string $key
  505. * @param string $value
  506. * @return $this
  507. */
  508. public function whereDate($key, $value)
  509. {
  510. $this->where($key, 'dates', $value);
  511. return $this;
  512. }
  513. /**
  514. * make WHERE month clause
  515. *
  516. * @param string $key
  517. * @param string $value
  518. * @return $this
  519. */
  520. public function whereMonth($key, $value)
  521. {
  522. $this->where($key, 'month', $value);
  523. return $this;
  524. }
  525. /**
  526. * make WHERE Year clause
  527. *
  528. * @param string $key
  529. * @param string $value
  530. * @return $this
  531. */
  532. public function whereYear($key, $value)
  533. {
  534. $this->where($key, 'year', $value);
  535. return $this;
  536. }
  537. /**
  538. * make macro for custom where clause
  539. *
  540. * @param string $name
  541. * @param callable $fn
  542. * @return bool
  543. */
  544. public static function macro($name, callable $fn)
  545. {
  546. if (!in_array($name, self::$_rulesMap)) {
  547. self::$_rulesMap[$name] = $fn;
  548. return true;
  549. }
  550. return false;
  551. }
  552. }