Query.php 12.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\sphinx;

use Yii;
11
use yii\base\InvalidCallException;
12
use yii\base\NotSupportedException;
13 14 15
use yii\db\Expression;

/**
16
 * Query represents a SELECT SQL statement.
17
 *
18 19
 * Query provides a set of methods to facilitate the specification of different clauses
 * in a SELECT statement. These methods can be chained together.
Paul Klimov committed
20
 *
21 22 23 24 25 26 27
 * By calling [[createCommand()]], we can get a [[Command]] instance which can be further
 * used to perform/execute the Sphinx query.
 *
 * For example,
 *
 * ~~~
 * $query = new Query;
28
 * $query->select('id, group_id')
29 30 31 32 33 34 35 36 37 38 39 40
 *     ->from('idx_item')
 *     ->limit(10);
 * // build and execute the query
 * $command = $query->createCommand();
 * // $command->sql returns the actual SQL
 * $rows = $command->queryAll();
 * ~~~
 *
 * Since Sphinx does not store the original indexed text, the snippets for the rows in query result
 * should be build separately via another query. You can simplify this workflow using [[snippetCallback]].
 *
 * Warning: even if you do not set any query limit, implicit LIMIT 0,20 is present by default!
Paul Klimov committed
41
 *
Carsten Brandt committed
42
 * @property Connection $connection Sphinx connection instance.
43
 *
44 45 46
 * @author Paul Klimov <klimov.paul@gmail.com>
 * @since 2.0
 */
