PageRenderTime 62ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/src/libraries/anahita/domain/query/builder.php

https://github.com/bhar1red/anahita
PHP | 808 lines | 544 code | 111 blank | 153 comment | 87 complexity | f34a5b7e2887150880ad3732e41e8875 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1
  1. <?php
  2. /**
  3. * LICENSE: ##LICENSE##
  4. *
  5. * @category Anahita
  6. * @package Anahita_Domain
  7. * @subpackage Query
  8. * @author Arash Sanieyan <ash@anahitapolis.com>
  9. * @author Rastin Mehr <rastin@anahitapolis.com>
  10. * @copyright 2008 - 2010 rmdStudio Inc./Peerglobe Technology Inc
  11. * @license GNU GPLv3 <http://www.gnu.org/licenses/gpl-3.0.html>
  12. * @version SVN: $Id$
  13. * @link http://www.anahitapolis.com
  14. */
  15. /**
  16. * Query Builder
  17. *
  18. * It Constructs different parts of a query and join them together as a string
  19. *
  20. * @category Anahita
  21. * @package Anahita_Domain
  22. * @subpackage Query
  23. * @author Arash Sanieyan <ash@anahitapolis.com>
  24. * @author Rastin Mehr <rastin@anahitapolis.com>
  25. * @license GNU GPLv3 <http://www.gnu.org/licenses/gpl-3.0.html>
  26. * @link http://www.anahitapolis.com
  27. */
  28. class AnDomainQueryBuilder extends KObject
  29. {
  30. /**
  31. * Singleton Instance
  32. *
  33. * @var AnDomainQueryBuilder
  34. */
  35. static protected $_instance;
  36. /**
  37. * Return an instance of a builder
  38. *
  39. * @return AnDomainQueryBuilder
  40. */
  41. static public function getInstance()
  42. {
  43. if ( !self::$_instance )
  44. {
  45. self::$_instance = new AnDomainQueryBuilder(new KConfig());
  46. }
  47. return self::$_instance;
  48. }
  49. /**
  50. * Database Adapter
  51. *
  52. * @var KDatabaseAbstract
  53. */
  54. protected $_store;
  55. /**
  56. * Current Query that's being built
  57. *
  58. * @return AnDomainQuery
  59. */
  60. protected $_query;
  61. /**
  62. * Builds an update query
  63. *
  64. * @param AnDomainQuery $query Query object
  65. *
  66. * @return string
  67. */
  68. public function update($query)
  69. {
  70. $resources = $query->getRepository()->getResources();
  71. foreach($resources as $resource)
  72. {
  73. $names[] = $query->getPrefix().$resource->getName().' AS '.$this->_store->quoteName($resource->getAlias());
  74. //only join update if there are no order
  75. if ( !empty($query->order) )
  76. break;
  77. }
  78. $operation = $query->operation['value'];
  79. if ( is_array($query->operation['value']) )
  80. {
  81. $operation = array();
  82. foreach($query->operation['value'] as $key => $value)
  83. {
  84. $operation[] = is_numeric($key) ? $value : '@col('.$key.')='.$this->_store->quoteValue($value);
  85. }
  86. $operation = implode(',', $operation);
  87. }
  88. //only join update if there are no order
  89. if ( empty($query->order) )
  90. {
  91. $resources = $query->getRepository()->getResources();
  92. foreach($resources->getLinks() as $link)
  93. {
  94. $query->where($link->child,'=',$link->parent);
  95. }
  96. }
  97. $clause = 'UPDATE '.implode($names,', ').' SET '.$operation;
  98. return $clause;
  99. }
  100. /**
  101. * Builds an delete query. Handles multiple resources at once
  102. *
  103. * @param AnDomainQuery $query Query object
  104. *
  105. * @return string
  106. */
  107. public function delete($query)
  108. {
  109. $resources = $query->getRepository()->getResources();
  110. foreach($resources->getLinks() as $link) {
  111. $query->where($link->child,'=',$link->parent);
  112. }
  113. $names = array();
  114. foreach($resources as $resource) {
  115. $query->from($resource->getName().' AS '.$this->_store->quoteName($resource->getAlias()));
  116. $names[] = $this->_store->quoteName($resource->getAlias());
  117. }
  118. $clause = 'DELETE '.implode(', ', $names);
  119. return $clause;
  120. }
  121. /**
  122. * Return the where clause of a query
  123. *
  124. * @param AnDomainQuery $query Query object
  125. *
  126. * @return string
  127. */
  128. public function select($query)
  129. {
  130. $clause = 'SELECT @DISTINCT';
  131. if ( $query->operation['type'] & AnDomainQuery::QUERY_SELECT_DEFAULT )
  132. {
  133. $description = $query->getRepository()->getDescription();
  134. $columns = AnDomainQueryHelper::parseColumns($query, $query->columns);
  135. foreach($description->getProperty() as $name => $property)
  136. {
  137. if ( isset($columns[$name]) )
  138. {
  139. $result = AnDomainQueryHelper::parseColumn($query, $name);
  140. if ( $result['columns'] instanceof AnDomainResourceColumn )
  141. {
  142. $columns[] = $columns[$name].' AS '.$this->_store->quoteValue($result['columns']->key());
  143. unset($columns[$name]);
  144. }
  145. }
  146. else if ( $property->isAttribute() )
  147. {
  148. $columns[] = $property->getColumn();
  149. }
  150. elseif ( $property->isRelationship() && $property->isManyToOne() )
  151. {
  152. $columns = array_merge($columns, array_values(KConfig::unbox($property->getColumns())));
  153. }
  154. }
  155. if ( $description->getInheritanceColumn() )
  156. $columns[] = $description->getInheritanceColumn();
  157. foreach($columns as $key => $column)
  158. {
  159. if ( $column instanceof AnDomainResourceColumn )
  160. {
  161. if ( $column->resource->getLink() ) {
  162. $columns[$key] = $column.' AS '.$this->_store->quoteValue((string)$column);
  163. continue;
  164. }
  165. else {
  166. $columns[$key] = (string)$column;
  167. }
  168. }
  169. if ( !is_numeric($key) ) {
  170. $columns[$key] = $column.' AS '.$this->_store->quoteValue($key);
  171. }
  172. }
  173. $clause .= ' '.implode(' , ', $columns);
  174. } else
  175. {
  176. $columns = (array) $query->operation['value'];
  177. $columns = AnDomainQueryHelper::parseColumns($query, $columns);
  178. $clause .= ' '.implode(' , ', $columns);
  179. }
  180. return $clause;
  181. }
  182. /**
  183. * Return the where clause of a query
  184. *
  185. * @return string
  186. */
  187. public function from($query)
  188. {
  189. $clause = '';
  190. $resource = $query->getRepository()->getResources()->main();
  191. $query->from($resource->getName().' AS '.$this->_store->quoteName($resource->getAlias()));
  192. if (!empty($query->from))
  193. {
  194. $clause = ' FROM '.implode(' , ', $query->from);
  195. }
  196. return $clause;
  197. }
  198. /**
  199. * Return the join clause
  200. *
  201. * @return string
  202. */
  203. public function join($query)
  204. {
  205. $clause = '';
  206. $repository = $query->getRepository();
  207. $resources = $repository->getResources();
  208. $links = $resources->getLinks();
  209. foreach($links as $link)
  210. {
  211. $type = strtoupper($link->resource->getLinkType());
  212. switch($type)
  213. {
  214. case 'STRONG' : $type = 'INNER';break;
  215. case 'WEAK' : $type = 'LEFT'; break;
  216. }
  217. $query->join($type, $link->resource->getName().' AS '.$this->_store->quoteName($link->resource->getAlias()),array(
  218. $link->child.'='.$link->parent
  219. ));
  220. }
  221. $this->_links($query, $query->link);
  222. if (!empty($query->join))
  223. {
  224. $joins = array();
  225. foreach ($query->join as $join)
  226. {
  227. $tmp = '';
  228. if (! empty($join['type'])) {
  229. $tmp .= $join['type'] . ' ';
  230. }
  231. $tmp .= 'JOIN ' . $join['resource'];
  232. $tmp .= ' ON ' . implode(' AND ', $join['condition']);
  233. $joins[] = $tmp;
  234. }
  235. $clause = implode(' ', $joins);
  236. }
  237. return $clause;
  238. }
  239. /**
  240. * Recursively adds a the links to the query
  241. *
  242. * @param AnDomainQuery $query Query object
  243. *
  244. * @param array
  245. */
  246. protected function _links($query, $links)
  247. {
  248. settype($links, 'array');
  249. foreach($links as $link)
  250. {
  251. $resource = $link['resource'];
  252. $description = $link['query']->getRepository()->getDescription();
  253. $conditions = array();
  254. foreach($link['conditions'] as $key => $value)
  255. {
  256. if ( is_numeric($key) )
  257. $conditions[] = $value;
  258. elseif ( $value instanceof AnDomainResourceColumn)
  259. $conditions[] = $key.' = '.(string)$value;
  260. else
  261. $conditions[] = $key.' = '.$this->_store->quoteValue($value);
  262. }
  263. if ( $link['bind_type'] )
  264. {
  265. $type = $this->_inheritanceTree($description);
  266. if ( !empty($type) && $type != '%' ) {
  267. $conditions[] = $link['bind_type'].' LIKE \''.$this->_inheritanceTree($description).'\'';
  268. }
  269. }
  270. $name = $this->_store->quoteName($link['resource_name']);
  271. $type = strtoupper($link['type']);
  272. switch($type)
  273. {
  274. case 'STRONG' : $type = 'INNER';break;
  275. case 'WEAK' : $type = 'LEFT';break;
  276. }
  277. //$name = $this->_store->quoteName($resource->getName());
  278. $query->join($type, $resource->getName().' AS '.$name, $conditions);
  279. $this->_links($query, $link->query->link);
  280. }
  281. }
  282. /**
  283. * Return the where clause of a query
  284. *
  285. * @return string
  286. */
  287. public function where($query)
  288. {
  289. $clauses = $this->_where($query);
  290. $clause = implode(' AND ', $clauses);
  291. if ( !empty($clause) )
  292. $clause = 'WHERE '.$clause;
  293. return $clause;
  294. }
  295. /**
  296. * Builds Query where clauses recursively
  297. *
  298. * @return array
  299. */
  300. protected function _where($query)
  301. {
  302. $link = null;
  303. $type_check = true;
  304. if ( $query instanceof KConfig )
  305. {
  306. $type_check = false;
  307. $query = $query->query;
  308. }
  309. $description = $query->getRepository()->getDescription();
  310. $clauses = array();
  311. if ( $description->getInheritanceColumn() && $type_check )
  312. {
  313. $resource = $description->getInheritanceColumn()->resource->getAlias();
  314. //the table type column name
  315. $type_column_name = $resource.'.'.$description->getInheritanceColumn()->name;
  316. $scopes = KConfig::unbox($query->instance_of);
  317. if ( empty($scopes) ) {
  318. $scopes = array($description);
  319. } else {
  320. if ( !is_array($scopes) ) {
  321. $scopes = array($scopes);
  322. }
  323. }
  324. foreach($scopes as $index => $scope) {
  325. $inheritance_type = $this->_inheritanceTree($scope);
  326. if ( !empty($inheritance_type) && $inheritance_type != '%' ) {
  327. $scopes[$index] = $type_column_name.' LIKE \''.$inheritance_type.'\'';;
  328. } else {
  329. unset($scopes[$index]);
  330. }
  331. }
  332. if ( !empty($scopes) ) {
  333. $clauses[] = '('.implode(' OR ', $scopes).')';
  334. }
  335. }
  336. if ( !empty($query->where) )
  337. {
  338. $clause = '';
  339. foreach($query->where as $where)
  340. $clause .= $this->_buildWhere($query, $where);
  341. if ( !empty($clause) )
  342. $clauses[] = '('.$clause.')';
  343. }
  344. foreach($query->link as $link)
  345. {
  346. $clauses = array_merge($clauses, $this->_where($link));
  347. }
  348. return $clauses;
  349. }
  350. /**
  351. * Builds WHERE part of a query
  352. *
  353. * @param AnDomainQuery $query Query object
  354. * @param array $where Where conditions
  355. *
  356. * @return string
  357. */
  358. protected function _buildWhere($query, $where)
  359. {
  360. $clause = '';
  361. if(isset($where['condition'])) {
  362. $clause .= ' '.$where['condition'];
  363. }
  364. //converts subclause to string only if there's at least one where statement in it
  365. if ( isset($where['clause']) )
  366. {
  367. if ( count($where['clause']) == 0 )
  368. return '';
  369. $clause .= ' (';
  370. foreach($where['clause'] as $subwhere) {
  371. $clause .= $this->_buildWhere($query, $subwhere);
  372. }
  373. $clause .= ' )';
  374. return $clause;
  375. }
  376. list($columns, $property) = array_values(AnDomainQueryHelper::parseColumn($query, $where['property']));
  377. $value = isset($where['value']) ? $where['value'] : null;
  378. $where['property'] = pick($columns, $where['property']);
  379. if ( $property && !$value instanceof AnDomainQuery )
  380. {
  381. $constraint = $where['constraint'];
  382. if ( is_object($value) || is_array($columns) )
  383. {
  384. if ( $value instanceof KObjectSet || is_array($value) )
  385. {
  386. $values = $value;
  387. $keys = array();
  388. $clauses = array();
  389. foreach($values as $value)
  390. {
  391. $columns = $property->serialize($value);
  392. foreach($columns as $column => $value) {
  393. $keys[$column][] = $value;
  394. }
  395. }
  396. foreach($keys as $key => $values ) {
  397. $clauses[] = $this->_buildWhere($query, array('property'=>$key,'constraint'=>$constraint, 'value'=>$values));
  398. }
  399. $clause .= ' ('.implode(' AND ', $clauses).')';
  400. return $clause;
  401. }
  402. else
  403. {
  404. $columns = $property->serialize($value);
  405. $clauses = array();
  406. foreach($columns as $column => $value)
  407. {
  408. $clauses[] = $this->_buildWhere($query, array('property'=>$column,'constraint'=>$constraint, 'value'=>$value));
  409. }
  410. $clause .= ' ('.implode(' AND ', $clauses).')';
  411. return $clause;
  412. }
  413. }
  414. }
  415. $clause .= ' '.$where['property'];
  416. if(isset($where['constraint']))
  417. {
  418. $value = $this->_store->quoteValue($where['value']);
  419. if ( $value === 'NULL' ) {
  420. //force correct constraint
  421. if ( strpos($where['constraint'], 'IS') === false )
  422. {
  423. if ( strpos($where['constraint'], '<>') !== false )
  424. $where['constraint'] = ' IS NOT ';
  425. elseif ( strpos($where['constraint'], '=') !== false )
  426. $where['constraint'] = ' IS ';
  427. }
  428. }
  429. if(in_array($where['constraint'], array('IN', 'NOT IN'))) {
  430. $value = ' ( '.$value. ' ) ';
  431. }
  432. $clause .= ' '.$where['constraint'].' '.$value;
  433. }
  434. return $clause;
  435. }
  436. /**
  437. * Return the group clause
  438. *
  439. * @return string
  440. */
  441. public function group($query)
  442. {
  443. $clause = '';
  444. if (!empty($query->group))
  445. {
  446. $columns = AnDomainQueryHelper::parseColumns($query, $query->group);
  447. $clause = ' GROUP BY '.implode(' , ', $columns);
  448. }
  449. return $clause;
  450. }
  451. /**
  452. * Return the having clause
  453. *
  454. * @return string
  455. */
  456. public function having($query)
  457. {
  458. $clause = '';
  459. if (!empty($query->having))
  460. {
  461. $clause = ' HAVING '.implode(' , ', $query->having);
  462. }
  463. return $clause;
  464. }
  465. /**
  466. * Return the order clause
  467. *
  468. * @return string
  469. */
  470. public function order($query)
  471. {
  472. $clause = '';
  473. if (!empty($query->order) )
  474. {
  475. $clause = 'ORDER BY ';
  476. $list = array();
  477. foreach ($query->order as $order) {
  478. $columns = AnDomainQueryHelper::parseColumns($query, $order['column']);
  479. foreach($columns as $column)
  480. $list[] = $column.' '.$order['direction'];
  481. }
  482. $clause .= implode(' , ', $list);
  483. }
  484. return $clause;
  485. }
  486. /**
  487. * Return the limit clause
  488. *
  489. * @return string
  490. */
  491. public function limit($query)
  492. {
  493. $clause = '';
  494. if (!empty($query->limit))
  495. {
  496. switch($query->operation['type'])
  497. {
  498. case AnDomainQuery::QUERY_SELECT_DEFAULT :
  499. case AnDomainQuery::QUERY_SELECT :
  500. $clause = ' LIMIT '.$query->offset.' , '.$query->limit;
  501. break;
  502. case AnDomainQuery::QUERY_UPDATE :
  503. if ( (int) $query->limit > 0 )
  504. $clause = ' LIMIT '.$query->limit;
  505. break;
  506. }
  507. }
  508. return $clause;
  509. }
  510. /**
  511. * Builds a query into a final query statement
  512. *
  513. * @param AnDomainQuery $query Query object
  514. *
  515. * @return string
  516. */
  517. public function build($query)
  518. {
  519. //clone the query so it won't be modified
  520. $query = clone $query;
  521. $parts = array();
  522. $this->_query = $query;
  523. $this->_store = $query->getRepository()->getStore();
  524. switch($query->operation['type'])
  525. {
  526. case AnDomainQuery::QUERY_SELECT_DEFAULT :
  527. case AnDomainQuery::QUERY_SELECT :
  528. $parts[] = $this->select($query);
  529. $parts[] = $this->from($query);
  530. $parts[] = '@MYSQL_JOIN_PLACEHOLDER';
  531. $parts[] = $this->where($query);
  532. $parts[] = $this->group($query);
  533. $parts[] = $this->having($query);
  534. $parts[] = $this->order($query);
  535. $parts[] = $this->limit($query);
  536. break;
  537. case AnDomainQuery::QUERY_UPDATE :
  538. $parts[] = $this->update($query);
  539. $parts[] = $this->where($query);
  540. $parts[] = $this->order($query);
  541. $parts[] = $this->limit($query);
  542. break;
  543. case AnDomainQuery::QUERY_DELETE :
  544. $parts[] = $this->delete($query);
  545. $parts[] = $this->from($query);
  546. $parts[] = $this->where($query);
  547. break;
  548. default :
  549. _die();
  550. }
  551. $string = implode(' ', $parts);
  552. $string = $this->parseMethods($query, $string);
  553. $string = str_replace('@MYSQL_JOIN_PLACEHOLDER', $this->join($query), $string);
  554. $string = $this->parseMethods($query, $string);
  555. if ( count($query->binds) ) {
  556. foreach($query->binds as $key => $value) {
  557. $key = ':'.$key;
  558. $value = $this->_store->quoteValue($value);
  559. $string = str_replace($key, $value, $string);
  560. }
  561. }
  562. $string = str_replace('@DISTINCT', $query->distinct ? 'DISTINCT' : '', $string);
  563. return $string;
  564. }
  565. /**
  566. * Creates a string representaiton of class hiearchy of an entity in format of
  567. * Parent Class 1,Parent Class 2,Parent Class n,Entity Class
  568. *
  569. * @param AnDomainDescriptionAbstract|string $description Entity Description or Class name
  570. *
  571. * @return string
  572. */
  573. protected function _inheritanceTree($description)
  574. {
  575. $inheritance = '';
  576. if ( $description instanceof KServiceIdentifier ||
  577. (is_string($description) && strpos($description, '.') && !strpos($description, ','))
  578. ) {
  579. $description = KService::get($description)->getRepository()->getDescription();
  580. }
  581. else if ( $description instanceof AnDomainRepositoryAbstract ) {
  582. $description = $description->getDescription();
  583. }
  584. if ( $description instanceof AnDomainDescriptionAbstract )
  585. {
  586. $inheritance = (string) $description->getInheritanceColumnValue();
  587. if ( $description->isAbstract() ) {
  588. $inheritance .= '%';
  589. }
  590. }
  591. elseif ( is_string($description) ) {
  592. $inheritance = strpos($description,'.') ? $description : $description.'%';
  593. }
  594. return $inheritance;
  595. }
  596. /**
  597. * Builds a query into a final query statement
  598. *
  599. * @param AnDomainQuery $query Query object
  600. * @param string $string A String object
  601. *
  602. * @return string
  603. */
  604. public function parseMethods($query, $string)
  605. {
  606. //replaces any @col(\w+) pattern with the correct column name
  607. if ( strpos($string,'@col(') )
  608. {
  609. $matches = array();
  610. if (preg_match_all('/@col\((.*?)\)/', $string, $matches))
  611. {
  612. $description = $query->getRepository()->getDescription();
  613. $replaces = array();
  614. foreach($matches[1] as $match) {
  615. $result = AnDomainQueryHelper::parseColumn($query, $match);
  616. if ( empty($result['columns']) ) {
  617. $replaces[] = $match;
  618. } else
  619. $replaces[] = (string) $result['columns'];
  620. }
  621. $string = str_replace($matches[0], $replaces, $string);
  622. }
  623. }
  624. if ( strpos($string,'@quote(') )
  625. {
  626. $matches = array();
  627. $replaces = array();
  628. if (preg_match_all('/@quote\((.*?)\)/', $string, $matches))
  629. {
  630. foreach($matches[1] as $match) {
  631. $replaces[] = $this->_store->quoteValue($match);
  632. }
  633. $string = str_replace($matches[0], $replaces, $string);
  634. }
  635. }
  636. if ( strpos($string,'@instanceof(') )
  637. {
  638. $matches = array();
  639. $replaces = array();
  640. if (preg_match_all('/\!?@instanceof\((.*?)\)/', $string, $matches))
  641. {
  642. foreach($matches[1] as $i => $match)
  643. {
  644. $operand = '';
  645. if ( $matches[0][$i][0] == '!' ) {
  646. $operand = 'NOT ';
  647. }
  648. $type_col = $query->getRepository()->getDescription()->getInheritanceColumn();
  649. $classes = explode(',', $match);
  650. $statements = array();
  651. foreach($classes as $class)
  652. {
  653. $class = $this->_store->quoteValue($class);
  654. $statements[] = $operand."FIND_IN_SET($class,$type_col)";
  655. }
  656. if ( $operand == 'NOT ' ) $operand = ' AND '; else $operand = ' OR ';
  657. if ( count($statements) == 1 )
  658. $statements = implode($operand, $statements);
  659. else
  660. $statements = '('.implode($operand, $statements).')';
  661. $replaces[] = $statements;
  662. }
  663. $string = str_replace($matches[0], $replaces, $string);
  664. }
  665. }
  666. if ( strpos($string,'@remove_from_set(') )
  667. {
  668. $matches = array();
  669. $replaces = array();
  670. if (preg_match_all('/@remove_from_set\((.*?)\)/', $string, $matches))
  671. {
  672. foreach($matches[1] as $i => $match)
  673. {
  674. list($set, $item) = explode(',', $match);
  675. $set = trim($set);
  676. $item = trim($item);
  677. $replaces[] = "TRIM(BOTH ',' FROM REPLACE(concat(',',$set,','),CONCAT(',',$item,','),','))";
  678. }
  679. $string = str_replace($matches[0],$replaces,$string);
  680. }
  681. }
  682. if ( strpos($string,'@set_length(') )
  683. {
  684. $matches = array();
  685. $replaces = array();
  686. if (preg_match_all('/@set_length\((.*?)\)/', $string, $matches))
  687. {
  688. foreach($matches[1] as $i => $match)
  689. {
  690. $replaces[] = "LENGTH($match) - LENGTH(REPLACE($match, ',', '')) + 1";
  691. }
  692. $string = str_replace($matches[0],$replaces,$string);
  693. }
  694. }
  695. return $string;
  696. }
  697. }