3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2016 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
30 * @copyright CiviCRM LLC (c) 2004-2016
34 * Class CRM_Utils_QueryFormatter
36 * This class is a bad idea. It exists for the unholy reason that a single installation
37 * may have up to three query engines (MySQL LIKE, MySQL FTS, Solr) processing the same
38 * query-text. It labors* to take the user's search expression and provide similar search
39 * semantics in different contexts. It is unknown whether this labor will be fruitful
42 class CRM_Utils_QueryFormatter
{
43 const LANG_SQL_LIKE
= 'like';
44 const LANG_SQL_FTS
= 'fts';
45 const LANG_SQL_FTSBOOL
= 'ftsbool';
46 const LANG_SOLR
= 'solr';
49 * Attempt to leave the text as-is.
51 const MODE_NONE
= 'simple';
54 * Attempt to treat the input text as a phrase
56 const MODE_PHRASE
= 'phrase';
59 * Attempt to treat the input text as a phrase with
60 * wildcards on each end.
62 const MODE_WILDPHRASE
= 'wildphrase';
65 * Attempt to treat individual word as if it
66 * had wildcards at the start and end.
68 const MODE_WILDWORDS
= 'wildwords';
71 * Attempt to treat individual word as if it
72 * had a wildcard at the end.
74 const MODE_WILDWORDS_SUFFIX
= 'wildwords-suffix';
76 static protected $singleton;
80 * @return CRM_Utils_QueryFormatter
82 public static function singleton($fresh = FALSE) {
83 if ($fresh || self
::$singleton === NULL) {
84 $mode = Civi
::settings()->get('fts_query_mode');
85 self
::$singleton = new CRM_Utils_QueryFormatter($mode);
87 return self
::$singleton;
100 public function __construct($mode) {
107 public function setMode($mode) {
114 public function getMode() {
119 * @param string $text
120 * @param string $language
121 * Eg LANG_SQL_LIKE, LANG_SQL_FTS, LANG_SOLR.
122 * @throws CRM_Core_Exception
125 public function format($text, $language) {
129 case self
::LANG_SOLR
:
130 case self
::LANG_SQL_FTS
:
131 $text = $this->_formatFts($text, $this->mode
);
134 case self
::LANG_SQL_FTSBOOL
:
135 $text = $this->_formatFtsBool($text, $this->mode
);
138 case self
::LANG_SQL_LIKE
:
139 $text = $this->_formatLike($text, $this->mode
);
146 if ($text === NULL) {
147 throw new CRM_Core_Exception("Unrecognized combination: language=[{$language}] mode=[{$this->mode}]");
156 * @param string $text
161 protected function _formatFts($text, $mode) {
164 // normalize user-inputted wildcards
165 $text = str_replace('%', '*', $text);
170 elseif (strpos($text, '*') !== FALSE) {
171 // if user supplies their own wildcards, then don't do any sophisticated changes
176 case self
::MODE_NONE
:
180 case self
::MODE_PHRASE
:
181 $result = '"' . $text . '"';
184 case self
::MODE_WILDPHRASE
:
185 $result = '"*' . $text . '*"';
188 case self
::MODE_WILDWORDS
:
189 $result = $this->mapWords($text, '*word*');
192 case self
::MODE_WILDWORDS_SUFFIX
:
193 $result = $this->mapWords($text, 'word*');
201 return $this->dedupeWildcards($result, '%');
207 * @param string $text
212 protected function _formatFtsBool($text, $mode) {
215 // normalize user-inputted wildcards
216 $text = str_replace('%', '*', $text);
221 elseif (strpos($text, '+') !== FALSE ||
strpos($text, '-') !== FALSE) {
222 // if user supplies their own include/exclude operators, use text as is (with trailing wildcard)
223 $result = $this->mapWords($text, 'word*');
225 elseif (strpos($text, '*') !== FALSE) {
226 // if user supplies their own wildcards, then don't do any sophisticated changes
227 $result = $this->mapWords($text, '+word');
229 elseif (preg_match('/^(["\']).*\1$/m', $text)) {
230 // if surrounded by quotes, use term as is
235 case self
::MODE_NONE
:
236 $result = $this->mapWords($text, '+word');
239 case self
::MODE_PHRASE
:
240 $result = '+"' . $text . '"';
243 case self
::MODE_WILDPHRASE
:
244 $result = '+"*' . $text . '*"';
247 case self
::MODE_WILDWORDS
:
248 $result = $this->mapWords($text, '+*word*');
251 case self
::MODE_WILDWORDS_SUFFIX
:
252 $result = $this->mapWords($text, '+word*');
260 return $this->dedupeWildcards($result, '%');
271 protected function _formatLike($text, $mode) {
277 elseif (strpos($text, '%') !== FALSE) {
278 // if user supplies their own wildcards, then don't do any sophisticated changes
283 case self
::MODE_NONE
:
284 case self
::MODE_PHRASE
:
285 case self
::MODE_WILDPHRASE
:
286 $result = "%" . $text . "%";
289 case self
::MODE_WILDWORDS
:
290 case self
::MODE_WILDWORDS_SUFFIX
:
291 $result = "%" . preg_replace('/[ \r\n]+/', '%', $text) . '%';
299 return $this->dedupeWildcards($result, '%');
303 * @param string $text
304 * User-supplied query string.
305 * @param string $template
306 * A prototypical description of each word, eg "word%" or "word*" or "*word*".
309 protected function mapWords($text, $template) {
311 foreach ($this->parseWords($text) as $word) {
312 $result[] = str_replace('word', $word, $template);
314 return implode(' ', $result);
321 protected function parseWords($text) {
322 return explode(' ', preg_replace('/[ \r\n\t]+/', ' ', trim($text)));
330 protected function dedupeWildcards($text, $wildcard) {
331 if ($text === NULL) {
335 // don't use preg_replace because $wildcard might be special char
336 while (strpos($text, "{$wildcard}{$wildcard}") !== FALSE) {
337 $text = str_replace("{$wildcard}{$wildcard}", "{$wildcard}", $text);
347 public static function getModes() {
351 self
::MODE_WILDPHRASE
,
352 self
::MODE_WILDWORDS
,
353 self
::MODE_WILDWORDS_SUFFIX
,
362 public static function getLanguages() {
366 self
::LANG_SQL_FTSBOOL
,
374 * Ex: drush eval 'civicrm_initialize(); CRM_Utils_QueryFormatter::dumpExampleTable("firstword secondword");'
376 public static function dumpExampleTable($text) {
377 $width = strlen($text) +
8;
380 $buf .= sprintf("%-{$width}s", 'mode');
381 foreach (self
::getLanguages() as $lang) {
382 $buf .= sprintf("%-{$width}s", $lang);
386 foreach (self
::getModes() as $mode) {
387 $formatter = new CRM_Utils_QueryFormatter($mode);
388 $buf .= sprintf("%-{$width}s", $mode);
389 foreach (self
::getLanguages() as $lang) {
390 $buf .= sprintf("%-{$width}s", $formatter->format($text, $lang));