47
class Query extends \yii\db\Query
48
{
49
    /**
50
     * @var string|Expression text, which should be searched in fulltext mode.
51
     * This value will be composed into MATCH operator inside the WHERE clause.
52
     * Note: this value will be processed by [[Connection::escapeMatchValue()]],
53 54
     * if you need to compose complex match condition use [[Expression]],
     * see [[match()]] for details.
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
     */
    public $match;
    /**
     * @var string WITHIN GROUP ORDER BY clause. This is a Sphinx specific extension
     * that lets you control how the best row within a group will to be selected.
     * The possible value matches the [[orderBy]] one.
     */
    public $within;
    /**
     * @var array per-query options in format: optionName => optionValue
     * They will compose OPTION clause. This is a Sphinx specific extension
     * that lets you control a number of per-query options.
     */
    public $options;
    /**
     * @var callable PHP callback, which should be used to fetch source data for the snippets.
     * Such callback will receive array of query result rows as an argument and must return the
     * array of snippet source strings in the order, which match one of incoming rows.
     * For example:
     * ~~~
     * $query = new Query;
     * $query->from('idx_item')
     *     ->match('pencil')
     *     ->snippetCallback(function ($rows) {
     *         $result = [];
     *         foreach ($rows as $row) {
     *             $result[] = file_get_contents('/path/to/index/files/' . $row['id'] . '.txt');
     *         }
     *         return $result;
     *     })
     *     ->all();
     * ~~~
     */
    public $snippetCallback;
    /**
     * @var array query options for the call snippet.
     */
    public $snippetOptions;
93

94 95 96 97 98
    /**
     * @var Connection the Sphinx connection used to generate the SQL statements.
     */
    private $_connection;

99

100
    /**
101 102
     * @param Connection $connection Sphinx connection instance
     * @return static the query object itself
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
     */
    public function setConnection($connection)
    {
        $this->_connection = $connection;

        return $this;
    }

    /**
     * @return Connection Sphinx connection instance
     */
    public function getConnection()
    {
        if ($this->_connection === null) {
            $this->_connection = $this->defaultConnection();
        }

        return $this->_connection;
    }

    /**
     * @return Connection default connection value.
     */
    protected function defaultConnection()
    {
128
        return Yii::$app->get('sphinx');
129 130 131 132
    }

    /**
     * Creates a Sphinx command that can be used to execute this query.
133
     * @param Connection $db the Sphinx connection used to generate the SQL statement.
134 135
     * If this parameter is not given, the `sphinx` application component will be used.
     * @return Command the created Sphinx command instance.
136
     */
137
    public function createCommand($db = null)
138
    {
139 140 141
        $this->setConnection($db);
        $db = $this->getConnection();
        list ($sql, $params) = $db->getQueryBuilder()->build($this);
142

143
        return $db->createCommand($sql, $params);
144 145 146
    }

    /**
147
     * @inheritdoc
148
     */
149
    public function populate($rows)
150
    {
151
        return parent::populate($this->fillUpSnippets($rows));
152 153 154
    }

    /**
155
     * @inheritdoc
156 157 158
     */
    public function one($db = null)
    {
159
        $row = parent::one($db);
160 161 162 163 164 165 166 167 168 169
        if ($row !== false) {
            list ($row) = $this->fillUpSnippets([$row]);
        }

        return $row;
    }

    /**
     * Sets the fulltext query text. This text will be composed into
     * MATCH operator inside the WHERE clause.
170
     * Note: this value will be processed by [[Connection::escapeMatchValue()]],
171
     * if you need to compose complex match condition use [[Expression]]:
172
     * ~~~
173 174 175 176
     * $query = new Query;
     * $query->from('my_index')
     *     ->match(new Expression(':match', ['match' => '@(content) ' . Yii::$app->sphinx->escapeMatchValue($matchValue)]))
     *     ->all();
177
     * ~~~
178
     *
179
     * @param string $query fulltext query text.
180 181 182 183 184 185 186 187 188
     * @return static the query object itself
     */
    public function match($query)
    {
        $this->match = $query;
        return $this;
    }

    /**
189
     * @inheritdoc
190
     */
191
    public function join($type, $table, $on = '', $params = [])
192
    {
193
        throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
194 195 196
    }

    /**
197
     * @inheritdoc
198
     */
199
    public function innerJoin($table, $on = '', $params = [])
200
    {
201
        throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
202 203 204
    }

    /**
205
     * @inheritdoc
206
     */
207
    public function leftJoin($table, $on = '', $params = [])
208
    {
209
        throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
210 211 212
    }

    /**
213
     * @inheritdoc
214
     */
215
    public function rightJoin($table, $on = '', $params = [])
216
    {
217
        throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
218 219 220 221
    }

    /**
     * Sets the query options.
222
     * @param array $options query options in format: optionName => optionValue
223 224 225 226 227 228 229 230 231 232 233 234
     * @return static the query object itself
     * @see addOptions()
     */
    public function options($options)
    {
        $this->options = $options;

        return $this;
    }

    /**
     * Adds additional query options.
235
     * @param array $options query options in format: optionName => optionValue
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
     * @return static the query object itself
     * @see options()
     */
    public function addOptions($options)
    {
        if (is_array($this->options)) {
            $this->options = array_merge($this->options, $options);
        } else {
            $this->options = $options;
        }

        return $this;
    }

    /**
     * Sets the WITHIN GROUP ORDER BY part of the query.
252 253 254 255 256 257
     * @param string|array $columns the columns (and the directions) to find best row within a group.
     * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array
     * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`).
     * The method will automatically quote the column names unless a column contains some parenthesis
     * (which means the column contains a DB expression).
     * @return static the query object itself
258 259 260 261 262 263 264 265 266 267 268
     * @see addWithin()
     */
    public function within($columns)
    {
        $this->within = $this->normalizeOrderBy($columns);

        return $this;
    }

    /**
     * Adds additional WITHIN GROUP ORDER BY columns to the query.
269 270 271 272 273 274
     * @param string|array $columns the columns (and the directions) to find best row within a group.
     * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array
     * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`).
     * The method will automatically quote the column names unless a column contains some parenthesis
     * (which means the column contains a DB expression).
     * @return static the query object itself
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
     * @see within()
     */
    public function addWithin($columns)
    {
        $columns = $this->normalizeOrderBy($columns);
        if ($this->within === null) {
            $this->within = $columns;
        } else {
            $this->within = array_merge($this->within, $columns);
        }

        return $this;
    }

    /**
     * Sets the PHP callback, which should be used to retrieve the source data
     * for the snippets building.
292 293
     * @param callable $callback PHP callback, which should be used to fetch source data for the snippets.
     * @return static the query object itself
294 295 296 297 298 299 300 301 302 303 304
     * @see snippetCallback
     */
    public function snippetCallback($callback)
    {
        $this->snippetCallback = $callback;

        return $this;
    }

    /**
     * Sets the call snippets query options.
305
     * @param array $options call snippet options in format: option_name => option_value
306 307 308 309 310 311 312 313 314 315 316 317 318
     * @return static the query object itself
     * @see snippetCallback
     */
    public function snippetOptions($options)
    {
        $this->snippetOptions = $options;

        return $this;
    }

    /**
     * Fills the query result rows with the snippets built from source determined by
     * [[snippetCallback]] result.
319
     * @param array $rows raw query result rows.
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
     * @return array|ActiveRecord[] query result rows with filled up snippets.
     */
    protected function fillUpSnippets($rows)
    {
        if ($this->snippetCallback === null) {
            return $rows;
        }
        $snippetSources = call_user_func($this->snippetCallback, $rows);
        $snippets = $this->callSnippets($snippetSources);
        $snippetKey = 0;
        foreach ($rows as $key => $row) {
            $rows[$key]['snippet'] = $snippets[$snippetKey];
            $snippetKey++;
        }

        return $rows;
    }

    /**
     * Builds a snippets from provided source data.
340
     * @param array $source the source data to extract a snippet from.
341
     * @throws InvalidCallException in case [[match]] is not specified.
342
     * @return array snippets list.
343 344 345 346 347 348 349 350
     */
    protected function callSnippets(array $source)
    {
        return $this->callSnippetsInternal($source, $this->from[0]);
    }

    /**
     * Builds a snippets from provided source data by the given index.
351 352 353
     * @param array $source the source data to extract a snippet from.
     * @param string $from name of the source index.
     * @return array snippets list.
354 355 356 357 358 359 360 361 362 363 364 365 366 367
     * @throws InvalidCallException in case [[match]] is not specified.
     */
    protected function callSnippetsInternal(array $source, $from)
    {
        $connection = $this->getConnection();
        $match = $this->match;
        if ($match === null) {
            throw new InvalidCallException('Unable to call snippets: "' . $this->className() . '::match" should be specified.');
        }

        return $connection->createCommand()
            ->callSnippets($from, $source, $match, $this->snippetOptions)
            ->queryColumn();
    }
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399

    /**
     * Creates a new Query object and copies its property values from an existing one.
     * The properties being copies are the ones to be used by query builders.
     * @param Query $from the source query object
     * @return Query the new Query object
     */
    public static function create($from)
    {
        return new self([
            'where' => $from->where,
            'limit' => $from->limit,
            'offset' => $from->offset,
            'orderBy' => $from->orderBy,
            'indexBy' => $from->indexBy,
            'select' => $from->select,
            'selectOption' => $from->selectOption,
            'distinct' => $from->distinct,
            'from' => $from->from,
            'groupBy' => $from->groupBy,
            'join' => $from->join,
            'having' => $from->having,
            'union' => $from->union,
            'params' => $from->params,
            // Sphinx specifics :
            'options' => $from->options,
            'within' => $from->within,
            'match' => $from->match,
            'snippetCallback' => $from->snippetCallback,
            'snippetOptions' => $from->snippetOptions,
        ]);
    }
400
}