4 +--------------------------------------------------------------------+
5 | CiviCRM version 4.6 |
6 +--------------------------------------------------------------------+
7 | Copyright CiviCRM LLC (c) 2004-2014 |
8 +--------------------------------------------------------------------+
9 | This file is a part of CiviCRM. |
11 | CiviCRM is free software; you can copy, modify, and distribute it |
12 | under the terms of the GNU Affero General Public License |
13 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
15 | CiviCRM is distributed in the hope that it will be useful, but |
16 | WITHOUT ANY WARRANTY; without even the implied warranty of |
17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
18 | See the GNU Affero General Public License for more details. |
20 | You should have received a copy of the GNU Affero General Public |
21 | License and the CiviCRM Licensing Exception along |
22 | with this program; if not, contact CiviCRM LLC |
23 | at info[AT]civicrm[DOT]org. If you have questions about the |
24 | GNU Affero General Public License or the licensing of CiviCRM, |
25 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
26 +--------------------------------------------------------------------+
30 * Class CRM_Utils_QueryFormatter
32 * This class is a bad idea. It exists for the unholy reason that a single installation
33 * may have up to three query engines (MySQL LIKE, MySQL FTS, Solr) processing the same
34 * query-text. It labors* to take the user's search expression and provide similar search
35 * semantics in different contexts. It is unknown whether this labor will be fruitful
38 class CRM_Utils_QueryFormatter
{
39 const LANG_SQL_LIKE
= 'like';
40 const LANG_SQL_FTS
= 'fts';
41 const LANG_SQL_FTSBOOL
= 'ftsbool';
42 const LANG_SOLR
= 'solr';
45 * Attempt to leave the text as-is.
47 const MODE_NONE
= 'simple';
50 * Attempt to treat the input text as a phrase
52 const MODE_PHRASE
= 'phrase';
55 * Attempt to treat the input text as a phrase with
56 * wildcards on each end.
58 const MODE_WILDPHRASE
= 'wildphrase';
61 * Attempt to treat individual word as if it
62 * had wildcards at the start and end.
64 const MODE_WILDWORDS
= 'wildwords';
67 * Attempt to treat individual word as if it
68 * had a wildcard at the end.
70 const MODE_WILDWORDS_SUFFIX
= 'wildwords-suffix';
72 static protected $singleton;
76 * @return CRM_Utils_QueryFormatter
78 public static function singleton($fresh = FALSE) {
79 if ($fresh || self
::$singleton === NULL) {
80 $mode = CRM_Core_BAO_Setting
::getItem(CRM_Core_BAO_Setting
::SEARCH_PREFERENCES_NAME
, 'fts_query_mode', NULL, self
::MODE_NONE
);
81 self
::$singleton = new CRM_Utils_QueryFormatter($mode);
83 return self
::$singleton;
87 * @var string eg MODE_NONE
95 public function __construct($mode) {
102 public function setMode($mode) {
109 public function getMode() {
114 * @param string $text
115 * @param string $language
116 * Eg LANG_SQL_LIKE, LANG_SQL_FTS, LANG_SOLR.
117 * @throws CRM_Core_Exception
120 public function format($text, $language) {
124 case self
::LANG_SOLR
:
125 case self
::LANG_SQL_FTS
:
126 $text = $this->_formatFts($text, $this->mode
);
129 case self
::LANG_SQL_FTSBOOL
:
130 $text = $this->_formatFtsBool($text, $this->mode
);
133 case self
::LANG_SQL_LIKE
:
134 $text = $this->_formatLike($text, $this->mode
);
141 if ($text === NULL) {
142 throw new CRM_Core_Exception("Unrecognized combination: language=[{$language}] mode=[{$this->mode}]");
148 protected function _formatFts($text, $mode) {
151 // normalize user-inputted wildcards
152 $text = str_replace('%', '*', $text);
157 elseif (strpos($text, '*') !== FALSE) {
158 // if user supplies their own wildcards, then don't do any sophisticated changes
163 case self
::MODE_NONE
:
167 case self
::MODE_PHRASE
:
168 $result = '"' . $text . '"';
171 case self
::MODE_WILDPHRASE
:
172 $result = '"*' . $text . '*"';
175 case self
::MODE_WILDWORDS
:
176 $result = $this->mapWords($text, '*word*');
179 case self
::MODE_WILDWORDS_SUFFIX
:
180 $result = $this->mapWords($text, 'word*');
188 return $this->dedupeWildcards($result, '%');
191 protected function _formatFtsBool($text, $mode) {
194 // normalize user-inputted wildcards
195 $text = str_replace('%', '*', $text);
200 elseif (strpos($text, '+') !== FALSE ||
strpos($text, '-') !== FALSE) {
201 // if user supplies their own include/exclude operators, use text as is (with trailing wildcard)
202 $result = $this->mapWords($text, 'word*');
204 elseif (strpos($text, '*') !== FALSE) {
205 // if user supplies their own wildcards, then don't do any sophisticated changes
206 $result = $this->mapWords($text, '+word');
208 elseif (preg_match('/^(["\']).*\1$/m', $text)) {
209 // if surrounded by quotes, use term as is
214 case self
::MODE_NONE
:
215 $result = $this->mapWords($text, '+word');
218 case self
::MODE_PHRASE
:
219 $result = '+"' . $text . '"';
222 case self
::MODE_WILDPHRASE
:
223 $result = '+"*' . $text . '*"';
226 case self
::MODE_WILDWORDS
:
227 $result = $this->mapWords($text, '+*word*');
230 case self
::MODE_WILDWORDS_SUFFIX
:
231 $result = $this->mapWords($text, '+word*');
239 return $this->dedupeWildcards($result, '%');
242 protected function _formatLike($text, $mode) {
248 elseif (strpos($text, '%') !== FALSE) {
249 // if user supplies their own wildcards, then don't do any sophisticated changes
254 case self
::MODE_NONE
:
255 case self
::MODE_PHRASE
:
256 case self
::MODE_WILDPHRASE
:
257 $result = "%" . $text . "%";
260 case self
::MODE_WILDWORDS
:
261 case self
::MODE_WILDWORDS_SUFFIX
:
262 $result = "%" . preg_replace('/[ \r\n]+/', '%', $text) . '%';
270 return $this->dedupeWildcards($result, '%');
274 * @param string $text
275 * User-supplied query string.
276 * @param string $template
277 * A prototypical description of each word, eg "word%" or "word*" or "*word*".
280 protected function mapWords($text, $template) {
282 foreach ($this->parseWords($text) as $word) {
283 $result[] = str_replace('word', $word, $template);
285 return implode(' ', $result);
292 protected function parseWords($text) {
293 return explode(' ', preg_replace('/[ \r\n\t]+/', ' ', trim($text)));
301 protected function dedupeWildcards($text, $wildcard) {
302 if ($text === NULL) {
306 // don't use preg_replace because $wildcard might be special char
307 while (strpos($text, "{$wildcard}{$wildcard}") !== FALSE) {
308 $text = str_replace("{$wildcard}{$wildcard}", "{$wildcard}", $text);
313 public static function getModes() {
317 self
::MODE_WILDPHRASE
,
318 self
::MODE_WILDWORDS
,
319 self
::MODE_WILDWORDS_SUFFIX
,
323 public static function getLanguages() {
327 self
::LANG_SQL_FTSBOOL
,
335 * Ex: drush eval 'civicrm_initialize(); CRM_Utils_QueryFormatter::dumpExampleTable("firstword secondword");'
337 public static function dumpExampleTable($text) {
338 $width = strlen($text) +
8;
341 $buf .= sprintf("%-{$width}s", 'mode');
342 foreach (self
::getLanguages() as $lang) {
343 $buf .= sprintf("%-{$width}s", $lang);
347 foreach (self
::getModes() as $mode) {
348 $formatter = new CRM_Utils_QueryFormatter($mode);
349 $buf .= sprintf("%-{$width}s", $mode);
350 foreach (self
::getLanguages() as $lang) {
351 $buf .= sprintf("%-{$width}s", $formatter->format($text, $lang));