PageRenderTime 46ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/ezcomponents/Search/src/query_builder.php

http://hppg.googlecode.com/
PHP | 302 lines | 190 code | 24 blank | 88 comment | 20 complexity | 101659c349639dae7410d926949ad2d7 MD5 | raw file
Possible License(s): GPL-3.0, BSD-3-Clause
  1. <?php
  2. /**
  3. * File containing the ezcSearchQueryBuilder class.
  4. *
  5. * @package Search
  6. * @version 1.0.9
  7. * @copyright Copyright (C) 2005-2010 eZ Systems AS. All rights reserved.
  8. * @license http://ez.no/licenses/new_bsd New BSD License
  9. */
  10. /**
  11. * ezcSearchQueryBuilder provides a method to add a natural language search
  12. * query to an exisiting query object.
  13. *
  14. * @package Search
  15. * @version 1.0.9
  16. * @mainclass
  17. */
  18. class ezcSearchQueryBuilder
  19. {
  20. /**
  21. * Holds the parser's state
  22. *
  23. * @var string
  24. */
  25. private $state;
  26. /**
  27. * Keeps a list of where clauses per nested level
  28. *
  29. * @var array(array(string))
  30. */
  31. private $stack;
  32. /**
  33. * Contains the current stack level
  34. *
  35. * @var int
  36. */
  37. private $stackLevel;
  38. /**
  39. * Contains the current stack elements query type ('default', 'and' or 'or').
  40. *
  41. * @var string
  42. */
  43. private $stackType;
  44. /**
  45. * Contains a prefix for the following clause ('+', '-' or null).
  46. *
  47. * @var mixed
  48. */
  49. private $prefix;
  50. /**
  51. * Parses the $searchQuery and adds the selection clauses to the $query object
  52. *
  53. * @param ezcSearchQuery $query
  54. * @param string $searchQuery
  55. * @param array(string) $searchFields
  56. */
  57. public function parseSearchQuery( ezcSearchQuery $query, $searchQuery, $searchFields )
  58. {
  59. $this->reset();
  60. $tokens = $this->tokenize( $searchQuery );
  61. $this->buildQuery( $query, $tokens, $searchFields );
  62. if ( $this->stackType[0] == 'and' || $this->stackType[0] == 'default' )
  63. {
  64. foreach ( $this->stack[0] as $element )
  65. {
  66. $query->where( $element );
  67. }
  68. }
  69. else
  70. {
  71. $query->where( $query->lOr( $this->stack[0] ) );
  72. }
  73. }
  74. /**
  75. * Resets the parser to its initial state.
  76. */
  77. public function reset()
  78. {
  79. $this->state = 'normal';
  80. $this->stackLevel = 0;
  81. $this->stack = array();
  82. $this->stack[$this->stackLevel] = array();
  83. $this->stackType = array();
  84. $this->stackType[$this->stackLevel] = 'default';
  85. $this->prefix = null;
  86. }
  87. /**
  88. * Tokenizes the search query into tokens
  89. *
  90. * @param string $searchQuery
  91. * @return array(ezcSearchQueryToken)
  92. */
  93. static protected function tokenize( $searchQuery )
  94. {
  95. $map = array(
  96. ' ' => ezcSearchQueryToken::SPACE,
  97. '\t' => ezcSearchQueryToken::SPACE,
  98. '"' => ezcSearchQueryToken::QUOTE,
  99. '+' => ezcSearchQueryToken::PLUS,
  100. '-' => ezcSearchQueryToken::MINUS,
  101. '(' => ezcSearchQueryToken::BRACE_OPEN,
  102. ')' => ezcSearchQueryToken::BRACE_CLOSE,
  103. 'and' => ezcSearchQueryToken::LOGICAL_AND,
  104. 'or' => ezcSearchQueryToken::LOGICAL_OR,
  105. ':' => ezcSearchQueryToken::COLON,
  106. );
  107. $tokens = array();
  108. $tokenArray = preg_split( '@(\s)|(["+():-])@', $searchQuery, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
  109. foreach ( $tokenArray as $token )
  110. {
  111. if ( isset( $map[strtolower( $token )] ) )
  112. {
  113. $tokens[] = new ezcSearchQueryToken( $map[strtolower( $token )], $token );
  114. }
  115. else
  116. {
  117. $tokens[] = new ezcSearchQueryToken( ezcSearchQueryToken::STRING, $token );
  118. }
  119. }
  120. return $tokens;
  121. }
  122. /**
  123. * Applies the current prefix to the clause in $string
  124. *
  125. * @param ezcSearchQuery $q
  126. * @param string $string
  127. *
  128. * @return string
  129. */
  130. private function processPrefix( ezcSearchQuery $q, $string )
  131. {
  132. switch ( $this->prefix )
  133. {
  134. case ezcSearchQueryToken::PLUS:
  135. $string = $q->important( $string );
  136. break;
  137. case ezcSearchQueryToken::MINUS:
  138. $string = $q->not( $string );
  139. break;
  140. }
  141. return $string;
  142. }
  143. /**
  144. * Assembles a query part for a search term for the fields passed in $searchFields
  145. *
  146. * If there is only one search field, it just processes the prefix. In case
  147. * there are multiple fields they are joined together with OR, unless the
  148. * whole clause is negated. In that case they're joined by AND.
  149. *
  150. * @param ezcSearchQuery $q
  151. * @param string $term
  152. * @param array(string) $searchFields
  153. *
  154. * @return string
  155. */
  156. private function constructSearchWhereClause( ezcSearchQuery $q, $term, $searchFields )
  157. {
  158. if ( count( $searchFields ) > 1 )
  159. {
  160. $parts = array();
  161. foreach ( $searchFields as $searchField )
  162. {
  163. $parts[] = $this->processPrefix( $q, $q->eq( $searchField, $term ) );
  164. }
  165. if ( $this->prefix == ezcSearchQueryToken::MINUS )
  166. {
  167. $ret = $q->lAnd( $parts );
  168. }
  169. else
  170. {
  171. $ret = $q->lOr( $parts );
  172. }
  173. }
  174. else
  175. {
  176. $ret = $this->processPrefix( $q, $q->eq( $searchFields[0], $term ) );
  177. }
  178. $this->prefix = null;
  179. return $ret;
  180. }
  181. /**
  182. * Walks over the $tokens and builds the query $q from them and the $searchFields
  183. *
  184. * @param ezcSearchQuery $q
  185. * @param array(ezcSearchQueryToken) $tokens
  186. * @param array(string) $searchFields
  187. *
  188. * @throws ezcSearchBuildQueryException if there is an uneven set of quotes.
  189. */
  190. protected function buildQuery( ezcSearchQuery $q, $tokens, $searchFields )
  191. {
  192. foreach ( $tokens as $token )
  193. {
  194. switch ( $this->state )
  195. {
  196. case 'normal':
  197. switch ( $token->type )
  198. {
  199. case ezcSearchQueryToken::SPACE:
  200. /* ignore */
  201. break;
  202. case ezcSearchQueryToken::STRING:
  203. $this->stack[$this->stackLevel][] = $this->constructSearchWhereClause( $q, $token->token, $searchFields );
  204. break;
  205. case ezcSearchQueryToken::QUOTE:
  206. $this->state = 'in-quotes';
  207. $string = '';
  208. break;
  209. case ezcSearchQueryToken::LOGICAL_OR:
  210. if ( $this->stackType[$this->stackLevel] === 'and' )
  211. {
  212. throw new ezcSearchBuildQueryException( 'You can not mix AND and OR without using "(" and ")".' );
  213. }
  214. else
  215. {
  216. $this->stackType[$this->stackLevel] = 'or';
  217. }
  218. break;
  219. case ezcSearchQueryToken::LOGICAL_AND:
  220. if ( $this->stackType[$this->stackLevel] === 'or' )
  221. {
  222. throw new ezcSearchBuildQueryException( 'You can not mix OR and AND without using "(" and ")".' );
  223. }
  224. else
  225. {
  226. $this->stackType[$this->stackLevel] = 'and';
  227. }
  228. break;
  229. case ezcSearchQueryToken::BRACE_OPEN:
  230. $this->stackLevel++;
  231. $this->stackType[$this->stackLevel] = 'default';
  232. break;
  233. case ezcSearchQueryToken::BRACE_CLOSE:
  234. $this->stackLevel--;
  235. if ( $this->stackType[$this->stackLevel + 1] == 'and' || $this->stackType[$this->stackLevel + 1] == 'default' )
  236. {
  237. $this->stack[$this->stackLevel][] = $q->lAnd( $this->stack[$this->stackLevel + 1] );
  238. }
  239. else
  240. {
  241. $this->stack[$this->stackLevel][] = $q->lOr( $this->stack[$this->stackLevel + 1] );
  242. }
  243. break;
  244. case ezcSearchQueryToken::PLUS:
  245. case ezcSearchQueryToken::MINUS:
  246. $this->prefix = $token->type;
  247. break;
  248. }
  249. break;
  250. case 'in-quotes':
  251. switch ( $token->type )
  252. {
  253. case ezcSearchQueryToken::QUOTE:
  254. $this->stack[$this->stackLevel][] = $this->constructSearchWhereClause( $q, $string, $searchFields );
  255. $this->state = 'normal';
  256. break;
  257. case ezcSearchQueryToken::STRING:
  258. case ezcSearchQueryToken::COLON:
  259. case ezcSearchQueryToken::SPACE:
  260. case ezcSearchQueryToken::LOGICAL_AND:
  261. case ezcSearchQueryToken::LOGICAL_OR:
  262. case ezcSearchQueryToken::PLUS:
  263. case ezcSearchQueryToken::MINUS:
  264. case ezcSearchQueryToken::BRACE_OPEN:
  265. case ezcSearchQueryToken::BRACE_CLOSE:
  266. $string .= $token->token;
  267. break;
  268. }
  269. break;
  270. }
  271. }
  272. if ( $this->state == 'in-quotes' )
  273. {
  274. throw new ezcSearchBuildQueryException( 'Unterminated quotes in query string.' );
  275. }
  276. }
  277. }
  278. ?>