├── README.md ├── composer.json └── src ├── Drivers ├── ConnectionBase.php ├── ConnectionInterface.php ├── MultiResultSet.php ├── MultiResultSetAdapterInterface.php ├── MultiResultSetInterface.php ├── Mysqli │ ├── Connection.php │ ├── MultiResultSetAdapter.php │ └── ResultSetAdapter.php ├── Pdo │ ├── Connection.php │ ├── MultiResultSetAdapter.php │ └── ResultSetAdapter.php ├── ResultSet.php ├── ResultSetAdapterInterface.php └── ResultSetInterface.php ├── Exception ├── ConnectionException.php ├── DatabaseException.php ├── ResultSetException.php └── SphinxQLException.php ├── Expression.php ├── Facet.php ├── Helper.php ├── MatchBuilder.php ├── Percolate.php └── SphinxQL.php /README.md: -------------------------------------------------------------------------------- 1 | Query Builder for SphinxQL 2 | ========================== 3 | 4 | [![Build Status](https://travis-ci.org/FoolCode/SphinxQL-Query-Builder.png)](https://travis-ci.org/FoolCode/SphinxQL-Query-Builder) 5 | [![Latest Stable Version](https://poser.pugx.org/foolz/sphinxql-query-builder/v/stable)](https://packagist.org/packages/foolz/sphinxql-query-builder) 6 | [![Latest Unstable Version](https://poser.pugx.org/foolz/sphinxql-query-builder/v/unstable)](https://packagist.org/packages/foolz/sphinxql-query-builder) 7 | [![Total Downloads](https://poser.pugx.org/foolz/sphinxql-query-builder/downloads)](https://packagist.org/packages/foolz/sphinxql-query-builder) 8 | 9 | ## About 10 | 11 | This is a SphinxQL Query Builder used to work with SphinxQL, a SQL dialect used with the Sphinx search engine and it's fork Manticore. It maps most of the functions listed in the [SphinxQL reference](http://sphinxsearch.com/docs/current.html#SphinxQL-reference) and is generally [faster](http://sphinxsearch.com/blog/2010/04/25/sphinxapi-vs-SphinxQL-benchmark/) than the available Sphinx API. 12 | 13 | This Query Builder has no dependencies except PHP 7.1 or later, `\MySQLi` extension, `PDO`, and [Sphinx](http://sphinxsearch.com)/[Manticore](https://manticoresearch.com). 14 | 15 | ### Missing methods? 16 | 17 | SphinxQL evolves very fast. 18 | 19 | Most of the new functions are static one liners like `SHOW PLUGINS`. We'll avoid trying to keep up with these methods, as they are easy to just call directly (`(new SphinxQL($conn))->query($sql)->execute()`). You're free to submit pull requests to support these methods. 20 | 21 | If any feature is unreachable through this library, open a new issue or send a pull request. 22 | 23 | ## Code Quality 24 | 25 | The majority of the methods in the package have been unit tested. 26 | 27 | The only methods that have not been fully tested are the Helpers, which are mostly simple shorthands for SQL strings. 28 | 29 | ## How to Contribute 30 | 31 | ### Pull Requests 32 | 33 | 1. Fork the SphinxQL Query Builder repository 34 | 2. Create a new branch for each feature or improvement 35 | 3. Submit a pull request from each branch to the **master** branch 36 | 37 | It is very important to separate new features or improvements into separate feature branches, and to send a pull 38 | request for each branch. This allows me to review and pull in new features or improvements individually. 39 | 40 | ### Style Guide 41 | 42 | All pull requests must adhere to the [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) standard. 43 | 44 | ### Unit Testing 45 | 46 | All pull requests must be accompanied by passing unit tests and complete code coverage. The SphinxQL Query Builder uses 47 | `phpunit` for testing. 48 | 49 | [Learn about PHPUnit](https://github.com/sebastianbergmann/phpunit/) 50 | 51 | ## Installation 52 | 53 | This is a Composer package. You can install this package with the following command: `composer require foolz/sphinxql-query-builder` 54 | 55 | ## Usage 56 | 57 | The following examples will omit the namespace. 58 | 59 | ```php 60 | setParams(array('host' => 'domain.tld', 'port' => 9306)); 67 | 68 | $query = (new SphinxQL($conn))->select('column_one', 'colume_two') 69 | ->from('index_ancient', 'index_main', 'index_delta') 70 | ->match('comment', 'my opinion is superior to yours') 71 | ->where('banned', '=', 1); 72 | 73 | $result = $query->execute(); 74 | ``` 75 | 76 | ### Drivers 77 | 78 | We support the following database connection drivers: 79 | 80 | * Foolz\SphinxQL\Drivers\Mysqli\Connection 81 | * Foolz\SphinxQL\Drivers\Pdo\Connection 82 | 83 | ### Connection 84 | 85 | * __$conn = new Connection()__ 86 | 87 | Create a new Connection instance to be used with the following methods or SphinxQL class. 88 | 89 | * __$conn->setParams($params = array('host' => '127.0.0.1', 'port' => 9306))__ 90 | 91 | Sets the connection parameters used to establish a connection to the server. Supported parameters: 'host', 'port', 'socket', 'options'. 92 | 93 | * __$conn->query($query)__ 94 | 95 | Performs the query on the server. Returns a [`ResultSet`](#resultset) object containing the query results. 96 | 97 | _More methods are available in the Connection class, but usually not necessary as these are handled automatically._ 98 | 99 | ### SphinxQL 100 | 101 | * __new SphinxQL($conn)__ 102 | 103 | Creates a SphinxQL instance used for generating queries. 104 | 105 | #### Bypass Query Escaping 106 | 107 | Often, you would need to call and run SQL functions that shouldn't be escaped in the query. You can bypass the query escape by wrapping the query in an `\Expression`. 108 | 109 | * __SphinxQL::expr($string)__ 110 | 111 | Returns the string without being escaped. 112 | 113 | #### Query Escaping 114 | 115 | There are cases when an input __must__ be escaped in the SQL statement. The following functions are used to handle any escaping required for the query. 116 | 117 | * __$sq->escape($value)__ 118 | 119 | Returns the escaped value. This is processed with the `\MySQLi::real_escape_string()` function. 120 | 121 | * __$sq->quoteIdentifier($identifier)__ 122 | 123 | Adds backtick quotes to the identifier. For array elements, use `$sq->quoteIdentifierArray($arr)`. 124 | 125 | * __$sq->quote($value)__ 126 | 127 | Adds quotes to the value and escapes it. For array elements, use `$sq->quoteArr($arr)`. 128 | 129 | * __$sq->escapeMatch($value)__ 130 | 131 | Escapes the string to be used in `MATCH`. 132 | 133 | * __$sq->halfEscapeMatch($value)__ 134 | 135 | Escapes the string to be used in `MATCH`. The following characters are allowed: `-`, `|`, and `"`. 136 | 137 | _Refer to `$sq->match()` for more information._ 138 | 139 | #### SELECT 140 | 141 | * __$sq = (new SphinxQL($conn))->select($column1, $column2, ...)->from($index1, $index2, ...)__ 142 | 143 | Begins a `SELECT` query statement. If no column is specified, the statement defaults to using `*`. Both `$column1` and `$index1` can be arrays. 144 | 145 | #### INSERT, REPLACE 146 | 147 | This will return an `INT` with the number of rows affected. 148 | 149 | * __$sq = (new SphinxQL($conn))->insert()->into($index)__ 150 | 151 | Begins an `INSERT`. 152 | 153 | * __$sq = (new SphinxQL($conn))->replace()->into($index)__ 154 | 155 | Begins an `REPLACE`. 156 | 157 | * __$sq->set($associative_array)__ 158 | 159 | Inserts an associative array, with the keys as the columns and values as the value for the respective column. 160 | 161 | * __$sq->value($column1, $value1)->value($column2, $value2)->value($column3, $value3)__ 162 | 163 | Sets the value of each column individually. 164 | 165 | * __$sq->columns($column1, $column2, $column3)->values($value1, $value2, $value3)->values($value11, $value22, $value33)__ 166 | 167 | Allows the insertion of multiple arrays of values in the specified columns. 168 | 169 | Both `$column1` and `$index1` can be arrays. 170 | 171 | #### UPDATE 172 | 173 | This will return an `INT` with the number of rows affected. 174 | 175 | * __$sq = (new SphinxQL($conn))->update($index)__ 176 | 177 | Begins an `UPDATE`. 178 | 179 | * __$sq->value($column1, $value1)->value($column2, $value2)__ 180 | 181 | Updates the selected columns with the respective value. 182 | 183 | * __$sq->set($associative_array)__ 184 | 185 | Inserts the associative array, where the keys are the columns and the respective values are the column values. 186 | 187 | #### DELETE 188 | 189 | Will return an array with an `INT` as first member, the number of rows deleted. 190 | 191 | * __$sq = (new SphinxQL($conn))->delete()->from($index)->where(...)__ 192 | 193 | Begins a `DELETE`. 194 | 195 | #### WHERE 196 | 197 | * __$sq->where($column, $operator, $value)__ 198 | 199 | Standard WHERE, extended to work with Sphinx filters and full-text. 200 | 201 | ```php 202 | where('column', 'value'); 205 | 206 | // WHERE `column` = 'value' 207 | $sq->where('column', '=', 'value'); 208 | 209 | // WHERE `column` >= 'value' 210 | $sq->where('column', '>=', 'value'); 211 | 212 | // WHERE `column` IN ('value1', 'value2', 'value3') 213 | $sq->where('column', 'IN', array('value1', 'value2', 'value3')); 214 | 215 | // WHERE `column` NOT IN ('value1', 'value2', 'value3') 216 | $sq->where('column', 'NOT IN', array('value1', 'value2', 'value3')); 217 | 218 | // WHERE `column` BETWEEN 'value1' AND 'value2' 219 | // WHERE `example` BETWEEN 10 AND 100 220 | $sq->where('column', 'BETWEEN', array('value1', 'value2')); 221 | ``` 222 | 223 | _It should be noted that `OR` and parenthesis are not supported and implemented in the SphinxQL dialect yet._ 224 | 225 | #### MATCH 226 | 227 | * __$sq->match($column, $value, $half = false)__ 228 | 229 | Search in full-text fields. Can be used multiple times in the same query. Column can be an array. Value can be an Expression to bypass escaping (and use your own custom solution). 230 | 231 | ```php 232 | match('title', 'Otoshimono') 234 | ->match('character', 'Nymph') 235 | ->match(array('hates', 'despises'), 'Oregano'); 236 | ``` 237 | 238 | By default, all inputs are escaped. The usage of `SphinxQL::expr($value)` is required to bypass the default escaping and quoting function. 239 | 240 | The `$half` argument, if set to `true`, will not escape and allow the usage of the following characters: `-`, `|`, `"`. If you plan to use this feature and expose it to public interfaces, it is __recommended__ that you wrap the query in a `try catch` block as the character order may `throw` a query error. 241 | 242 | ```php 243 | select() 250 | ->from('rt') 251 | ->match('title', 'Sora no || Otoshimono', true) 252 | ->match('title', SphinxQL::expr('"Otoshimono"/3')) 253 | ->match('loves', SphinxQL::expr(custom_escaping_fn('(you | me)'))); 254 | ->execute(); 255 | } 256 | catch (\Foolz\SphinxQL\DatabaseException $e) 257 | { 258 | // an error is thrown because two `|` one after the other aren't allowed 259 | } 260 | ``` 261 | 262 | #### GROUP, WITHIN GROUP, ORDER, OFFSET, LIMIT, OPTION 263 | 264 | * __$sq->groupBy($column)__ 265 | 266 | `GROUP BY $column` 267 | 268 | * __$sq->withinGroupOrderBy($column, $direction = null)__ 269 | 270 | `WITHIN GROUP ORDER BY $column [$direction]` 271 | 272 | Direction can be omitted with `null`, or be `ASC` or `DESC` case insensitive. 273 | 274 | * __$sq->orderBy($column, $direction = null)__ 275 | 276 | `ORDER BY $column [$direction]` 277 | 278 | Direction can be omitted with `null`, or be `ASC` or `DESC` case insensitive. 279 | 280 | * __$sq->offset($offset)__ 281 | 282 | `LIMIT $offset, 9999999999999` 283 | 284 | Set the offset. Since SphinxQL doesn't support the `OFFSET` keyword, `LIMIT` has been set at an extremely high number. 285 | 286 | * __$sq->limit($limit)__ 287 | 288 | `LIMIT $limit` 289 | 290 | * __$sq->limit($offset, $limit)__ 291 | 292 | `LIMIT $offset, $limit` 293 | 294 | * __$sq->option($name, $value)__ 295 | 296 | `OPTION $name = $value` 297 | 298 | Set a SphinxQL option such as `max_matches` or `reverse_scan` for the query. 299 | 300 | #### TRANSACTION 301 | 302 | * __(new SphinxQL($conn))->transactionBegin()__ 303 | 304 | Begins a transaction. 305 | 306 | * __(new SphinxQL($conn))->transactionCommit()__ 307 | 308 | Commits a transaction. 309 | 310 | * __(new SphinxQL($conn))->transactionRollback()__ 311 | 312 | Rollbacks a transaction. 313 | 314 | #### Executing and Compiling 315 | 316 | * __$sq->execute()__ 317 | 318 | Compiles, executes, and __returns__ a [`ResultSet`](#resultset) object containing the query results. 319 | 320 | * __$sq->executeBatch()__ 321 | 322 | Compiles, executes, and __returns__ a [`MultiResultSet`](#multiresultset) object containing the multi-query results. 323 | 324 | * __$sq->compile()__ 325 | 326 | Compiles the query. 327 | 328 | * __$sq->getCompiled()__ 329 | 330 | Returns the last query compiled. 331 | 332 | * __$sq->getResult()__ 333 | 334 | Returns the [`ResultSet`](#resultset) or [` MultiResultSet`](#multiresultset) object, depending on whether single or multi-query have been executed last. 335 | 336 | #### Multi-Query 337 | 338 | * __$sq->enqueue(SphinxQL $next = null)__ 339 | 340 | Queues the query. If a $next is provided, $next is appended and returned, otherwise a new SphinxQL object is returned. 341 | 342 | * __$sq->executeBatch()__ 343 | 344 | Returns a [`MultiResultSet`](#multiresultset) object containing the multi-query results. 345 | 346 | ```php 347 | conn)) 351 | ->select() 352 | ->from('rt') 353 | ->match('title', 'sora') 354 | ->enqueue((new SphinxQL($this->conn))->query('SHOW META')) // this returns the object with SHOW META query 355 | ->enqueue() // this returns a new object 356 | ->select() 357 | ->from('rt') 358 | ->match('content', 'nymph') 359 | ->executeBatch(); 360 | ``` 361 | 362 | `$result` will contain [`MultiResultSet`](#multiresultset) object. Sequential calls to the `$result->getNext()` method allow you to get a [`ResultSet`](#resultset) object containing the results of the next enqueued query. 363 | 364 | 365 | #### Query results 366 | 367 | ##### ResultSet 368 | 369 | Contains the results of the query execution. 370 | 371 | * __$result->fetchAllAssoc()__ 372 | 373 | Fetches all result rows as an associative array. 374 | 375 | * __$result->fetchAllNum()__ 376 | 377 | Fetches all result rows as a numeric array. 378 | 379 | * __$result->fetchAssoc()__ 380 | 381 | Fetch a result row as an associative array. 382 | 383 | * __$result->fetchNum()__ 384 | 385 | Fetch a result row as a numeric array. 386 | 387 | * __$result->getAffectedRows()__ 388 | 389 | Returns the number of affected rows in the case of a DML query. 390 | 391 | ##### MultiResultSet 392 | 393 | Contains the results of the multi-query execution. 394 | 395 | * __$result->getNext()__ 396 | 397 | Returns a [`ResultSet`](#resultset) object containing the results of the next query. 398 | 399 | 400 | ### Helper 401 | 402 | The `Helper` class contains useful methods that don't need "query building". 403 | 404 | Remember to `->execute()` to get a result. 405 | 406 | * __Helper::pairsToAssoc($result)__ 407 | 408 | Takes the pairs from a SHOW command and returns an associative array key=>value 409 | 410 | The following methods return a prepared `SphinxQL` object. You can also use `->enqueue($next_object)`: 411 | 412 | ```php 413 | conn)) 417 | ->select() 418 | ->from('rt') 419 | ->where('gid', 9003) 420 | ->enqueue((new Helper($this->conn))->showMeta()) // this returns the object with SHOW META query prepared 421 | ->enqueue() // this returns a new object 422 | ->select() 423 | ->from('rt') 424 | ->where('gid', 201) 425 | ->executeBatch(); 426 | ``` 427 | 428 | * `(new Helper($conn))->showMeta() => 'SHOW META'` 429 | * `(new Helper($conn))->showWarnings() => 'SHOW WARNINGS'` 430 | * `(new Helper($conn))->showStatus() => 'SHOW STATUS'` 431 | * `(new Helper($conn))->showTables() => 'SHOW TABLES'` 432 | * `(new Helper($conn))->showVariables() => 'SHOW VARIABLES'` 433 | * `(new Helper($conn))->setVariable($name, $value, $global = false)` 434 | * `(new Helper($conn))->callSnippets($data, $index, $query, $options = array())` 435 | * `(new Helper($conn))->callKeywords($text, $index, $hits = null)` 436 | * `(new Helper($conn))->describe($index)` 437 | * `(new Helper($conn))->createFunction($udf_name, $returns, $soname)` 438 | * `(new Helper($conn))->dropFunction($udf_name)` 439 | * `(new Helper($conn))->attachIndex($disk_index, $rt_index)` 440 | * `(new Helper($conn))->flushRtIndex($index)` 441 | * `(new Helper($conn))->optimizeIndex($index)` 442 | * `(new Helper($conn))->showIndexStatus($index)` 443 | * `(new Helper($conn))->flushRamchunk($index)` 444 | 445 | ### Percolate 446 | The `Percolate` class provides methods for the "Percolate query" feature of Manticore Search. 447 | For more information about percolate queries refer the [Percolate Query](https://docs.manticoresearch.com/latest/html/searching/percolate_query.html) documentation. 448 | 449 | #### INSERT 450 | 451 | The Percolate class provide a dedicated helper for inserting queries in a `percolate` index. 452 | 453 | ```php 454 | insert('full text query terms',false) 459 | ->into('pq') 460 | ->tags(['tag1','tag2']) 461 | ->filter('price>3') 462 | ->execute(); 463 | ``` 464 | 465 | * __`$pq = (new Percolate($conn))->insert($query,$noEscape)`__ 466 | 467 | Begins an ``INSERT``. A single query is allowed to be added per insert. By default, the query string is escaped. Optional second parameter `$noEscape` can be set to `true` for not applying the escape. 468 | 469 | * __`$pq->into($index)`__ 470 | 471 | Set the percolate index for insert. 472 | 473 | * __`$pq->tags($tags)`__ 474 | 475 | Set a list of tags per query. Accepts array of strings or string delimited by comma 476 | 477 | * __`$pq->filter($filter)`__ 478 | Sets an attribute filtering string. The string must look the same as string of an WHERE attribute filters clause 479 | 480 | * __`$pq->execute()`__ 481 | 482 | Execute the `INSERT`. 483 | 484 | #### CALLPQ 485 | 486 | Searches for stored queries that provide matching for input documents. 487 | 488 | ```php 489 | callPQ() 493 | ->from('pq') 494 | ->documents(['multiple documents', 'go this way']) 495 | ->options([ 496 | Percolate::OPTION_VERBOSE => 1, 497 | Percolate::OPTION_DOCS_JSON => 1 498 | ]) 499 | ->execute(); 500 | ``` 501 | 502 | * __`$pq = (new Percolate($conn))->callPQ()`__ 503 | 504 | Begins a `CALL PQ` 505 | 506 | * __`$pq->from($index)`__ 507 | 508 | Set percolate index. 509 | 510 | * __`$pq->documents($docs)`__ 511 | 512 | Set the incoming documents. $docs can be: 513 | 514 | - a single plain string (requires `Percolate::OPTION_DOCS_JSON` set to 0) 515 | - array of plain strings (requires `Percolate::OPTION_DOCS_JSON` set to 0) 516 | - a single JSON document 517 | - an array of JSON documents 518 | - a JSON object containing an array of JSON objects 519 | 520 | 521 | * __`$pq->options($options)`__ 522 | 523 | Set options of `CALL PQ`. Refer the Manticore docs for more information about the `CALL PQ` parameters. 524 | 525 | - __Percolate::OPTION_DOCS_JSON__ (`as docs_json`) default to 1 (docs are json objects). Needs to be set to 0 for plain string documents. 526 | Documents added as associative arrays will be converted to JSON when sending the query to Manticore. 527 | - __Percolate::OPTION_VERBOSE__ (`as verbose`) more information is printed by following `SHOW META`, default is 0 528 | - __Percolate::OPTION_QUERY__ (`as query`) returns all stored queries fields , default is 0 529 | - __Percolate::OPTION_DOCS__ (`as docs`) provide result set as per document matched (instead of per query), default is 0 530 | 531 | * `$pq->execute()` 532 | 533 | Execute the `CALL PQ`. 534 | 535 | ## Laravel 536 | 537 | Laravel's dependency injection and realtime facades brings more convenience to SphinxQL Query Builder usage. 538 | 539 | ```php 540 | // Register connection: 541 | use Foolz\SphinxQL\Drivers\ConnectionInterface; 542 | use Foolz\SphinxQL\Drivers\Mysqli\Connection; 543 | use Illuminate\Support\ServiceProvider; 544 | 545 | class AppServiceProvider extends ServiceProvider 546 | { 547 | public function register() 548 | { 549 | $this->app->singleton(ConnectionInterface::class, function ($app) { 550 | $conn = new Connection(); 551 | $conn->setParams(['host' => 'domain.tld', 'port' => 9306]); 552 | return $conn; 553 | }); 554 | } 555 | } 556 | 557 | // In another file: 558 | use Facades\Foolz\SphinxQL\SphinxQL; 559 | 560 | $result = SphinxQL::select('column_one', 'colume_two') 561 | ->from('index_ancient', 'index_main', 'index_delta') 562 | ->match('comment', 'my opinion is superior to yours') 563 | ->where('banned', '=', 1) 564 | ->execute(); 565 | ``` 566 | 567 | Facade access also works with `Helper` and `Percolate`. 568 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foolz/sphinxql-query-builder", 3 | "replace": {"foolz/sphinxql": "self.version"}, 4 | "type": "library", 5 | "description": "A PHP query builder for SphinxQL. Uses MySQLi to connect to the Sphinx server.", 6 | "keywords": ["database", "sphinxql", "sphinx", "search", "SQL", "query builder"], 7 | "homepage": "http://www.foolz.us", 8 | "license": "Apache-2.0", 9 | "authors": [{"name": "foolz", "email": "support@foolz.us"}], 10 | "support": { 11 | "email": "support@foolz.us", 12 | "irc": "irc://irc.irchighway.net/fooldriver" 13 | }, 14 | "require": { 15 | "php": "^7.1 || ^8" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^7 || ^8 || ^9" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Foolz\\SphinxQL\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Foolz\\SphinxQL\\Tests\\": "tests/SphinxQL" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Drivers/ConnectionBase.php: -------------------------------------------------------------------------------- 1 | '127.0.0.1', 'port' => 9306, 'socket' => null); 18 | 19 | /** 20 | * Internal connection object. 21 | * @var mysqli|PDO 22 | */ 23 | protected $connection; 24 | 25 | /** 26 | * Sets one or more connection parameters. 27 | * 28 | * @param array $params Associative array of parameters and values. 29 | */ 30 | public function setParams(array $params) 31 | { 32 | foreach ($params as $param => $value) { 33 | $this->setParam($param, $value); 34 | } 35 | } 36 | 37 | /** 38 | * Set a single connection parameter. Valid parameters include: 39 | * 40 | * * string host - The hostname, IP address, or unix socket 41 | * * int port - The port to the host 42 | * * array options - MySQLi options/values, as an associative array. Example: array(MYSQLI_OPT_CONNECT_TIMEOUT => 2) 43 | * 44 | * @param string $param Name of the parameter to modify. 45 | * @param mixed $value Value to which the parameter will be set. 46 | */ 47 | public function setParam($param, $value) 48 | { 49 | if ($param === 'host') { 50 | if ($value === 'localhost') { 51 | $value = '127.0.0.1'; 52 | } elseif (stripos($value, 'unix:') === 0) { 53 | $param = 'socket'; 54 | } 55 | } 56 | if ($param === 'socket') { 57 | if (stripos($value, 'unix:') === 0) { 58 | $value = substr($value, 5); 59 | } 60 | $this->connection_params['host'] = null; 61 | } 62 | 63 | $this->connection_params[$param] = $value; 64 | } 65 | 66 | /** 67 | * Returns the connection parameters (host, port, connection timeout) for the current instance. 68 | * 69 | * @return array $params The current connection parameters 70 | */ 71 | public function getParams() 72 | { 73 | return $this->connection_params; 74 | } 75 | 76 | /** 77 | * Returns the current connection established. 78 | * 79 | * @return mysqli|PDO Internal connection object 80 | * @throws ConnectionException If no connection has been established or open 81 | */ 82 | public function getConnection() 83 | { 84 | if (!is_null($this->connection)) { 85 | return $this->connection; 86 | } 87 | 88 | throw new ConnectionException('The connection to the server has not been established yet.'); 89 | } 90 | 91 | /** 92 | * Adds quotes around values when necessary. 93 | * Based on FuelPHP's quoting function. 94 | * @inheritdoc 95 | */ 96 | public function quote($value) 97 | { 98 | if ($value === null) { 99 | return 'null'; 100 | } elseif ($value === true) { 101 | return 1; 102 | } elseif ($value === false) { 103 | return 0; 104 | } elseif ($value instanceof Expression) { 105 | // Use the raw expression 106 | return $value->value(); 107 | } elseif (is_int($value)) { 108 | return (int) $value; 109 | } elseif (is_float($value)) { 110 | // Convert to non-locale aware float to prevent possible commas 111 | return sprintf('%F', $value); 112 | } elseif (is_array($value)) { 113 | // Supports MVA attributes 114 | return '('.implode(',', $this->quoteArr($value)).')'; 115 | } 116 | 117 | return $this->escape($value); 118 | } 119 | 120 | /** 121 | * @inheritdoc 122 | */ 123 | public function quoteArr(array $array = array()) 124 | { 125 | $result = array(); 126 | 127 | foreach ($array as $key => $item) { 128 | $result[$key] = $this->quote($item); 129 | } 130 | 131 | return $result; 132 | } 133 | 134 | /** 135 | * Closes and unset the connection to the Sphinx server. 136 | * 137 | * @return $this 138 | * @throws ConnectionException 139 | */ 140 | public function close() 141 | { 142 | $this->connection = null; 143 | 144 | return $this; 145 | } 146 | 147 | /** 148 | * Establishes a connection if needed 149 | * @throws ConnectionException 150 | */ 151 | protected function ensureConnection() 152 | { 153 | try { 154 | $this->getConnection(); 155 | } catch (ConnectionException $e) { 156 | $this->connect(); 157 | } 158 | } 159 | 160 | /** 161 | * Establishes a connection to the Sphinx server. 162 | * 163 | * @return bool True if connected 164 | * @throws ConnectionException If a connection error was encountered 165 | */ 166 | abstract public function connect(); 167 | 168 | } 169 | -------------------------------------------------------------------------------- /src/Drivers/ConnectionInterface.php: -------------------------------------------------------------------------------- 1 | quote() on every element of the array passed. 60 | * 61 | * @param array $array The array of elements to quote 62 | * 63 | * @return array The array of quotes elements 64 | * @throws DatabaseException 65 | * @throws ConnectionException 66 | */ 67 | public function quoteArr(array $array = array()); 68 | } 69 | -------------------------------------------------------------------------------- /src/Drivers/MultiResultSet.php: -------------------------------------------------------------------------------- 1 | adapter = $adapter; 45 | } 46 | 47 | /** 48 | * @inheritdoc 49 | * @throws DatabaseException 50 | */ 51 | public function getStored() 52 | { 53 | $this->store(); 54 | 55 | return $this->stored; 56 | } 57 | 58 | /** 59 | * @inheritdoc 60 | * @throws DatabaseException 61 | */ 62 | #[\ReturnTypeWillChange] 63 | public function offsetExists($offset) 64 | { 65 | $this->store(); 66 | 67 | return $this->storedValid($offset); 68 | } 69 | 70 | /** 71 | * @inheritdoc 72 | * @throws DatabaseException 73 | */ 74 | #[\ReturnTypeWillChange] 75 | public function offsetGet($offset) 76 | { 77 | $this->store(); 78 | 79 | return $this->stored[$offset]; 80 | } 81 | 82 | /** 83 | * @inheritdoc 84 | * @codeCoverageIgnore 85 | */ 86 | #[\ReturnTypeWillChange] 87 | public function offsetSet($offset, $value) 88 | { 89 | throw new \BadMethodCallException('Not implemented'); 90 | } 91 | 92 | /** 93 | * @inheritdoc 94 | * @codeCoverageIgnore 95 | */ 96 | #[\ReturnTypeWillChange] 97 | public function offsetUnset($offset) 98 | { 99 | throw new \BadMethodCallException('Not implemented'); 100 | } 101 | 102 | /** 103 | * @inheritdoc 104 | */ 105 | #[\ReturnTypeWillChange] 106 | public function next() 107 | { 108 | $this->rowSet = $this->getNext(); 109 | } 110 | 111 | /** 112 | * @inheritdoc 113 | */ 114 | #[\ReturnTypeWillChange] 115 | public function key() 116 | { 117 | return (int)$this->cursor; 118 | } 119 | 120 | /** 121 | * @inheritdoc 122 | */ 123 | #[\ReturnTypeWillChange] 124 | public function rewind() 125 | { 126 | // we actually can't roll this back unless it was stored first 127 | $this->cursor = 0; 128 | $this->next_cursor = 0; 129 | $this->rowSet = $this->getNext(); 130 | } 131 | 132 | /** 133 | * @inheritdoc 134 | * @throws DatabaseException 135 | */ 136 | #[\ReturnTypeWillChange] 137 | public function count() 138 | { 139 | $this->store(); 140 | 141 | return count($this->stored); 142 | } 143 | 144 | /** 145 | * @inheritdoc 146 | */ 147 | #[\ReturnTypeWillChange] 148 | public function valid() 149 | { 150 | if ($this->stored !== null) { 151 | return $this->storedValid(); 152 | } 153 | 154 | return $this->adapter->valid(); 155 | } 156 | 157 | /** 158 | * @inheritdoc 159 | */ 160 | #[\ReturnTypeWillChange] 161 | public function current() 162 | { 163 | $rowSet = $this->rowSet; 164 | unset($this->rowSet); 165 | 166 | return $rowSet; 167 | } 168 | 169 | /** 170 | * @param null|int $cursor 171 | * 172 | * @return bool 173 | */ 174 | protected function storedValid($cursor = null) 175 | { 176 | $cursor = (!is_null($cursor) ? $cursor : $this->cursor); 177 | 178 | return $cursor >= 0 && $cursor < count($this->stored); 179 | } 180 | 181 | /** 182 | * @inheritdoc 183 | */ 184 | public function getNext() 185 | { 186 | $this->cursor = $this->next_cursor; 187 | 188 | if ($this->stored !== null) { 189 | $resultSet = !$this->storedValid() ? false : $this->stored[$this->cursor]; 190 | } else { 191 | if ($this->next_cursor > 0) { 192 | $this->adapter->getNext(); 193 | } 194 | 195 | $resultSet = !$this->adapter->valid() ? false : $this->adapter->current(); 196 | } 197 | 198 | $this->next_cursor++; 199 | 200 | return $resultSet; 201 | } 202 | 203 | /** 204 | * @inheritdoc 205 | */ 206 | public function store() 207 | { 208 | if ($this->stored !== null) { 209 | return $this; 210 | } 211 | 212 | // don't let users mix storage and driver cursors 213 | if ($this->next_cursor > 0) { 214 | throw new DatabaseException('The MultiResultSet is using the driver cursors, store() can\'t fetch all the data'); 215 | } 216 | 217 | $store = array(); 218 | while ($set = $this->getNext()) { 219 | // this relies on stored being null! 220 | $store[] = $set->store(); 221 | } 222 | 223 | $this->cursor = 0; 224 | $this->next_cursor = 0; 225 | 226 | // if we write the array straight to $this->stored it won't be null anymore and functions relying on null will break 227 | $this->stored = $store; 228 | 229 | return $this; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/Drivers/MultiResultSetAdapterInterface.php: -------------------------------------------------------------------------------- 1 | internal_encoding; 33 | } 34 | 35 | /** 36 | * @inheritdoc 37 | */ 38 | public function connect() 39 | { 40 | $data = $this->getParams(); 41 | $conn = mysqli_init(); 42 | 43 | if (!empty($data['options'])) { 44 | foreach ($data['options'] as $option => $value) { 45 | $conn->options($option, $value); 46 | } 47 | } 48 | 49 | set_error_handler(function () {}); 50 | try { 51 | if (!$conn->real_connect($data['host'], null, null, null, (int) $data['port'], $data['socket'])) { 52 | throw new ConnectionException('Connection Error: ['.$conn->connect_errno.']'.$conn->connect_error); 53 | } 54 | } finally { 55 | restore_error_handler(); 56 | } 57 | 58 | $conn->set_charset('utf8'); 59 | $this->connection = $conn; 60 | $this->mbPush(); 61 | 62 | return true; 63 | } 64 | 65 | /** 66 | * Pings the Sphinx server. 67 | * 68 | * @return bool True if connected, false otherwise 69 | * @throws ConnectionException 70 | */ 71 | public function ping() 72 | { 73 | $this->ensureConnection(); 74 | 75 | return $this->getConnection()->ping(); 76 | } 77 | 78 | /** 79 | * @inheritdoc 80 | */ 81 | public function close() 82 | { 83 | $this->mbPop(); 84 | $this->getConnection()->close(); 85 | 86 | return parent::close(); 87 | } 88 | 89 | /** 90 | * @inheritdoc 91 | */ 92 | public function query($query) 93 | { 94 | $this->ensureConnection(); 95 | 96 | set_error_handler(function () {}); 97 | try { 98 | /** 99 | * ManticoreSearch/Sphinx silence warnings thrown by php mysqli/mysqlnd 100 | * 101 | * unknown command (code=9) - status() command not implemented by Sphinx/ManticoreSearch 102 | * ERROR mysqli::prepare(): (08S01/1047): unknown command (code=22) - prepare() not implemented by Sphinx/Manticore 103 | */ 104 | $resource = @$this->getConnection()->query($query); 105 | } finally { 106 | restore_error_handler(); 107 | } 108 | 109 | if ($this->getConnection()->error) { 110 | throw new DatabaseException('['.$this->getConnection()->errno.'] '. 111 | $this->getConnection()->error.' [ '.$query.']'); 112 | } 113 | 114 | return new ResultSet(new ResultSetAdapter($this, $resource)); 115 | } 116 | 117 | /** 118 | * @inheritdoc 119 | */ 120 | public function multiQuery(array $queue) 121 | { 122 | $count = count($queue); 123 | 124 | if ($count === 0) { 125 | throw new SphinxQLException('The Queue is empty.'); 126 | } 127 | 128 | $this->ensureConnection(); 129 | 130 | $this->getConnection()->multi_query(implode(';', $queue)); 131 | 132 | if ($this->getConnection()->error) { 133 | throw new DatabaseException('['.$this->getConnection()->errno.'] '. 134 | $this->getConnection()->error.' [ '.implode(';', $queue).']'); 135 | } 136 | 137 | return new MultiResultSet(new MultiResultSetAdapter($this)); 138 | } 139 | 140 | /** 141 | * Escapes the input with \MySQLi::real_escape_string. 142 | * Based on FuelPHP's escaping function. 143 | * @inheritdoc 144 | */ 145 | public function escape($value) 146 | { 147 | $this->ensureConnection(); 148 | 149 | if (($value = $this->getConnection()->real_escape_string((string) $value)) === false) { 150 | // @codeCoverageIgnoreStart 151 | throw new DatabaseException($this->getConnection()->error, $this->getConnection()->errno); 152 | // @codeCoverageIgnoreEnd 153 | } 154 | 155 | return "'".$value."'"; 156 | } 157 | 158 | /** 159 | * Enter UTF-8 multi-byte workaround mode. 160 | */ 161 | public function mbPush() 162 | { 163 | $this->internal_encoding = mb_internal_encoding(); 164 | mb_internal_encoding('UTF-8'); 165 | 166 | return $this; 167 | } 168 | 169 | /** 170 | * Exit UTF-8 multi-byte workaround mode. 171 | */ 172 | public function mbPop() 173 | { 174 | // TODO: add test case for #155 175 | if ($this->getInternalEncoding()) { 176 | mb_internal_encoding($this->getInternalEncoding()); 177 | $this->internal_encoding = null; 178 | } 179 | 180 | return $this; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/Drivers/Mysqli/MultiResultSetAdapter.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 27 | } 28 | 29 | /** 30 | * @inheritdoc 31 | * @throws ConnectionException 32 | */ 33 | public function getNext() 34 | { 35 | if ( 36 | !$this->valid() || 37 | !$this->connection->getConnection()->more_results() 38 | ) { 39 | $this->valid = false; 40 | } else { 41 | $this->connection->getConnection()->next_result(); 42 | } 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | * @throws ConnectionException 48 | */ 49 | public function current() 50 | { 51 | $adapter = new ResultSetAdapter($this->connection, $this->connection->getConnection()->store_result()); 52 | return new ResultSet($adapter); 53 | } 54 | 55 | /** 56 | * @inheritdoc 57 | * @throws ConnectionException 58 | */ 59 | public function valid() 60 | { 61 | return $this->connection->getConnection()->errno == 0 && $this->valid; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Drivers/Mysqli/ResultSetAdapter.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 33 | $this->result = $result; 34 | } 35 | 36 | /** 37 | * @inheritdoc 38 | * @throws ConnectionException 39 | */ 40 | public function getAffectedRows() 41 | { 42 | return $this->connection->getConnection()->affected_rows; 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | */ 48 | public function getNumRows() 49 | { 50 | return $this->result->num_rows; 51 | } 52 | 53 | /** 54 | * @inheritdoc 55 | */ 56 | public function getFields() 57 | { 58 | return $this->result->fetch_fields(); 59 | } 60 | 61 | /** 62 | * @inheritdoc 63 | */ 64 | public function isDml() 65 | { 66 | return !($this->result instanceof mysqli_result); 67 | } 68 | 69 | /** 70 | * @inheritdoc 71 | */ 72 | public function store() 73 | { 74 | $this->result->data_seek(0); 75 | 76 | return $this->result->fetch_all(MYSQLI_NUM); 77 | } 78 | 79 | /** 80 | * @inheritdoc 81 | */ 82 | public function toRow($num) 83 | { 84 | $this->result->data_seek($num); 85 | } 86 | 87 | /** 88 | * @inheritdoc 89 | */ 90 | public function freeResult() 91 | { 92 | $this->result->free_result(); 93 | } 94 | 95 | /** 96 | * @inheritdoc 97 | */ 98 | public function rewind() 99 | { 100 | $this->valid = true; 101 | $this->result->data_seek(0); 102 | } 103 | 104 | /** 105 | * @inheritdoc 106 | */ 107 | public function valid() 108 | { 109 | return $this->valid; 110 | } 111 | 112 | /** 113 | * @inheritdoc 114 | */ 115 | public function fetch($assoc = true) 116 | { 117 | if ($assoc) { 118 | $row = $this->result->fetch_assoc(); 119 | } else { 120 | $row = $this->result->fetch_row(); 121 | } 122 | 123 | if (!$row) { 124 | $this->valid = false; 125 | } 126 | 127 | return $row; 128 | } 129 | 130 | /** 131 | * @inheritdoc 132 | */ 133 | public function fetchAll($assoc = true) 134 | { 135 | if ($assoc) { 136 | $row = $this->result->fetch_all(MYSQLI_ASSOC); 137 | } else { 138 | $row = $this->result->fetch_all(MYSQLI_NUM); 139 | } 140 | 141 | if (empty($row)) { 142 | $this->valid = false; 143 | } 144 | 145 | return $row; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Drivers/Pdo/Connection.php: -------------------------------------------------------------------------------- 1 | ensureConnection(); 22 | 23 | $statement = $this->connection->prepare($query); 24 | 25 | try { 26 | $statement->execute(); 27 | } catch (PDOException $exception) { 28 | throw new DatabaseException('[' . $exception->getCode() . '] ' . $exception->getMessage() . ' [' . $query . ']', 29 | (int)$exception->getCode(), $exception); 30 | } 31 | 32 | return new ResultSet(new ResultSetAdapter($statement)); 33 | } 34 | 35 | /** 36 | * @inheritdoc 37 | */ 38 | public function connect() 39 | { 40 | $params = $this->getParams(); 41 | 42 | $dsn = 'mysql:'; 43 | if (isset($params['host']) && $params['host'] != '') { 44 | $dsn .= 'host=' . $params['host'] . ';'; 45 | } 46 | if (isset($params['port'])) { 47 | $dsn .= 'port=' . $params['port'] . ';'; 48 | } 49 | if (isset($params['charset'])) { 50 | $dsn .= 'charset=' . $params['charset'] . ';'; 51 | } 52 | 53 | if (isset($params['socket']) && $params['socket'] != '') { 54 | $dsn .= 'unix_socket=' . $params['socket'] . ';'; 55 | } 56 | 57 | try { 58 | $con = new PDO($dsn); 59 | } catch (PDOException $exception) { 60 | throw new ConnectionException($exception->getMessage(), $exception->getCode(), $exception); 61 | } 62 | 63 | $this->connection = $con; 64 | $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 65 | 66 | return true; 67 | } 68 | 69 | /** 70 | * @return bool 71 | * @throws ConnectionException 72 | */ 73 | public function ping() 74 | { 75 | $this->ensureConnection(); 76 | 77 | return $this->connection !== null; 78 | } 79 | 80 | /** 81 | * @inheritdoc 82 | */ 83 | public function multiQuery(array $queue) 84 | { 85 | $this->ensureConnection(); 86 | 87 | if (count($queue) === 0) { 88 | throw new SphinxQLException('The Queue is empty.'); 89 | } 90 | 91 | try { 92 | $statement = $this->connection->query(implode(';', $queue)); 93 | } catch (PDOException $exception) { 94 | throw new DatabaseException($exception->getMessage() .' [ '.implode(';', $queue).']', $exception->getCode(), $exception); 95 | } 96 | 97 | return new MultiResultSet(new MultiResultSetAdapter($statement)); 98 | } 99 | 100 | /** 101 | * @inheritdoc 102 | */ 103 | public function escape($value) 104 | { 105 | $this->ensureConnection(); 106 | 107 | return $this->connection->quote($value); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Drivers/Pdo/MultiResultSetAdapter.php: -------------------------------------------------------------------------------- 1 | statement = $statement; 27 | } 28 | 29 | /** 30 | * @inheritdoc 31 | */ 32 | public function getNext() 33 | { 34 | if ( 35 | !$this->valid() || 36 | !$this->statement->nextRowset() 37 | ) { 38 | $this->valid = false; 39 | } 40 | } 41 | 42 | /** 43 | * @inheritdoc 44 | */ 45 | public function current() 46 | { 47 | return new ResultSet(new ResultSetAdapter($this->statement)); 48 | } 49 | 50 | /** 51 | * @inheritdoc 52 | */ 53 | public function valid() 54 | { 55 | return $this->statement && $this->valid; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Drivers/Pdo/ResultSetAdapter.php: -------------------------------------------------------------------------------- 1 | statement = $statement; 27 | } 28 | 29 | /** 30 | * @inheritdoc 31 | */ 32 | public function getAffectedRows() 33 | { 34 | return $this->statement->rowCount(); 35 | } 36 | 37 | /** 38 | * @inheritdoc 39 | */ 40 | public function getNumRows() 41 | { 42 | return $this->statement->rowCount(); 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | */ 48 | public function getFields() 49 | { 50 | $fields = array(); 51 | 52 | for ($i = 0; $i < $this->statement->columnCount(); $i++) { 53 | $fields[] = (object)$this->statement->getColumnMeta($i); 54 | } 55 | 56 | return $fields; 57 | } 58 | 59 | /** 60 | * @inheritdoc 61 | */ 62 | public function isDml() 63 | { 64 | return $this->statement->columnCount() == 0; 65 | } 66 | 67 | /** 68 | * @inheritdoc 69 | */ 70 | public function store() 71 | { 72 | return $this->statement->fetchAll(PDO::FETCH_NUM); 73 | } 74 | 75 | /** 76 | * @inheritdoc 77 | */ 78 | public function toRow($num) 79 | { 80 | throw new \BadMethodCallException('Not implemented'); 81 | } 82 | 83 | /** 84 | * @inheritdoc 85 | */ 86 | public function freeResult() 87 | { 88 | $this->statement->closeCursor(); 89 | } 90 | 91 | /** 92 | * @inheritdoc 93 | */ 94 | public function rewind() 95 | { 96 | 97 | } 98 | 99 | /** 100 | * @inheritdoc 101 | */ 102 | public function valid() 103 | { 104 | return $this->valid; 105 | } 106 | 107 | /** 108 | * @inheritdoc 109 | */ 110 | public function fetch($assoc = true) 111 | { 112 | if ($assoc) { 113 | $row = $this->statement->fetch(PDO::FETCH_ASSOC); 114 | } else { 115 | $row = $this->statement->fetch(PDO::FETCH_NUM); 116 | } 117 | 118 | if (!$row) { 119 | $this->valid = false; 120 | $row = null; 121 | } 122 | 123 | return $row; 124 | } 125 | 126 | /** 127 | * @inheritdoc 128 | */ 129 | public function fetchAll($assoc = true) 130 | { 131 | if ($assoc) { 132 | $row = $this->statement->fetchAll(PDO::FETCH_ASSOC); 133 | } else { 134 | $row = $this->statement->fetchAll(PDO::FETCH_NUM); 135 | } 136 | 137 | if (empty($row)) { 138 | $this->valid = false; 139 | } 140 | 141 | return $row; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Drivers/ResultSet.php: -------------------------------------------------------------------------------- 1 | adapter = $adapter; 56 | $this->init(); 57 | 58 | if ($adapter instanceof PdoResultSetAdapter) { //only for pdo for some reason 59 | $this->store(); 60 | } 61 | } 62 | 63 | /** 64 | * @inheritdoc 65 | */ 66 | public function hasRow($num) 67 | { 68 | return $num >= 0 && $num < $this->num_rows; 69 | } 70 | 71 | /** 72 | * @inheritdoc 73 | */ 74 | public function hasNextRow() 75 | { 76 | return $this->cursor + 1 < $this->num_rows; 77 | } 78 | 79 | /** 80 | * @inheritdoc 81 | */ 82 | public function getAffectedRows() 83 | { 84 | return $this->affected_rows; 85 | } 86 | 87 | /** 88 | * @inheritdoc 89 | */ 90 | #[\ReturnTypeWillChange] 91 | public function offsetExists($offset) 92 | { 93 | return $this->hasRow($offset); 94 | } 95 | 96 | /** 97 | * @inheritdoc 98 | * @throws ResultSetException 99 | */ 100 | #[\ReturnTypeWillChange] 101 | public function offsetGet($offset) 102 | { 103 | return $this->toRow($offset)->fetchAssoc(); 104 | } 105 | 106 | /** 107 | * @inheritdoc 108 | * @codeCoverageIgnore 109 | */ 110 | #[\ReturnTypeWillChange] 111 | public function offsetSet($offset, $value) 112 | { 113 | throw new \BadMethodCallException('Not implemented'); 114 | } 115 | 116 | /** 117 | * @inheritdoc 118 | * @codeCoverageIgnore 119 | */ 120 | #[\ReturnTypeWillChange] 121 | public function offsetUnset($offset) 122 | { 123 | throw new \BadMethodCallException('Not implemented'); 124 | } 125 | 126 | /** 127 | * @inheritdoc 128 | */ 129 | #[\ReturnTypeWillChange] 130 | public function current() 131 | { 132 | $row = $this->fetched; 133 | unset($this->fetched); 134 | 135 | return $row; 136 | } 137 | 138 | /** 139 | * @inheritdoc 140 | */ 141 | #[\ReturnTypeWillChange] 142 | public function next() 143 | { 144 | $this->fetched = $this->fetch(true); 145 | } 146 | 147 | /** 148 | * @inheritdoc 149 | */ 150 | #[\ReturnTypeWillChange] 151 | public function key() 152 | { 153 | return (int)$this->cursor; 154 | } 155 | 156 | /** 157 | * @inheritdoc 158 | */ 159 | #[\ReturnTypeWillChange] 160 | public function valid() 161 | { 162 | if ($this->stored !== null) { 163 | return $this->hasRow($this->cursor); 164 | } 165 | 166 | return $this->adapter->valid(); 167 | } 168 | 169 | /** 170 | * @inheritdoc 171 | */ 172 | #[\ReturnTypeWillChange] 173 | public function rewind() 174 | { 175 | if ($this->stored === null) { 176 | $this->adapter->rewind(); 177 | } 178 | 179 | $this->next_cursor = 0; 180 | 181 | $this->fetched = $this->fetch(true); 182 | } 183 | 184 | /** 185 | * Returns the number of rows in the result set 186 | * @inheritdoc 187 | */ 188 | #[\ReturnTypeWillChange] 189 | public function count() 190 | { 191 | return $this->num_rows; 192 | } 193 | 194 | protected function init() 195 | { 196 | if ($this->adapter->isDml()) { 197 | $this->affected_rows = $this->adapter->getAffectedRows(); 198 | } else { 199 | $this->num_rows = $this->adapter->getNumRows(); 200 | $this->fields = $this->adapter->getFields(); 201 | } 202 | } 203 | 204 | /** 205 | * @param array $numeric_array 206 | * 207 | * @return array 208 | */ 209 | protected function makeAssoc($numeric_array) 210 | { 211 | $assoc_array = array(); 212 | foreach ($numeric_array as $col_key => $col_value) { 213 | $assoc_array[$this->fields[$col_key]->name] = $col_value; 214 | } 215 | 216 | return $assoc_array; 217 | } 218 | 219 | /** 220 | * @param bool $assoc 221 | * 222 | * @return array|bool|null 223 | */ 224 | protected function fetchFromStore($assoc = true) 225 | { 226 | if ($this->stored === null) { 227 | return false; 228 | } 229 | 230 | $row = isset($this->stored[$this->cursor]) ? $this->stored[$this->cursor] : null; 231 | 232 | if ($row !== null) { 233 | $row = $assoc ? $this->makeAssoc($row) : $row; 234 | } 235 | 236 | return $row; 237 | } 238 | 239 | /** 240 | * @param bool $assoc 241 | * 242 | * @return array|bool 243 | */ 244 | protected function fetchAllFromStore($assoc) 245 | { 246 | if ($this->stored === null) { 247 | return false; 248 | } 249 | 250 | $result_from_store = array(); 251 | 252 | $this->cursor = $this->next_cursor; 253 | while ($row = $this->fetchFromStore($assoc)) { 254 | $result_from_store[] = $row; 255 | $this->cursor = ++$this->next_cursor; 256 | } 257 | 258 | return $result_from_store; 259 | } 260 | 261 | /** 262 | * @param bool $assoc 263 | * 264 | * @return array 265 | */ 266 | protected function fetchAll($assoc = true) 267 | { 268 | $fetch_all_result = $this->fetchAllFromStore($assoc); 269 | 270 | if ($fetch_all_result === false) { 271 | $fetch_all_result = $this->adapter->fetchAll($assoc); 272 | } 273 | 274 | $this->cursor = $this->num_rows; 275 | $this->next_cursor = $this->cursor + 1; 276 | 277 | return $fetch_all_result; 278 | } 279 | 280 | /** 281 | * @inheritdoc 282 | */ 283 | public function store() 284 | { 285 | if ($this->stored !== null) { 286 | return $this; 287 | } 288 | 289 | if ($this->adapter->isDml()) { 290 | $this->stored = $this->affected_rows; 291 | } else { 292 | $this->stored = $this->adapter->store(); 293 | } 294 | 295 | return $this; 296 | } 297 | 298 | /** 299 | * @inheritdoc 300 | */ 301 | public function getStored() 302 | { 303 | $this->store(); 304 | if ($this->adapter->isDml()) { 305 | return $this->getAffectedRows(); 306 | } 307 | 308 | return $this->fetchAllAssoc(); 309 | } 310 | 311 | /** 312 | * @inheritdoc 313 | */ 314 | public function toRow($num) 315 | { 316 | if (!$this->hasRow($num)) { 317 | throw new ResultSetException('The row does not exist.'); 318 | } 319 | 320 | $this->cursor = $num; 321 | $this->next_cursor = $num; 322 | 323 | if ($this->stored === null) { 324 | $this->adapter->toRow($this->cursor); 325 | } 326 | 327 | return $this; 328 | } 329 | 330 | /** 331 | * @inheritdoc 332 | */ 333 | public function toNextRow() 334 | { 335 | $this->toRow(++$this->cursor); 336 | 337 | return $this; 338 | } 339 | 340 | /** 341 | * @inheritdoc 342 | */ 343 | public function fetchAllAssoc() 344 | { 345 | return $this->fetchAll(true); 346 | } 347 | 348 | /** 349 | * @inheritdoc 350 | */ 351 | public function fetchAllNum() 352 | { 353 | return $this->fetchAll(false); 354 | } 355 | 356 | /** 357 | * @inheritdoc 358 | */ 359 | public function fetchAssoc() 360 | { 361 | return $this->fetch(true); 362 | } 363 | 364 | /** 365 | * @inheritdoc 366 | */ 367 | public function fetchNum() 368 | { 369 | return $this->fetch(false); 370 | } 371 | 372 | /** 373 | * @param bool $assoc 374 | * 375 | * @return array|null 376 | */ 377 | protected function fetch($assoc = true) 378 | { 379 | $this->cursor = $this->next_cursor; 380 | 381 | $row = $this->fetchFromStore($assoc); 382 | 383 | if ($row === false) { 384 | $row = $this->adapter->fetch($assoc); 385 | } 386 | 387 | $this->next_cursor++; 388 | 389 | return $row; 390 | } 391 | 392 | /** 393 | * @inheritdoc 394 | */ 395 | public function freeResult() 396 | { 397 | $this->adapter->freeResult(); 398 | 399 | return $this; 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /src/Drivers/ResultSetAdapterInterface.php: -------------------------------------------------------------------------------- 1 | string = $string; 26 | } 27 | 28 | /** 29 | * Return the unmodified expression 30 | * 31 | * @return string The unaltered content of the expression 32 | */ 33 | public function value() 34 | { 35 | return (string) $this->string; 36 | } 37 | 38 | /** 39 | * Returns the unmodified expression 40 | * 41 | * @return string The unaltered content of the expression 42 | */ 43 | public function __toString() 44 | { 45 | return $this->value(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Facet.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 69 | } 70 | 71 | /** 72 | * Returns the currently attached connection 73 | * 74 | * @returns ConnectionInterface|null 75 | */ 76 | public function getConnection() 77 | { 78 | return $this->connection; 79 | } 80 | 81 | /** 82 | * Sets the connection to be used 83 | * 84 | * @param ConnectionInterface $connection 85 | * 86 | * @return Facet 87 | */ 88 | public function setConnection(ConnectionInterface $connection = null) 89 | { 90 | $this->connection = $connection; 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * Facet the columns 97 | * 98 | * Gets the arguments passed as $facet->facet('one', 'two') 99 | * Using it with array maps values as column names 100 | * 101 | * Examples: 102 | * $query->facet('idCategory'); 103 | * // FACET idCategory 104 | * 105 | * $query->facet('idCategory', 'year'); 106 | * // FACET idCategory, year 107 | * 108 | * $query->facet(array('categories' => 'idCategory', 'year', 'type' => 'idType')); 109 | * // FACET idCategory AS categories, year, idType AS type 110 | * 111 | * @param array|string $columns Array or multiple string arguments containing column names 112 | * 113 | * @return Facet 114 | */ 115 | public function facet($columns = null) 116 | { 117 | if (!is_array($columns)) { 118 | $columns = \func_get_args(); 119 | } 120 | 121 | foreach ($columns as $key => $column) { 122 | if (is_int($key)) { 123 | if (is_array($column)) { 124 | $this->facet($column); 125 | } else { 126 | $this->facet[] = array($column, null); 127 | } 128 | } else { 129 | $this->facet[] = array($column, $key); 130 | } 131 | } 132 | 133 | return $this; 134 | } 135 | 136 | /** 137 | * Facet a function 138 | * 139 | * Gets the function passed as $facet->facetFunction('FUNCTION', array('param1', 'param2', ...)) 140 | * 141 | * Examples: 142 | * $query->facetFunction('category'); 143 | * 144 | * @param string $function Function name 145 | * @param array|string $params Array or multiple string arguments containing column names 146 | * 147 | * @return Facet 148 | */ 149 | public function facetFunction($function, $params = null) 150 | { 151 | if (is_array($params)) { 152 | $params = implode(',', $params); 153 | } 154 | 155 | $this->facet[] = new Expression($function.'('.$params.')'); 156 | 157 | return $this; 158 | } 159 | 160 | /** 161 | * GROUP BY clause 162 | * Adds to the previously added columns 163 | * 164 | * @param string $column A column to group by 165 | * 166 | * @return Facet 167 | */ 168 | public function by($column) 169 | { 170 | $this->by = $column; 171 | 172 | return $this; 173 | } 174 | 175 | /** 176 | * ORDER BY clause 177 | * Adds to the previously added columns 178 | * 179 | * @param string $column The column to order on 180 | * @param string $direction The ordering direction (asc/desc) 181 | * 182 | * @return Facet 183 | */ 184 | public function orderBy($column, $direction = null) 185 | { 186 | $this->order_by[] = array('column' => $column, 'direction' => $direction); 187 | 188 | return $this; 189 | } 190 | 191 | /** 192 | * Facet a function 193 | * 194 | * Gets the function passed as $facet->facetFunction('FUNCTION', array('param1', 'param2', ...)) 195 | * 196 | * Examples: 197 | * $query->facetFunction('category'); 198 | * 199 | * @param string $function Function name 200 | * @param array $params Array string arguments containing column names 201 | * @param string $direction The ordering direction (asc/desc) 202 | * 203 | * @return Facet 204 | */ 205 | public function orderByFunction($function, $params = null, $direction = null) 206 | { 207 | if (is_array($params)) { 208 | $params = implode(',', $params); 209 | } 210 | 211 | $this->order_by[] = array('column' => new Expression($function.'('.$params.')'), 'direction' => $direction); 212 | 213 | return $this; 214 | } 215 | 216 | /** 217 | * LIMIT clause 218 | * Supports also LIMIT offset, limit 219 | * 220 | * @param int $offset Offset if $limit is specified, else limit 221 | * @param null|int $limit The limit to set, null for no limit 222 | * 223 | * @return Facet 224 | */ 225 | public function limit($offset, $limit = null) 226 | { 227 | if ($limit === null) { 228 | $this->limit = (int) $offset; 229 | 230 | return $this; 231 | } 232 | 233 | $this->offset($offset); 234 | $this->limit = (int) $limit; 235 | 236 | return $this; 237 | } 238 | 239 | /** 240 | * OFFSET clause 241 | * 242 | * @param int $offset The offset 243 | * 244 | * @return Facet 245 | */ 246 | public function offset($offset) 247 | { 248 | $this->offset = (int) $offset; 249 | 250 | return $this; 251 | } 252 | 253 | /** 254 | * Compiles the statements for FACET 255 | * 256 | * @return Facet 257 | * @throws SphinxQLException In case no column in facet 258 | */ 259 | public function compileFacet() 260 | { 261 | $query = 'FACET '; 262 | 263 | if (!empty($this->facet)) { 264 | $facets = array(); 265 | foreach ($this->facet as $array) { 266 | if ($array instanceof Expression) { 267 | $facets[] = $array; 268 | } elseif ($array[1] === null) { 269 | $facets[] = $array[0]; 270 | } else { 271 | $facets[] = $array[0].' AS '.$array[1]; 272 | } 273 | } 274 | $query .= implode(', ', $facets).' '; 275 | } else { 276 | throw new SphinxQLException('There is no column in facet.'); 277 | } 278 | 279 | if (!empty($this->by)) { 280 | $query .= 'BY '.$this->by.' '; 281 | } 282 | 283 | if (!empty($this->order_by)) { 284 | $query .= 'ORDER BY '; 285 | 286 | $order_arr = array(); 287 | 288 | foreach ($this->order_by as $order) { 289 | $order_sub = $order['column'].' '; 290 | $order_sub .= ((strtolower($order['direction']) === 'desc') ? 'DESC' : 'ASC'); 291 | 292 | $order_arr[] = $order_sub; 293 | } 294 | 295 | $query .= implode(', ', $order_arr).' '; 296 | } 297 | 298 | if ($this->limit !== null || $this->offset !== null) { 299 | if ($this->offset === null) { 300 | $this->offset = 0; 301 | } 302 | 303 | if ($this->limit === null) { 304 | $this->limit = 9999999999999; 305 | } 306 | 307 | $query .= 'LIMIT '.((int) $this->offset).', '.((int) $this->limit).' '; 308 | } 309 | 310 | $this->query = trim($query); 311 | 312 | return $this; 313 | } 314 | 315 | /** 316 | * Get String with SQL facet 317 | * 318 | * @return string 319 | * @throws SphinxQLException 320 | */ 321 | public function getFacet() 322 | { 323 | return $this->compileFacet()->query; 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/Helper.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 24 | } 25 | 26 | /** 27 | * Returns a new SphinxQL instance 28 | * 29 | * @return SphinxQL 30 | */ 31 | protected function getSphinxQL() 32 | { 33 | return new SphinxQL($this->connection); 34 | } 35 | 36 | /** 37 | * Prepares a query in SphinxQL (not executed) 38 | * 39 | * @param $sql 40 | * 41 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 42 | */ 43 | protected function query($sql) 44 | { 45 | return $this->getSphinxQL()->query($sql); 46 | } 47 | 48 | /** 49 | * Converts the columns from queries like SHOW VARIABLES to simpler key-value 50 | * 51 | * @param array $result The result of an executed query 52 | * 53 | * @return array Associative array with Variable_name as key and Value as value 54 | * @todo make non static 55 | */ 56 | public static function pairsToAssoc($result) 57 | { 58 | $ordered = array(); 59 | 60 | foreach ($result as $item) { 61 | $ordered[$item['Variable_name']] = $item['Value']; 62 | } 63 | 64 | return $ordered; 65 | } 66 | 67 | /** 68 | * Runs query: SHOW META 69 | * 70 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 71 | */ 72 | public function showMeta() 73 | { 74 | return $this->query('SHOW META'); 75 | } 76 | 77 | /** 78 | * Runs query: SHOW WARNINGS 79 | * 80 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 81 | */ 82 | public function showWarnings() 83 | { 84 | return $this->query('SHOW WARNINGS'); 85 | } 86 | 87 | /** 88 | * Runs query: SHOW STATUS 89 | * 90 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 91 | */ 92 | public function showStatus() 93 | { 94 | return $this->query('SHOW STATUS'); 95 | } 96 | 97 | /** 98 | * Runs query: SHOW TABLES 99 | * 100 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 101 | * @throws Exception\ConnectionException 102 | * @throws Exception\DatabaseException 103 | */ 104 | public function showTables( $index ) 105 | { 106 | $queryAppend = ''; 107 | if ( ! empty( $index ) ) { 108 | $queryAppend = ' LIKE ' . $this->connection->quote($index); 109 | } 110 | return $this->query( 'SHOW TABLES' . $queryAppend ); 111 | } 112 | 113 | /** 114 | * Runs query: SHOW VARIABLES 115 | * 116 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 117 | */ 118 | public function showVariables() 119 | { 120 | return $this->query('SHOW VARIABLES'); 121 | } 122 | 123 | /** 124 | * SET syntax 125 | * 126 | * @param string $name The name of the variable 127 | * @param mixed $value The value of the variable 128 | * @param bool $global True if the variable should be global, false otherwise 129 | * 130 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 131 | * @throws Exception\ConnectionException 132 | * @throws Exception\DatabaseException 133 | */ 134 | public function setVariable($name, $value, $global = false) 135 | { 136 | $query = 'SET '; 137 | 138 | if ($global) { 139 | $query .= 'GLOBAL '; 140 | } 141 | 142 | $user_var = strpos($name, '@') === 0; 143 | 144 | $query .= $name.' '; 145 | 146 | // user variables must always be processed as arrays 147 | if ($user_var && !is_array($value)) { 148 | $query .= '= ('.$this->connection->quote($value).')'; 149 | } elseif (is_array($value)) { 150 | $query .= '= ('.implode(', ', $this->connection->quoteArr($value)).')'; 151 | } else { 152 | $query .= '= '.$this->connection->quote($value); 153 | } 154 | 155 | return $this->query($query); 156 | } 157 | 158 | /** 159 | * CALL SNIPPETS syntax 160 | * 161 | * @param string|array $data The document text (or documents) to search 162 | * @param string $index 163 | * @param string $query Search query used for highlighting 164 | * @param array $options Associative array of additional options 165 | * 166 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 167 | * @throws Exception\ConnectionException 168 | * @throws Exception\DatabaseException 169 | */ 170 | public function callSnippets($data, $index, $query, $options = array()) 171 | { 172 | $documents = array(); 173 | if (is_array($data)) { 174 | $documents[] = '('.implode(', ', $this->connection->quoteArr($data)).')'; 175 | } else { 176 | $documents[] = $this->connection->quote($data); 177 | } 178 | 179 | array_unshift($options, $index, $query); 180 | 181 | $arr = $this->connection->quoteArr($options); 182 | foreach ($arr as $key => &$val) { 183 | if (is_string($key)) { 184 | $val .= ' AS '.$key; 185 | } 186 | } 187 | 188 | return $this->query('CALL SNIPPETS('.implode(', ', array_merge($documents, $arr)).')'); 189 | } 190 | 191 | /** 192 | * CALL KEYWORDS syntax 193 | * 194 | * @param string $text 195 | * @param string $index 196 | * @param null|string $hits 197 | * 198 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 199 | * @throws Exception\ConnectionException 200 | * @throws Exception\DatabaseException 201 | */ 202 | public function callKeywords($text, $index, $hits = null) 203 | { 204 | $arr = array($text, $index); 205 | if ($hits !== null) { 206 | $arr[] = $hits; 207 | } 208 | 209 | return $this->query('CALL KEYWORDS('.implode(', ', $this->connection->quoteArr($arr)).')'); 210 | } 211 | 212 | /** 213 | * DESCRIBE syntax 214 | * 215 | * @param string $index The name of the index 216 | * 217 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 218 | */ 219 | public function describe($index) 220 | { 221 | return $this->query('DESCRIBE '.$index); 222 | } 223 | 224 | /** 225 | * CREATE FUNCTION syntax 226 | * 227 | * @param string $udf_name 228 | * @param string $returns Whether INT|BIGINT|FLOAT|STRING 229 | * @param string $so_name 230 | * 231 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 232 | * @throws Exception\ConnectionException 233 | * @throws Exception\DatabaseException 234 | */ 235 | public function createFunction($udf_name, $returns, $so_name) 236 | { 237 | return $this->query('CREATE FUNCTION '.$udf_name. 238 | ' RETURNS '.$returns.' SONAME '.$this->connection->quote($so_name)); 239 | } 240 | 241 | /** 242 | * DROP FUNCTION syntax 243 | * 244 | * @param string $udf_name 245 | * 246 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 247 | */ 248 | public function dropFunction($udf_name) 249 | { 250 | return $this->query('DROP FUNCTION '.$udf_name); 251 | } 252 | 253 | /** 254 | * ATTACH INDEX * TO RTINDEX * syntax 255 | * 256 | * @param string $disk_index 257 | * @param string $rt_index 258 | * 259 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 260 | */ 261 | public function attachIndex($disk_index, $rt_index) 262 | { 263 | return $this->query('ATTACH INDEX '.$disk_index.' TO RTINDEX '.$rt_index); 264 | } 265 | 266 | /** 267 | * FLUSH RTINDEX syntax 268 | * 269 | * @param string $index 270 | * 271 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 272 | */ 273 | public function flushRtIndex($index) 274 | { 275 | return $this->query('FLUSH RTINDEX '.$index); 276 | } 277 | 278 | /** 279 | * TRUNCATE RTINDEX syntax 280 | * 281 | * @param string $index 282 | * 283 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 284 | */ 285 | public function truncateRtIndex($index) 286 | { 287 | return $this->query('TRUNCATE RTINDEX '.$index); 288 | } 289 | 290 | /** 291 | * OPTIMIZE INDEX syntax 292 | * 293 | * @param string $index 294 | * 295 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 296 | */ 297 | public function optimizeIndex($index) 298 | { 299 | return $this->query('OPTIMIZE INDEX '.$index); 300 | } 301 | 302 | /** 303 | * SHOW INDEX STATUS syntax 304 | * 305 | * @param $index 306 | * 307 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 308 | */ 309 | public function showIndexStatus($index) 310 | { 311 | return $this->query('SHOW INDEX '.$index.' STATUS'); 312 | } 313 | 314 | /** 315 | * FLUSH RAMCHUNK syntax 316 | * 317 | * @param $index 318 | * 319 | * @return SphinxQL A SphinxQL object ready to be ->execute(); 320 | */ 321 | public function flushRamchunk($index) 322 | { 323 | return $this->query('FLUSH RAMCHUNK '.$index); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/MatchBuilder.php: -------------------------------------------------------------------------------- 1 | sphinxql = $sphinxql; 37 | } 38 | 39 | /** 40 | * Match text or sub expression. 41 | * 42 | * Examples: 43 | * $match->match('test'); 44 | * // test 45 | * 46 | * $match->match('test case'); 47 | * // (test case) 48 | * 49 | * $match->match(function ($m) { 50 | * $m->match('a')->orMatch('b'); 51 | * }); 52 | * // (a | b) 53 | * 54 | * $sub = new MatchBuilder($sphinxql); 55 | * $sub->match('a')->orMatch('b'); 56 | * $match->match($sub); 57 | * // (a | b) 58 | * 59 | * @param string|MatchBuilder|\Closure $keywords The text or expression to match. 60 | * 61 | * @return $this 62 | */ 63 | public function match($keywords = null) 64 | { 65 | if ($keywords !== null) { 66 | $this->tokens[] = array('MATCH' => $keywords); 67 | } 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Provide an alternation match. 74 | * 75 | * Examples: 76 | * $match->match('test')->orMatch(); 77 | * // test | 78 | * 79 | * $match->match('test')->orMatch('case'); 80 | * // test | case 81 | * 82 | * @param string|MatchBuilder|\Closure $keywords The text or expression to alternatively match. 83 | * 84 | * @return $this 85 | */ 86 | public function orMatch($keywords = null) 87 | { 88 | $this->tokens[] = array('OPERATOR' => '| '); 89 | $this->match($keywords); 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Provide an optional match. 96 | * 97 | * Examples: 98 | * $match->match('test')->maybe(); 99 | * // test MAYBE 100 | * 101 | * $match->match('test')->maybe('case'); 102 | * // test MAYBE case 103 | * 104 | * @param string|MatchBuilder|\Closure $keywords The text or expression to optionally match. 105 | * 106 | * @return $this 107 | */ 108 | public function maybe($keywords = null) 109 | { 110 | $this->tokens[] = array('OPERATOR' => 'MAYBE '); 111 | $this->match($keywords); 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * Do not match a keyword. 118 | * 119 | * Examples: 120 | * $match->not()->match('test'); 121 | * // -test 122 | * 123 | * $match->not('test'); 124 | * // -test 125 | * 126 | * @param string $keyword The word not to match. 127 | * 128 | * @return $this 129 | */ 130 | public function not($keyword = null) 131 | { 132 | $this->tokens[] = array('OPERATOR' => '-'); 133 | $this->match($keyword); 134 | 135 | return $this; 136 | } 137 | 138 | /** 139 | * Specify which field(s) to search. 140 | * 141 | * Examples: 142 | * $match->field('*')->match('test'); 143 | * // @* test 144 | * 145 | * $match->field('title')->match('test'); 146 | * // @title test 147 | * 148 | * $match->field('body', 50)->match('test'); 149 | * // @body[50] test 150 | * 151 | * $match->field('title', 'body')->match('test'); 152 | * // @(title,body) test 153 | * 154 | * $match->field(['title', 'body'])->match('test'); 155 | * // @(title,body) test 156 | * 157 | * $match->field('@relaxed')->field('nosuchfield')->match('test'); 158 | * // @@relaxed @nosuchfield test 159 | * 160 | * @param string|array $fields Field or fields to search. 161 | * @param int $limit Maximum position limit in field a match is allowed at. 162 | * 163 | * @return $this 164 | */ 165 | public function field($fields, $limit = null) 166 | { 167 | if (is_string($fields)) { 168 | $fields = func_get_args(); 169 | $limit = null; 170 | } 171 | if (!is_string(end($fields))) { 172 | $limit = array_pop($fields); 173 | } 174 | $this->tokens[] = array( 175 | 'FIELD' => '@', 176 | 'fields' => $fields, 177 | 'limit' => $limit, 178 | ); 179 | 180 | return $this; 181 | } 182 | 183 | /** 184 | * Specify which field(s) not to search. 185 | * 186 | * Examples: 187 | * $match->ignoreField('title')->match('test'); 188 | * // @!title test 189 | * 190 | * $match->ignoreField('title', 'body')->match('test'); 191 | * // @!(title,body) test 192 | * 193 | * $match->ignoreField(['title', 'body'])->match('test'); 194 | * // @!(title,body) test 195 | * 196 | * @param string|array $fields Field or fields to ignore during search. 197 | * 198 | * @return $this 199 | */ 200 | public function ignoreField($fields) 201 | { 202 | if (is_string($fields)) { 203 | $fields = func_get_args(); 204 | } 205 | $this->tokens[] = array( 206 | 'FIELD' => '@!', 207 | 'fields' => $fields, 208 | 'limit' => null, 209 | ); 210 | 211 | return $this; 212 | } 213 | 214 | /** 215 | * Match an exact phrase. 216 | * 217 | * Example: 218 | * $match->phrase('test case'); 219 | * // "test case" 220 | * 221 | * @param string $keywords The phrase to match. 222 | * 223 | * @return $this 224 | */ 225 | public function phrase($keywords) 226 | { 227 | $this->tokens[] = array('PHRASE' => $keywords); 228 | 229 | return $this; 230 | } 231 | 232 | /** 233 | * Provide an optional phrase. 234 | * 235 | * Example: 236 | * $match->phrase('test case')->orPhrase('another case'); 237 | * // "test case" | "another case" 238 | * 239 | * @param string $keywords The phrase to match. 240 | * 241 | * @return $this 242 | */ 243 | public function orPhrase($keywords) 244 | { 245 | $this->tokens[] = array('OPERATOR' => '| '); 246 | $this->phrase($keywords); 247 | 248 | return $this; 249 | } 250 | 251 | /** 252 | * Match if keywords are close enough. 253 | * 254 | * Example: 255 | * $match->proximity('test case', 5); 256 | * // "test case"~5 257 | * 258 | * @param string $keywords The words to match. 259 | * @param int $distance The upper limit on separation between words. 260 | * 261 | * @return $this 262 | */ 263 | public function proximity($keywords, $distance) 264 | { 265 | $this->tokens[] = array( 266 | 'PROXIMITY' => $distance, 267 | 'keywords' => $keywords, 268 | ); 269 | 270 | return $this; 271 | } 272 | 273 | /** 274 | * Match if enough keywords are present. 275 | * 276 | * Examples: 277 | * $match->quorum('this is a test case', 3); 278 | * // "this is a test case"/3 279 | * 280 | * $match->quorum('this is a test case', 0.5); 281 | * // "this is a test case"/0.5 282 | * 283 | * @param string $keywords The words to match. 284 | * @param int|float $threshold The minimum number or percent of words that must match. 285 | * 286 | * @return $this 287 | */ 288 | public function quorum($keywords, $threshold) 289 | { 290 | $this->tokens[] = array( 291 | 'QUORUM' => $threshold, 292 | 'keywords' => $keywords, 293 | ); 294 | 295 | return $this; 296 | } 297 | 298 | /** 299 | * Assert keywords or expressions must be matched in order. 300 | * 301 | * Examples: 302 | * $match->match('test')->before(); 303 | * // test << 304 | * 305 | * $match->match('test')->before('case'); 306 | * // test << case 307 | * 308 | * @param string|Match|\Closure $keywords The text or expression that must come after. 309 | * 310 | * @return $this 311 | */ 312 | public function before($keywords = null) 313 | { 314 | $this->tokens[] = array('OPERATOR' => '<< '); 315 | $this->match($keywords); 316 | 317 | return $this; 318 | } 319 | 320 | /** 321 | * Assert a keyword must be matched exactly as written. 322 | * 323 | * Examples: 324 | * $match->match('test')->exact('cases'); 325 | * // test =cases 326 | * 327 | * $match->match('test')->exact()->phrase('specific cases'); 328 | * // test ="specific cases" 329 | * 330 | * @param string $keyword The word that must be matched exactly. 331 | * 332 | * @return $this 333 | */ 334 | public function exact($keyword = null) 335 | { 336 | $this->tokens[] = array('OPERATOR' => '='); 337 | $this->match($keyword); 338 | 339 | return $this; 340 | } 341 | 342 | /** 343 | * Boost the IDF score of a keyword. 344 | * 345 | * Examples: 346 | * $match->match('test')->boost(1.2); 347 | * // test^1.2 348 | * 349 | * $match->match('test')->boost('case', 1.2); 350 | * // test case^1.2 351 | * 352 | * @param string $keyword The word to modify the score of. 353 | * @param float $amount The amount to boost the score. 354 | * 355 | * @return $this 356 | */ 357 | public function boost($keyword, $amount = null) 358 | { 359 | if ($amount === null) { 360 | $amount = $keyword; 361 | } else { 362 | $this->match($keyword); 363 | } 364 | $this->tokens[] = array('BOOST' => $amount); 365 | 366 | return $this; 367 | } 368 | 369 | /** 370 | * Assert keywords or expressions must be matched close to each other. 371 | * 372 | * Examples: 373 | * $match->match('test')->near(3); 374 | * // test NEAR/3 375 | * 376 | * $match->match('test')->near('case', 3); 377 | * // test NEAR/3 case 378 | * 379 | * @param string|Match|\Closure $keywords The text or expression to match nearby. 380 | * @param int $distance Maximum distance to the match. 381 | * 382 | * @return $this 383 | */ 384 | public function near($keywords, $distance = null) 385 | { 386 | $this->tokens[] = array('NEAR' => $distance ?: $keywords); 387 | if ($distance !== null) { 388 | $this->match($keywords); 389 | } 390 | 391 | return $this; 392 | } 393 | 394 | /** 395 | * Assert matches must be in the same sentence. 396 | * 397 | * Examples: 398 | * $match->match('test')->sentence(); 399 | * // test SENTENCE 400 | * 401 | * $match->match('test')->sentence('case'); 402 | * // test SENTENCE case 403 | * 404 | * @param string|Match|\Closure $keywords The text or expression that must be in the sentence. 405 | * 406 | * @return $this 407 | */ 408 | public function sentence($keywords = null) 409 | { 410 | $this->tokens[] = array('OPERATOR' => 'SENTENCE '); 411 | $this->match($keywords); 412 | 413 | return $this; 414 | } 415 | 416 | /** 417 | * Assert matches must be in the same paragraph. 418 | * 419 | * Examples: 420 | * $match->match('test')->paragraph(); 421 | * // test PARAGRAPH 422 | * 423 | * $match->match('test')->paragraph('case'); 424 | * // test PARAGRAPH case 425 | * 426 | * @param string|Match|\Closure $keywords The text or expression that must be in the paragraph. 427 | * 428 | * @return $this 429 | */ 430 | public function paragraph($keywords = null) 431 | { 432 | $this->tokens[] = array('OPERATOR' => 'PARAGRAPH '); 433 | $this->match($keywords); 434 | 435 | return $this; 436 | } 437 | 438 | /** 439 | * Assert matches must be in the specified zone(s). 440 | * 441 | * Examples: 442 | * $match->zone('th'); 443 | * // ZONE:(th) 444 | * 445 | * $match->zone(['h3', 'h4']); 446 | * // ZONE:(h3,h4) 447 | * 448 | * $match->zone('th', 'test'); 449 | * // ZONE:(th) test 450 | * 451 | * @param string|array $zones The zone or zones to search. 452 | * @param string|Match|\Closure $keywords The text or expression that must be in these zones. 453 | * 454 | * @return $this 455 | */ 456 | public function zone($zones, $keywords = null) 457 | { 458 | if (is_string($zones)) { 459 | $zones = array($zones); 460 | } 461 | $this->tokens[] = array('ZONE' => $zones); 462 | $this->match($keywords); 463 | 464 | return $this; 465 | } 466 | 467 | 468 | /** 469 | * Assert matches must be in the same instance of the specified zone. 470 | * 471 | * Examples: 472 | * $match->zonespan('th'); 473 | * // ZONESPAN:(th) 474 | * 475 | * $match->zonespan('th', 'test'); 476 | * // ZONESPAN:(th) test 477 | * 478 | * @param string $zone The zone to search. 479 | * @param string|Match|\Closure $keywords The text or expression that must be in this zone. 480 | * 481 | * @return $this 482 | */ 483 | public function zonespan($zone, $keywords = null) 484 | { 485 | $this->tokens[] = array('ZONESPAN' => $zone); 486 | $this->match($keywords); 487 | 488 | return $this; 489 | } 490 | 491 | /** 492 | * Build the match expression. 493 | * 494 | * @return $this 495 | */ 496 | public function compile() 497 | { 498 | $query = ''; 499 | foreach ($this->tokens as $token) { 500 | if (key($token) == 'MATCH') { 501 | if ($token['MATCH'] instanceof Expression) { 502 | $query .= $token['MATCH']->value().' '; 503 | } elseif ($token['MATCH'] instanceof MatchBuilder) { 504 | $query .= '('.$token['MATCH']->compile()->getCompiled().') '; 505 | } elseif ($token['MATCH'] instanceof \Closure) { 506 | $sub = new static($this->sphinxql); 507 | call_user_func($token['MATCH'], $sub); 508 | $query .= '('.$sub->compile()->getCompiled().') '; 509 | } elseif (strpos($token['MATCH'], ' ') === false) { 510 | $query .= $this->sphinxql->escapeMatch($token['MATCH']).' '; 511 | } else { 512 | $query .= '('.$this->sphinxql->escapeMatch($token['MATCH']).') '; 513 | } 514 | } elseif (key($token) == 'OPERATOR') { 515 | $query .= $token['OPERATOR']; 516 | } elseif (key($token) == 'FIELD') { 517 | $query .= $token['FIELD']; 518 | if (count($token['fields']) == 1) { 519 | $query .= $token['fields'][0]; 520 | } else { 521 | $query .= '('.implode(',', $token['fields']).')'; 522 | } 523 | if ($token['limit']) { 524 | $query .= '['.$token['limit'].']'; 525 | } 526 | $query .= ' '; 527 | } elseif (key($token) == 'PHRASE') { 528 | $query .= '"'.$this->sphinxql->escapeMatch($token['PHRASE']).'" '; 529 | } elseif (key($token) == 'PROXIMITY') { 530 | $query .= '"'.$this->sphinxql->escapeMatch($token['keywords']).'"~'; 531 | $query .= $token['PROXIMITY'].' '; 532 | } elseif (key($token) == 'QUORUM') { 533 | $query .= '"'.$this->sphinxql->escapeMatch($token['keywords']).'"/'; 534 | $query .= $token['QUORUM'].' '; 535 | } elseif (key($token) == 'BOOST') { 536 | $query = rtrim($query).'^'.$token['BOOST'].' '; 537 | } elseif (key($token) == 'NEAR') { 538 | $query .= 'NEAR/'.$token['NEAR'].' '; 539 | } elseif (key($token) == 'ZONE') { 540 | $query .= 'ZONE:('.implode(',', $token['ZONE']).') '; 541 | } elseif (key($token) == 'ZONESPAN') { 542 | $query .= 'ZONESPAN:('.$token['ZONESPAN'].') '; 543 | } 544 | } 545 | $this->last_compiled = trim($query); 546 | 547 | return $this; 548 | } 549 | 550 | /** 551 | * Returns the latest compiled match expression. 552 | * 553 | * @return string The last compiled match expression. 554 | */ 555 | public function getCompiled() 556 | { 557 | return $this->last_compiled; 558 | } 559 | } 560 | -------------------------------------------------------------------------------- /src/Percolate.php: -------------------------------------------------------------------------------- 1 | insert('full text query terms', noEscape = false) // Allowed only one insert per query (Symbol @ indicates field in sphinx.conf) 15 | * No escape tag cancels characters shielding (default on) 16 | * ->into('pq') // Index for insert 17 | * ->tags(['tag1','tag2']) // Adding tags. Can be array ['tag1','tag2'] or string delimited by coma 18 | * ->filter('price>3') // Adding filter (Allowed only one) 19 | * ->execute(); 20 | * 21 | * 22 | * ### CALL PQ ### 23 | * 24 | * 25 | * $query = (new Percolate($conn)) 26 | * ->callPQ() 27 | * ->from('pq') // Index for call pq 28 | * ->documents(['multiple documents', 'go this way']) // see getDocuments function 29 | * ->options([ // See https://docs.manticoresearch.com/latest/html/searching/percolate_query.html#call-pq 30 | * Percolate::OPTION_VERBOSE => 1, 31 | * Percolate::OPTION_DOCS_JSON => 1 32 | * ]) 33 | * ->execute(); 34 | * 35 | * 36 | */ 37 | class Percolate 38 | { 39 | 40 | /** 41 | * @var ConnectionInterface 42 | */ 43 | protected $connection; 44 | 45 | /** 46 | * Documents for CALL PQ 47 | * 48 | * @var array|string 49 | */ 50 | protected $documents; 51 | 52 | /** 53 | * Index name 54 | * 55 | * @var string 56 | */ 57 | protected $index; 58 | 59 | /** 60 | * Insert query 61 | * 62 | * @var string 63 | */ 64 | protected $query; 65 | 66 | /** 67 | * Options for CALL PQ 68 | * @var array 69 | */ 70 | protected $options = [self::OPTION_DOCS_JSON => 1]; 71 | 72 | /** 73 | * @var string 74 | */ 75 | protected $filters = ''; 76 | 77 | /** 78 | * Query type (call | insert) 79 | * 80 | * @var string 81 | */ 82 | protected $type = 'call'; 83 | 84 | /** INSERT STATEMENT **/ 85 | 86 | protected $tags = []; 87 | 88 | /** 89 | * Throw exceptions flag. 90 | * Activates if option OPTION_DOCS_JSON setted 91 | * 92 | * @var int 93 | */ 94 | protected $throwExceptions = 0; 95 | /** 96 | * @var array 97 | */ 98 | protected $escapeChars = [ 99 | '\\' => '\\\\', 100 | '-' => '\\-', 101 | '~' => '\\~', 102 | '<' => '\\<', 103 | '"' => '\\"', 104 | "'" => "\\'", 105 | '/' => '\\/', 106 | '!' => '\\!' 107 | ]; 108 | 109 | /** @var SphinxQL */ 110 | protected $sphinxQL; 111 | 112 | /** 113 | * CALL PQ option constants 114 | */ 115 | const OPTION_DOCS_JSON = 'as docs_json'; 116 | const OPTION_DOCS = 'as docs'; 117 | const OPTION_VERBOSE = 'as verbose'; 118 | const OPTION_QUERY = 'as query'; 119 | 120 | /** 121 | * @param ConnectionInterface $connection 122 | */ 123 | public function __construct(ConnectionInterface $connection) 124 | { 125 | $this->connection = $connection; 126 | $this->sphinxQL = new SphinxQL($this->connection); 127 | 128 | } 129 | 130 | 131 | /** 132 | * Clear all fields after execute 133 | */ 134 | private function clear() 135 | { 136 | $this->documents = null; 137 | $this->index = null; 138 | $this->query = null; 139 | $this->options = [self::OPTION_DOCS_JSON => 1]; 140 | $this->type = 'call'; 141 | $this->filters = ''; 142 | $this->tags = []; 143 | } 144 | 145 | /** 146 | * Analog into function 147 | * Sets index name for query 148 | * 149 | * @param string $index 150 | * 151 | * @return $this 152 | * @throws SphinxQLException 153 | */ 154 | public function from($index) 155 | { 156 | if (empty($index)) { 157 | throw new SphinxQLException('Index can\'t be empty'); 158 | } 159 | 160 | $this->index = trim($index); 161 | return $this; 162 | } 163 | 164 | /** 165 | * Analog from function 166 | * Sets index name for query 167 | * 168 | * @param string $index 169 | * 170 | * @return $this 171 | * @throws SphinxQLException 172 | */ 173 | public function into($index) 174 | { 175 | if (empty($index)) { 176 | throw new SphinxQLException('Index can\'t be empty'); 177 | } 178 | $this->index = trim($index); 179 | return $this; 180 | } 181 | 182 | /** 183 | * Replacing bad chars 184 | * 185 | * @param string $query 186 | * 187 | * @return string mixed 188 | */ 189 | protected function escapeString($query) 190 | { 191 | return str_replace( 192 | array_keys($this->escapeChars), 193 | array_values($this->escapeChars), 194 | $query); 195 | } 196 | 197 | 198 | /** 199 | * @param $query 200 | * @return mixed 201 | */ 202 | protected function clearString($query) 203 | { 204 | return str_replace( 205 | array_keys(array_merge($this->escapeChars, ['@' => ''])), 206 | ['', '', '', '', '', '', '', '', '', ''], 207 | $query); 208 | } 209 | 210 | /** 211 | * Adding tags for insert query 212 | * 213 | * @param array|string $tags 214 | * 215 | * @return $this 216 | */ 217 | public function tags($tags) 218 | { 219 | if (is_array($tags)) { 220 | $tags = array_map([$this, 'escapeString'], $tags); 221 | $tags = implode(',', $tags); 222 | } else { 223 | $tags = $this->escapeString($tags); 224 | } 225 | $this->tags = $tags; 226 | return $this; 227 | } 228 | 229 | /** 230 | * Add filter for insert query 231 | * 232 | * @param string $filter 233 | * @return $this 234 | * 235 | * @throws SphinxQLException 236 | */ 237 | public function filter($filter) 238 | { 239 | $this->filters = $this->clearString($filter); 240 | return $this; 241 | } 242 | 243 | /** 244 | * Add insert query 245 | * 246 | * @param string $query 247 | * @param bool $noEscape 248 | * 249 | * @return $this 250 | * @throws SphinxQLException 251 | */ 252 | public function insert($query, $noEscape = false) 253 | { 254 | $this->clear(); 255 | 256 | if (empty($query)) { 257 | throw new SphinxQLException('Query can\'t be empty'); 258 | } 259 | if (!$noEscape) { 260 | $query = $this->escapeString($query); 261 | } 262 | $this->query = $query; 263 | $this->type = 'insert'; 264 | 265 | return $this; 266 | } 267 | 268 | /** 269 | * Generate array for insert, from setted class parameters 270 | * 271 | * @return array 272 | */ 273 | private function generateInsert() 274 | { 275 | $insertArray = ['query' => $this->query]; 276 | 277 | if (!empty($this->tags)) { 278 | $insertArray['tags'] = $this->tags; 279 | } 280 | 281 | if (!empty($this->filters)) { 282 | $insertArray['filters'] = $this->filters; 283 | } 284 | 285 | return $insertArray; 286 | } 287 | 288 | /** 289 | * Executs query and clear class parameters 290 | * 291 | * @return Drivers\ResultSetInterface 292 | * @throws Exception\ConnectionException 293 | * @throws Exception\DatabaseException 294 | * @throws SphinxQLException 295 | */ 296 | public function execute() 297 | { 298 | 299 | if ($this->type == 'insert') { 300 | return $this->sphinxQL 301 | ->insert() 302 | ->into($this->index) 303 | ->set($this->generateInsert()) 304 | ->execute(); 305 | } 306 | 307 | return $this->sphinxQL 308 | ->query("CALL PQ ('" . 309 | $this->index . "', " . $this->getDocuments() . " " . $this->getOptions() . ")") 310 | ->execute(); 311 | } 312 | 313 | /** 314 | * Set one option for CALL PQ 315 | * 316 | * @param string $key 317 | * @param int $value 318 | * 319 | * @return $this 320 | * @throws SphinxQLException 321 | */ 322 | private function setOption($key, $value) 323 | { 324 | $value = intval($value); 325 | if (!in_array($key, [ 326 | self::OPTION_DOCS_JSON, 327 | self::OPTION_DOCS, 328 | self::OPTION_VERBOSE, 329 | self::OPTION_QUERY 330 | ])) { 331 | throw new SphinxQLException('Unknown option'); 332 | } 333 | 334 | if ($value != 0 && $value != 1) { 335 | throw new SphinxQLException('Option value can be only 1 or 0'); 336 | } 337 | 338 | if ($key == self::OPTION_DOCS_JSON) { 339 | $this->throwExceptions = 1; 340 | } 341 | 342 | $this->options[$key] = $value; 343 | return $this; 344 | } 345 | 346 | /** 347 | * Set document parameter for CALL PQ 348 | * 349 | * @param array|string $documents 350 | * @return $this 351 | * @throws SphinxQLException 352 | */ 353 | public function documents($documents) 354 | { 355 | if (empty($documents)) { 356 | throw new SphinxQLException('Document can\'t be empty'); 357 | } 358 | $this->documents = $documents; 359 | 360 | return $this; 361 | } 362 | 363 | /** 364 | * Set options for CALL PQ 365 | * 366 | * @param array $options 367 | * @return $this 368 | * @throws SphinxQLException 369 | */ 370 | public function options(array $options) 371 | { 372 | foreach ($options as $option => $value) { 373 | $this->setOption($option, $value); 374 | } 375 | return $this; 376 | } 377 | 378 | 379 | /** 380 | * Get and prepare options for CALL PQ 381 | * 382 | * @return string string 383 | */ 384 | protected function getOptions() 385 | { 386 | $options = ''; 387 | if (!empty($this->options)) { 388 | foreach ($this->options as $option => $value) { 389 | $options .= ', ' . $value . ' ' . $option; 390 | } 391 | } 392 | 393 | return $options; 394 | } 395 | 396 | /** 397 | * Check is array associative 398 | * @param array $arr 399 | * @return bool 400 | */ 401 | private function isAssocArray(array $arr) 402 | { 403 | if (array() === $arr) { 404 | return false; 405 | } 406 | return array_keys($arr) !== range(0, count($arr) - 1); 407 | } 408 | 409 | /** 410 | * Get documents for CALL PQ. If option setted JSON - returns json_encoded 411 | * 412 | * Now selection of supported types work automatically. You don't need set 413 | * OPTION_DOCS_JSON to 1 or 0. But if you will set this option, 414 | * automatically enables exceptions on unsupported types 415 | * 416 | * 417 | * 1) If expect json = 0: 418 | * a) doc can be 'catch me' 419 | * b) doc can be multiple ['catch me if can','catch me'] 420 | * 421 | * 2) If expect json = 1: 422 | * a) doc can be jsonOBJECT {"subject": "document about orange"} 423 | * b) docs can be jsonARRAY of jsonOBJECTS [{"subject": "document about orange"}, {"doc": "document about orange"}] 424 | * c) docs can be phpArray of jsonObjects ['{"subject": "document about orange"}', '{"doc": "document about orange"}'] 425 | * d) doc can be associate array ['subject'=>'document about orange'] 426 | * e) docs can be array of associate arrays [['subject'=>'document about orange'], ['doc'=>'document about orange']] 427 | * 428 | * 429 | * 430 | * 431 | * @return string 432 | * @throws SphinxQLException 433 | */ 434 | protected function getDocuments() 435 | { 436 | if (!empty($this->documents)) { 437 | 438 | if ($this->throwExceptions) { 439 | 440 | if ($this->options[self::OPTION_DOCS_JSON]) { 441 | 442 | if (!is_array($this->documents)) { 443 | $json = $this->prepareFromJson($this->documents); 444 | if (!$json) { 445 | throw new SphinxQLException('Documents must be in json format'); 446 | } 447 | } else { 448 | if (!$this->isAssocArray($this->documents) && !is_array($this->documents[0])) { 449 | throw new SphinxQLException('Documents array must be associate'); 450 | } 451 | } 452 | } 453 | } 454 | 455 | if (is_array($this->documents)) { 456 | 457 | // If input is phpArray with json like 458 | // ->documents(['{"body": "body of doc 1", "title": "title of doc 1"}', 459 | // '{"subject": "subject of doc 3"}']) 460 | // 461 | // Checking first symbol of first array value. If [ or { - call checkJson 462 | 463 | if (!empty($this->documents[0]) && !is_array($this->documents[0]) && 464 | ($this->documents[0][0] == '[' || $this->documents[0][0] == '{')) { 465 | 466 | $json = $this->prepareFromJson($this->documents); 467 | if ($json) { 468 | $this->options[self::OPTION_DOCS_JSON] = 1; 469 | return $json; 470 | } 471 | 472 | } else { 473 | if (!$this->isAssocArray($this->documents)) { 474 | 475 | // if incoming single array like ['catch me if can', 'catch me'] 476 | 477 | if (is_string($this->documents[0])) { 478 | $this->options[self::OPTION_DOCS_JSON] = 0; 479 | return '(' . $this->quoteString(implode('\', \'', $this->documents)) . ')'; 480 | } 481 | 482 | // if doc is array of associate arrays [['foo'=>'bar'], ['foo1'=>'bar1']] 483 | if (!empty($this->documents[0]) && $this->isAssocArray($this->documents[0])) { 484 | $this->options[self::OPTION_DOCS_JSON] = 1; 485 | return $this->convertArrayForQuery($this->documents); 486 | } 487 | 488 | } else { 489 | if ($this->isAssocArray($this->documents)) { 490 | // Id doc is associate array ['foo'=>'bar'] 491 | $this->options[self::OPTION_DOCS_JSON] = 1; 492 | return $this->quoteString(json_encode($this->documents)); 493 | } 494 | } 495 | } 496 | 497 | } else { 498 | if (is_string($this->documents)) { 499 | 500 | $json = $this->prepareFromJson($this->documents); 501 | if ($json) { 502 | $this->options[self::OPTION_DOCS_JSON] = 1; 503 | return $json; 504 | } 505 | 506 | $this->options[self::OPTION_DOCS_JSON] = 0; 507 | return $this->quoteString($this->documents); 508 | } 509 | } 510 | } 511 | throw new SphinxQLException('Documents can\'t be empty'); 512 | } 513 | 514 | 515 | /** 516 | * Set type 517 | * 518 | * @return $this 519 | */ 520 | public function callPQ() 521 | { 522 | $this->clear(); 523 | $this->type = 'call'; 524 | return $this; 525 | } 526 | 527 | 528 | /** 529 | * Prepares documents for insert in valid format. 530 | * $data can be jsonArray of jsonObjects, 531 | * phpArray of jsonObjects, valid json or string 532 | * 533 | * @param string|array $data 534 | * 535 | * @return bool|string 536 | */ 537 | private function prepareFromJson($data) 538 | { 539 | if (is_array($data)) { 540 | if (is_array($data[0])) { 541 | return false; 542 | } 543 | $return = []; 544 | foreach ($data as $item) { 545 | $return[] = $this->prepareFromJson($item); 546 | } 547 | 548 | return '(' . implode(', ', $return) . ')'; 549 | } 550 | $array = json_decode($data, true); 551 | 552 | if (json_last_error() == JSON_ERROR_NONE) { // if json 553 | if ( ! empty($array[0])) { // If docs is jsonARRAY of jsonOBJECTS 554 | return $this->convertArrayForQuery($array); 555 | } 556 | 557 | // If docs is jsonOBJECT 558 | return $this->quoteString($data); 559 | } 560 | 561 | return false; 562 | } 563 | 564 | 565 | /** 566 | * Converts array of associate arrays to valid for query statement 567 | * ('jsonOBJECT1', 'jsonOBJECT2' ...) 568 | * 569 | * 570 | * @param array $array 571 | * @return string 572 | */ 573 | private function convertArrayForQuery(array $array) 574 | { 575 | $documents = []; 576 | foreach ($array as $document) { 577 | $documents[] = json_encode($document); 578 | } 579 | 580 | return '(' . $this->quoteString(implode('\', \'', $documents)) . ')'; 581 | } 582 | 583 | 584 | /** 585 | * Adding single quotes to string 586 | * 587 | * @param string $string 588 | * @return string 589 | */ 590 | private function quoteString($string) 591 | { 592 | return '\'' . $string . '\''; 593 | } 594 | 595 | 596 | /** 597 | * Get last compiled query 598 | * 599 | * @return string 600 | */ 601 | public function getLastQuery() 602 | { 603 | return $this->sphinxQL->getCompiled(); 604 | } 605 | } 606 | -------------------------------------------------------------------------------- /src/SphinxQL.php: -------------------------------------------------------------------------------- 1 | '\\\\', 184 | '(' => '\(', 185 | ')' => '\)', 186 | '|' => '\|', 187 | '-' => '\-', 188 | '!' => '\!', 189 | '@' => '\@', 190 | '~' => '\~', 191 | '"' => '\"', 192 | '&' => '\&', 193 | '/' => '\/', 194 | '^' => '\^', 195 | '$' => '\$', 196 | '=' => '\=', 197 | '<' => '\<', 198 | ); 199 | 200 | /** 201 | * An array of escaped characters for fullEscapeMatch() 202 | * @var array 203 | */ 204 | protected $escape_half_chars = array( 205 | '\\' => '\\\\', 206 | '(' => '\(', 207 | ')' => '\)', 208 | '!' => '\!', 209 | '@' => '\@', 210 | '~' => '\~', 211 | '&' => '\&', 212 | '/' => '\/', 213 | '^' => '\^', 214 | '$' => '\$', 215 | '=' => '\=', 216 | '<' => '\<', 217 | ); 218 | 219 | /** 220 | * @param ConnectionInterface|null $connection 221 | */ 222 | public function __construct(ConnectionInterface $connection = null) 223 | { 224 | $this->connection = $connection; 225 | } 226 | 227 | /** 228 | * Sets Query Type 229 | * 230 | */ 231 | public function setType(string $type) 232 | { 233 | return $this->type = $type; 234 | } 235 | 236 | /** 237 | * Returns the currently attached connection 238 | * 239 | * @returns ConnectionInterface 240 | */ 241 | public function getConnection() 242 | { 243 | return $this->connection; 244 | } 245 | 246 | /** 247 | * Avoids having the expressions escaped 248 | * 249 | * Examples: 250 | * $query->where('time', '>', SphinxQL::expr('CURRENT_TIMESTAMP')); 251 | * // WHERE time > CURRENT_TIMESTAMP 252 | * 253 | * @param string $string The string to keep unaltered 254 | * 255 | * @return Expression The new Expression 256 | * @todo make non static 257 | */ 258 | public static function expr($string = '') 259 | { 260 | return new Expression($string); 261 | } 262 | 263 | /** 264 | * Runs the query built 265 | * 266 | * @return ResultSetInterface The result of the query 267 | * @throws DatabaseException 268 | * @throws ConnectionException 269 | * @throws SphinxQLException 270 | */ 271 | public function execute() 272 | { 273 | // pass the object so execute compiles it by itself 274 | return $this->last_result = $this->getConnection()->query($this->compile()->getCompiled()); 275 | } 276 | 277 | /** 278 | * Executes a batch of queued queries 279 | * 280 | * @return MultiResultSetInterface The array of results 281 | * @throws SphinxQLException In case no query is in queue 282 | * @throws Exception\DatabaseException 283 | * @throws ConnectionException 284 | */ 285 | public function executeBatch() 286 | { 287 | if (count($this->getQueue()) == 0) { 288 | throw new SphinxQLException('There is no Queue present to execute.'); 289 | } 290 | 291 | $queue = array(); 292 | 293 | foreach ($this->getQueue() as $query) { 294 | $queue[] = $query->compile()->getCompiled(); 295 | } 296 | 297 | return $this->last_result = $this->getConnection()->multiQuery($queue); 298 | } 299 | 300 | /** 301 | * Enqueues the current object and returns a new one or the supplied one 302 | * 303 | * @param SphinxQL|null $next 304 | * 305 | * @return SphinxQL A new SphinxQL object with the current object referenced 306 | */ 307 | public function enqueue(SphinxQL $next = null) 308 | { 309 | if ($next === null) { 310 | $next = new static($this->getConnection()); 311 | } 312 | 313 | $next->setQueuePrev($this); 314 | 315 | return $next; 316 | } 317 | 318 | /** 319 | * Returns the ordered array of enqueued objects 320 | * 321 | * @return SphinxQL[] The ordered array of enqueued objects 322 | */ 323 | public function getQueue() 324 | { 325 | $queue = array(); 326 | $curr = $this; 327 | 328 | do { 329 | if ($curr->type != null) { 330 | $queue[] = $curr; 331 | } 332 | } while ($curr = $curr->getQueuePrev()); 333 | 334 | return array_reverse($queue); 335 | } 336 | 337 | /** 338 | * Gets the enqueued object 339 | * 340 | * @return SphinxQL|null 341 | */ 342 | public function getQueuePrev() 343 | { 344 | return $this->queue_prev; 345 | } 346 | 347 | /** 348 | * Sets the reference to the enqueued object 349 | * 350 | * @param SphinxQL $query The object to set as previous 351 | * 352 | * @return self 353 | */ 354 | public function setQueuePrev($query) 355 | { 356 | $this->queue_prev = $query; 357 | 358 | return $this; 359 | } 360 | 361 | /** 362 | * Returns the result of the last query 363 | * 364 | * @return array The result of the last query 365 | */ 366 | public function getResult() 367 | { 368 | return $this->last_result; 369 | } 370 | 371 | /** 372 | * Returns the latest compiled query 373 | * 374 | * @return string The last compiled query 375 | */ 376 | public function getCompiled() 377 | { 378 | return $this->last_compiled; 379 | } 380 | 381 | /** 382 | * Begins transaction 383 | * @throws DatabaseException 384 | * @throws ConnectionException 385 | */ 386 | public function transactionBegin() 387 | { 388 | $this->getConnection()->query('BEGIN'); 389 | } 390 | 391 | /** 392 | * Commits transaction 393 | * @throws DatabaseException 394 | * @throws ConnectionException 395 | */ 396 | public function transactionCommit() 397 | { 398 | $this->getConnection()->query('COMMIT'); 399 | } 400 | 401 | /** 402 | * Rollbacks transaction 403 | * @throws DatabaseException 404 | * @throws ConnectionException 405 | */ 406 | public function transactionRollback() 407 | { 408 | $this->getConnection()->query('ROLLBACK'); 409 | } 410 | 411 | /** 412 | * Runs the compile function 413 | * 414 | * @return self 415 | * @throws ConnectionException 416 | * @throws DatabaseException 417 | * @throws SphinxQLException 418 | */ 419 | public function compile() 420 | { 421 | switch ($this->type) { 422 | case 'select': 423 | $this->compileSelect(); 424 | break; 425 | case 'insert': 426 | case 'replace': 427 | $this->compileInsert(); 428 | break; 429 | case 'update': 430 | $this->compileUpdate(); 431 | break; 432 | case 'delete': 433 | $this->compileDelete(); 434 | break; 435 | case 'query': 436 | $this->compileQuery(); 437 | break; 438 | } 439 | 440 | return $this; 441 | } 442 | 443 | /** 444 | * @return self 445 | */ 446 | public function compileQuery() 447 | { 448 | $this->last_compiled = $this->query; 449 | 450 | return $this; 451 | } 452 | 453 | /** 454 | * Compiles the MATCH part of the queries 455 | * Used by: SELECT, DELETE, UPDATE 456 | * 457 | * @return string The compiled MATCH 458 | * @throws Exception\ConnectionException 459 | * @throws Exception\DatabaseException 460 | */ 461 | public function compileMatch() 462 | { 463 | $query = ''; 464 | 465 | if (!empty($this->match)) { 466 | $query .= 'WHERE MATCH('; 467 | 468 | $matched = array(); 469 | 470 | foreach ($this->match as $match) { 471 | $pre = ''; 472 | if ($match['column'] instanceof \Closure) { 473 | $sub = new MatchBuilder($this); 474 | call_user_func($match['column'], $sub); 475 | $pre .= $sub->compile()->getCompiled(); 476 | } elseif ($match['column'] instanceof MatchBuilder) { 477 | $pre .= $match['column']->compile()->getCompiled(); 478 | } elseif (empty($match['column'])) { 479 | $pre .= ''; 480 | } elseif (is_array($match['column'])) { 481 | $pre .= '@('.implode(',', $match['column']).') '; 482 | } else { 483 | $pre .= '@'.$match['column'].' '; 484 | } 485 | 486 | if ($match['half']) { 487 | $pre .= $this->halfEscapeMatch($match['value']); 488 | } else { 489 | $pre .= $this->escapeMatch($match['value']); 490 | } 491 | 492 | if ($pre !== '') { 493 | $matched[] = '('.$pre.')'; 494 | } 495 | } 496 | 497 | $matched = implode(' ', $matched); 498 | $query .= $this->getConnection()->escape(trim($matched)).') '; 499 | } 500 | 501 | return $query; 502 | } 503 | 504 | /** 505 | * Compiles the WHERE part of the queries 506 | * It interacts with the MATCH() and of course isn't usable stand-alone 507 | * Used by: SELECT, DELETE, UPDATE 508 | * 509 | * @return string The compiled WHERE 510 | * @throws ConnectionException 511 | * @throws DatabaseException 512 | */ 513 | public function compileWhere() 514 | { 515 | $query = ''; 516 | 517 | if (empty($this->match) && !empty($this->where)) { 518 | $query .= 'WHERE '; 519 | } 520 | 521 | if (!empty($this->where)) { 522 | foreach ($this->where as $key => $where) { 523 | if ($key > 0 || !empty($this->match)) { 524 | $query .= 'AND '; 525 | } 526 | $query .= $this->compileFilterCondition($where); 527 | } 528 | } 529 | 530 | return $query; 531 | } 532 | 533 | /** 534 | * @param array $filter 535 | * 536 | * @return string 537 | * @throws ConnectionException 538 | * @throws DatabaseException 539 | */ 540 | public function compileFilterCondition($filter) 541 | { 542 | $query = ''; 543 | 544 | if (!empty($filter)) { 545 | if (strtoupper($filter['operator']) === 'BETWEEN') { 546 | $query .= $filter['column']; 547 | $query .= ' BETWEEN '; 548 | $query .= $this->getConnection()->quote($filter['value'][0]).' AND ' 549 | .$this->getConnection()->quote($filter['value'][1]).' '; 550 | } else { 551 | // id can't be quoted! 552 | if ($filter['column'] === 'id') { 553 | $query .= 'id '; 554 | } else { 555 | $query .= $filter['column'].' '; 556 | } 557 | 558 | if (in_array(strtoupper($filter['operator']), array('IN', 'NOT IN'), true)) { 559 | $query .= strtoupper($filter['operator']).' ('.implode(', ', $this->getConnection()->quoteArr($filter['value'])).') '; 560 | } else { 561 | $query .= $filter['operator'].' '.$this->getConnection()->quote($filter['value']).' '; 562 | } 563 | } 564 | } 565 | 566 | return $query; 567 | } 568 | 569 | /** 570 | * Compiles the statements for SELECT 571 | * 572 | * @return self 573 | * @throws ConnectionException 574 | * @throws DatabaseException 575 | * @throws SphinxQLException 576 | */ 577 | public function compileSelect() 578 | { 579 | $query = ''; 580 | 581 | if ($this->type == 'select') { 582 | $query .= 'SELECT '; 583 | 584 | if (!empty($this->select)) { 585 | $query .= implode(', ', $this->select).' '; 586 | } else { 587 | $query .= '* '; 588 | } 589 | } 590 | 591 | if (!empty($this->from)) { 592 | if ($this->from instanceof \Closure) { 593 | $sub = new static($this->getConnection()); 594 | call_user_func($this->from, $sub); 595 | $query .= 'FROM ('.$sub->compile()->getCompiled().') '; 596 | } elseif ($this->from instanceof SphinxQL) { 597 | $query .= 'FROM ('.$this->from->compile()->getCompiled().') '; 598 | } else { 599 | $query .= 'FROM '.implode(', ', $this->from).' '; 600 | } 601 | } 602 | 603 | $query .= $this->compileMatch().$this->compileWhere(); 604 | 605 | if (!empty($this->group_by)) { 606 | $query .= 'GROUP '; 607 | if ($this->group_n_by !== null) { 608 | $query .= $this->group_n_by.' '; 609 | } 610 | $query .= 'BY '.implode(', ', $this->group_by).' '; 611 | } 612 | 613 | if (!empty($this->within_group_order_by)) { 614 | $query .= 'WITHIN GROUP ORDER BY '; 615 | 616 | $order_arr = array(); 617 | 618 | foreach ($this->within_group_order_by as $order) { 619 | $order_sub = $order['column'].' '; 620 | 621 | if ($order['direction'] !== null) { 622 | $order_sub .= ((strtolower($order['direction']) === 'desc') ? 'DESC' : 'ASC'); 623 | } 624 | 625 | $order_arr[] = $order_sub; 626 | } 627 | 628 | $query .= implode(', ', $order_arr).' '; 629 | } 630 | 631 | if (!empty($this->having)) { 632 | $query .= 'HAVING '.$this->compileFilterCondition($this->having); 633 | } 634 | 635 | if (!empty($this->order_by)) { 636 | $query .= 'ORDER BY '; 637 | 638 | $order_arr = array(); 639 | 640 | foreach ($this->order_by as $order) { 641 | $order_sub = $order['column'].' '; 642 | 643 | if ($order['direction'] !== null) { 644 | $order_sub .= ((strtolower($order['direction']) === 'desc') ? 'DESC' : 'ASC'); 645 | } 646 | 647 | $order_arr[] = $order_sub; 648 | } 649 | 650 | $query .= implode(', ', $order_arr).' '; 651 | } 652 | 653 | if ($this->limit !== null || $this->offset !== null) { 654 | if ($this->offset === null) { 655 | $this->offset = 0; 656 | } 657 | 658 | if ($this->limit === null) { 659 | $this->limit = 9999999999999; 660 | } 661 | 662 | $query .= 'LIMIT '.((int) $this->offset).', '.((int) $this->limit).' '; 663 | } 664 | 665 | if (!empty($this->options)) { 666 | $options = array(); 667 | 668 | foreach ($this->options as $option) { 669 | if ($option['value'] instanceof Expression) { 670 | $option['value'] = $option['value']->value(); 671 | } elseif (is_array($option['value'])) { 672 | array_walk( 673 | $option['value'], 674 | function (&$val, $key) { 675 | $val = $key.'='.$val; 676 | } 677 | ); 678 | $option['value'] = '('.implode(', ', $option['value']).')'; 679 | } else { 680 | $option['value'] = $this->getConnection()->quote($option['value']); 681 | } 682 | 683 | $options[] = $option['name'].' = '.$option['value']; 684 | } 685 | 686 | $query .= 'OPTION '.implode(', ', $options).' '; 687 | } 688 | 689 | if (!empty($this->facets)) { 690 | $facets = array(); 691 | 692 | foreach ($this->facets as $facet) { 693 | // dynamically set the own SphinxQL connection if the Facet doesn't own one 694 | if ($facet->getConnection() === null) { 695 | $facet->setConnection($this->getConnection()); 696 | $facets[] = $facet->getFacet(); 697 | // go back to the status quo for reuse 698 | $facet->setConnection(); 699 | } else { 700 | $facets[] = $facet->getFacet(); 701 | } 702 | } 703 | 704 | $query .= implode(' ', $facets); 705 | } 706 | 707 | $query = trim($query); 708 | $this->last_compiled = $query; 709 | 710 | return $this; 711 | } 712 | 713 | /** 714 | * Compiles the statements for INSERT or REPLACE 715 | * 716 | * @return self 717 | * @throws ConnectionException 718 | * @throws DatabaseException 719 | */ 720 | public function compileInsert() 721 | { 722 | if ($this->type == 'insert') { 723 | $query = 'INSERT '; 724 | } else { 725 | $query = 'REPLACE '; 726 | } 727 | 728 | if ($this->into !== null) { 729 | $query .= 'INTO '.$this->into.' '; 730 | } 731 | 732 | if (!empty($this->columns)) { 733 | $query .= '('.implode(', ', $this->columns).') '; 734 | } 735 | 736 | if (!empty($this->values)) { 737 | $query .= 'VALUES '; 738 | $query_sub = array(); 739 | 740 | foreach ($this->values as $value) { 741 | $query_sub[] = '('.implode(', ', $this->getConnection()->quoteArr($value)).')'; 742 | } 743 | 744 | $query .= implode(', ', $query_sub); 745 | } 746 | 747 | $query = trim($query); 748 | $this->last_compiled = $query; 749 | 750 | return $this; 751 | } 752 | 753 | /** 754 | * Compiles the statements for UPDATE 755 | * 756 | * @return self 757 | * @throws ConnectionException 758 | * @throws DatabaseException 759 | */ 760 | public function compileUpdate() 761 | { 762 | $query = 'UPDATE '; 763 | 764 | if ($this->into !== null) { 765 | $query .= $this->into.' '; 766 | } 767 | 768 | if (!empty($this->set)) { 769 | $query .= 'SET '; 770 | 771 | $query_sub = array(); 772 | 773 | foreach ($this->set as $column => $value) { 774 | // MVA support 775 | if (is_array($value)) { 776 | $query_sub[] = $column 777 | .' = ('.implode(', ', $this->getConnection()->quoteArr($value)).')'; 778 | } else { 779 | $query_sub[] = $column 780 | .' = '.$this->getConnection()->quote($value); 781 | } 782 | } 783 | 784 | $query .= implode(', ', $query_sub).' '; 785 | } 786 | 787 | $query .= $this->compileMatch().$this->compileWhere(); 788 | 789 | $query = trim($query); 790 | $this->last_compiled = $query; 791 | 792 | return $this; 793 | } 794 | 795 | /** 796 | * Compiles the statements for DELETE 797 | * 798 | * @return self 799 | * @throws ConnectionException 800 | * @throws DatabaseException 801 | */ 802 | public function compileDelete() 803 | { 804 | $query = 'DELETE '; 805 | 806 | if (!empty($this->from)) { 807 | $query .= 'FROM '.$this->from[0].' '; 808 | } 809 | 810 | if (!empty($this->match)) { 811 | $query .= $this->compileMatch(); 812 | } 813 | if (!empty($this->where)) { 814 | $query .= $this->compileWhere(); 815 | } 816 | 817 | $query = trim($query); 818 | $this->last_compiled = $query; 819 | 820 | return $this; 821 | } 822 | 823 | /** 824 | * Sets a query to be executed 825 | * 826 | * @param string $sql A SphinxQL query to execute 827 | * 828 | * @return self 829 | */ 830 | public function query($sql) 831 | { 832 | $this->type = 'query'; 833 | $this->query = $sql; 834 | 835 | return $this; 836 | } 837 | 838 | /** 839 | * Select the columns 840 | * 841 | * Gets the arguments passed as $sphinxql->select('one', 'two') 842 | * Using it without arguments equals to having '*' as argument 843 | * Using it with array maps values as column names 844 | * 845 | * Examples: 846 | * $query->select('title'); 847 | * // SELECT title 848 | * 849 | * $query->select('title', 'author', 'date'); 850 | * // SELECT title, author, date 851 | * 852 | * $query->select(['id', 'title']); 853 | * // SELECT id, title 854 | * 855 | * @param array|string $columns Array or multiple string arguments containing column names 856 | * 857 | * @return self 858 | */ 859 | public function select($columns = null) 860 | { 861 | $this->reset(); 862 | $this->type = 'select'; 863 | 864 | if (is_array($columns)) { 865 | $this->select = $columns; 866 | } else { 867 | $this->select = \func_get_args(); 868 | } 869 | 870 | return $this; 871 | } 872 | 873 | /** 874 | * Alters which arguments to select 875 | * 876 | * Query is assumed to be in SELECT mode 877 | * See select() for usage 878 | * 879 | * @param array|string $columns Array or multiple string arguments containing column names 880 | * 881 | * @return self 882 | */ 883 | public function setSelect($columns = null) 884 | { 885 | if (is_array($columns)) { 886 | $this->select = $columns; 887 | } else { 888 | $this->select = \func_get_args(); 889 | } 890 | 891 | return $this; 892 | } 893 | 894 | /** 895 | * Get the columns staged to select 896 | * 897 | * @return array 898 | */ 899 | public function getSelect() 900 | { 901 | return $this->select; 902 | } 903 | 904 | /** 905 | * Activates the INSERT mode 906 | * 907 | * @return self 908 | */ 909 | public function insert() 910 | { 911 | $this->reset(); 912 | $this->type = 'insert'; 913 | 914 | return $this; 915 | } 916 | 917 | /** 918 | * Activates the REPLACE mode 919 | * 920 | * @return self 921 | */ 922 | public function replace() 923 | { 924 | $this->reset(); 925 | $this->type = 'replace'; 926 | 927 | return $this; 928 | } 929 | 930 | /** 931 | * Activates the UPDATE mode 932 | * 933 | * @param string $index The index to update into 934 | * 935 | * @return self 936 | */ 937 | public function update($index) 938 | { 939 | $this->reset(); 940 | $this->type = 'update'; 941 | $this->into($index); 942 | 943 | return $this; 944 | } 945 | 946 | /** 947 | * Activates the DELETE mode 948 | * 949 | * @return self 950 | */ 951 | public function delete() 952 | { 953 | $this->reset(); 954 | $this->type = 'delete'; 955 | 956 | return $this; 957 | } 958 | 959 | /** 960 | * FROM clause (Sphinx-specific since it works with multiple indexes) 961 | * func_get_args()-enabled 962 | * 963 | * @param array $array An array of indexes to use 964 | * 965 | * @return self 966 | */ 967 | public function from($array = null) 968 | { 969 | if (is_string($array)) { 970 | $this->from = \func_get_args(); 971 | } 972 | 973 | if (is_array($array) || $array instanceof \Closure || $array instanceof SphinxQL) { 974 | $this->from = $array; 975 | } 976 | 977 | return $this; 978 | } 979 | 980 | /** 981 | * MATCH clause (Sphinx-specific) 982 | * 983 | * @param mixed $column The column name (can be array, string, Closure, or MatchBuilder) 984 | * @param string $value The value 985 | * @param bool $half Exclude ", |, - control characters from being escaped 986 | * 987 | * @return self 988 | */ 989 | public function match($column, $value = null, $half = false) 990 | { 991 | if ($column === '*' || (is_array($column) && in_array('*', $column))) { 992 | $column = array(); 993 | } 994 | 995 | $this->match[] = array('column' => $column, 'value' => $value, 'half' => $half); 996 | 997 | return $this; 998 | } 999 | 1000 | /** 1001 | * WHERE clause 1002 | * 1003 | * Examples: 1004 | * $query->where('column', 'value'); 1005 | * // WHERE column = 'value' 1006 | * 1007 | * $query->where('column', '=', 'value'); 1008 | * // WHERE column = 'value' 1009 | * 1010 | * $query->where('column', '>=', 'value') 1011 | * // WHERE column >= 'value' 1012 | * 1013 | * $query->where('column', 'IN', array('value1', 'value2', 'value3')); 1014 | * // WHERE column IN ('value1', 'value2', 'value3') 1015 | * 1016 | * $query->where('column', 'BETWEEN', array('value1', 'value2')) 1017 | * // WHERE column BETWEEN 'value1' AND 'value2' 1018 | * // WHERE example BETWEEN 10 AND 100 1019 | * 1020 | * @param string $column The column name 1021 | * @param Expression|string|null|bool|array|int|float $operator The operator to use (if value is not null, you can 1022 | * use only string) 1023 | * @param Expression|string|null|bool|array|int|float $value The value to check against 1024 | * 1025 | * @return self 1026 | */ 1027 | public function where($column, $operator, $value = null) 1028 | { 1029 | if ($value === null) { 1030 | $value = $operator; 1031 | $operator = '='; 1032 | } 1033 | 1034 | $this->where[] = array( 1035 | 'column' => $column, 1036 | 'operator' => $operator, 1037 | 'value' => $value, 1038 | ); 1039 | 1040 | return $this; 1041 | } 1042 | 1043 | /** 1044 | * GROUP BY clause 1045 | * Adds to the previously added columns 1046 | * 1047 | * @param string $column A column to group by 1048 | * 1049 | * @return self 1050 | */ 1051 | public function groupBy($column) 1052 | { 1053 | $this->group_by[] = $column; 1054 | 1055 | return $this; 1056 | } 1057 | 1058 | /** 1059 | * GROUP N BY clause (SphinxQL-specific) 1060 | * Changes 'GROUP BY' into 'GROUP N BY' 1061 | * 1062 | * @param int $n Number of items per group 1063 | * 1064 | * @return self 1065 | */ 1066 | public function groupNBy($n) 1067 | { 1068 | $this->group_n_by = (int) $n; 1069 | 1070 | return $this; 1071 | } 1072 | 1073 | /** 1074 | * WITHIN GROUP ORDER BY clause (SphinxQL-specific) 1075 | * Adds to the previously added columns 1076 | * Works just like a classic ORDER BY 1077 | * 1078 | * @param string $column The column to group by 1079 | * @param string $direction The group by direction (asc/desc) 1080 | * 1081 | * @return self 1082 | */ 1083 | public function withinGroupOrderBy($column, $direction = null) 1084 | { 1085 | $this->within_group_order_by[] = array('column' => $column, 'direction' => $direction); 1086 | 1087 | return $this; 1088 | } 1089 | 1090 | /** 1091 | * HAVING clause 1092 | * 1093 | * Examples: 1094 | * $sq->having('column', 'value'); 1095 | * // HAVING column = 'value' 1096 | * 1097 | * $sq->having('column', '=', 'value'); 1098 | * // HAVING column = 'value' 1099 | * 1100 | * $sq->having('column', '>=', 'value') 1101 | * // HAVING column >= 'value' 1102 | * 1103 | * $sq->having('column', 'IN', array('value1', 'value2', 'value3')); 1104 | * // HAVING column IN ('value1', 'value2', 'value3') 1105 | * 1106 | * $sq->having('column', 'BETWEEN', array('value1', 'value2')) 1107 | * // HAVING column BETWEEN 'value1' AND 'value2' 1108 | * // HAVING example BETWEEN 10 AND 100 1109 | * 1110 | * @param string $column The column name 1111 | * @param string $operator The operator to use 1112 | * @param string $value The value to check against 1113 | * 1114 | * @return self 1115 | */ 1116 | public function having($column, $operator, $value = null) 1117 | { 1118 | if ($value === null) { 1119 | $value = $operator; 1120 | $operator = '='; 1121 | } 1122 | 1123 | $this->having = array( 1124 | 'column' => $column, 1125 | 'operator' => $operator, 1126 | 'value' => $value, 1127 | ); 1128 | 1129 | return $this; 1130 | } 1131 | 1132 | /** 1133 | * ORDER BY clause 1134 | * Adds to the previously added columns 1135 | * 1136 | * @param string $column The column to order on 1137 | * @param string $direction The ordering direction (asc/desc) 1138 | * 1139 | * @return self 1140 | */ 1141 | public function orderBy($column, $direction = null) 1142 | { 1143 | $this->order_by[] = array('column' => $column, 'direction' => $direction); 1144 | 1145 | return $this; 1146 | } 1147 | 1148 | /** 1149 | * LIMIT clause 1150 | * Supports also LIMIT offset, limit 1151 | * 1152 | * @param int $offset Offset if $limit is specified, else limit 1153 | * @param null|int $limit The limit to set, null for no limit 1154 | * 1155 | * @return self 1156 | */ 1157 | public function limit($offset, $limit = null) 1158 | { 1159 | if ($limit === null) { 1160 | $this->limit = (int) $offset; 1161 | 1162 | return $this; 1163 | } 1164 | 1165 | $this->offset($offset); 1166 | $this->limit = (int) $limit; 1167 | 1168 | return $this; 1169 | } 1170 | 1171 | /** 1172 | * OFFSET clause 1173 | * 1174 | * @param int $offset The offset 1175 | * 1176 | * @return self 1177 | */ 1178 | public function offset($offset) 1179 | { 1180 | $this->offset = (int) $offset; 1181 | 1182 | return $this; 1183 | } 1184 | 1185 | /** 1186 | * OPTION clause (SphinxQL-specific) 1187 | * Used by: SELECT 1188 | * 1189 | * @param string $name Option name 1190 | * @param Expression|array|string|int|bool|float|null $value Option value 1191 | * 1192 | * @return self 1193 | */ 1194 | public function option($name, $value) 1195 | { 1196 | $this->options[] = array('name' => $name, 'value' => $value); 1197 | 1198 | return $this; 1199 | } 1200 | 1201 | /** 1202 | * INTO clause 1203 | * Used by: INSERT, REPLACE 1204 | * 1205 | * @param string $index The index to insert/replace into 1206 | * 1207 | * @return self 1208 | */ 1209 | public function into($index) 1210 | { 1211 | $this->into = $index; 1212 | 1213 | return $this; 1214 | } 1215 | 1216 | /** 1217 | * Set columns 1218 | * Used in: INSERT, REPLACE 1219 | * func_get_args()-enabled 1220 | * 1221 | * @param array $array The array of columns 1222 | * 1223 | * @return self 1224 | */ 1225 | public function columns($array = array()) 1226 | { 1227 | if (is_array($array)) { 1228 | $this->columns = $array; 1229 | } else { 1230 | $this->columns = \func_get_args(); 1231 | } 1232 | 1233 | return $this; 1234 | } 1235 | 1236 | /** 1237 | * Set VALUES 1238 | * Used in: INSERT, REPLACE 1239 | * func_get_args()-enabled 1240 | * 1241 | * @param array $array The array of values matching the columns from $this->columns() 1242 | * 1243 | * @return self 1244 | */ 1245 | public function values($array) 1246 | { 1247 | if (is_array($array)) { 1248 | $this->values[] = $array; 1249 | } else { 1250 | $this->values[] = \func_get_args(); 1251 | } 1252 | 1253 | return $this; 1254 | } 1255 | 1256 | /** 1257 | * Set column and relative value 1258 | * Used in: INSERT, REPLACE 1259 | * 1260 | * @param string $column The column name 1261 | * @param string $value The value 1262 | * 1263 | * @return self 1264 | */ 1265 | public function value($column, $value) 1266 | { 1267 | if ($this->type === 'insert' || $this->type === 'replace') { 1268 | $this->columns[] = $column; 1269 | $this->values[0][] = $value; 1270 | } else { 1271 | $this->set[$column] = $value; 1272 | } 1273 | 1274 | return $this; 1275 | } 1276 | 1277 | /** 1278 | * Allows passing an array with the key as column and value as value 1279 | * Used in: INSERT, REPLACE, UPDATE 1280 | * 1281 | * @param array $array Array of key-values 1282 | * 1283 | * @return self 1284 | */ 1285 | public function set($array) 1286 | { 1287 | if ($this->columns === array_keys($array)) { 1288 | $this->values($array); 1289 | } else { 1290 | foreach ($array as $key => $item) { 1291 | $this->value($key, $item); 1292 | } 1293 | } 1294 | 1295 | return $this; 1296 | } 1297 | 1298 | /** 1299 | * Allows passing an array with the key as column and value as value 1300 | * Used in: INSERT, REPLACE, UPDATE 1301 | * 1302 | * @param Facet $facet 1303 | * 1304 | * @return self 1305 | */ 1306 | public function facet($facet) 1307 | { 1308 | $this->facets[] = $facet; 1309 | 1310 | return $this; 1311 | } 1312 | 1313 | /** 1314 | * Sets the characters used for escapeMatch(). 1315 | * 1316 | * @param array $array The array of characters to escape 1317 | * 1318 | * @return self 1319 | */ 1320 | public function setFullEscapeChars($array = array()) 1321 | { 1322 | if (!empty($array)) { 1323 | $this->escape_full_chars = $this->compileEscapeChars($array); 1324 | } 1325 | 1326 | return $this; 1327 | } 1328 | 1329 | /** 1330 | * Sets the characters used for halfEscapeMatch(). 1331 | * 1332 | * @param array $array The array of characters to escape 1333 | * 1334 | * @return self 1335 | */ 1336 | public function setHalfEscapeChars($array = array()) 1337 | { 1338 | if (!empty($array)) { 1339 | $this->escape_half_chars = $this->compileEscapeChars($array); 1340 | } 1341 | 1342 | return $this; 1343 | } 1344 | 1345 | /** 1346 | * Compiles an array containing the characters and escaped characters into a key/value configuration. 1347 | * 1348 | * @param array $array The array of characters to escape 1349 | * 1350 | * @return array An array of the characters and it's escaped counterpart 1351 | */ 1352 | public function compileEscapeChars($array = array()) 1353 | { 1354 | $result = array(); 1355 | foreach ($array as $character) { 1356 | $result[$character] = '\\'.$character; 1357 | } 1358 | 1359 | return $result; 1360 | } 1361 | 1362 | /** 1363 | * Escapes the query for the MATCH() function 1364 | * 1365 | * @param string $string The string to escape for the MATCH 1366 | * 1367 | * @return string The escaped string 1368 | */ 1369 | public function escapeMatch($string) 1370 | { 1371 | if (is_null($string)) { 1372 | return ''; 1373 | } 1374 | 1375 | if ($string instanceof Expression) { 1376 | return $string->value(); 1377 | } 1378 | 1379 | return mb_strtolower(str_replace(array_keys($this->escape_full_chars), array_values($this->escape_full_chars), $string), 'utf8'); 1380 | } 1381 | 1382 | /** 1383 | * Escapes the query for the MATCH() function 1384 | * Allows some of the control characters to pass through for use with a search field: -, |, " 1385 | * It also does some tricks to wrap/unwrap within " the string and prevents errors 1386 | * 1387 | * @param string $string The string to escape for the MATCH 1388 | * 1389 | * @return string The escaped string 1390 | */ 1391 | public function halfEscapeMatch($string) 1392 | { 1393 | if ($string instanceof Expression) { 1394 | return $string->value(); 1395 | } 1396 | 1397 | $string = str_replace(array_keys($this->escape_half_chars), array_values($this->escape_half_chars), $string); 1398 | 1399 | // this manages to lower the error rate by a lot 1400 | if (mb_substr_count($string, '"', 'utf8') % 2 !== 0) { 1401 | $string .= '"'; 1402 | } 1403 | 1404 | $string = preg_replace('/-[\s-]*-/u', '-', $string); 1405 | 1406 | $from_to_preg = array( 1407 | '/([-|])\s*$/u' => '\\\\\1', 1408 | '/\|[\s|]*\|/u' => '|', 1409 | '/(\S+)-(\S+)/u' => '\1\-\2', 1410 | '/(\S+)\s+-\s+(\S+)/u' => '\1 \- \2', 1411 | ); 1412 | 1413 | $string = mb_strtolower(preg_replace(array_keys($from_to_preg), array_values($from_to_preg), $string), 'utf8'); 1414 | 1415 | return $string; 1416 | } 1417 | 1418 | /** 1419 | * Clears the existing query build for new query when using the same SphinxQL instance. 1420 | * 1421 | * @return self 1422 | */ 1423 | public function reset() 1424 | { 1425 | $this->query = null; 1426 | $this->select = array(); 1427 | $this->from = array(); 1428 | $this->where = array(); 1429 | $this->match = array(); 1430 | $this->group_by = array(); 1431 | $this->group_n_by = null; 1432 | $this->within_group_order_by = array(); 1433 | $this->having = array(); 1434 | $this->order_by = array(); 1435 | $this->offset = null; 1436 | $this->limit = null; 1437 | $this->into = null; 1438 | $this->columns = array(); 1439 | $this->values = array(); 1440 | $this->set = array(); 1441 | $this->options = array(); 1442 | 1443 | return $this; 1444 | } 1445 | 1446 | /** 1447 | * @return self 1448 | */ 1449 | public function resetWhere() 1450 | { 1451 | $this->where = array(); 1452 | 1453 | return $this; 1454 | } 1455 | 1456 | /** 1457 | * @return self 1458 | */ 1459 | public function resetMatch() 1460 | { 1461 | $this->match = array(); 1462 | 1463 | return $this; 1464 | } 1465 | 1466 | /** 1467 | * @return self 1468 | */ 1469 | public function resetGroupBy() 1470 | { 1471 | $this->group_by = array(); 1472 | $this->group_n_by = null; 1473 | 1474 | return $this; 1475 | } 1476 | 1477 | /** 1478 | * @return self 1479 | */ 1480 | public function resetWithinGroupOrderBy() 1481 | { 1482 | $this->within_group_order_by = array(); 1483 | 1484 | return $this; 1485 | } 1486 | 1487 | /** 1488 | * @return self 1489 | */ 1490 | public function resetFacets() 1491 | { 1492 | $this->facets = array(); 1493 | 1494 | return $this; 1495 | } 1496 | 1497 | /** 1498 | * @return self 1499 | */ 1500 | public function resetHaving() 1501 | { 1502 | $this->having = array(); 1503 | 1504 | return $this; 1505 | } 1506 | 1507 | /** 1508 | * @return self 1509 | */ 1510 | public function resetOrderBy() 1511 | { 1512 | $this->order_by = array(); 1513 | 1514 | return $this; 1515 | } 1516 | 1517 | /** 1518 | * @return self 1519 | */ 1520 | public function resetOptions() 1521 | { 1522 | $this->options = array(); 1523 | 1524 | return $this; 1525 | } 1526 | } 1527 | --------------------------------------------------------------------------------