├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── UPGRADE.md
├── composer.json
└── src
├── ActiveDataProvider.php
├── ActiveFixture.php
├── ActiveQuery.php
├── ActiveRecord.php
├── ColumnSchema.php
├── Command.php
├── Connection.php
├── IndexSchema.php
├── MatchBuilder.php
├── MatchExpression.php
├── Query.php
├── QueryBuilder.php
├── Schema.php
└── gii
└── model
├── Generator.php
├── default
└── model.php
└── form.php
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Yii Framework 2 sphinx extension Change Log
2 | ===========================================
3 |
4 | 2.0.17 under development
5 | ------------------------
6 |
7 | - no changes in this release.
8 |
9 |
10 | 2.0.16 March 21, 2024
11 | ---------------------
12 |
13 | - Enh #144: Add `UINT_SET` attribute type support (@vjik)
14 |
15 |
16 | 2.0.15 November 18, 2022
17 | ------------------------
18 |
19 | - Bug #127: Fixed warning "Undefined array key" in Query at facets collecting when moving to PHP 8.1 (kulikov6)
20 |
21 |
22 | 2.0.14 December 30, 2021
23 | ------------------------
24 |
25 | - Enh #123: Added implementation of methods for creating, deleting and updating real-time index schema for Sphinx version 3 (troizet)
26 |
27 |
28 | 2.0.13 February 22, 2020
29 | ------------------------
30 |
31 | - Bug #114: Fixes exception "Call to a member function getSnippetSource() on array" when call `\yii\sphinx\ActiveQuery::search` broken by v2.0.11 (miketsoft)
32 |
33 |
34 | 2.0.12 July 02, 2019
35 | --------------------
36 |
37 | - Bug #111: Fixed SQL error when running Yii 2.0.21+ (brandonkelly)
38 |
39 |
40 | 2.0.11 September 23, 2018
41 | -------------------------
42 |
43 | - Bug #101: Fixed `yii\sphinx\ActiveQuery::all()` unable to follow instructions given by method `indexBy()` (bitdevelopment)
44 |
45 |
46 | 2.0.10 February 13, 2018
47 | ------------------------
48 |
49 | - Bug #90: Fixed `yii\sphinx\Schema::findColumns()` unable to merge 'field' and 'attribute' columns with same name (maz0717, klimov-paul)
50 | - Bug #92: Fixed `yii\sphinx\QueryBuilder::buildInCondition()` incompatibility with PHP 7.2 (klimov-paul)
51 | - Enh #93: `yii\sphinx\QueryBuilder::callSnippets()` now automatically casts snippet source to string (klimov-paul)
52 | - Enh: `yii\sphinx\QueryBuilder` now supports `Traversable` objects for use in `in` conditions (klimov-paul)
53 |
54 |
55 | 2.0.9 November 03, 2017
56 | -----------------------
57 |
58 | - Bug #75: Fixed empty MVA field value is fetches as array with null element instead of empty array (batyrmastyr, klimov-paul)
59 | - Bug #78: Fixed `yii\sphinx\Query::where()` does not add params from directly passed `yii\db\Expression` (klimov-paul)
60 | - Bug #82: Fixed `yii\sphinx\Query::select()` does not apply alias for `yii\db\Expression` value (klimov-paul)
61 | - Bug #83: Fixed `yii\sphinx\ActiveRecord::update()` causes attribute value lost in case of field update (klimov-paul)
62 | - Bug #87: Fixed `yii\sphinx\Command::getRawSql()` does not parse float params (klimov-paul)
63 | - Bug: Usage of deprecated `yii\base\Object` changed to `yii\base\BaseObject` allowing compatibility with PHP 7.2 (klimov-paul)
64 | - Enh #73: `isRuntime` field of `yii\sphinx\IndexSchema` renamed to `isRt` for consistency with official docs (klimov-paul)
65 |
66 |
67 | 2.0.8 May 15, 2017
68 | ------------------
69 |
70 | - Bug #71: Fixed PHP type for `sql_attr_timestamp` attribute incorrectly detected as string (klimov-paul)
71 |
72 |
73 | 2.0.7 February 15, 2017
74 | -----------------------
75 |
76 | - Bug #69: Fixed `yii\sphinx\Query::andFilterWhere()` quotes integer column value in case comparison operator is used (klimov-paul)
77 | - Enh #67: Added support for `yii\db\QueryInterface::emulateExecution()` to force returning an empty result for a query (klimov-paul)
78 | - Enh #70: Added `filterMatch()` method to `yii\sphinx\MatchExpression` to allow easy addition of search filter conditions by ignoring empty search fields (klimov-paul)
79 |
80 |
81 | 2.0.6 September 02, 2016
82 | ------------------------
83 |
84 | - Bug #8: Fixed usage of the float values in SphinxQL bound params (klimov-paul)
85 | - Bug #45: Fixed `yii\sphinx\Schema` unable to determine primary key for distribute index (klimov-paul)
86 | - Bug #61: Fixed `yii\sphinx\QueryBuilder::callSnippets()` unable to handle 'match' specified as `yii\db\Expression` instance (klimov-paul)
87 | - Enh #26: Added `yii\sphinx\Query::groupLimit` allowing limit matches in 'group by' (klimov-paul)
88 | - Enh #53: Added `yii\sphinx\MatchExpression` allowing advanced composition of 'MATCH' expressions (sa-kirich, klimov-paul)
89 | - Enh #56: `yii\sphinx\Schema` now able to get schema for distributed index in case at least one (not only the first one) of local indexes is available (kdietrich, klimov-paul)
90 | - Enh #58: Added fallback for distributed index columns detection at `yii\sphinx\Schema` (klimov-paul)
91 |
92 |
93 | 2.0.5 September 23, 2015
94 | ------------------------
95 |
96 | - Bug #13: Fixed `yii\sphinx\ActiveDataProvider` breaks the pagination if `yii\data\Pagination::validatePage` enabled even, if `yii\sphinx\Query::showMeta` is not set (klimov-paul)
97 | - Bug #21: `yii\sphinx\Query` unable to retrieve facet named in camel-case notation (klimov-paul)
98 | - Bug #27: Fixed `yii\sphinx\ActiveQuery::search()` produces 'unbuffered query' error if 'facet' or 'show meta' are used (klimov-paul)
99 | - Bug #30: Fixed `yii\sphinx\ActiveQuery` does not perform typecast for condition values (klimov-paul)
100 | - Bug #31: Fixed `yii\sphinx\QueryBuilder::buildInCondition()` fails produces invalid SphinxQL for empty values (klimov-paul)
101 | - Bug #43: Fixed `yii\sphinx\QueryBuilder::buildWithin()` does not define sort order for `SORT_ASC` (klimov-paul)
102 | - Enh #11: `yii\sphinx\ActiveDataProvider` now disables `yii\data\Pagination::validatePage` automatically if `yii\sphinx\Query::showMeta` is set (klimov-paul)
103 | - Enh #11: `yii\sphinx\ActiveDataProvider` now disables `yii\data\Pagination::validatePage` automatically if `yii\sphinx\Query::showMeta` is set (klimov-paul)
104 | - Enh #17: Using total_found instead of total in `yii\sphinx\ActiveDataProvider::prepareTotalCount` (lmuzinic)
105 | - Enh #29: Added `yii\sphinx\Command` automatically skips `null` values while inserting data (klimov-paul)
106 |
107 |
108 | 2.0.4 May 10, 2015
109 | ------------------
110 |
111 | - Enh: Fetching 'SHOW META' info added to `yii\sphinx\Query` (klimov-paul)
112 | - Enh #2053: Added fixture support via `yii\sphinx\ActiveFixture` (klimov-paul)
113 | - Enh #5234: Facets fetching added to `yii\sphinx\Query` (klimov-paul)
114 |
115 |
116 | 2.0.3 March 01, 2015
117 | --------------------
118 |
119 | - Bug #7198: `yii\sphinx\Query` no longer attempts to call snippets for the empty query result set (Hrumpa)
120 |
121 |
122 | 2.0.2 January 11, 2015
123 | ----------------------
124 |
125 | - Bug #6621: Creating sub query at `yii\sphinx\Query::queryScalar()` fixed (klimov-paul)
126 |
127 |
128 | 2.0.1 December 07, 2014
129 | -----------------------
130 |
131 | - Bug #5601: Simple conditions in Query::where() and ActiveQuery::where() did not allow `yii\db\Expression` to be used as the value (cebe, stevekr)
132 | - Bug #5634: Fixed `yii\sphinx\QueryBuilder` does not support comparison operators (>,<,>= etc) in where specification (klimov-paul)
133 | - Bug #6164: Added missing support for `yii\db\Exression` to QueryBuilder `LIKE` conditions (cebe)
134 | - Enh #5223: Query builder now supports selecting sub-queries as columns (qiangxue)
135 |
136 |
137 | 2.0.0 October 12, 2014
138 | ----------------------
139 |
140 | - Enh #5211: `yii\sphinx\Query` now supports 'HAVING' (klimov-paul)
141 |
142 |
143 | 2.0.0-rc September 27, 2014
144 | ---------------------------
145 |
146 | - Bug #3668: Escaping of the special characters at 'MATCH' statement added (klimov-paul)
147 | - Bug #4018: AR relation eager loading does not work with db models (klimov-paul)
148 | - Bug #4375: Distributed indexes support provided (klimov-paul)
149 | - Bug #4830: `ActiveQuery` instance reusage ability granted (klimov-paul)
150 | - Enh #3520: Added `unlinkAll()`-method to active record to remove all records of a model relation (NmDimas, samdark, cebe)
151 | - Enh #4048: Added `init` event to `ActiveQuery` classes (qiangxue)
152 | - Enh #4086: changedAttributes of afterSave Event now contain old values (dizews)
153 | - Enh: Added support for using sub-queries when building a DB query with `IN` condition (qiangxue)
154 | - Chg #2287: Split `yii\sphinx\ColumnSchema::typecast()` into two methods `phpTypecast()` and `dbTypecast()` to allow specifying PDO type explicitly (cebe)
155 |
156 |
157 | 2.0.0-beta April 13, 2014
158 | -------------------------
159 |
160 | - Bug #1993: afterFind event in AR is now called after relations have been populated (cebe, creocoder)
161 | - Bug #2160: SphinxQL does not support `OFFSET` (qiangxue, romeo7)
162 | - Enh #1398: Refactor ActiveRecord to use BaseActiveRecord class of the framework (klimov-paul)
163 | - Enh #2002: Added filterWhere() method to yii\spinx\Query to allow easy addition of search filter conditions by ignoring empty search fields (samdark, cebe)
164 | - Enh #2892: ActiveRecord dirty attributes are now reset after call to `afterSave()` so information about changed attributes is available in `afterSave`-event (cebe)
165 | - Enh #3964: Gii generator for Active Record model added (klimov-paul)
166 | - Chg #2281: Renamed `ActiveRecord::create()` to `populateRecord()` and changed signature. This method will not call instantiate() anymore (cebe)
167 | - Chg #2146: Removed `ActiveRelation` class and moved the functionality to `ActiveQuery`.
168 | All relational queries are now directly served by `ActiveQuery` allowing to use
169 | custom scopes in relations (cebe)
170 |
171 |
172 | 2.0.0-alpha, December 1, 2013
173 | -----------------------------
174 |
175 | - Initial release.
176 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com)
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions
6 | are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in
12 | the documentation and/or other materials provided with the
13 | distribution.
14 | * Neither the name of Yii Software LLC nor the names of its
15 | contributors may be used to endorse or promote products derived
16 | from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 | POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Sphinx Extension for Yii 2
6 |
7 |
8 |
9 | This extension adds [Sphinx](https://sphinxsearch.com/docs) full text search engine extension for the [Yii framework 2.0](https://www.yiiframework.com).
10 | It supports all Sphinx features including [Real-time Indexes](https://sphinxsearch.com/docs/current.html#rt-indexes).
11 |
12 | For license information check the [LICENSE](LICENSE.md)-file.
13 |
14 | Documentation is at [docs/guide/README.md](docs/guide/README.md).
15 |
16 | [](https://packagist.org/packages/yiisoft/yii2-sphinx)
17 | [](https://packagist.org/packages/yiisoft/yii2-sphinx)
18 | [](https://github.com/yiisoft/yii2-sphinx/actions?query=workflow%3Abuild)
19 |
20 |
21 | Requirements
22 | ------------
23 |
24 | At least Sphinx version 2.0 is required. However, in order to use all extension features, Sphinx version 2.2.3 or
25 | higher is required.
26 |
27 |
28 | Installation
29 | ------------
30 |
31 | The preferred way to install this extension is through [composer](https://getcomposer.org/download/).
32 |
33 | Either run
34 |
35 | ```
36 | php composer.phar require --prefer-dist yiisoft/yii2-sphinx
37 | ```
38 |
39 | or add
40 |
41 | ```json
42 | "yiisoft/yii2-sphinx": "~2.0.0"
43 | ```
44 |
45 | to the require section of your composer.json.
46 |
47 |
48 | Configuration
49 | -------------
50 |
51 | This extension interacts with Sphinx search daemon using MySQL protocol and [SphinxQL](https://sphinxsearch.com/docs/current.html#sphinxql) query language.
52 | In order to setup Sphinx "searchd" to support MySQL protocol following configuration should be added:
53 |
54 | ```
55 | searchd
56 | {
57 | listen = localhost:9306:mysql41
58 | ...
59 | }
60 | ```
61 |
62 | To use this extension, simply add the following code in your application configuration:
63 |
64 | ```php
65 | return [
66 | //....
67 | 'components' => [
68 | 'sphinx' => [
69 | 'class' => 'yii\sphinx\Connection',
70 | 'dsn' => 'mysql:host=127.0.0.1;port=9306;',
71 | 'username' => '',
72 | 'password' => '',
73 | ],
74 | ],
75 | ];
76 | ```
77 |
--------------------------------------------------------------------------------
/UPGRADE.md:
--------------------------------------------------------------------------------
1 | Upgrading Instructions for Yii Framework v2 Sphinx Extension
2 | ============================================================
3 |
4 | !!!IMPORTANT!!!
5 |
6 | The following upgrading instructions are cumulative. That is,
7 | if you want to upgrade from version A to version C and there is
8 | version B between A and C, you need to following the instructions
9 | for both A and B.
10 |
11 | Upgrade from Yii 2.0.5
12 | ----------------------
13 |
14 | * The signature of the `\yii\sphinx\QueryBuilder::buildGroupBy()` method has been changed.
15 | Make sure you invoke this method correctly. In case you are extending related class and override this method,
16 | you should check, if it matches parent declaration.
17 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yiisoft/yii2-sphinx",
3 | "description": "Sphinx full text search engine extension for the Yii framework",
4 | "keywords": ["yii2", "sphinx", "active-record", "search", "fulltext"],
5 | "type": "yii2-extension",
6 | "license": "BSD-3-Clause",
7 | "support": {
8 | "issues": "https://github.com/yiisoft/yii2-sphinx/issues",
9 | "forum": "https://www.yiiframework.com/forum/",
10 | "wiki": "https://www.yiiframework.com/wiki/",
11 | "irc": "ircs://irc.libera.chat:6697/yii",
12 | "source": "https://github.com/yiisoft/yii2-sphinx"
13 | },
14 | "authors": [
15 | {
16 | "name": "Paul Klimov",
17 | "email": "klimov.paul@gmail.com"
18 | }
19 | ],
20 | "require": {
21 | "yiisoft/yii2": "~2.0.13",
22 | "ext-pdo": "*",
23 | "ext-pdo_mysql": "*"
24 | },
25 | "require-dev": {
26 | "phpunit/phpunit": "4.8.27|~5.7.21|^6.2"
27 | },
28 | "repositories": [
29 | {
30 | "type": "composer",
31 | "url": "https://asset-packagist.org"
32 | }
33 | ],
34 | "autoload": {
35 | "psr-4": { "yii\\sphinx\\": "src" }
36 | },
37 | "extra": {
38 | "branch-alias": {
39 | "dev-master": "2.0.x-dev"
40 | }
41 | },
42 | "config": {
43 | "sort-packages": true,
44 | "allow-plugins": {
45 | "yiisoft/yii2-composer": true
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/ActiveDataProvider.php:
--------------------------------------------------------------------------------
1 | Post::find()->facets(['author_id', 'category_id']),
22 | * 'pagination' => [
23 | * 'pageSize' => 20,
24 | * ],
25 | * ]);
26 | *
27 | * // get the posts in the current page
28 | * $posts = $provider->getModels();
29 | *
30 | * // get all facets
31 | * $facets = $provider->getFacets();
32 | *
33 | * // get particular facet
34 | * $authorFacet = $provider->getFacet('author_id');
35 | * ```
36 | *
37 | * In case [[Query::showMeta]] is set ActiveDataProvider will fetch total count value from the query meta information,
38 | * avoiding extra counting query:
39 | *
40 | * ```php
41 | * $provider = new ActiveDataProvider([
42 | * 'query' => Post::find()->showMeta(true),
43 | * 'pagination' => [
44 | * 'pageSize' => 20,
45 | * ],
46 | * ]);
47 | *
48 | * $totalCount = $provider->getTotalCount(); // fetched from meta information
49 | * ```
50 | *
51 | * Note: when using 'meta' information results total count will be fetched after pagination limit applying,
52 | * which eliminates ability to verify if requested page number actually exist. Data provider disables `yii\data\Pagination::validatePage`
53 | * automatically in this case.
54 | *
55 | * Note: because pagination offset and limit may exceed Sphinx 'max_matches' bounds, data provider will set 'max_matches'
56 | * option automatically based on those values. However, if [[Query::showMeta]] is set, such adjustment is not performed
57 | * as it will break total count calculation, so you'll have to deal with 'max_matches' bounds on your own.
58 | *
59 | * @property array $facets Query facet results.
60 | * @property array $meta Search query meta info.
61 | *
62 | * @author Paul Klimov
63 | * @since 2.0.4
64 | */
65 | class ActiveDataProvider extends \yii\data\ActiveDataProvider
66 | {
67 | /**
68 | * @var array search query meta info in format: name => value.
69 | */
70 | private $_meta;
71 | /**
72 | * @var array query facet results.
73 | */
74 | private $_facets;
75 |
76 |
77 | /**
78 | * @param array $facets query facet results.
79 | */
80 | public function setFacets($facets)
81 | {
82 | $this->_facets = $facets;
83 | }
84 |
85 | /**
86 | * @return array query facet results.
87 | */
88 | public function getFacets()
89 | {
90 | if (!is_array($this->_facets)) {
91 | $this->prepare();
92 | }
93 | return $this->_facets;
94 | }
95 |
96 | /**
97 | * @param array $meta search query meta info
98 | */
99 | public function setMeta($meta)
100 | {
101 | $this->_meta = $meta;
102 | }
103 |
104 | /**
105 | * @return array search query meta info
106 | */
107 | public function getMeta()
108 | {
109 | if (!is_array($this->_meta)) {
110 | $this->prepare();
111 | }
112 | return $this->_meta;
113 | }
114 |
115 | /**
116 | * Returns results of the specified facet.
117 | * @param string $name facet name
118 | * @throws InvalidCallException if requested facet does not present in results.
119 | * @return array facet results.
120 | */
121 | public function getFacet($name)
122 | {
123 | $facets = $this->getFacets();
124 | if (!isset($facets[$name])) {
125 | throw new InvalidCallException("Facet '{$name}' does not present.");
126 | }
127 | return $facets[$name];
128 | }
129 |
130 | /**
131 | * {@inheritdoc}
132 | */
133 | protected function prepareModels()
134 | {
135 | if (!$this->query instanceof Query) {
136 | throw new InvalidConfigException('The "query" property must be an instance "' . Query::className() . '" or its subclasses.');
137 | }
138 | $query = clone $this->query;
139 | if (($pagination = $this->getPagination()) !== false) {
140 | if (empty($query->showMeta)) {
141 | $pagination->totalCount = $this->getTotalCount();
142 | $limit = $pagination->getLimit();
143 | $offset = $pagination->getOffset();
144 | // pagination may exceed 'max_matches' boundary producing query error
145 | if (!isset($query->options['max_matches'])) {
146 | $query->options['max_matches'] = $offset + $limit;
147 | }
148 | $query->limit($limit)->offset($offset);
149 | } else {
150 | // pagination fails to validate page number, if total count is unknown at this stage
151 | $pagination->validatePage = false;
152 | $query->limit($pagination->getLimit())->offset($pagination->getOffset());
153 | }
154 | }
155 | if (($sort = $this->getSort()) !== false) {
156 | $query->addOrderBy($sort->getOrders());
157 | }
158 |
159 | $results = $query->search($this->db);
160 | $this->setMeta($results['meta']);
161 | $this->setFacets($results['facets']);
162 |
163 | if ($pagination !== false) {
164 | $pagination->totalCount = $this->getTotalCount();
165 | }
166 |
167 | return $results['hits'];
168 | }
169 |
170 | /**
171 | * {@inheritdoc}
172 | */
173 | protected function prepareTotalCount()
174 | {
175 | if (!$this->query instanceof Query) {
176 | throw new InvalidConfigException('The "query" property must be an instance "' . Query::className() . '" or its subclasses.');
177 | }
178 |
179 | if (!empty($this->query->showMeta)) {
180 | $meta = $this->getMeta();
181 |
182 | if (isset($meta['total_found'])) {
183 | return (int) $meta['total_found'];
184 | }
185 |
186 | if (isset($meta['total'])) {
187 | return (int) $meta['total'];
188 | }
189 | }
190 |
191 | $query = clone $this->query;
192 | return (int) $query->limit(-1)->offset(-1)->orderBy([])->facets([])->showMeta(false)->count('*', $this->db);
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/src/ActiveFixture.php:
--------------------------------------------------------------------------------
1 |
31 | * @since 2.0.4
32 | */
33 | class ActiveFixture extends BaseActiveFixture
34 | {
35 | /**
36 | * @var Connection|array|string the Sphinx connection object or the application component ID of the Sphinx connection
37 | * or a configuration array for creating the object.
38 | */
39 | public $db = 'sphinx';
40 | /**
41 | * @var string the name of the Sphinx index that this fixture is about. If this property is not set,
42 | * the index name will be determined via [[modelClass]].
43 | * @see modelClass
44 | */
45 | public $indexName;
46 | /**
47 | * @var string|bool the file path or path alias of the data file that contains the fixture data
48 | * to be returned by [[getData()]]. If this is not set, it will default to `FixturePath/data/IndexName.php`,
49 | * where `FixturePath` stands for the directory containing this fixture class, and `IndexName` stands for the
50 | * name of the index associated with this fixture. You can set this property to be false to prevent loading any data.
51 | */
52 | public $dataFile;
53 |
54 | /**
55 | * @var IndexSchema the schema for the index associated with this fixture
56 | */
57 | private $_index;
58 |
59 |
60 | /**
61 | * {@inheritdoc}
62 | */
63 | public function init()
64 | {
65 | parent::init();
66 | if (!isset($this->modelClass) && !isset($this->indexName)) {
67 | throw new InvalidConfigException('Either "modelClass" or "indexName" must be set.');
68 | }
69 | }
70 |
71 | /**
72 | * Loads the fixture.
73 | *
74 | * The default implementation will first clean up the table by calling [[resetIndex()]].
75 | * It will then populate the index with the data returned by [[getData()]].
76 | *
77 | * If you override this method, you should consider calling the parent implementation
78 | * so that the data returned by [[getData()]] can be populated into the index.
79 | */
80 | public function load()
81 | {
82 | $this->resetIndex();
83 | $this->data = [];
84 | $index = $this->getIndexSchema();
85 | foreach ($this->getData() as $alias => $row) {
86 | $this->db->createCommand()->insert($index->name, $row)->execute();
87 | $this->data[$alias] = $row;
88 | }
89 | }
90 |
91 | /**
92 | * Returns the fixture data.
93 | *
94 | * The default implementation will try to return the fixture data by including the external file specified by [[dataFile]].
95 | * The file should return an array of data rows (column name => column value), each corresponding to a row in the index.
96 | *
97 | * If the data file does not exist, an empty array will be returned.
98 | *
99 | * @return array the data rows to be inserted into the index.
100 | */
101 | protected function getData()
102 | {
103 | if ($this->dataFile === null) {
104 | $class = new \ReflectionClass($this);
105 | $dataFile = dirname($class->getFileName()) . '/data/' . $this->getIndexSchema()->name . '.php';
106 | return is_file($dataFile) ? require($dataFile) : [];
107 | } else {
108 | return parent::getData();
109 | }
110 | }
111 |
112 | /**
113 | * Truncates the specified index removing all existing data from it.
114 | * This method is called before populating fixture data into the index associated with this fixture.
115 | */
116 | protected function resetIndex()
117 | {
118 | $index = $this->getIndexSchema();
119 | $this->db->createCommand()->truncateIndex($index->name)->execute();
120 | }
121 |
122 | /**
123 | * @return IndexSchema the schema information of the database table associated with this fixture.
124 | * @throws \yii\base\InvalidConfigException if the index does not exist or not a runtime type
125 | */
126 | public function getIndexSchema()
127 | {
128 | if ($this->_index !== null) {
129 | return $this->_index;
130 | }
131 |
132 | $db = $this->db;
133 | $indexName = $this->indexName;
134 | if ($indexName === null) {
135 | /* @var $modelClass ActiveRecord */
136 | $modelClass = $this->modelClass;
137 | $indexName = $modelClass::indexName();
138 | }
139 |
140 | $this->_index = $db->getSchema()->getIndexSchema($indexName);
141 | if ($this->_index === null) {
142 | throw new InvalidConfigException("Index does not exist: {$indexName}");
143 | }
144 | if (!$this->_index->isRt) {
145 | throw new InvalidConfigException("'{$indexName}' is not a runtime index. Only runtime indexes are supported.'");
146 | }
147 |
148 | return $this->_index;
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/ActiveQuery.php:
--------------------------------------------------------------------------------
1 | with('source')->asArray()->all();
39 | * ```
40 | *
41 | * ActiveQuery allows to build the snippets using sources provided by ActiveRecord.
42 | * You can use [[snippetByModel()]] method to enable this.
43 | * For example:
44 | *
45 | * ```php
46 | * class Article extends ActiveRecord
47 | * {
48 | * public function getSource()
49 | * {
50 | * return $this->hasOne('db', ArticleDb::className(), ['id' => 'id']);
51 | * }
52 | *
53 | * public function getSnippetSource()
54 | * {
55 | * return $this->source->content;
56 | * }
57 | *
58 | * ...
59 | * }
60 | *
61 | * $articles = Article::find()->with('source')->snippetByModel()->all();
62 | * ```
63 | *
64 | * Relational query
65 | * ----------------
66 | *
67 | * In relational context ActiveQuery represents a relation between two Active Record classes.
68 | *
69 | * Relational ActiveQuery instances are usually created by calling [[ActiveRecord::hasOne()]] and
70 | * [[ActiveRecord::hasMany()]]. An Active Record class declares a relation by defining
71 | * a getter method which calls one of the above methods and returns the created ActiveQuery object.
72 | *
73 | * A relation is specified by [[link]] which represents the association between columns
74 | * of different tables; and the multiplicity of the relation is indicated by [[multiple]].
75 | *
76 | * If a relation involves a junction table, it may be specified by [[via()]].
77 | * This methods may only be called in a relational context. Same is true for [[inverseOf()]], which
78 | * marks a relation as inverse of another relation.
79 | *
80 | * @author Paul Klimov
81 | * @since 2.0
82 | */
83 | class ActiveQuery extends Query implements ActiveQueryInterface
84 | {
85 | use ActiveQueryTrait;
86 | use ActiveRelationTrait;
87 |
88 | /**
89 | * @event Event an event that is triggered when the query is initialized via [[init()]].
90 | */
91 | const EVENT_INIT = 'init';
92 |
93 | /**
94 | * @var string the SQL statement to be executed for retrieving AR records.
95 | * This is set by [[ActiveRecord::findBySql()]].
96 | */
97 | public $sql;
98 |
99 |
100 | /**
101 | * Constructor.
102 | * @param array $modelClass the model class associated with this query
103 | * @param array $config configurations to be applied to the newly created query object
104 | */
105 | public function __construct($modelClass, $config = [])
106 | {
107 | $this->modelClass = $modelClass;
108 | parent::__construct($config);
109 | }
110 |
111 | /**
112 | * Initializes the object.
113 | * This method is called at the end of the constructor. The default implementation will trigger
114 | * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end
115 | * to ensure triggering of the event.
116 | */
117 | public function init()
118 | {
119 | parent::init();
120 | $this->trigger(self::EVENT_INIT);
121 | }
122 |
123 | /**
124 | * Sets the [[snippetCallback]] to [[fetchSnippetSourceFromModels()]], which allows to
125 | * fetch the snippet source strings from the Active Record models, using method
126 | * [[ActiveRecord::getSnippetSource()]].
127 | * For example:
128 | *
129 | * ```php
130 | * class Article extends ActiveRecord
131 | * {
132 | * public function getSnippetSource()
133 | * {
134 | * return file_get_contents('/path/to/source/files/' . $this->id . '.txt');;
135 | * }
136 | * }
137 | *
138 | * $articles = Article::find()->snippetByModel()->all();
139 | * ```
140 | *
141 | * Warning: this option should NOT be used with [[asArray]] at the same time!
142 | * @return $this the query object itself
143 | */
144 | public function snippetByModel()
145 | {
146 | $this->snippetCallback([$this, 'fetchSnippetSourceFromModels']);
147 |
148 | return $this;
149 | }
150 |
151 | /**
152 | * Executes query and returns all results as an array.
153 | * @param Connection $db the DB connection used to create the DB command.
154 | * If null, the DB connection returned by [[modelClass]] will be used.
155 | * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned.
156 | */
157 | public function all($db = null)
158 | {
159 | return parent::all($db);
160 | }
161 |
162 | /**
163 | * Executes query and returns a single row of result.
164 | * @param Connection $db the DB connection used to create the DB command.
165 | * If null, the DB connection returned by [[modelClass]] will be used.
166 | * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
167 | * the query result may be either an array or an ActiveRecord object. Null will be returned
168 | * if the query results in nothing.
169 | */
170 | public function one($db = null)
171 | {
172 | $row = parent::one($db);
173 | if ($row !== false) {
174 | $models = $this->populate([$row]);
175 | return reset($models) ?: null;
176 | } else {
177 | return null;
178 | }
179 | }
180 |
181 | /**
182 | * {@inheritdoc}
183 | */
184 | public function populate($rows)
185 | {
186 | if (empty($rows)) {
187 | return [];
188 | }
189 |
190 | $models = $this->createModels($rows);
191 | if (!empty($this->with)) {
192 | $this->findWith($this->with, $models);
193 | }
194 | $models = parent::populate($models);
195 | if (!$this->asArray) {
196 | foreach ($models as $model) {
197 | $model->afterFind();
198 | }
199 | }
200 |
201 | return $models;
202 | }
203 |
204 | /**
205 | * Creates a DB command that can be used to execute this query.
206 | * @param Connection $db the DB connection used to create the DB command.
207 | * If null, the DB connection returned by [[modelClass]] will be used.
208 | * @return Command the created DB command instance.
209 | */
210 | public function createCommand($db = null)
211 | {
212 | if ($this->primaryModel !== null) {
213 | // lazy loading a relational query
214 | if ($this->via instanceof self) {
215 | // via pivot index
216 | $viaModels = $this->via->findJunctionRows([$this->primaryModel]);
217 | $this->filterByModels($viaModels);
218 | } elseif (is_array($this->via)) {
219 | // via relation
220 | /* @var $viaQuery ActiveQuery */
221 | list($viaName, $viaQuery) = $this->via;
222 | if ($viaQuery->multiple) {
223 | $viaModels = $viaQuery->all();
224 | $this->primaryModel->populateRelation($viaName, $viaModels);
225 | } else {
226 | $model = $viaQuery->one();
227 | $this->primaryModel->populateRelation($viaName, $model);
228 | $viaModels = $model === null ? [] : [$model];
229 | }
230 | $this->filterByModels($viaModels);
231 | } else {
232 | $this->filterByModels([$this->primaryModel]);
233 | }
234 | }
235 |
236 | $this->setConnection($db);
237 | $db = $this->getConnection();
238 |
239 | if ($this->sql === null) {
240 | list ($sql, $params) = $db->getQueryBuilder()->build($this);
241 | } else {
242 | $sql = $this->sql;
243 | $params = $this->params;
244 | }
245 |
246 | return $db->createCommand($sql, $params);
247 | }
248 |
249 | /**
250 | * {@inheritdoc}
251 | */
252 | protected function defaultConnection()
253 | {
254 | $modelClass = $this->modelClass;
255 |
256 | return $modelClass::getDb();
257 | }
258 |
259 | /**
260 | * Fetches the source for the snippets using [[ActiveRecord::getSnippetSource()]] method.
261 | * @param ActiveRecord[] $models raw query result rows.
262 | * @throws \yii\base\InvalidCallException if [[asArray]] enabled.
263 | * @return array snippet source strings
264 | */
265 | protected function fetchSnippetSourceFromModels($models)
266 | {
267 | if ($this->asArray) {
268 | throw new InvalidCallException('"' . __METHOD__ . '" unable to determine snippet source from plain array. Either disable "asArray" option or use regular "snippetCallback"');
269 | }
270 | $result = [];
271 | foreach ($models as $model) {
272 | $result[] = $model->getSnippetSource();
273 | }
274 |
275 | return $result;
276 | }
277 |
278 | /**
279 | * {@inheritdoc}
280 | */
281 | protected function callSnippets(array $source)
282 | {
283 | $from = $this->from;
284 | if ($from === null) {
285 | /* @var $modelClass ActiveRecord */
286 | $modelClass = $this->modelClass;
287 | $tableName = $modelClass::indexName();
288 | $from = [$tableName];
289 | }
290 |
291 | return $this->callSnippetsInternal($source, $from[0]);
292 | }
293 | }
294 |
--------------------------------------------------------------------------------
/src/ActiveRecord.php:
--------------------------------------------------------------------------------
1 |
28 | * @since 2.0
29 | */
30 | abstract class ActiveRecord extends BaseActiveRecord
31 | {
32 | /**
33 | * The insert operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
34 | */
35 | const OP_INSERT = 0x01;
36 | /**
37 | * The update operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
38 | */
39 | const OP_UPDATE = 0x02;
40 | /**
41 | * The delete operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
42 | */
43 | const OP_DELETE = 0x04;
44 | /**
45 | * All three operations: insert, update, delete.
46 | * This is a shortcut of the expression: OP_INSERT | OP_UPDATE | OP_DELETE.
47 | */
48 | const OP_ALL = 0x07;
49 |
50 | /**
51 | * @var string current snippet value for this Active Record instance.
52 | * It will be filled up automatically when instance found using [[Query::snippetCallback]]
53 | * or [[ActiveQuery::snippetByModel()]].
54 | */
55 | private $_snippet;
56 |
57 |
58 | /**
59 | * Returns the Sphinx connection used by this AR class.
60 | * By default, the "sphinx" application component is used as the Sphinx connection.
61 | * You may override this method if you want to use a different Sphinx connection.
62 | * @return Connection the Sphinx connection used by this AR class.
63 | */
64 | public static function getDb()
65 | {
66 | return \Yii::$app->get('sphinx');
67 | }
68 |
69 | /**
70 | * Creates an [[ActiveQuery]] instance with a given SQL statement.
71 | *
72 | * Note that because the SQL statement is already specified, calling additional
73 | * query modification methods (such as `where()`, `order()`) on the created [[ActiveQuery]]
74 | * instance will have no effect. However, calling `with()`, `asArray()` or `indexBy()` is
75 | * still fine.
76 | *
77 | * Below is an example:
78 | *
79 | * ```php
80 | * $customers = Article::findBySql("SELECT * FROM `idx_article` WHERE MATCH('development')")->all();
81 | * ```
82 | *
83 | * @param string $sql the SQL statement to be executed
84 | * @param array $params parameters to be bound to the SQL statement during execution.
85 | * @return ActiveQuery the newly created [[ActiveQuery]] instance
86 | */
87 | public static function findBySql($sql, $params = [])
88 | {
89 | $query = static::find();
90 | $query->sql = $sql;
91 |
92 | return $query->params($params);
93 | }
94 |
95 | /**
96 | * Updates the whole table using the provided attribute values and conditions.
97 | * For example, to change the status to be 1 for all articles which status is 2:
98 | *
99 | * ```php
100 | * Article::updateAll(['status' => 1], 'status = 2');
101 | * ```
102 | *
103 | * @param array $attributes attribute values (name-value pairs) to be saved into the table
104 | * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
105 | * Please refer to [[Query::where()]] on how to specify this parameter.
106 | * @param array $params the parameters (name => value) to be bound to the query.
107 | * @return int the number of rows updated
108 | */
109 | public static function updateAll($attributes, $condition = '', $params = [])
110 | {
111 | $command = static::getDb()->createCommand();
112 | $command->update(static::indexName(), $attributes, $condition, $params);
113 |
114 | return $command->execute();
115 | }
116 |
117 | /**
118 | * Deletes rows in the index using the provided conditions.
119 | *
120 | * For example, to delete all articles whose status is 3:
121 | *
122 | * ```php
123 | * Article::deleteAll('status = 3');
124 | * ```
125 | *
126 | * @param string|array $condition the conditions that will be put in the WHERE part of the DELETE SQL.
127 | * Please refer to [[Query::where()]] on how to specify this parameter.
128 | * @param array $params the parameters (name => value) to be bound to the query.
129 | * @return int the number of rows deleted
130 | */
131 | public static function deleteAll($condition = '', $params = [])
132 | {
133 | $command = static::getDb()->createCommand();
134 | $command->delete(static::indexName(), $condition, $params);
135 |
136 | return $command->execute();
137 | }
138 |
139 | /**
140 | * {@inheritdoc}
141 | * @return ActiveQuery the newly created [[ActiveQuery]] instance.
142 | */
143 | public static function find()
144 | {
145 | return Yii::createObject(ActiveQuery::className(), [get_called_class()]);
146 | }
147 |
148 | /**
149 | * Declares the name of the Sphinx index associated with this AR class.
150 | * By default this method returns the class name as the index name by calling [[Inflector::camel2id()]].
151 | * For example, 'Article' becomes 'article', and 'StockItem' becomes
152 | * 'stock_item'. You may override this method if the index is not named after this convention.
153 | * @return string the index name
154 | */
155 | public static function indexName()
156 | {
157 | return Inflector::camel2id(StringHelper::basename(get_called_class()), '_');
158 | }
159 |
160 | /**
161 | * Returns the schema information of the Sphinx index associated with this AR class.
162 | * @return IndexSchema the schema information of the Sphinx index associated with this AR class.
163 | * @throws InvalidConfigException if the index for the AR class does not exist.
164 | */
165 | public static function getIndexSchema()
166 | {
167 | $schema = static::getDb()->getIndexSchema(static::indexName());
168 | if ($schema !== null) {
169 | return $schema;
170 | } else {
171 | throw new InvalidConfigException("The index does not exist: " . static::indexName());
172 | }
173 | }
174 |
175 | /**
176 | * Returns the primary key name for this AR class.
177 | * The default implementation will return the primary key as declared
178 | * in the Sphinx index, which is associated with this AR class.
179 | *
180 | * Note that an array should be returned even for a table with single primary key.
181 | *
182 | * @return string[] the primary keys of the associated Sphinx index.
183 | */
184 | public static function primaryKey()
185 | {
186 | return [static::getIndexSchema()->primaryKey];
187 | }
188 |
189 | /**
190 | * Builds a snippet from provided data and query, using specified index settings.
191 | * @param string|array $source is the source data to extract a snippet from.
192 | * It could be either a single string or array of strings.
193 | * @param string $match the full-text query to build snippets for.
194 | * @param array $options list of options in format: optionName => optionValue
195 | * @return string|array built snippet in case "source" is a string, list of built snippets
196 | * in case "source" is an array.
197 | */
198 | public static function callSnippets($source, $match, $options = [])
199 | {
200 | $command = static::getDb()->createCommand();
201 | $command->callSnippets(static::indexName(), $source, $match, $options);
202 | if (is_array($source)) {
203 | return $command->queryColumn();
204 | } else {
205 | return $command->queryScalar();
206 | }
207 | }
208 |
209 | /**
210 | * Returns tokenized and normalized forms of the keywords, and, optionally, keyword statistics.
211 | * @param string $text the text to break down to keywords.
212 | * @param bool $fetchStatistic whether to return document and hit occurrence statistics
213 | * @return array keywords and statistics
214 | */
215 | public static function callKeywords($text, $fetchStatistic = false)
216 | {
217 | $command = static::getDb()->createCommand();
218 | $command->callKeywords(static::indexName(), $text, $fetchStatistic);
219 |
220 | return $command->queryAll();
221 | }
222 |
223 | /**
224 | * @param string $snippet
225 | */
226 | public function setSnippet($snippet)
227 | {
228 | $this->_snippet = $snippet;
229 | }
230 |
231 | /**
232 | * Returns current snippet value or generates new one from given match.
233 | * @param string $match snippet source query
234 | * @param array $options list of options in format: optionName => optionValue
235 | * @return string snippet value
236 | */
237 | public function getSnippet($match = null, $options = [])
238 | {
239 | if ($match !== null) {
240 | $this->_snippet = $this->fetchSnippet($match, $options);
241 | }
242 |
243 | return $this->_snippet;
244 | }
245 |
246 | /**
247 | * Builds up the snippet value from the given query.
248 | * @param string $match the full-text query to build snippets for.
249 | * @param array $options list of options in format: optionName => optionValue
250 | * @return string snippet value.
251 | */
252 | protected function fetchSnippet($match, $options = [])
253 | {
254 | return static::callSnippets($this->getSnippetSource(), $match, $options);
255 | }
256 |
257 | /**
258 | * Returns the string, which should be used as a source to create snippet for this
259 | * Active Record instance.
260 | * Child classes must implement this method to return the actual snippet source text.
261 | * For example:
262 | *
263 | * ```php
264 | * public function getSnippetSource()
265 | * {
266 | * return $this->snippetSourceRelation->content;
267 | * }
268 | * ```
269 | *
270 | * @return string snippet source string.
271 | * @throws \yii\base\NotSupportedException if this is not supported by the Active Record class
272 | */
273 | public function getSnippetSource()
274 | {
275 | throw new NotSupportedException($this->className() . ' does not provide snippet source.');
276 | }
277 |
278 | /**
279 | * Declares which operations should be performed within a transaction in different scenarios.
280 | * The supported DB operations are: [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]],
281 | * which correspond to the [[insert()]], [[update()]] and [[delete()]] methods, respectively.
282 | * By default, these methods are NOT enclosed in a transaction.
283 | *
284 | * In some scenarios, to ensure data consistency, you may want to enclose some or all of them
285 | * in transactions. You can do so by overriding this method and returning the operations
286 | * that need to be transactional. For example,
287 | *
288 | * ```php
289 | * return [
290 | * 'admin' => self::OP_INSERT,
291 | * 'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
292 | * // the above is equivalent to the following:
293 | * // 'api' => self::OP_ALL,
294 | *
295 | * ];
296 | * ```
297 | *
298 | * The above declaration specifies that in the "admin" scenario, the insert operation ([[insert()]])
299 | * should be done in a transaction; and in the "api" scenario, all the operations should be done
300 | * in a transaction.
301 | *
302 | * @return array the declarations of transactional operations. The array keys are scenarios names,
303 | * and the array values are the corresponding transaction operations.
304 | */
305 | public function transactions()
306 | {
307 | return [];
308 | }
309 |
310 | /**
311 | * Returns the list of all attribute names of the model.
312 | * The default implementation will return all column names of the table associated with this AR class.
313 | * @return array list of attribute names.
314 | */
315 | public function attributes()
316 | {
317 | return array_keys(static::getIndexSchema()->columns);
318 | }
319 |
320 | /**
321 | * Inserts a row into the associated Sphinx index using the attribute values of this record.
322 | *
323 | * This method performs the following steps in order:
324 | *
325 | * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation
326 | * fails, it will skip the rest of the steps;
327 | * 2. call [[afterValidate()]] when `$runValidation` is true.
328 | * 3. call [[beforeSave()]]. If the method returns false, it will skip the
329 | * rest of the steps;
330 | * 4. insert the record into index. If this fails, it will skip the rest of the steps;
331 | * 5. call [[afterSave()]];
332 | *
333 | * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
334 | * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]]
335 | * will be raised by the corresponding methods.
336 | *
337 | * Only the [[changedAttributes|changed attribute values]] will be inserted.
338 | *
339 | * For example, to insert an article record:
340 | *
341 | * ```php
342 | * $article = new Article();
343 | * $article->id = $id;
344 | * $article->genre_id = $genreId;
345 | * $article->content = $content;
346 | * $article->insert();
347 | * ```
348 | *
349 | * @param bool $runValidation whether to perform validation before saving the record.
350 | * If the validation fails, the record will not be inserted.
351 | * @param array $attributes list of attributes that need to be saved. Defaults to null,
352 | * meaning all attributes that are loaded from index will be saved.
353 | * @return bool whether the attributes are valid and the record is inserted successfully.
354 | * @throws \Exception in case insert failed.
355 | */
356 | public function insert($runValidation = true, $attributes = null)
357 | {
358 | if ($runValidation && !$this->validate($attributes)) {
359 | return false;
360 | }
361 | $db = static::getDb();
362 | if ($this->isTransactional(self::OP_INSERT) && $db->getTransaction() === null) {
363 | $transaction = $db->beginTransaction();
364 | try {
365 | $result = $this->insertInternal($attributes);
366 | if ($result === false) {
367 | $transaction->rollBack();
368 | } else {
369 | $transaction->commit();
370 | }
371 | } catch (\Exception $e) {
372 | $transaction->rollBack();
373 | throw $e;
374 | }
375 | } else {
376 | $result = $this->insertInternal($attributes);
377 | }
378 |
379 | return $result;
380 | }
381 |
382 | /**
383 | * @see ActiveRecord::insert()
384 | */
385 | private function insertInternal($attributes = null)
386 | {
387 | if (!$this->beforeSave(true)) {
388 | return false;
389 | }
390 | $values = $this->getDirtyAttributes($attributes);
391 | if (empty($values)) {
392 | foreach ($this->getPrimaryKey(true) as $key => $value) {
393 | $values[$key] = $value;
394 | }
395 | }
396 | $db = static::getDb();
397 | $command = $db->createCommand()->insert($this->indexName(), $values);
398 | if (!$command->execute()) {
399 | return false;
400 | }
401 |
402 | $changedAttributes = array_fill_keys(array_keys($values), null);
403 | $this->setOldAttributes($values);
404 | $this->afterSave(true, $changedAttributes);
405 |
406 | return true;
407 | }
408 |
409 | /**
410 | * Saves the changes to this active record into the associated Sphinx index.
411 | *
412 | * This method performs the following steps in order:
413 | *
414 | * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation
415 | * fails, it will skip the rest of the steps;
416 | * 2. call [[afterValidate()]] when `$runValidation` is true.
417 | * 3. call [[beforeSave()]]. If the method returns false, it will skip the
418 | * rest of the steps;
419 | * 4. save the record into index. If this fails, it will skip the rest of the steps;
420 | * 5. call [[afterSave()]];
421 | *
422 | * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
423 | * [[EVENT_BEFORE_UPDATE]], [[EVENT_AFTER_UPDATE]] and [[EVENT_AFTER_VALIDATE]]
424 | * will be raised by the corresponding methods.
425 | *
426 | * Only the [[changedAttributes|changed attribute values]] will be saved into database.
427 | *
428 | * For example, to update an article record:
429 | *
430 | * ```php
431 | * $article = Article::findOne($id);
432 | * $article->genre_id = $genreId;
433 | * $article->group_id = $groupId;
434 | * $article->update();
435 | * ```
436 | *
437 | * Note that it is possible the update does not affect any row in the table.
438 | * In this case, this method will return 0. For this reason, you should use the following
439 | * code to check if update() is successful or not:
440 | *
441 | * ```php
442 | * if ($this->update() !== false) {
443 | * // update successful
444 | * } else {
445 | * // update failed
446 | * }
447 | * ```
448 | *
449 | * @param bool $runValidation whether to perform validation before saving the record.
450 | * If the validation fails, the record will not be inserted into the database.
451 | * @param array $attributeNames list of attributes that need to be saved. Defaults to null,
452 | * meaning all attributes that are loaded from DB will be saved.
453 | * @return int|false the number of rows affected, or `false` if validation fails
454 | * or [[beforeSave()]] stops the updating process.
455 | * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
456 | * being updated is outdated.
457 | * @throws \Exception in case update failed.
458 | */
459 | public function update($runValidation = true, $attributeNames = null)
460 | {
461 | if ($runValidation && !$this->validate($attributeNames)) {
462 | return false;
463 | }
464 | $db = static::getDb();
465 | if ($this->isTransactional(self::OP_UPDATE) && $db->getTransaction() === null) {
466 | $transaction = $db->beginTransaction();
467 | try {
468 | $result = $this->updateInternal($attributeNames);
469 | if ($result === false) {
470 | $transaction->rollBack();
471 | } else {
472 | $transaction->commit();
473 | }
474 | } catch (\Exception $e) {
475 | $transaction->rollBack();
476 | throw $e;
477 | }
478 | } else {
479 | $result = $this->updateInternal($attributeNames);
480 | }
481 |
482 | return $result;
483 | }
484 |
485 | /**
486 | * @see CActiveRecord::update()
487 | * @throws StaleObjectException
488 | */
489 | protected function updateInternal($attributes = null)
490 | {
491 | if (!$this->beforeSave(false)) {
492 | return false;
493 | }
494 | $values = $this->getDirtyAttributes($attributes);
495 | if (empty($values)) {
496 | $this->afterSave(false, $values);
497 | return 0;
498 | }
499 |
500 | // Replace is supported only by runtime indexes and necessary only for field update
501 | $useReplace = false;
502 | $indexSchema = $this->getIndexSchema();
503 | if ($this->getIndexSchema()->isRt) {
504 | foreach ($values as $name => $value) {
505 | $columnSchema = $indexSchema->getColumn($name);
506 | if ($columnSchema->isField) {
507 | $useReplace = true;
508 | break;
509 | }
510 | }
511 | }
512 |
513 | if ($useReplace) {
514 | $values = array_merge(
515 | $this->getAttributes(),
516 | $values,
517 | $this->getOldPrimaryKey(true)
518 | );
519 | $command = static::getDb()->createCommand();
520 | $command->replace(static::indexName(), $values);
521 | // We do not check the return value of replace because it's possible
522 | // that the REPLACE statement doesn't change anything and thus returns 0.
523 | $rows = $command->execute();
524 | } else {
525 | $condition = $this->getOldPrimaryKey(true);
526 | $lock = $this->optimisticLock();
527 | if ($lock !== null) {
528 | if (!isset($values[$lock])) {
529 | $values[$lock] = $this->$lock + 1;
530 | }
531 | $condition[$lock] = $this->$lock;
532 | }
533 | // We do not check the return value of updateAll() because it's possible
534 | // that the UPDATE statement doesn't change anything and thus returns 0.
535 | $rows = $this->updateAll($values, $condition);
536 |
537 | if ($lock !== null && !$rows) {
538 | throw new StaleObjectException('The object being updated is outdated.');
539 | }
540 |
541 | if (isset($values[$lock])) {
542 | $this->$lock = $values[$lock];
543 | }
544 | }
545 |
546 | $changedAttributes = [];
547 | foreach ($values as $name => $value) {
548 | $changedAttributes[$name] = $this->getOldAttribute($name);
549 | $this->setOldAttribute($name, $value);
550 | }
551 | $this->afterSave(false, $changedAttributes);
552 |
553 | return $rows;
554 | }
555 |
556 | /**
557 | * Deletes the index entry corresponding to this active record.
558 | *
559 | * This method performs the following steps in order:
560 | *
561 | * 1. call [[beforeDelete()]]. If the method returns false, it will skip the
562 | * rest of the steps;
563 | * 2. delete the record from the index;
564 | * 3. call [[afterDelete()]].
565 | *
566 | * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]]
567 | * will be raised by the corresponding methods.
568 | *
569 | * @return int|false the number of rows deleted, or `false` if the deletion is unsuccessful for some reason.
570 | * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
571 | * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
572 | * being deleted is outdated.
573 | * @throws \Exception in case delete failed.
574 | */
575 | public function delete()
576 | {
577 | $db = static::getDb();
578 | $transaction = $this->isTransactional(self::OP_DELETE) && $db->getTransaction() === null ? $db->beginTransaction() : null;
579 | try {
580 | $result = false;
581 | if ($this->beforeDelete()) {
582 | // we do not check the return value of deleteAll() because it's possible
583 | // the record is already deleted in the database and thus the method will return 0
584 | $condition = $this->getOldPrimaryKey(true);
585 | $lock = $this->optimisticLock();
586 | if ($lock !== null) {
587 | $condition[$lock] = $this->$lock;
588 | }
589 | $result = $this->deleteAll($condition);
590 | if ($lock !== null && !$result) {
591 | throw new StaleObjectException('The object being deleted is outdated.');
592 | }
593 | $this->setOldAttributes(null);
594 | $this->afterDelete();
595 | }
596 | if ($transaction !== null) {
597 | if ($result === false) {
598 | $transaction->rollBack();
599 | } else {
600 | $transaction->commit();
601 | }
602 | }
603 | } catch (\Exception $e) {
604 | if ($transaction !== null) {
605 | $transaction->rollBack();
606 | }
607 | throw $e;
608 | }
609 |
610 | return $result;
611 | }
612 |
613 | /**
614 | * Returns a value indicating whether the given active record is the same as the current one.
615 | * The comparison is made by comparing the index names and the primary key values of the two active records.
616 | * If one of the records [[isNewRecord|is new]] they are also considered not equal.
617 | * @param ActiveRecord $record record to compare to
618 | * @return bool whether the two active records refer to the same row in the same index.
619 | */
620 | public function equals($record)
621 | {
622 | if ($this->isNewRecord || $record->isNewRecord) {
623 | return false;
624 | }
625 |
626 | return $this->indexName() === $record->indexName() && $this->getPrimaryKey() === $record->getPrimaryKey();
627 | }
628 |
629 | /**
630 | * {@inheritdoc}
631 | */
632 | public static function populateRecord($record, $row)
633 | {
634 | $columns = static::getIndexSchema()->columns;
635 | foreach ($row as $name => $value) {
636 | if (isset($columns[$name])) {
637 | if ($columns[$name]->isMva) {
638 | if (empty($value)) {
639 | $row[$name] = [];
640 | } else {
641 | $mvaValue = explode(',', $value);
642 | $row[$name] = array_map([$columns[$name], 'phpTypecast'], $mvaValue);
643 | }
644 | } else {
645 | $row[$name] = $columns[$name]->phpTypecast($value);
646 | }
647 | }
648 | }
649 | parent::populateRecord($record, $row);
650 | }
651 |
652 | /**
653 | * Returns a value indicating whether the specified operation is transactional in the current [[scenario]].
654 | * @param int $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]].
655 | * @return bool whether the specified operation is transactional in the current [[scenario]].
656 | */
657 | public function isTransactional($operation)
658 | {
659 | $scenario = $this->getScenario();
660 | $transactions = $this->transactions();
661 |
662 | return isset($transactions[$scenario]) && ($transactions[$scenario] & $operation);
663 | }
664 | }
665 |
--------------------------------------------------------------------------------
/src/ColumnSchema.php:
--------------------------------------------------------------------------------
1 |
17 | * @since 2.0
18 | */
19 | class ColumnSchema extends BaseObject
20 | {
21 | /**
22 | * @var string name of this column (without quotes).
23 | */
24 | public $name;
25 | /**
26 | * @var string abstract type of this column. Possible abstract types include:
27 | * string, text, boolean, smallint, integer, bigint, float, decimal, datetime,
28 | * timestamp, time, date, binary, and money.
29 | */
30 | public $type;
31 | /**
32 | * @var string the PHP type of this column. Possible PHP types include:
33 | * string, boolean, integer, double.
34 | */
35 | public $phpType;
36 | /**
37 | * @var string the DB type of this column. Possible DB types vary according to the type of DBMS.
38 | */
39 | public $dbType;
40 | /**
41 | * @var bool whether this column is a primary key
42 | */
43 | public $isPrimaryKey;
44 | /**
45 | * @var bool whether this column is an attribute
46 | */
47 | public $isAttribute;
48 | /**
49 | * @var bool whether this column is a indexed field
50 | */
51 | public $isField;
52 | /**
53 | * @var bool whether this column is a multi value attribute (MVA)
54 | */
55 | public $isMva;
56 |
57 |
58 | /**
59 | * Converts the input value according to [[phpType]] after retrieval from the database.
60 | * If the value is null or an [[Expression]], it will not be converted.
61 | * @param mixed $value input value
62 | * @return mixed converted value
63 | */
64 | public function phpTypecast($value)
65 | {
66 | return $this->typecast($value);
67 | }
68 |
69 | /**
70 | * Converts the input value according to [[type]] and [[dbType]] for use in a db query.
71 | * If the value is null or an [[Expression]], it will not be converted.
72 | * @param mixed $value input value
73 | * @return mixed converted value. This may also be an array containing the value as the first element
74 | * and the PDO type as the second element.
75 | */
76 | public function dbTypecast($value)
77 | {
78 | // the default implementation does the same as casting for PHP, but it should be possible
79 | // to override this with annotation of explicit PDO type.
80 | return $this->typecast($value);
81 | }
82 |
83 | /**
84 | * Converts the input value according to [[phpType]] after retrieval from the database.
85 | * If the value is null or an [[Expression]], it will not be converted.
86 | * @param mixed $value input value
87 | * @return mixed converted value
88 | * @since 2.0.3
89 | */
90 | protected function typecast($value)
91 | {
92 | if ($value === '' && $this->type !== Schema::TYPE_STRING) {
93 | return null;
94 | }
95 | if ($value === null || gettype($value) === $this->phpType || $value instanceof Expression) {
96 | return $value;
97 | }
98 | switch ($this->phpType) {
99 | case 'resource':
100 | case 'string':
101 | return is_resource($value) ? $value : (string) $value;
102 | case 'int':
103 | case 'integer':
104 | return (int) $value;
105 | case 'bool':
106 | case 'boolean':
107 | return (bool) $value;
108 | case 'double':
109 | return (double) $value;
110 | }
111 |
112 | return $value;
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/Command.php:
--------------------------------------------------------------------------------
1 | createCommand("SELECT * FROM `idx_article` WHERE MATCH('programming')")->queryAll();
27 | * ```
28 | *
29 | * Command supports SQL statement preparation and parameter binding just as [[\yii\db\Command]] does.
30 | *
31 | * Command also supports building SQL statements by providing methods such as [[insert()]],
32 | * [[update()]], etc. For example,
33 | *
34 | * ```php
35 | * $connection->createCommand()->update('idx_article', [
36 | * 'genre_id' => 15,
37 | * 'author_id' => 157,
38 | * ])->execute();
39 | * ```
40 | *
41 | * To build SELECT SQL statements, please use [[Query]] and [[QueryBuilder]] instead.
42 | *
43 | * @author Paul Klimov
44 | * @since 2.0
45 | */
46 | class Command extends \yii\db\Command
47 | {
48 | /**
49 | * @var \yii\sphinx\Connection the Sphinx connection that this command is associated with.
50 | */
51 | public $db;
52 |
53 |
54 | /**
55 | * Creates a batch INSERT command.
56 | * For example,
57 | *
58 | * ```php
59 | * $connection->createCommand()->batchInsert('idx_user', ['name', 'age'], [
60 | * ['Tom', 30],
61 | * ['Jane', 20],
62 | * ['Linda', 25],
63 | * ])->execute();
64 | * ```
65 | *
66 | * Note that the values in each row must match the corresponding column names.
67 | *
68 | * @param string $index the index that new rows will be inserted into.
69 | * @param array $columns the column names
70 | * @param array $rows the rows to be batch inserted into the index
71 | * @return $this the command object itself
72 | */
73 | public function batchInsert($index, $columns, $rows)
74 | {
75 | $params = [];
76 | $sql = $this->db->getQueryBuilder()->batchInsert($index, $columns, $rows, $params);
77 |
78 | return $this->setSql($sql)->bindValues($params);
79 | }
80 |
81 | /**
82 | * Creates an REPLACE command.
83 | * For example,
84 | *
85 | * ```php
86 | * $connection->createCommand()->replace('idx_user', [
87 | * 'name' => 'Sam',
88 | * 'age' => 30,
89 | * ])->execute();
90 | * ```
91 | *
92 | * The method will properly escape the column names, and bind the values to be replaced.
93 | *
94 | * Note that the created command is not executed until [[execute()]] is called.
95 | *
96 | * @param string $index the index that new rows will be replaced into.
97 | * @param array $columns the column data (name => value) to be replaced into the index.
98 | * @return $this the command object itself
99 | */
100 | public function replace($index, $columns)
101 | {
102 | $params = [];
103 | $sql = $this->db->getQueryBuilder()->replace($index, $columns, $params);
104 |
105 | return $this->setSql($sql)->bindValues($params);
106 | }
107 |
108 | /**
109 | * Creates a batch REPLACE command.
110 | * For example,
111 | *
112 | * ```php
113 | * $connection->createCommand()->batchReplace('idx_user', ['name', 'age'], [
114 | * ['Tom', 30],
115 | * ['Jane', 20],
116 | * ['Linda', 25],
117 | * ])->execute();
118 | * ```
119 | *
120 | * Note that the values in each row must match the corresponding column names.
121 | *
122 | * @param string $index the index that new rows will be replaced.
123 | * @param array $columns the column names
124 | * @param array $rows the rows to be batch replaced in the index
125 | * @return $this the command object itself
126 | */
127 | public function batchReplace($index, $columns, $rows)
128 | {
129 | $params = [];
130 | $sql = $this->db->getQueryBuilder()->batchReplace($index, $columns, $rows, $params);
131 |
132 | return $this->setSql($sql)->bindValues($params);
133 | }
134 |
135 | /**
136 | * Creates an UPDATE command.
137 | * For example,
138 | *
139 | * ```php
140 | * $connection->createCommand()->update('user', ['status' => 1], 'age > 30')->execute();
141 | * ```
142 | *
143 | * The method will properly escape the column names and bind the values to be updated.
144 | *
145 | * Note that the created command is not executed until [[execute()]] is called.
146 | *
147 | * @param string $index the index to be updated.
148 | * @param array $columns the column data (name => value) to be updated.
149 | * @param string|array $condition the condition that will be put in the WHERE part. Please
150 | * refer to [[Query::where()]] on how to specify condition.
151 | * @param array $params the parameters to be bound to the command
152 | * @param array $options list of options in format: optionName => optionValue
153 | * @return $this the command object itself
154 | */
155 | public function update($index, $columns, $condition = '', $params = [], $options = [])
156 | {
157 | $sql = $this->db->getQueryBuilder()->update($index, $columns, $condition, $params, $options);
158 |
159 | return $this->setSql($sql)->bindValues($params);
160 | }
161 |
162 | /**
163 | * Creates a SQL command for truncating a runtime index.
164 | * @param string $index the index to be truncated. The name will be properly quoted by the method.
165 | * @return $this the command object itself
166 | */
167 | public function truncateIndex($index)
168 | {
169 | $sql = $this->db->getQueryBuilder()->truncateIndex($index);
170 |
171 | return $this->setSql($sql);
172 | }
173 |
174 | /**
175 | * Builds a snippet from provided data and query, using specified index settings.
176 | * @param string $index name of the index, from which to take the text processing settings.
177 | * @param string|array $source is the source data to extract a snippet from.
178 | * It could be either a single string or array of strings.
179 | * @param string $match the full-text query to build snippets for.
180 | * @param array $options list of options in format: optionName => optionValue
181 | * @return $this the command object itself
182 | */
183 | public function callSnippets($index, $source, $match, $options = [])
184 | {
185 | $params = [];
186 | $sql = $this->db->getQueryBuilder()->callSnippets($index, $source, $match, $options, $params);
187 |
188 | return $this->setSql($sql)->bindValues($params);
189 | }
190 |
191 | /**
192 | * Returns tokenized and normalized forms of the keywords, and, optionally, keyword statistics.
193 | * @param string $index the name of the index from which to take the text processing settings
194 | * @param string $text the text to break down to keywords.
195 | * @param bool $fetchStatistic whether to return document and hit occurrence statistics
196 | * @return $this the command object itself
197 | */
198 | public function callKeywords($index, $text, $fetchStatistic = false)
199 | {
200 | $params = [];
201 | $sql = $this->db->getQueryBuilder()->callKeywords($index, $text, $fetchStatistic, $params);
202 |
203 | return $this->setSql($sql)->bindValues($params);
204 | }
205 |
206 | // Float handling :
207 |
208 | /**
209 | * @var array list of 'float' type params, which should be inserted into SQL directly instead of binding.
210 | * @see Connection::enableFloatConversion
211 | * @since 2.0.6
212 | */
213 | private $floatParams = [];
214 |
215 | /**
216 | * {@inheritdoc}
217 | */
218 | public function bindValue($name, $value, $dataType = null)
219 | {
220 | if ($this->db->enableFloatConversion && $dataType === null && is_float($value)) {
221 | $this->floatParams[$name] = $value;
222 | return $this;
223 | }
224 | return parent::bindValue($name, $value, $dataType);
225 | }
226 |
227 | /**
228 | * {@inheritdoc}
229 | */
230 | public function bindValues($values)
231 | {
232 | if ($this->db->enableFloatConversion) {
233 | if (empty($values)) {
234 | return $this;
235 | }
236 |
237 | foreach ($values as $name => $value) {
238 | if (is_float($value)) {
239 | $this->floatParams[$name] = $value;
240 | unset($values[$name]);
241 | }
242 | }
243 | }
244 |
245 | return parent::bindValues($values);
246 | }
247 |
248 | /**
249 | * {@inheritdoc}
250 | */
251 | public function prepare($forRead = null)
252 | {
253 | if ($this->pdoStatement && empty($this->floatParams)) {
254 | $this->bindPendingParams();
255 | return;
256 | }
257 |
258 | $sql = $this->getSql();
259 |
260 | if ($this->db->getTransaction()) {
261 | // master is in a transaction. use the same connection.
262 | $forRead = false;
263 | }
264 | if ($forRead || $forRead === null && $this->db->getSchema()->isReadQuery($sql)) {
265 | $pdo = $this->db->getSlavePdo();
266 | } else {
267 | $pdo = $this->db->getMasterPdo();
268 | }
269 |
270 | if (!empty($this->floatParams)) {
271 | $sql = $this->parseFloatParams($sql);
272 | }
273 |
274 | try {
275 | $this->pdoStatement = $pdo->prepare($sql);
276 | $this->bindPendingParams();
277 | } catch (\Exception $e) {
278 | $message = $e->getMessage() . "\nFailed to prepare SphinxQL: $sql";
279 | $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
280 | throw new Exception($message, $errorInfo, (int) $e->getCode(), $e);
281 | }
282 | }
283 |
284 | /**
285 | * Parses given SQL replacing bound [[floatParams]] with their values.
286 | * @param string $sql source SQL.
287 | * @return string adjusted SQL.
288 | */
289 | private function parseFloatParams($sql)
290 | {
291 | foreach ($this->floatParams as $name => $value) {
292 | if (strncmp($name, ':', 1) !== 0) {
293 | $name = ':' . $name;
294 | }
295 | // unable to use `str_replace()` because particular param name may be a substring of another param name
296 | $sql = preg_replace('/' . preg_quote($name) . '\b/s', $value, $sql);
297 | };
298 |
299 | return $sql;
300 | }
301 |
302 | /**
303 | * {@inheritdoc}
304 | */
305 | public function getRawSql()
306 | {
307 | return $this->parseFloatParams(parent::getRawSql());
308 | }
309 |
310 | // Not Supported :
311 |
312 | /**
313 | * {@inheritdoc}
314 | */
315 | public function createTable($table, $columns, $options = null)
316 | {
317 | $sql = $this->db->getQueryBuilder()->createTable($table, $columns, $options);
318 |
319 | return $this->setSql($sql);
320 | }
321 |
322 | /**
323 | * {@inheritdoc}
324 | */
325 | public function renameTable($table, $newName)
326 | {
327 | throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
328 | }
329 |
330 | /**
331 | * {@inheritdoc}
332 | */
333 | public function dropTable($table)
334 | {
335 | $sql = $this->db->getQueryBuilder()->dropTable($table);
336 |
337 | return $this->setSql($sql);
338 | }
339 |
340 | /**
341 | * {@inheritdoc}
342 | */
343 | public function truncateTable($table)
344 | {
345 | $sql = $this->db->getQueryBuilder()->truncateIndex($table);
346 |
347 | return $this->setSql($sql);
348 | }
349 |
350 | /**
351 | * {@inheritdoc}
352 | */
353 | public function addColumn($table, $column, $type)
354 | {
355 | $sql = $this->db->getQueryBuilder()->addColumn($table, $column, $type);
356 |
357 | return $this->setSql($sql);;
358 | }
359 |
360 | /**
361 | * {@inheritdoc}
362 | */
363 | public function dropColumn($table, $column)
364 | {
365 | $sql = $this->db->getQueryBuilder()->dropColumn($table, $column);
366 |
367 | return $this->setSql($sql);
368 | }
369 |
370 | /**
371 | * {@inheritdoc}
372 | */
373 | public function renameColumn($table, $oldName, $newName)
374 | {
375 | throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
376 | }
377 |
378 | /**
379 | * {@inheritdoc}
380 | */
381 | public function alterColumn($table, $column, $type)
382 | {
383 | throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
384 | }
385 |
386 | /**
387 | * {@inheritdoc}
388 | */
389 | public function addPrimaryKey($name, $table, $columns)
390 | {
391 | throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
392 | }
393 |
394 | /**
395 | * {@inheritdoc}
396 | */
397 | public function dropPrimaryKey($name, $table)
398 | {
399 | throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
400 | }
401 |
402 | /**
403 | * {@inheritdoc}
404 | */
405 | public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null)
406 | {
407 | throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
408 | }
409 |
410 | /**
411 | * {@inheritdoc}
412 | */
413 | public function dropForeignKey($name, $table)
414 | {
415 | throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
416 | }
417 |
418 | /**
419 | * {@inheritdoc}
420 | */
421 | public function createIndex($name, $table, $columns, $unique = false)
422 | {
423 | throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
424 | }
425 |
426 | /**
427 | * {@inheritdoc}
428 | */
429 | public function dropIndex($name, $table)
430 | {
431 | throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
432 | }
433 |
434 | /**
435 | * {@inheritdoc}
436 | */
437 | public function resetSequence($table, $value = null)
438 | {
439 | throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
440 | }
441 |
442 | /**
443 | * {@inheritdoc}
444 | */
445 | public function checkIntegrity($check = true, $schema = '', $table = '')
446 | {
447 | throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
448 | }
449 | }
450 |
--------------------------------------------------------------------------------
/src/Connection.php:
--------------------------------------------------------------------------------
1 | 'mysql:host=127.0.0.1;port=9306;',
33 | * 'username' => $username,
34 | * 'password' => $password,
35 | * ]);
36 | * $connection->open();
37 | * ```
38 | *
39 | * After the Sphinx connection is established, one can execute SQL statements like the following:
40 | *
41 | * ```php
42 | * $command = $connection->createCommand("SELECT * FROM idx_article WHERE MATCH('programming')");
43 | * $articles = $command->queryAll();
44 | * $command = $connection->createCommand('UPDATE idx_article SET status=2 WHERE id=1');
45 | * $command->execute();
46 | * ```
47 | *
48 | * For more information about how to perform various DB queries, please refer to [[Command]].
49 | *
50 | * This class supports transactions exactly as "yii\db\Connection".
51 | *
52 | * Note: while this class extends "yii\db\Connection" some of its methods are not supported.
53 | *
54 | * @method \yii\sphinx\Schema getSchema() The schema information for this Sphinx connection
55 | * @method \yii\sphinx\QueryBuilder getQueryBuilder() the query builder for this Sphinx connection
56 | *
57 | * @property-read string $lastInsertID The row ID of the last row inserted, or the last value retrieved from
58 | * the sequence object.
59 | *
60 | * @author Paul Klimov
61 | * @since 2.0
62 | */
63 | class Connection extends \yii\db\Connection
64 | {
65 | /**
66 | * {@inheritdoc}
67 | */
68 | public $schemaMap = [
69 | 'mysqli' => 'yii\sphinx\Schema', // MySQL
70 | 'mysql' => 'yii\sphinx\Schema', // MySQL
71 | ];
72 | /**
73 | * @var bool whether to enable conversion of the float query params into the direct literal SQL insertion.
74 | * This allows processing of the float values, since PDO does not provide specific param type for float binding,
75 | * while Sphinx is unable to process float values passed as quoted strings.
76 | * @since 2.0.6
77 | */
78 | public $enableFloatConversion = true;
79 |
80 |
81 | /**
82 | * Obtains the schema information for the named index.
83 | * @param string $name index name.
84 | * @param bool $refresh whether to reload the table schema even if it is found in the cache.
85 | * @return IndexSchema index schema information. Null if the named index does not exist.
86 | */
87 | public function getIndexSchema($name, $refresh = false)
88 | {
89 | return $this->getSchema()->getIndexSchema($name, $refresh);
90 | }
91 |
92 | /**
93 | * Quotes a index name for use in a query.
94 | * If the index name contains schema prefix, the prefix will also be properly quoted.
95 | * If the index name is already quoted or contains special characters including '(', '[[' and '{{',
96 | * then this method will do nothing.
97 | * @param string $name index name
98 | * @return string the properly quoted index name
99 | */
100 | public function quoteIndexName($name)
101 | {
102 | return $this->getSchema()->quoteIndexName($name);
103 | }
104 |
105 | /**
106 | * Alias of [[quoteIndexName()]].
107 | * @param string $name table name
108 | * @return string the properly quoted table name
109 | */
110 | public function quoteTableName($name)
111 | {
112 | return $this->quoteIndexName($name);
113 | }
114 |
115 | /**
116 | * Creates a command for execution.
117 | * @param string $sql the SQL statement to be executed
118 | * @param array $params the parameters to be bound to the SQL statement
119 | * @return Command the Sphinx command
120 | */
121 | public function createCommand($sql = null, $params = [])
122 | {
123 | $command = new Command([
124 | 'db' => $this,
125 | 'sql' => $sql,
126 | ]);
127 |
128 | return $command->bindValues($params);
129 | }
130 |
131 | /**
132 | * This method is not supported by Sphinx.
133 | * @param string $sequenceName name of the sequence object
134 | * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
135 | * @throws \yii\base\NotSupportedException always.
136 | */
137 | public function getLastInsertID($sequenceName = '')
138 | {
139 | throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
140 | }
141 |
142 | /**
143 | * Escapes all special characters from 'MATCH' statement argument.
144 | * Make sure you are using this method whenever composing 'MATCH' search statement.
145 | * Note: this method does not perform quoting, you should place the result in the quotes
146 | * an perform additional escaping for it manually, the best way to do it is using PDO parameter.
147 | * @param string $str string to be escaped.
148 | * @return string the properly escaped string.
149 | */
150 | public function escapeMatchValue($str)
151 | {
152 | return str_replace(
153 | ['\\', '/', '"', '(', ')', '|', '-', '!', '@', '~', '&', '^', '$', '=', '>', '<', "\x00", "\n", "\r", "\x1a"],
154 | ['\\\\', '\\/', '\\"', '\\(', '\\)', '\\|', '\\-', '\\!', '\\@', '\\~', '\\&', '\\^', '\\$', '\\=', '\\>', '\\<', "\\x00", "\\n", "\\r", "\\x1a"],
155 | $str
156 | );
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/IndexSchema.php:
--------------------------------------------------------------------------------
1 |
19 | * @since 2.0
20 | */
21 | class IndexSchema extends BaseObject
22 | {
23 | /**
24 | * @var string name of this index.
25 | */
26 | public $name;
27 | /**
28 | * @var string type of the index.
29 | */
30 | public $type;
31 | /**
32 | * @var bool whether this index is a real-time index.
33 | */
34 | public $isRt;
35 | /**
36 | * @var string primary key of this index.
37 | */
38 | public $primaryKey;
39 | /**
40 | * @var ColumnSchema[] column metadata of this index. Each array element is a [[ColumnSchema]] object, indexed by column names.
41 | */
42 | public $columns = [];
43 |
44 |
45 | /**
46 | * Gets the named column metadata.
47 | * This is a convenient method for retrieving a named column even if it does not exist.
48 | * @param string $name column name
49 | * @return ColumnSchema metadata of the named column. Null if the named column does not exist.
50 | */
51 | public function getColumn($name)
52 | {
53 | return isset($this->columns[$name]) ? $this->columns[$name] : null;
54 | }
55 |
56 | /**
57 | * Returns the names of all columns in this table.
58 | * @return array list of column names
59 | */
60 | public function getColumnNames()
61 | {
62 | return array_keys($this->columns);
63 | }
64 |
65 | /**
66 | * @deprecated
67 | * This method is deprecated, use [[isRt]] instead.
68 | * @return bool whether this index is a real-time index.
69 | * @since 2.0.9
70 | */
71 | public function isIsRuntime()
72 | {
73 | return $this->isRt;
74 | }
75 |
76 | /**
77 | * @deprecated
78 | * This method is deprecated, use [[isRt]] instead.
79 | * @param bool $isRuntime whether this index is a real-time index.
80 | * @since 2.0.9
81 | */
82 | public function setIsRuntime($isRuntime)
83 | {
84 | $this->isRt = $isRuntime;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/MatchBuilder.php:
--------------------------------------------------------------------------------
1 |
21 | * @author Paul Klimov
22 | * @since 2.0.6
23 | */
24 | class MatchBuilder extends BaseObject
25 | {
26 | /**
27 | * The prefix for automatically generated query binding parameters.
28 | */
29 | const PARAM_PREFIX = ':mp';
30 |
31 | /**
32 | * @var Connection the Sphinx connection.
33 | */
34 | public $db;
35 |
36 | /**
37 | * @var array map of MATCH keywords to builder methods.
38 | * These methods are used by [[buildMatch]] to build MATCH expression from array syntax.
39 | */
40 | protected $matchBuilders = [
41 | 'AND' => 'buildAndMatch',
42 | 'OR' => 'buildAndMatch',
43 | 'IGNORE' => 'buildIgnoreMatch',
44 | 'PROXIMITY' => 'buildProximityMatch',
45 | 'MAYBE' => 'buildMultipleMatch',
46 | 'SENTENCE' => 'buildMultipleMatch',
47 | 'PARAGRAPH' => 'buildMultipleMatch',
48 | 'ZONE' => 'buildZoneMatch',
49 | 'ZONESPAN' => 'buildZoneMatch',
50 | ];
51 | /**
52 | * @var array map of MATCH operators.
53 | * These operators are used for replacement of verbose operators.
54 | */
55 | protected $matchOperators = [
56 | 'AND' => ' ',
57 | 'OR' => ' | ',
58 | 'NOT' => ' !',
59 | '=' => ' ',
60 | ];
61 |
62 |
63 | /**
64 | * Constructor.
65 | * @param Connection $connection the Sphinx connection.
66 | * @param array $config name-value pairs that will be used to initialize the object properties
67 | */
68 | public function __construct($connection, $config = [])
69 | {
70 | $this->db = $connection;
71 | parent::__construct($config);
72 | }
73 |
74 | /**
75 | * Generates the MATCH expression from given [[MatchExpression]] object.
76 | * @param MatchExpression $match the [[MatchExpression]] object from which the MATCH expression will be generated.
77 | * @return string generated MATCH expression.
78 | */
79 | public function build($match)
80 | {
81 | $params = $match->params;
82 | $expression = $this->buildMatch($match->match, $params);
83 | return $this->parseParams($expression, $params);
84 | }
85 |
86 | /**
87 | * Create MATCH expression.
88 | * @param string|array $match MATCH specification.
89 | * @param array $params the expression parameters to be populated
90 | * @return string the MATCH expression
91 | */
92 | public function buildMatch($match, &$params)
93 | {
94 | if (empty($match)) {
95 | return '';
96 | }
97 |
98 | if ($match instanceof Expression) {
99 | return $this->buildMatchValue($match, $params);
100 | }
101 |
102 | if (!is_array($match)) {
103 | return $match;
104 | }
105 |
106 | if (isset($match[0])) {
107 | // operator format: operator, operand 1, operand 2, ...
108 | $operator = strtoupper($match[0]);
109 | if (isset($this->matchBuilders[$operator])) {
110 | $method = $this->matchBuilders[$operator];
111 | } else {
112 | $method = 'buildSimpleMatch';
113 | }
114 | array_shift($match);
115 | return $this->$method($operator, $match, $params);
116 | }
117 |
118 | // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
119 | return $this->buildHashMatch($match, $params);
120 | }
121 |
122 | /**
123 | * Creates a MATCH based on column-value pairs.
124 | * @param array $match the match condition
125 | * @param array $params the expression parameters to be populated
126 | * @return string the MATCH expression
127 | */
128 | public function buildHashMatch($match, &$params)
129 | {
130 | $parts = [];
131 |
132 | foreach ($match as $column => $value) {
133 | $parts[] = $this->buildMatchColumn($column) . ' ' . $this->buildMatchValue($value, $params);
134 | }
135 |
136 | return count($parts) === 1 ? $parts[0] : '(' . implode(') (', $parts) . ')';
137 | }
138 |
139 | /**
140 | * Connects two or more MATCH expressions with the `AND` or `OR` operator
141 | * @param string $operator the operator which is used for connecting the given operands
142 | * @param array $operands the Match expressions to connect
143 | * @param array $params the expression parameters to be populated
144 | * @return string the MATCH expression
145 | */
146 | public function buildAndMatch($operator, $operands, &$params)
147 | {
148 | $parts = [];
149 | foreach ($operands as $operand) {
150 | if (is_array($operand) || is_object($operand)) {
151 | $operand = $this->buildMatch($operand, $params);
152 | }
153 |
154 | if ($operand !== '') {
155 | $parts[] = $operand;
156 | }
157 | }
158 |
159 | if (empty($parts)) {
160 | return '';
161 | }
162 |
163 | return '(' . implode(')' . ($operator === 'OR' ? ' | ' : ' ') . '(', $parts) . ')';
164 | }
165 |
166 | /**
167 | * Create MAYBE, SENTENCE or PARAGRAPH expressions.
168 | * @param string $operator the operator which is used for Create Match expressions
169 | * @param array $operands the Match expressions
170 | * @param array &$params the expression parameters to be populated
171 | * @return string the MATCH expression
172 | */
173 | public function buildMultipleMatch($operator, $operands, &$params)
174 | {
175 | if (count($operands) < 3) {
176 | throw new InvalidParamException("Operator '$operator' requires three or more operands.");
177 | }
178 |
179 | $column = array_shift($operands);
180 |
181 | $phNames = [];
182 |
183 | foreach ($operands as $operand) {
184 | $phNames[] = $this->buildMatchValue($operand, $params);
185 | }
186 |
187 | return $this->buildMatchColumn($column) . ' ' . implode(' ' . $operator . ' ', $phNames);
188 | }
189 |
190 | /**
191 | * Create MATCH expressions for zones.
192 | * @param string $operator the operator which is used for Create Match expressions
193 | * @param array $operands the Match expressions
194 | * @param array &$params the expression parameters to be populated
195 | * @return string the MATCH expression
196 | */
197 | public function buildZoneMatch($operator, $operands, &$params)
198 | {
199 | if (!isset($operands[0])) {
200 | throw new InvalidParamException("Operator '$operator' requires exactly one operand.");
201 | }
202 |
203 | $zones = (array)$operands[0];
204 |
205 | return "$operator: (" . implode(',', $zones) . ")";
206 | }
207 |
208 | /**
209 | * Create PROXIMITY expressions
210 | * @param string $operator the operator which is used for Create Match expressions
211 | * @param array $operands the Match expressions
212 | * @param array &$params the expression parameters to be populated
213 | * @return string the MATCH expression
214 | */
215 | public function buildProximityMatch($operator, $operands, &$params)
216 | {
217 | if (!isset($operands[0], $operands[1], $operands[2])) {
218 | throw new InvalidParamException("Operator '$operator' requires three operands.");
219 | }
220 |
221 | list($column, $value, $proximity) = $operands;
222 |
223 | return $this->buildMatchColumn($column) . ' ' . $this->buildMatchValue($value, $params) . '~' . (int) $proximity;
224 | }
225 |
226 | /**
227 | * Create ignored MATCH expressions
228 | * @param string $operator the operator which is used for Create Match expressions
229 | * @param array $operands the Match expressions
230 | * @param array &$params the expression parameters to be populated
231 | * @return string the MATCH expression
232 | */
233 | public function buildIgnoreMatch($operator, $operands, &$params)
234 | {
235 | if (!isset($operands[0], $operands[1])) {
236 | throw new InvalidParamException("Operator '$operator' requires two operands.");
237 | }
238 |
239 | list($column, $value) = $operands;
240 |
241 | return $this->buildMatchColumn($column, true) . ' ' . $this->buildMatchValue($value, $params);
242 | }
243 |
244 | /**
245 | * Creates an Match expressions like `"column" operator value`.
246 | * @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc.
247 | * @param array $operands contains two column names.
248 | * @param array $params the expression parameters to be populated
249 | * @return string the MATCH expression
250 | * @throws InvalidParamException on invalid operands count.
251 | */
252 | public function buildSimpleMatch($operator, $operands, &$params)
253 | {
254 | if (count($operands) !== 2) {
255 | throw new InvalidParamException("Operator '$operator' requires two operands.");
256 | }
257 |
258 | list($column, $value) = $operands;
259 |
260 | if (isset($this->matchOperators[$operator])) {
261 | $operator = $this->matchOperators[$operator];
262 | }
263 |
264 | return $this->buildMatchColumn($column) . $operator . $this->buildMatchValue($value, $params);
265 | }
266 |
267 | /**
268 | * Create placeholder for expression of MATCH
269 | * @param string|array|Expression $value
270 | * @param array $params the expression parameters to be populated
271 | * @return string the MATCH expression
272 | */
273 | protected function buildMatchValue($value, &$params)
274 | {
275 | if (empty($value)) {
276 | return '""';
277 | }
278 |
279 | if ($value instanceof Expression) {
280 | $params = array_merge($params, $value->params);
281 | return $value->expression;
282 | }
283 |
284 | $parts = [];
285 | foreach ((array) $value as $v) {
286 | if ($v instanceof Expression) {
287 | $params = array_merge($params, $v->params);
288 | $parts[] = $v->expression;
289 | } else {
290 | $phName = self::PARAM_PREFIX . count($params);
291 | $parts[] = $phName;
292 | $params[$phName] = $v;
293 | }
294 | }
295 |
296 | return implode(' | ', $parts);
297 | }
298 |
299 | /**
300 | * Created column as string for expression of MATCH
301 | * @param string $column column specification.
302 | * @param bool $ignored whether column should be specified as 'ignored'.
303 | * @return string the column statement.
304 | */
305 | protected function buildMatchColumn($column, $ignored = false)
306 | {
307 | if (empty($column)) {
308 | return '';
309 | }
310 |
311 | if ($column === '*') {
312 | return '@*';
313 | }
314 |
315 | return '@' . ($ignored ? '!' : '') . (strpos($column, ',') === false ? $column : '(' . $column . ')');
316 | }
317 |
318 | /**
319 | * Returns the actual MATCH expression by inserting parameter values into the corresponding placeholders.
320 | * @param string $expression the expression string which is needed to prepare.
321 | * @param array $params the binding parameters for inserting.
322 | * @return string parsed expression.
323 | */
324 | protected function parseParams($expression, $params)
325 | {
326 | if (empty($params)) {
327 | return $expression;
328 | }
329 |
330 | foreach ($params as $name => $value) {
331 | if (strncmp($name, ':', 1) !== 0) {
332 | $name = ':' . $name;
333 | }
334 | // unable to use `str_replace()` because particular param name may be a substring of another param name
335 | $pattern = "/" . preg_quote($name, '/') . '\b/';
336 | $value = '"' . $this->db->escapeMatchValue($value) . '"';
337 | $expression = preg_replace($pattern, $value, $expression, -1, $cnt);
338 | }
339 |
340 | return $expression;
341 | }
342 | }
343 |
--------------------------------------------------------------------------------
/src/MatchExpression.php:
--------------------------------------------------------------------------------
1 | match(new MatchExpression('@title :title', ['title' => 'Yii']))
25 | * ->all();
26 | * ```
27 | *
28 | * You may use [[match()]], [[andMatch()]] and [[orMatch()]] to combine several conditions.
29 | * For example:
30 | *
31 | * ```php
32 | * use yii\sphinx\Query;
33 | * use yii\sphinx\MatchExpression;
34 | *
35 | * $rows = (new Query())
36 | * ->match(
37 | * // produces '((@title "Yii") (@author "Paul")) | (@content "Sphinx")' :
38 | * (new MatchExpression())
39 | * ->match(['title' => 'Yii'])
40 | * ->andMatch(['author' => 'Paul'])
41 | * ->orMatch(['content' => 'Sphinx'])
42 | * )
43 | * ->all();
44 | * ```
45 | *
46 | * You may as well compose expressions with special operators like 'MAYBE', 'PROXIMITY' etc.
47 | * For example:
48 | *
49 | * ```php
50 | * use yii\sphinx\Query;
51 | * use yii\sphinx\MatchExpression;
52 | *
53 | * $rows = (new Query())
54 | * ->match(
55 | * // produces '@title "Yii" MAYBE "Sphinx"' :
56 | * (new MatchExpression())->match([
57 | * 'maybe',
58 | * 'title',
59 | * 'Yii',
60 | * 'Sphinx',
61 | * ])
62 | * )
63 | * ->all();
64 | *
65 | * $rows = (new Query())
66 | * ->match(
67 | * // produces '@title "Yii"~10' :
68 | * (new MatchExpression())->match([
69 | * 'proximity',
70 | * 'title',
71 | * 'Yii',
72 | * 10,
73 | * ])
74 | * )
75 | * ->all();
76 | * ```
77 | *
78 | * Note: parameters passed via [[params]] or generated from array conditions will be automatically escaped
79 | * using [[Connection::escapeMatchValue()]].
80 | *
81 | * @see MatchBuilder
82 | * @see https://sphinxsearch.com/docs/current.html#extended-syntax
83 | *
84 | * @author Paul Klimov
85 | * @since 2.0.6
86 | */
87 | class MatchExpression extends BaseObject
88 | {
89 | /**
90 | * @var string|array|Expression MATCH expression.
91 | * For example: `['title' => 'Yii', 'content' => 'Sphinx']`.
92 | * Note: being specified as a plain string this value will not be quoted or escaped, do not pass
93 | * possible unsecured values (like the ones obtained from HTTP request) as a direct value.
94 | * @see match()
95 | */
96 | public $match;
97 | /**
98 | * @var array list of match expression parameter values indexed by parameter placeholders.
99 | * For example, `[':name' => 'Dan', ':age' => 31]`.
100 | * These parameters will be automatically escaped using [[Connection::escapeMatchValue()]] and inserted into MATCH
101 | * expression as a quoted strings.
102 | */
103 | public $params = [];
104 |
105 |
106 | /**
107 | * Constructor.
108 | * @param string $match the MATCH expression
109 | * @param array $params expression parameters.
110 | * @param array $config name-value pairs that will be used to initialize the object properties
111 | */
112 | public function __construct($match = null, $params = [], $config = [])
113 | {
114 | $this->match = $match;
115 | $this->params = $params;
116 | parent::__construct($config);
117 | }
118 |
119 | /**
120 | * Sets the MATCH expression.
121 | *
122 | * The method requires a `$condition` parameter, and optionally a `$params` parameter
123 | * specifying the values to be parsed into the expression.
124 | *
125 | * The `$condition` parameter should be either a string (e.g. `'@name "John"'`) or an array.
126 | *
127 | * @param string|array|Expression $condition the conditions that should be put in the MATCH expression.
128 | * @param array $params the parameters (name => value) to be parsed into the query.
129 | * @return $this the expression object itself
130 | * @see andMatch()
131 | * @see orMatch()
132 | */
133 | public function match($condition, $params = [])
134 | {
135 | $this->match = $condition;
136 | $this->addParams($params);
137 | return $this;
138 | }
139 |
140 | /**
141 | * Adds an additional MATCH condition to the existing one.
142 | * The new condition and the existing one will be joined using the 'AND' (' ') operator.
143 | * @param string|array|Expression $condition the new MATCH condition. Please refer to [[match()]]
144 | * on how to specify this parameter.
145 | * @param array $params the parameters (name => value) to be parsed into the query.
146 | * @return $this the expression object itself
147 | * @see match()
148 | * @see orMatch()
149 | */
150 | public function andMatch($condition, $params = [])
151 | {
152 | if ($this->match === null) {
153 | $this->match = $condition;
154 | } else {
155 | $this->match = ['and', $this->match, $condition];
156 | }
157 | $this->addParams($params);
158 | return $this;
159 | }
160 |
161 | /**
162 | * Adds an additional MATCH condition to the existing one.
163 | * The new condition and the existing one will be joined using the 'OR' ('|') operator.
164 | * @param string|array|Expression $condition the new WHERE condition. Please refer to [[match()]]
165 | * on how to specify this parameter.
166 | * @param array $params the parameters (name => value) to be parsed into the query.
167 | * @return $this the expression object itself
168 | * @see match()
169 | * @see andMatch()
170 | */
171 | public function orMatch($condition, $params = [])
172 | {
173 | if ($this->match === null) {
174 | $this->match = $condition;
175 | } else {
176 | $this->match = ['or', $this->match, $condition];
177 | }
178 | $this->addParams($params);
179 | return $this;
180 | }
181 |
182 | /**
183 | * Sets the parameters to be parsed into the query.
184 | * @param array $params list of expression parameter values indexed by parameter placeholders.
185 | * For example, `[':name' => 'Dan', ':age' => 31]`.
186 | * @return $this the expression object itself
187 | * @see addParams()
188 | */
189 | public function params($params)
190 | {
191 | $this->params = $params;
192 | return $this;
193 | }
194 |
195 | /**
196 | * Adds additional parameters to be parsed into the query.
197 | * @param array $params list of expression parameter values indexed by parameter placeholders.
198 | * For example, `[':name' => 'Dan', ':age' => 31]`.
199 | * @return $this the expression object itself
200 | * @see params()
201 | */
202 | public function addParams($params)
203 | {
204 | if (!empty($params)) {
205 | if (empty($this->params)) {
206 | $this->params = $params;
207 | } else {
208 | foreach ($params as $name => $value) {
209 | if (is_int($name)) {
210 | $this->params[] = $value;
211 | } else {
212 | $this->params[$name] = $value;
213 | }
214 | }
215 | }
216 | }
217 | return $this;
218 | }
219 |
220 | /**
221 | * Sets the MATCH part of the query but ignores [[isEmpty()|empty operands]].
222 | *
223 | * This method is similar to [[match()]]. The main difference is that this method will
224 | * remove [[isEmpty()|empty query operands]]. As a result, this method is best suited
225 | * for building query conditions based on filter values entered by users.
226 | *
227 | * The following code shows the difference between this method and [[match()]]:
228 | *
229 | * ```php
230 | * // MATCH (@title :title)
231 | * $query->filterMatch(['name' => null, 'title' => 'foo']);
232 | * // MATCH (@title :title)
233 | * $query->match(['title' => 20]);
234 | * // MATCH (@name :name @title :title)
235 | * $query->match(['name' => null, 'age' => 20]);
236 | * ```
237 | *
238 | * Note that unlike [[match()]], you cannot pass binding parameters to this method.
239 | *
240 | * @param array $condition the conditions that should be put in the MATCH part.
241 | * See [[match()]] on how to specify this parameter.
242 | * @return $this the query object itself
243 | * @see where()
244 | * @see andFilterMatch()
245 | * @see orFilterMatch()
246 | * @since 2.0.7
247 | */
248 | public function filterMatch(array $condition)
249 | {
250 | $condition = $this->filterCondition($condition);
251 | if ($condition !== []) {
252 | $this->match($condition);
253 | }
254 | return $this;
255 | }
256 |
257 | /**
258 | * Adds an additional MATCH condition to the existing one but ignores [[isEmpty()|empty operands]].
259 | * The new condition and the existing one will be joined using the 'AND' operator.
260 | *
261 | * This method is similar to [[andMatch()]]. The main difference is that this method will
262 | * remove [[isEmpty()|empty query operands]]. As a result, this method is best suited
263 | * for building query conditions based on filter values entered by users.
264 | *
265 | * @param array $condition the new MATCH condition. Please refer to [[match()]]
266 | * on how to specify this parameter.
267 | * @return $this the query object itself
268 | * @see filterMatch()
269 | * @see orFilterMatch()
270 | * @since 2.0.7
271 | */
272 | public function andFilterMatch(array $condition)
273 | {
274 | $condition = $this->filterCondition($condition);
275 | if ($condition !== []) {
276 | $this->andMatch($condition);
277 | }
278 | return $this;
279 | }
280 |
281 | /**
282 | * Adds an additional MATCH condition to the existing one but ignores [[isEmpty()|empty operands]].
283 | * The new condition and the existing one will be joined using the 'OR' operator.
284 | *
285 | * This method is similar to [[orMatch()]]. The main difference is that this method will
286 | * remove [[isEmpty()|empty query operands]]. As a result, this method is best suited
287 | * for building query conditions based on filter values entered by users.
288 | *
289 | * @param array $condition the new MATCH condition. Please refer to [[match()]]
290 | * on how to specify this parameter.
291 | * @return $this the query object itself
292 | * @see filterMatch()
293 | * @see andFilterMatch()
294 | * @since 2.0.7
295 | */
296 | public function orFilterMatch(array $condition)
297 | {
298 | $condition = $this->filterCondition($condition);
299 | if ($condition !== []) {
300 | $this->orMatch($condition);
301 | }
302 | return $this;
303 | }
304 |
305 | /**
306 | * Removes [[isEmpty()|empty operands]] from the given query condition.
307 | *
308 | * @param array $condition the original condition
309 | * @return array the condition with [[isEmpty()|empty operands]] removed.
310 | * @since 2.0.7
311 | */
312 | protected function filterCondition($condition)
313 | {
314 | if (!is_array($condition)) {
315 | return $condition;
316 | }
317 |
318 | if (!isset($condition[0])) {
319 | // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
320 | foreach ($condition as $name => $value) {
321 | if ($this->isEmpty($value)) {
322 | unset($condition[$name]);
323 | }
324 | }
325 | return $condition;
326 | }
327 |
328 | // operator format: operator, operand 1, operand 2, ...
329 |
330 | $operator = array_shift($condition);
331 |
332 | switch (strtoupper($operator)) {
333 | case 'NOT':
334 | case 'AND':
335 | case 'OR':
336 | foreach ($condition as $i => $operand) {
337 | $subCondition = $this->filterCondition($operand);
338 | if ($this->isEmpty($subCondition)) {
339 | unset($condition[$i]);
340 | } else {
341 | $condition[$i] = $subCondition;
342 | }
343 | }
344 |
345 | if (empty($condition)) {
346 | return [];
347 | }
348 | break;
349 | case 'SENTENCE':
350 | case 'PARAGRAPH':
351 | $column = array_shift($condition);
352 | foreach ($condition as $i => $operand) {
353 | if ($this->isEmpty($operand)) {
354 | unset($condition[$i]);
355 | }
356 | }
357 |
358 | if (empty($condition)) {
359 | return [];
360 | }
361 |
362 | array_unshift($condition, $column);
363 | break;
364 | case 'ZONE':
365 | case 'ZONESPAN':
366 | foreach ($condition as $i => $operand) {
367 | if ($this->isEmpty($operand)) {
368 | unset($condition[$i]);
369 | }
370 | }
371 |
372 | if (empty($condition)) {
373 | return [];
374 | }
375 | break;
376 | default:
377 | if (array_key_exists(1, $condition) && $this->isEmpty($condition[1])) {
378 | return [];
379 | }
380 | }
381 |
382 | array_unshift($condition, $operator);
383 |
384 | return $condition;
385 | }
386 |
387 | /**
388 | * Returns a value indicating whether the give value is "empty".
389 | *
390 | * The value is considered "empty", if one of the following conditions is satisfied:
391 | *
392 | * - it is `null`,
393 | * - an empty string (`''`),
394 | * - a string containing only whitespace characters,
395 | * - or an empty array.
396 | *
397 | * @param mixed $value
398 | * @return bool if the value is empty
399 | * @since 2.0.7
400 | */
401 | protected function isEmpty($value)
402 | {
403 | return $value === '' || $value === [] || $value === null || is_string($value) && trim($value) === '';
404 | }
405 | }
406 |
--------------------------------------------------------------------------------
/src/Query.php:
--------------------------------------------------------------------------------
1 | select('id, group_id')
29 | * ->from('idx_item')
30 | * ->limit(10);
31 | * // build and execute the query
32 | * $command = $query->createCommand();
33 | * // $command->sql returns the actual SQL
34 | * $rows = $command->queryAll();
35 | * ```
36 | *
37 | * Since Sphinx does not store the original indexed text, the snippets for the rows in query result
38 | * should be build separately via another query. You can simplify this workflow using [[snippetCallback]].
39 | *
40 | * Warning: even if you do not set any query limit, implicit LIMIT 0,20 is present by default!
41 | *
42 | * @property Connection $connection Sphinx connection instance.
43 | *
44 | * @author Paul Klimov
45 | * @since 2.0
46 | */
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 | * if you need to compose complex match condition use [[Expression]],
54 | * see [[match()]] for details.
55 | */
56 | public $match;
57 | /**
58 | * @var string WITHIN GROUP ORDER BY clause. This is a Sphinx specific extension
59 | * that lets you control how the best row within a group will to be selected.
60 | * The possible value matches the [[orderBy]] one.
61 | */
62 | public $within;
63 | /**
64 | * @var array per-query options in format: optionName => optionValue
65 | * They will compose OPTION clause. This is a Sphinx specific extension
66 | * that lets you control a number of per-query options.
67 | */
68 | public $options;
69 | /**
70 | * @var callable PHP callback, which should be used to fetch source data for the snippets.
71 | * Such callback will receive array of query result rows as an argument and must return the
72 | * array of snippet source strings in the order, which match one of incoming rows.
73 | * For example:
74 | *
75 | * ```php
76 | * $query = new Query();
77 | * $query->from('idx_item')
78 | * ->match('pencil')
79 | * ->snippetCallback(function ($rows) {
80 | * $result = [];
81 | * foreach ($rows as $row) {
82 | * $result[] = file_get_contents('/path/to/index/files/' . $row['id'] . '.txt');
83 | * }
84 | * return $result;
85 | * })
86 | * ->all();
87 | * ```
88 | */
89 | public $snippetCallback;
90 | /**
91 | * @var array query options for the call snippet.
92 | */
93 | public $snippetOptions;
94 | /**
95 | * @var array facet search specifications.
96 | * For example:
97 | *
98 | * ```php
99 | * [
100 | * 'group_id',
101 | * 'brand_id' => [
102 | * 'order' => ['COUNT(*)' => SORT_ASC],
103 | * ],
104 | * 'price' => [
105 | * 'select' => 'INTERVAL(price,200,400,600,800) AS price',
106 | * 'order' => ['FACET()' => SORT_ASC],
107 | * ],
108 | * 'name_in_json' => [
109 | * 'select' => [new Expression('json_attr.name AS name_in_json')],
110 | * ],
111 | * ]
112 | * ```
113 | *
114 | * You need to use [[search()]] method in order to fetch facet results.
115 | *
116 | * Note: if you specify custom select for the facet, ensure facet name has corresponding column inside it.
117 | */
118 | public $facets = [];
119 | /**
120 | * @var bool|string|Expression whether to automatically perform 'SHOW META' query against main one.
121 | * You may set this value to be string or [[Expression]] instance, in this case its value will be used
122 | * as 'LIKE' condition for 'SHOW META' statement.
123 | * You need to use [[search()]] method in order to fetch 'meta' results.
124 | */
125 | public $showMeta;
126 | /**
127 | * @var int groups limit: to return (no more than) N top matches for each group.
128 | * This option will take effect only if [[groupBy]] is set.
129 | * @since 2.0.6
130 | */
131 | public $groupLimit;
132 |
133 | /**
134 | * @var Connection the Sphinx connection used to generate the SQL statements.
135 | */
136 | private $_connection;
137 |
138 |
139 | /**
140 | * @param Connection $connection Sphinx connection instance
141 | * @return $this the query object itself
142 | */
143 | public function setConnection($connection)
144 | {
145 | $this->_connection = $connection;
146 |
147 | return $this;
148 | }
149 |
150 | /**
151 | * @return Connection Sphinx connection instance
152 | */
153 | public function getConnection()
154 | {
155 | if ($this->_connection === null) {
156 | $this->_connection = $this->defaultConnection();
157 | }
158 |
159 | return $this->_connection;
160 | }
161 |
162 | /**
163 | * @return Connection default connection value.
164 | */
165 | protected function defaultConnection()
166 | {
167 | return Yii::$app->get('sphinx');
168 | }
169 |
170 | /**
171 | * Creates a Sphinx command that can be used to execute this query.
172 | * @param Connection $db the Sphinx connection used to generate the SQL statement.
173 | * If this parameter is not given, the `sphinx` application component will be used.
174 | * @return Command the created Sphinx command instance.
175 | */
176 | public function createCommand($db = null)
177 | {
178 | $this->setConnection($db);
179 | $db = $this->getConnection();
180 | list ($sql, $params) = $db->getQueryBuilder()->build($this);
181 |
182 | return $db->createCommand($sql, $params);
183 | }
184 |
185 | /**
186 | * {@inheritdoc}
187 | */
188 | public function populate($rows)
189 | {
190 | return parent::populate($this->fillUpSnippets($rows));
191 | }
192 |
193 | /**
194 | * {@inheritdoc}
195 | */
196 | public function one($db = null)
197 | {
198 | $row = parent::one($db);
199 | if ($row !== false) {
200 | list ($row) = $this->fillUpSnippets([$row]);
201 | }
202 |
203 | return $row;
204 | }
205 |
206 | /**
207 | * Executes the query and returns the complete search result including e.g. hits, facets.
208 | * @param Connection $db the Sphinx connection used to generate the SQL statement.
209 | * @return array the query results.
210 | */
211 | public function search($db = null)
212 | {
213 | if (!empty($this->emulateExecution)) {
214 | return [
215 | 'hits' => [],
216 | 'facets' => [],
217 | 'meta' => [],
218 | ];
219 | }
220 |
221 | $command = $this->createCommand($db);
222 | $dataReader = $command->query();
223 | $rawRows = $dataReader->readAll();
224 |
225 | $facets = [];
226 | foreach ($this->facets as $facetKey => $facetValue) {
227 | $dataReader->nextResult();
228 | $rawFacetResults = $dataReader->readAll();
229 |
230 | if (is_numeric($facetKey)) {
231 | $facet = [
232 | 'name' => $facetValue,
233 | 'value' => $facetValue,
234 | 'count' => 'count(*)',
235 | ];
236 | } else {
237 | $facet = array_merge(
238 | [
239 | 'name' => $facetKey,
240 | 'value' => $facetKey,
241 | 'count' => 'count(*)',
242 | ],
243 | $facetValue
244 | );
245 | }
246 |
247 | foreach ($rawFacetResults as $rawFacetResult) {
248 | $rawFacetResult['value'] = isset($rawFacetResult[strtolower($facet['value'])]) ? $rawFacetResult[strtolower($facet['value'])] : null;
249 | $rawFacetResult['count'] = $rawFacetResult[$facet['count']];
250 | $facets[$facet['name']][] = $rawFacetResult;
251 | }
252 | }
253 |
254 | $meta = [];
255 | if (!empty($this->showMeta)) {
256 | $dataReader->nextResult();
257 | $rawMetaResults = $dataReader->readAll();
258 | foreach ($rawMetaResults as $rawMetaResult) {
259 | $meta[$rawMetaResult['Variable_name']] = $rawMetaResult['Value'];
260 | }
261 | }
262 |
263 | // rows should be populated after all data read from cursor, avoiding possible 'unbuffered query' error
264 | $rows = $this->populate($rawRows);
265 |
266 | return [
267 | 'hits' => $rows,
268 | 'facets' => $facets,
269 | 'meta' => $meta,
270 | ];
271 | }
272 |
273 | /**
274 | * Sets the fulltext query text. This text will be composed into
275 | * MATCH operator inside the WHERE clause.
276 | * Note: this value will be processed by [[Connection::escapeMatchValue()]],
277 | * if you need to compose complex match condition use [[Expression]]:
278 | *
279 | * ```php
280 | * $query = new Query();
281 | * $query->from('my_index')
282 | * ->match(new Expression(':match', ['match' => '@(content) ' . Yii::$app->sphinx->escapeMatchValue($matchValue)]))
283 | * ->all();
284 | * ```
285 | *
286 | * @param string|Expression|MatchExpression $query fulltext query text.
287 | * @return $this the query object itself.
288 | */
289 | public function match($query)
290 | {
291 | $this->match = $query;
292 | return $this;
293 | }
294 |
295 | /**
296 | * {@inheritdoc}
297 | */
298 | public function join($type, $table, $on = '', $params = [])
299 | {
300 | throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
301 | }
302 |
303 | /**
304 | * {@inheritdoc}
305 | */
306 | public function innerJoin($table, $on = '', $params = [])
307 | {
308 | throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
309 | }
310 |
311 | /**
312 | * {@inheritdoc}
313 | */
314 | public function leftJoin($table, $on = '', $params = [])
315 | {
316 | throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
317 | }
318 |
319 | /**
320 | * {@inheritdoc}
321 | */
322 | public function rightJoin($table, $on = '', $params = [])
323 | {
324 | throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
325 | }
326 |
327 | /**
328 | * {@inheritdoc}
329 | * @since 2.0.9
330 | */
331 | public function getTablesUsedInFrom()
332 | {
333 | // feature not supported, returning a stub:
334 | return [];
335 | }
336 |
337 | /**
338 | * Sets the query options.
339 | * @param array $options query options in format: optionName => optionValue
340 | * @return $this the query object itself
341 | * @see addOptions()
342 | */
343 | public function options($options)
344 | {
345 | $this->options = $options;
346 |
347 | return $this;
348 | }
349 |
350 | /**
351 | * Adds additional query options.
352 | * @param array $options query options in format: optionName => optionValue
353 | * @return $this the query object itself
354 | * @see options()
355 | */
356 | public function addOptions($options)
357 | {
358 | if (is_array($this->options)) {
359 | $this->options = array_merge($this->options, $options);
360 | } else {
361 | $this->options = $options;
362 | }
363 |
364 | return $this;
365 | }
366 |
367 | /**
368 | * Sets the WITHIN GROUP ORDER BY part of the query.
369 | * @param string|array $columns the columns (and the directions) to find best row within a group.
370 | * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array
371 | * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`).
372 | * The method will automatically quote the column names unless a column contains some parenthesis
373 | * (which means the column contains a DB expression).
374 | * @return $this the query object itself
375 | * @see addWithin()
376 | */
377 | public function within($columns)
378 | {
379 | $this->within = $this->normalizeOrderBy($columns);
380 |
381 | return $this;
382 | }
383 |
384 | /**
385 | * Adds additional WITHIN GROUP ORDER BY columns to the query.
386 | * @param string|array $columns the columns (and the directions) to find best row within a group.
387 | * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array
388 | * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`).
389 | * The method will automatically quote the column names unless a column contains some parenthesis
390 | * (which means the column contains a DB expression).
391 | * @return $this the query object itself
392 | * @see within()
393 | */
394 | public function addWithin($columns)
395 | {
396 | $columns = $this->normalizeOrderBy($columns);
397 | if ($this->within === null) {
398 | $this->within = $columns;
399 | } else {
400 | $this->within = array_merge($this->within, $columns);
401 | }
402 |
403 | return $this;
404 | }
405 |
406 | /**
407 | * Sets groups limit: to return (no more than) N top matches for each group.
408 | * This option will take effect only if [[groupBy]] is set.
409 | * @param int $limit group limit.
410 | * @return $this the query object itself.
411 | * @since 2.0.6
412 | */
413 | public function groupLimit($limit)
414 | {
415 | $this->groupLimit = $limit;
416 | return $this;
417 | }
418 |
419 | /**
420 | * Sets FACET part of the query.
421 | * @param array $facets facet specifications.
422 | * @return $this the query object itself
423 | */
424 | public function facets($facets)
425 | {
426 | $this->facets = $facets;
427 | return $this;
428 | }
429 |
430 | /**
431 | * Adds additional FACET part of the query.
432 | * @param array $facets facet specifications.
433 | * @return $this the query object itself
434 | */
435 | public function addFacets($facets)
436 | {
437 | if (is_array($this->facets)) {
438 | $this->facets = array_merge($this->facets, $facets);
439 | } else {
440 | $this->facets = $facets;
441 | }
442 | return $this;
443 | }
444 |
445 | /**
446 | * Sets whether to automatically perform 'SHOW META' for the search query.
447 | * @param bool|string|Expression $showMeta whether to automatically perform 'SHOW META'
448 | * @return $this the query object itself
449 | * @see showMeta
450 | */
451 | public function showMeta($showMeta)
452 | {
453 | $this->showMeta = $showMeta;
454 | return $this;
455 | }
456 |
457 | /**
458 | * Sets the PHP callback, which should be used to retrieve the source data
459 | * for the snippets building.
460 | * @param callable $callback PHP callback, which should be used to fetch source data for the snippets.
461 | * @return $this the query object itself
462 | * @see snippetCallback
463 | */
464 | public function snippetCallback($callback)
465 | {
466 | $this->snippetCallback = $callback;
467 |
468 | return $this;
469 | }
470 |
471 | /**
472 | * Sets the call snippets query options.
473 | * @param array $options call snippet options in format: option_name => option_value
474 | * @return $this the query object itself
475 | * @see snippetCallback
476 | */
477 | public function snippetOptions($options)
478 | {
479 | $this->snippetOptions = $options;
480 |
481 | return $this;
482 | }
483 |
484 | /**
485 | * Fills the query result rows with the snippets built from source determined by
486 | * [[snippetCallback]] result.
487 | * @param array $rows raw query result rows.
488 | * @return array|ActiveRecord[] query result rows with filled up snippets.
489 | */
490 | protected function fillUpSnippets($rows)
491 | {
492 | if ($this->snippetCallback === null || empty($rows)) {
493 | return $rows;
494 | }
495 | $snippetSources = call_user_func($this->snippetCallback, $rows);
496 | $snippets = $this->callSnippets($snippetSources);
497 | $snippetKey = 0;
498 | foreach ($rows as $key => $row) {
499 | $rows[$key]['snippet'] = $snippets[$snippetKey];
500 | $snippetKey++;
501 | }
502 |
503 | return $rows;
504 | }
505 |
506 | /**
507 | * Builds a snippets from provided source data.
508 | * @param array $source the source data to extract a snippet from.
509 | * @throws InvalidCallException in case [[match]] is not specified.
510 | * @return array snippets list.
511 | */
512 | protected function callSnippets(array $source)
513 | {
514 | return $this->callSnippetsInternal($source, $this->from[0]);
515 | }
516 |
517 | /**
518 | * Builds a snippets from provided source data by the given index.
519 | * @param array $source the source data to extract a snippet from.
520 | * @param string $from name of the source index.
521 | * @return array snippets list.
522 | * @throws InvalidCallException in case [[match]] is not specified.
523 | */
524 | protected function callSnippetsInternal(array $source, $from)
525 | {
526 | $connection = $this->getConnection();
527 | $match = $this->match;
528 | if ($match === null) {
529 | throw new InvalidCallException('Unable to call snippets: "' . $this->className() . '::match" should be specified.');
530 | }
531 |
532 | return $connection->createCommand()
533 | ->callSnippets($from, $source, $match, $this->snippetOptions)
534 | ->queryColumn();
535 | }
536 |
537 | /**
538 | * {@inheritdoc}
539 | */
540 | protected function queryScalar($selectExpression, $db)
541 | {
542 | if (!empty($this->emulateExecution)) {
543 | return null;
544 | }
545 |
546 | $select = $this->select;
547 | $limit = $this->limit;
548 | $offset = $this->offset;
549 |
550 | $this->select = [$selectExpression];
551 | $this->limit = null;
552 | $this->offset = null;
553 | $command = $this->createCommand($db);
554 |
555 | $this->select = $select;
556 | $this->limit = $limit;
557 | $this->offset = $offset;
558 |
559 | if (empty($this->groupBy) && empty($this->union) && !$this->distinct) {
560 | return $command->queryScalar();
561 | }
562 |
563 | return (new Query)->select([$selectExpression])
564 | ->from(['c' => $this])
565 | ->createCommand($command->db)
566 | ->queryScalar();
567 | }
568 |
569 | /**
570 | * Creates a new Query object and copies its property values from an existing one.
571 | * The properties being copies are the ones to be used by query builders.
572 | * @param Query $from the source query object
573 | * @return Query the new Query object
574 | */
575 | public static function create($from)
576 | {
577 | return new self([
578 | 'where' => $from->where,
579 | 'limit' => $from->limit,
580 | 'offset' => $from->offset,
581 | 'orderBy' => $from->orderBy,
582 | 'indexBy' => $from->indexBy,
583 | 'select' => $from->select,
584 | 'selectOption' => $from->selectOption,
585 | 'distinct' => $from->distinct,
586 | 'from' => $from->from,
587 | 'groupBy' => $from->groupBy,
588 | 'join' => $from->join,
589 | 'having' => $from->having,
590 | 'union' => $from->union,
591 | 'params' => $from->params,
592 | // Sphinx specifics :
593 | 'groupLimit' => $from->groupLimit,
594 | 'options' => $from->options,
595 | 'within' => $from->within,
596 | 'match' => $from->match,
597 | 'snippetCallback' => $from->snippetCallback,
598 | 'snippetOptions' => $from->snippetOptions,
599 | ]);
600 | }
601 | }
602 |
--------------------------------------------------------------------------------
/src/Schema.php:
--------------------------------------------------------------------------------
1 | index type.
23 | * @property-read QueryBuilder $queryBuilder The query builder for this connection.
24 | *
25 | * @author Paul Klimov
26 | * @since 2.0
27 | */
28 | class Schema extends BaseObject
29 | {
30 | /**
31 | * The following are the supported abstract column data types.
32 | */
33 | const TYPE_PK = 'pk';
34 | const TYPE_STRING = 'string';
35 | const TYPE_INTEGER = 'integer';
36 | const TYPE_BIGINT = 'bigint';
37 | const TYPE_FLOAT = 'float';
38 | const TYPE_TIMESTAMP = 'timestamp';
39 | const TYPE_BOOLEAN = 'boolean';
40 |
41 | /**
42 | * @var Connection the Sphinx connection
43 | */
44 | public $db;
45 | /**
46 | * @var array list of ALL index names in the Sphinx
47 | */
48 | private $_indexNames;
49 | /**
50 | * @var array list of ALL index types in the Sphinx (index name => index type)
51 | */
52 | private $_indexTypes;
53 | /**
54 | * @var array list of loaded index metadata (index name => IndexSchema)
55 | */
56 | private $_indexes = [];
57 | /**
58 | * @var QueryBuilder the query builder for this Sphinx connection
59 | */
60 | private $_builder;
61 |
62 |
63 | /**
64 | * @var array mapping from physical column types (keys) to abstract column types (values)
65 | */
66 | public $typeMap = [
67 | 'field' => self::TYPE_STRING,
68 | 'string' => self::TYPE_STRING,
69 | 'ordinal' => self::TYPE_STRING,
70 | 'integer' => self::TYPE_INTEGER,
71 | 'int' => self::TYPE_INTEGER,
72 | 'uint' => self::TYPE_INTEGER,
73 | 'bigint' => self::TYPE_BIGINT,
74 | 'timestamp' => self::TYPE_TIMESTAMP,
75 | 'bool' => self::TYPE_BOOLEAN,
76 | 'float' => self::TYPE_FLOAT,
77 | 'mva' => self::TYPE_INTEGER,
78 | 'uint_set' => self::TYPE_INTEGER,
79 | ];
80 |
81 | /**
82 | * Loads the metadata for the specified index.
83 | * @param string $name index name
84 | * @return IndexSchema|null driver dependent index metadata. `null` - if the index does not exist.
85 | */
86 | protected function loadIndexSchema($name)
87 | {
88 | $index = new IndexSchema();
89 | $this->resolveIndexNames($index, $name);
90 | $this->resolveIndexType($index);
91 |
92 | if ($this->findColumns($index)) {
93 | return $index;
94 | }
95 | return null;
96 | }
97 |
98 | /**
99 | * Resolves the index name.
100 | * @param IndexSchema $index the index metadata object
101 | * @param string $name the index name
102 | */
103 | protected function resolveIndexNames($index, $name)
104 | {
105 | $index->name = str_replace('`', '', $name);
106 | }
107 |
108 | /**
109 | * Resolves the index name.
110 | * @param IndexSchema $index the index metadata object
111 | */
112 | protected function resolveIndexType($index)
113 | {
114 | $indexTypes = $this->getIndexTypes();
115 | $index->type = array_key_exists($index->name, $indexTypes) ? $indexTypes[$index->name] : 'unknown';
116 | $index->isRt = ($index->type == 'rt');
117 | }
118 |
119 | /**
120 | * Obtains the metadata for the named index.
121 | * @param string $name index name. The index name may contain schema name if any. Do not quote the index name.
122 | * @param bool $refresh whether to reload the index schema even if it is found in the cache.
123 | * @return IndexSchema|null index metadata. `null` - if the named index does not exist.
124 | */
125 | public function getIndexSchema($name, $refresh = false)
126 | {
127 | if (isset($this->_indexes[$name]) && !$refresh) {
128 | return $this->_indexes[$name];
129 | }
130 |
131 | $db = $this->db;
132 | $realName = $this->getRawIndexName($name);
133 |
134 | if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) {
135 | /* @var $cache Cache */
136 | $cache = is_string($db->schemaCache) ? Yii::$app->get($db->schemaCache, false) : $db->schemaCache;
137 | if ($cache instanceof Cache) {
138 | $key = $this->getCacheKey($name);
139 | if ($refresh || ($index = $cache->get($key)) === false) {
140 | $index = $this->loadIndexSchema($realName);
141 | if ($index !== null) {
142 | $cache->set($key, $index, $db->schemaCacheDuration, new TagDependency([
143 | 'tags' => $this->getCacheTag(),
144 | ]));
145 | }
146 | }
147 |
148 | return $this->_indexes[$name] = $index;
149 | }
150 | }
151 |
152 | return $this->_indexes[$name] = $this->loadIndexSchema($realName);
153 | }
154 |
155 | /**
156 | * Returns the cache key for the specified index name.
157 | * @param string $name the index name
158 | * @return mixed the cache key
159 | */
160 | protected function getCacheKey($name)
161 | {
162 | return [
163 | __CLASS__,
164 | $this->db->dsn,
165 | $this->db->username,
166 | $name,
167 | ];
168 | }
169 |
170 | /**
171 | * Returns the cache tag name.
172 | * This allows [[refresh()]] to invalidate all cached index schemas.
173 | * @return string the cache tag name
174 | */
175 | protected function getCacheTag()
176 | {
177 | return md5(serialize([
178 | __CLASS__,
179 | $this->db->dsn,
180 | $this->db->username,
181 | ]));
182 | }
183 |
184 | /**
185 | * Returns the metadata for all indexes in the database.
186 | * @param bool $refresh whether to fetch the latest available index schemas. If this is false,
187 | * cached data may be returned if available.
188 | * @return IndexSchema[] the metadata for all indexes in the Sphinx.
189 | * Each array element is an instance of [[IndexSchema]] or its child class.
190 | */
191 | public function getIndexSchemas($refresh = false)
192 | {
193 | $indexes = [];
194 | foreach ($this->getIndexNames($refresh) as $name) {
195 | if (($index = $this->getIndexSchema($name, $refresh)) !== null) {
196 | $indexes[] = $index;
197 | }
198 | }
199 |
200 | return $indexes;
201 | }
202 |
203 | /**
204 | * Returns all index names in the Sphinx.
205 | * @param bool $refresh whether to fetch the latest available index names. If this is false,
206 | * index names fetched previously (if available) will be returned.
207 | * @return string[] all index names in the Sphinx.
208 | */
209 | public function getIndexNames($refresh = false)
210 | {
211 | if (!isset($this->_indexNames) || $refresh) {
212 | $this->initIndexesInfo();
213 | }
214 |
215 | return $this->_indexNames;
216 | }
217 |
218 | /**
219 | * Returns all index types in the Sphinx.
220 | * @param bool $refresh whether to fetch the latest available index types. If this is false,
221 | * index types fetched previously (if available) will be returned.
222 | * @return array all index types in the Sphinx in format: index name => index type.
223 | */
224 | public function getIndexTypes($refresh = false)
225 | {
226 | if (!isset($this->_indexTypes) || $refresh) {
227 | $this->initIndexesInfo();
228 | }
229 |
230 | return $this->_indexTypes;
231 | }
232 |
233 | /**
234 | * Initializes information about name and type of all index in the Sphinx.
235 | */
236 | protected function initIndexesInfo()
237 | {
238 | $this->_indexNames = [];
239 | $this->_indexTypes = [];
240 | $indexes = $this->findIndexes();
241 | foreach ($indexes as $index) {
242 | $indexName = $index['Index'];
243 | $this->_indexNames[] = $indexName;
244 | $this->_indexTypes[$indexName] = $index['Type'];
245 | }
246 | }
247 |
248 | /**
249 | * Returns all index names in the Sphinx.
250 | * @return array all index names in the Sphinx.
251 | */
252 | protected function findIndexes()
253 | {
254 | $sql = 'SHOW TABLES';
255 |
256 | return $this->db->createCommand($sql)->queryAll();
257 | }
258 |
259 | /**
260 | * @return QueryBuilder the query builder for this connection.
261 | */
262 | public function getQueryBuilder()
263 | {
264 | if ($this->_builder === null) {
265 | $this->_builder = $this->createQueryBuilder();
266 | }
267 |
268 | return $this->_builder;
269 | }
270 |
271 | /**
272 | * Determines the PDO type for the given PHP data value.
273 | * @param mixed $data the data whose PDO type is to be determined
274 | * @return int the PDO type
275 | * @see https://www.php.net/manual/en/pdo.constants.php
276 | */
277 | public function getPdoType($data)
278 | {
279 | static $typeMap = [
280 | // php type => PDO type
281 | 'bool' => \PDO::PARAM_BOOL,
282 | 'boolean' => \PDO::PARAM_BOOL,
283 | 'int' => \PDO::PARAM_INT,
284 | 'integer' => \PDO::PARAM_INT,
285 | 'string' => \PDO::PARAM_STR,
286 | 'resource' => \PDO::PARAM_LOB,
287 | 'NULL' => \PDO::PARAM_NULL,
288 | ];
289 | $type = gettype($data);
290 |
291 | return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR;
292 | }
293 |
294 | /**
295 | * Refreshes the schema.
296 | * This method cleans up all cached index schemas so that they can be re-created later
297 | * to reflect the Sphinx schema change.
298 | */
299 | public function refresh()
300 | {
301 | /* @var $cache Cache */
302 | $cache = is_string($this->db->schemaCache) ? Yii::$app->get($this->db->schemaCache, false) : $this->db->schemaCache;
303 | if ($this->db->enableSchemaCache && $cache instanceof Cache) {
304 | TagDependency::invalidate($cache, $this->getCacheTag());
305 | }
306 | $this->_indexNames = [];
307 | $this->_indexes = [];
308 | }
309 |
310 | /**
311 | * Creates a query builder for the Sphinx.
312 | * @return QueryBuilder query builder instance
313 | */
314 | public function createQueryBuilder()
315 | {
316 | return new QueryBuilder($this->db);
317 | }
318 |
319 | /**
320 | * Quotes a string value for use in a query.
321 | * Note that if the parameter is not a string, it will be returned without change.
322 | * @param string $str string to be quoted
323 | * @return string the properly quoted string
324 | * @see https://www.php.net/manual/en/function.PDO-quote.php
325 | */
326 | public function quoteValue($str)
327 | {
328 | if (is_string($str)) {
329 | return $this->db->getSlavePdo()->quote($str);
330 | }
331 | return $str;
332 | }
333 |
334 | /**
335 | * Quotes a index name for use in a query.
336 | * If the index name contains schema prefix, the prefix will also be properly quoted.
337 | * If the index name is already quoted or contains '(' or '{{',
338 | * then this method will do nothing.
339 | * @param string $name index name
340 | * @return string the properly quoted index name
341 | * @see quoteSimpleTableName
342 | */
343 | public function quoteIndexName($name)
344 | {
345 | if (strpos($name, '(') !== false || strpos($name, '{{') !== false) {
346 | return $name;
347 | }
348 |
349 | return $this->quoteSimpleIndexName($name);
350 | }
351 |
352 | /**
353 | * Quotes a column name for use in a query.
354 | * If the column name contains prefix, the prefix will also be properly quoted.
355 | * If the column name is already quoted or contains '(', '[[' or '{{',
356 | * then this method will do nothing.
357 | * @param string $name column name
358 | * @return string the properly quoted column name
359 | * @see quoteSimpleColumnName
360 | */
361 | public function quoteColumnName($name)
362 | {
363 | if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) {
364 | return $name;
365 | }
366 | if (($pos = strrpos($name, '.')) !== false) {
367 | $prefix = $this->quoteIndexName(substr($name, 0, $pos)) . '.';
368 | $name = substr($name, $pos + 1);
369 | } else {
370 | $prefix = '';
371 | }
372 |
373 | return $prefix . $this->quoteSimpleColumnName($name);
374 | }
375 |
376 | /**
377 | * Quotes a index name for use in a query.
378 | * A simple index name has no schema prefix.
379 | * @param string $name index name
380 | * @return string the properly quoted index name
381 | */
382 | public function quoteSimpleIndexName($name)
383 | {
384 | return strpos($name, "`") !== false ? $name : "`" . $name . "`";
385 | }
386 |
387 | /**
388 | * Quotes a column name for use in a query.
389 | * A simple column name has no prefix.
390 | * @param string $name column name
391 | * @return string the properly quoted column name
392 | */
393 | public function quoteSimpleColumnName($name)
394 | {
395 | return strpos($name, '`') !== false || $name === '*' ? $name : '`' . $name . '`';
396 | }
397 |
398 | /**
399 | * Returns the actual name of a given index name.
400 | * This method will strip off curly brackets from the given index name
401 | * and replace the percentage character '%' with [[Connection::indexPrefix]].
402 | * @param string $name the index name to be converted
403 | * @return string the real name of the given index name
404 | */
405 | public function getRawIndexName($name)
406 | {
407 | if (strpos($name, '{{') !== false) {
408 | $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name);
409 |
410 | return str_replace('%', $this->db->tablePrefix, $name);
411 | } else {
412 | return $name;
413 | }
414 | }
415 |
416 | /**
417 | * Extracts the PHP type from abstract DB type.
418 | * @param ColumnSchema $column the column schema information
419 | * @return string PHP type name
420 | */
421 | protected function getColumnPhpType($column)
422 | {
423 | static $typeMap = [ // abstract type => php type
424 | 'smallint' => 'integer',
425 | 'integer' => 'integer',
426 | 'bigint' => 'integer',
427 | 'timestamp' => 'integer',
428 | 'boolean' => 'boolean',
429 | 'float' => 'double',
430 | ];
431 | if (isset($typeMap[$column->type])) {
432 | if ($column->type === 'bigint') {
433 | return PHP_INT_SIZE == 8 ? 'integer' : 'string';
434 | } elseif ($column->type === 'integer' || $column->type === 'timestamp') {
435 | return PHP_INT_SIZE == 4 ? 'string' : 'integer';
436 | } else {
437 | return $typeMap[$column->type];
438 | }
439 | } else {
440 | return 'string';
441 | }
442 | }
443 |
444 | /**
445 | * Collects the metadata of index columns.
446 | * @param IndexSchema $index the index metadata
447 | * @return bool whether the index exists in the database
448 | * @throws \Exception if DB query fails
449 | */
450 | protected function findColumns($index)
451 | {
452 | $sql = 'DESCRIBE ' . $this->quoteSimpleIndexName($index->name);
453 | try {
454 | $columns = $this->db->createCommand($sql)->queryAll();
455 | } catch (\Exception $e) {
456 | $previous = $e->getPrevious();
457 | if ($previous instanceof \PDOException && strpos($previous->getMessage(), 'SQLSTATE[42S02') !== false) {
458 | // index does not exist
459 | // https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error
460 | return false;
461 | }
462 | throw $e;
463 | }
464 |
465 | if (empty($columns[0]['Agent'])) {
466 | foreach ($columns as $info) {
467 | $column = $this->loadColumnSchema($info);
468 | if (isset($index->columns[$column->name])) {
469 | $column = $this->mergeColumnSchema($index->columns[$column->name], $column);
470 | }
471 | $index->columns[$column->name] = $column;
472 | if ($column->isPrimaryKey) {
473 | $index->primaryKey = $column->name;
474 | }
475 | }
476 |
477 | return true;
478 | }
479 |
480 | // Distributed index :
481 | foreach ($columns as $column) {
482 | if (!empty($column['Type']) && strcasecmp('local', $column['Type']) !== 0) {
483 | // skip type 'agent'
484 | continue;
485 | }
486 |
487 | $agent = $this->getIndexSchema($column['Agent']);
488 | if ($agent !== null) {
489 | $index->columns = $agent->columns;
490 | $index->primaryKey = $agent->primaryKey;
491 | return true;
492 | }
493 | }
494 |
495 | $this->applyDefaultColumns($index);
496 |
497 | return true;
498 | }
499 |
500 | /**
501 | * Sets up the default columns for given index.
502 | * This method should be used in case there is no way to find actual columns,
503 | * like in some distributed indexes.
504 | * @param IndexSchema $index the index metadata
505 | * @since 2.0.6
506 | */
507 | protected function applyDefaultColumns($index)
508 | {
509 | $column = $this->loadColumnSchema([
510 | 'Field' => 'id',
511 | 'Type' => 'bigint',
512 | ]);
513 | $index->columns[$column->name] = $column;
514 | $index->primaryKey = 'id';
515 | }
516 |
517 | /**
518 | * Loads the column information into a [[ColumnSchema]] object.
519 | * @param array $info column information
520 | * @return ColumnSchema the column schema object
521 | */
522 | protected function loadColumnSchema($info)
523 | {
524 | $column = new ColumnSchema();
525 |
526 | $column->name = $info['Field'];
527 | $column->dbType = $info['Type'];
528 |
529 | $column->isPrimaryKey = ($column->name === 'id');
530 |
531 | $type = $info['Type'];
532 | if (isset($this->typeMap[$type])) {
533 | $column->type = $this->typeMap[$type];
534 | } else {
535 | $column->type = self::TYPE_STRING;
536 | }
537 |
538 | $column->isField = ($type === 'field');
539 | $column->isAttribute = !$column->isField;
540 |
541 | $column->isMva = $type === 'mva' || $type === 'uint_set';
542 |
543 | $column->phpType = $this->getColumnPhpType($column);
544 |
545 | return $column;
546 | }
547 |
548 | /**
549 | * Merges two column schemas into a single one.
550 | * @param ColumnSchema $origin original column schema.
551 | * @param ColumnSchema $override column schema to be applied over original one.
552 | * @return ColumnSchema merge result.
553 | * @since 2.0.10
554 | */
555 | protected function mergeColumnSchema($origin, $override)
556 | {
557 | $override->dbType .= ',' . $override->dbType;
558 |
559 | if ($override->isField) {
560 | $origin->isField = true;
561 | }
562 | if ($override->isAttribute) {
563 | $origin->isAttribute = true;
564 | $origin->type = $override->type;
565 | $origin->phpType = $override->phpType;
566 |
567 | if ($override->isMva) {
568 | $origin->isMva = true;
569 | }
570 | }
571 |
572 | return $origin;
573 | }
574 |
575 | /**
576 | * Converts a DB exception to a more concrete one if possible.
577 | *
578 | * @param \Exception $e
579 | * @param string $rawSql SQL that produced exception
580 | * @return Exception
581 | */
582 | public function convertException(\Exception $e, $rawSql)
583 | {
584 | if ($e instanceof Exception) {
585 | return $e;
586 | }
587 | $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql";
588 | $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
589 | return new Exception($message, $errorInfo, (int) $e->getCode(), $e);
590 | }
591 |
592 | /**
593 | * Returns a value indicating whether a SQL statement is for read purpose.
594 | * @param string $sql the SQL statement
595 | * @return bool whether a SQL statement is for read purpose.
596 | */
597 | public function isReadQuery($sql)
598 | {
599 | $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i';
600 | return preg_match($pattern, $sql) > 0;
601 | }
602 | }
603 |
--------------------------------------------------------------------------------
/src/gii/model/Generator.php:
--------------------------------------------------------------------------------
1 |
21 | * @since 2.0
22 | */
23 | class Generator extends \yii\gii\Generator
24 | {
25 | public $db = 'sphinx';
26 | public $ns = 'app\models';
27 | public $indexName;
28 | public $modelClass;
29 | public $baseClass = 'yii\sphinx\ActiveRecord';
30 | public $useIndexPrefix = false;
31 |
32 |
33 | /**
34 | * {@inheritdoc}
35 | */
36 | public function getName()
37 | {
38 | return 'Sphinx Model Generator';
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | public function getDescription()
45 | {
46 | return 'This generator generates an ActiveRecord class for the specified Sphinx index.';
47 | }
48 |
49 | /**
50 | * {@inheritdoc}
51 | */
52 | public function rules()
53 | {
54 | return array_merge(parent::rules(), [
55 | [['db', 'ns', 'indexName', 'modelClass', 'baseClass'], 'filter', 'filter' => 'trim'],
56 | [['ns'], 'filter', 'filter' => function($value) { return trim($value, '\\'); }],
57 |
58 | [['db', 'ns', 'indexName', 'baseClass'], 'required'],
59 | [['db', 'modelClass'], 'match', 'pattern' => '/^\w+$/', 'message' => 'Only word characters are allowed.'],
60 | [['ns', 'baseClass'], 'match', 'pattern' => '/^[\w\\\\]+$/', 'message' => 'Only word characters and backslashes are allowed.'],
61 | [['indexName'], 'match', 'pattern' => '/^(\w+\.)?([\w\*]+)$/', 'message' => 'Only word characters, and optionally an asterisk and/or a dot are allowed.'],
62 | [['db'], 'validateDb'],
63 | [['ns'], 'validateNamespace'],
64 | [['indexName'], 'validateIndexName'],
65 | [['modelClass'], 'validateModelClass', 'skipOnEmpty' => false],
66 | [['baseClass'], 'validateClass', 'params' => ['extends' => ActiveRecord::className()]],
67 | [['enableI18N'], 'boolean'],
68 | [['useIndexPrefix'], 'boolean'],
69 | [['messageCategory'], 'validateMessageCategory', 'skipOnEmpty' => false],
70 | ]);
71 | }
72 |
73 | /**
74 | * {@inheritdoc}
75 | */
76 | public function attributeLabels()
77 | {
78 | return array_merge(parent::attributeLabels(), [
79 | 'ns' => 'Namespace',
80 | 'db' => 'Sphinx Connection ID',
81 | 'indexName' => 'Index Name',
82 | 'modelClass' => 'Model Class',
83 | 'baseClass' => 'Base Class',
84 | ]);
85 | }
86 |
87 | /**
88 | * {@inheritdoc}
89 | */
90 | public function hints()
91 | {
92 | return array_merge(parent::hints(), [
93 | 'ns' => 'This is the namespace of the ActiveRecord class to be generated, e.g., app\models
',
94 | 'db' => 'This is the ID of the Sphinx application component.',
95 | 'indexName' => 'This is the name of the Sphinx index that the new ActiveRecord class is associated with, e.g. post
.
96 | The index name may end with asterisk to match multiple table names, e.g. idx_*
97 | will match indexes, which name starts with idx_
. In this case, multiple ActiveRecord classes
98 | will be generated, one for each matching index name; and the class names will be generated from
99 | the matching characters. For example, index idx_post
will generate Post
100 | class.',
101 | 'modelClass' => 'This is the name of the ActiveRecord class to be generated. The class name should not contain
102 | the namespace part as it is specified in "Namespace". You do not need to specify the class name
103 | if "Index Name" ends with asterisk, in which case multiple ActiveRecord classes will be generated.',
104 | 'baseClass' => 'This is the base class of the new ActiveRecord class. It should be a fully qualified namespaced class name.',
105 | 'useIndexPrefix' => 'This indicates whether the index name returned by the generated ActiveRecord class
106 | should consider the tablePrefix
setting of the Sphinx connection. For example, if the
107 | index name is idx_post
and tablePrefix=idx_
, the ActiveRecord class
108 | will return the table name as {{%post}}
.',
109 | ]);
110 | }
111 |
112 | /**
113 | * {@inheritdoc}
114 | */
115 | public function autoCompleteData()
116 | {
117 | $db = $this->getDbConnection();
118 | if ($db !== null) {
119 | return [
120 | 'indexName' => function () use ($db) {
121 | return $db->getSchema()->getIndexNames();
122 | },
123 | ];
124 | } else {
125 | return [];
126 | }
127 | }
128 |
129 | /**
130 | * {@inheritdoc}
131 | */
132 | public function requiredTemplates()
133 | {
134 | return ['model.php'];
135 | }
136 |
137 | /**
138 | * {@inheritdoc}
139 | */
140 | public function stickyAttributes()
141 | {
142 | return array_merge(parent::stickyAttributes(), ['ns', 'db', 'baseClass']);
143 | }
144 |
145 | /**
146 | * {@inheritdoc}
147 | */
148 | public function generate()
149 | {
150 | $files = [];
151 | $db = $this->getDbConnection();
152 | foreach ($this->getIndexNames() as $indexName) {
153 | $className = $this->generateClassName($indexName);
154 | $indexSchema = $db->getIndexSchema($indexName);
155 | $params = [
156 | 'indexName' => $indexName,
157 | 'className' => $className,
158 | 'indexSchema' => $indexSchema,
159 | 'labels' => $this->generateLabels($indexSchema),
160 | 'rules' => $this->generateRules($indexSchema),
161 | ];
162 | $files[] = new CodeFile(
163 | Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $className . '.php',
164 | $this->render('model.php', $params)
165 | );
166 | }
167 |
168 | return $files;
169 | }
170 |
171 | /**
172 | * Generates the attribute labels for the specified table.
173 | * @param \yii\db\TableSchema $table the table schema
174 | * @return array the generated attribute labels (name => label)
175 | */
176 | public function generateLabels($table)
177 | {
178 | $labels = [];
179 | foreach ($table->columns as $column) {
180 | if (!strcasecmp($column->name, 'id')) {
181 | $labels[$column->name] = 'ID';
182 | } else {
183 | $label = Inflector::camel2words($column->name);
184 | if (substr_compare($label, ' id', -3, 3, true) === 0) {
185 | $label = substr($label, 0, -3) . ' ID';
186 | }
187 | $labels[$column->name] = $label;
188 | }
189 | }
190 |
191 | return $labels;
192 | }
193 |
194 | /**
195 | * Generates validation rules for the specified index.
196 | * @param \yii\sphinx\IndexSchema $index the index schema
197 | * @return array the generated validation rules
198 | */
199 | public function generateRules($index)
200 | {
201 | $types = [];
202 | foreach ($index->columns as $column) {
203 | if ($column->isMva) {
204 | $types['safe'][] = $column->name;
205 | continue;
206 | }
207 | if ($column->isPrimaryKey) {
208 | $types['required'][] = $column->name;
209 | $types['unique'][] = $column->name;
210 | }
211 | switch ($column->type) {
212 | case Schema::TYPE_PK:
213 | case Schema::TYPE_INTEGER:
214 | case Schema::TYPE_BIGINT:
215 | $types['integer'][] = $column->name;
216 | break;
217 | case Schema::TYPE_BOOLEAN:
218 | $types['boolean'][] = $column->name;
219 | break;
220 | case Schema::TYPE_FLOAT:
221 | $types['number'][] = $column->name;
222 | break;
223 | case Schema::TYPE_TIMESTAMP:
224 | $types['safe'][] = $column->name;
225 | break;
226 | default: // strings
227 | $types['string'][] = $column->name;
228 | }
229 | }
230 | $rules = [];
231 | foreach ($types as $type => $columns) {
232 | $rules[] = "[['" . implode("', '", $columns) . "'], '$type']";
233 | }
234 |
235 | return $rules;
236 | }
237 |
238 | /**
239 | * Validates the [[db]] attribute.
240 | */
241 | public function validateDb()
242 | {
243 | if (!Yii::$app->has($this->db)) {
244 | $this->addError('db', 'There is no application component named "' . $this->db . '".');
245 | } elseif (!Yii::$app->get($this->db) instanceof Connection) {
246 | $this->addError('db', 'The "' . $this->db . '" application component must be a Sphinx connection instance.');
247 | }
248 | }
249 |
250 | /**
251 | * Validates the [[ns]] attribute.
252 | */
253 | public function validateNamespace()
254 | {
255 | $this->ns = ltrim($this->ns, '\\');
256 | $path = Yii::getAlias('@' . str_replace('\\', '/', $this->ns), false);
257 | if ($path === false) {
258 | $this->addError('ns', 'Namespace must be associated with an existing directory.');
259 | }
260 | }
261 |
262 | /**
263 | * Validates the [[modelClass]] attribute.
264 | */
265 | public function validateModelClass()
266 | {
267 | if ($this->isReservedKeyword($this->modelClass)) {
268 | $this->addError('modelClass', 'Class name cannot be a reserved PHP keyword.');
269 | }
270 | if ((empty($this->indexName) || substr_compare($this->indexName, '*', -1, 1)) && $this->modelClass == '') {
271 | $this->addError('modelClass', 'Model Class cannot be blank if table name does not end with asterisk.');
272 | }
273 | }
274 |
275 | /**
276 | * Validates the [[indexName]] attribute.
277 | */
278 | public function validateIndexName()
279 | {
280 | if (strpos($this->indexName, '*') !== false && substr_compare($this->indexName, '*', -1, 1)) {
281 | $this->addError('indexName', 'Asterisk is only allowed as the last character.');
282 |
283 | return;
284 | }
285 | $tables = $this->getIndexNames();
286 | if (empty($tables)) {
287 | $this->addError('indexName', "Table '{$this->indexName}' does not exist.");
288 | } else {
289 | foreach ($tables as $table) {
290 | $class = $this->generateClassName($table);
291 | if ($this->isReservedKeyword($class)) {
292 | $this->addError('indexName', "Table '$table' will generate a class which is a reserved PHP keyword.");
293 | break;
294 | }
295 | }
296 | }
297 | }
298 |
299 | private $_indexNames;
300 | private $_classNames;
301 |
302 | /**
303 | * @return array the index names that match the pattern specified by [[indexName]].
304 | */
305 | protected function getIndexNames()
306 | {
307 | if ($this->_indexNames !== null) {
308 | return $this->_indexNames;
309 | }
310 | $db = $this->getDbConnection();
311 | if ($db === null) {
312 | return [];
313 | }
314 | $indexNames = [];
315 | if (strpos($this->indexName, '*') !== false) {
316 | $indexNames = $db->getSchema()->getIndexNames();
317 | } elseif (($index = $db->getIndexSchema($this->indexName, true)) !== null) {
318 | $indexNames[] = $this->indexName;
319 | $this->_classNames[$this->indexName] = $this->modelClass;
320 | }
321 |
322 | return $this->_indexNames = $indexNames;
323 | }
324 |
325 | /**
326 | * Generates the table name by considering table prefix.
327 | * If [[useIndexPrefix]] is false, the table name will be returned without change.
328 | * @param string $indexName the table name (which may contain schema prefix)
329 | * @return string the generated table name
330 | */
331 | public function generateIndexName($indexName)
332 | {
333 | if (!$this->useIndexPrefix) {
334 | return $indexName;
335 | }
336 |
337 | $db = $this->getDbConnection();
338 | if (preg_match("/^{$db->tablePrefix}(.*?)$/", $indexName, $matches)) {
339 | $indexName = '{{%' . $matches[1] . '}}';
340 | } elseif (preg_match("/^(.*?){$db->tablePrefix}$/", $indexName, $matches)) {
341 | $indexName = '{{' . $matches[1] . '%}}';
342 | }
343 | return $indexName;
344 | }
345 |
346 | /**
347 | * Generates a class name from the specified table name.
348 | * @param string $indexName the table name (which may contain schema prefix)
349 | * @return string the generated class name
350 | */
351 | protected function generateClassName($indexName)
352 | {
353 | if (isset($this->_classNames[$indexName])) {
354 | return $this->_classNames[$indexName];
355 | }
356 |
357 | if (($pos = strrpos($indexName, '.')) !== false) {
358 | $indexName = substr($indexName, $pos + 1);
359 | }
360 |
361 | $db = $this->getDbConnection();
362 | $patterns = [];
363 | $patterns[] = "/^{$db->tablePrefix}(.*?)$/";
364 | $patterns[] = "/^(.*?){$db->tablePrefix}$/";
365 | if (strpos($this->indexName, '*') !== false) {
366 | $pattern = $this->indexName;
367 | if (($pos = strrpos($pattern, '.')) !== false) {
368 | $pattern = substr($pattern, $pos + 1);
369 | }
370 | $patterns[] = '/^' . str_replace('*', '(\w+)', $pattern) . '$/';
371 | }
372 | $className = $indexName;
373 | foreach ($patterns as $pattern) {
374 | if (preg_match($pattern, $indexName, $matches)) {
375 | $className = $matches[1];
376 | break;
377 | }
378 | }
379 |
380 | return $this->_classNames[$indexName] = Inflector::id2camel($className, '_');
381 | }
382 |
383 | /**
384 | * @return Connection the Sphinx connection as specified by [[db]].
385 | */
386 | protected function getDbConnection()
387 | {
388 | return Yii::$app->get($this->db, false);
389 | }
390 | }
391 |
--------------------------------------------------------------------------------
/src/gii/model/default/model.php:
--------------------------------------------------------------------------------
1 | label) */
12 | /* @var $rules string[] list of validation rules */
13 |
14 | echo "
16 |
17 | namespace = $generator->ns ?>;
18 |
19 | use Yii;
20 |
21 | /**
22 | * This is the model class for index "= $indexName ?>".
23 | *
24 | columns as $column): ?>
25 | * @property = $column->isMva ? 'array' : $column->phpType ?> = "\${$column->name}\n" ?>
26 |
27 | */
28 | class = $className ?> extends = '\\' . ltrim($generator->baseClass, '\\') . "\n" ?>
29 | {
30 | /**
31 | * {@inheritdoc}
32 | */
33 | public static function indexName()
34 | {
35 | return '= $generator->generateIndexName($indexName) ?>';
36 | }
37 | db !== 'sphinx'): ?>
38 |
39 | /**
40 | * @return \yii\sphinx\Connection the database connection used by this AR class.
41 | */
42 | public static function getDb()
43 | {
44 | return Yii::$app->get('= $generator->db ?>');
45 | }
46 |
47 |
48 | /**
49 | * {@inheritdoc}
50 | */
51 | public function rules()
52 | {
53 | return [= "\n " . implode(",\n ", $rules) . "\n " ?>];
54 | }
55 |
56 | /**
57 | * {@inheritdoc}
58 | */
59 | public function attributeLabels()
60 | {
61 | return [
62 | $label): ?>
63 | = "'$name' => " . $generator->generateString($label) . ",\n" ?>
64 |
65 | ];
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/gii/model/form.php:
--------------------------------------------------------------------------------
1 | field($generator, 'indexName');
7 | echo $form->field($generator, 'modelClass');
8 | echo $form->field($generator, 'ns');
9 | echo $form->field($generator, 'baseClass');
10 | echo $form->field($generator, 'db');
11 | echo $form->field($generator, 'useIndexPrefix')->checkbox();
12 | echo $form->field($generator, 'enableI18N')->checkbox();
13 | echo $form->field($generator, 'messageCategory');
14 |
--------------------------------------------------------------------------------