Qiang Xue
11 years ago
43 changed files with 4469 additions and 667 deletions
@ -0,0 +1,199 @@
|
||||
<?php |
||||
/** |
||||
* @link http://www.yiiframework.com/ |
||||
* @copyright Copyright (c) 2008 Yii Software LLC |
||||
* @license http://www.yiiframework.com/license/ |
||||
*/ |
||||
|
||||
namespace yii\elasticsearch; |
||||
|
||||
use yii\db\ActiveQueryInterface; |
||||
use yii\db\ActiveQueryTrait; |
||||
|
||||
/** |
||||
* ActiveQuery represents a [[Query]] associated with an [[ActiveRecord]] class. |
||||
* |
||||
* ActiveQuery instances are usually created by [[ActiveRecord::find()]]. |
||||
* |
||||
* ActiveQuery mainly provides the following methods to retrieve the query results: |
||||
* |
||||
* - [[one()]]: returns a single record populated with the first row of data. |
||||
* - [[all()]]: returns all records based on the query results. |
||||
* - [[count()]]: returns the number of records. |
||||
* - [[scalar()]]: returns the value of the first column in the first row of the query result. |
||||
* - [[column()]]: returns the value of the first column in the query result. |
||||
* - [[exists()]]: returns a value indicating whether the query result has data or not. |
||||
* |
||||
* Because ActiveQuery extends from [[Query]], one can use query methods, such as [[where()]], |
||||
* [[orderBy()]] to customize the query options. |
||||
* |
||||
* ActiveQuery also provides the following additional query options: |
||||
* |
||||
* - [[with()]]: list of relations that this query should be performed with. |
||||
* - [[indexBy()]]: the name of the column by which the query result should be indexed. |
||||
* - [[asArray()]]: whether to return each record as an array. |
||||
* |
||||
* These options can be configured using methods of the same name. For example: |
||||
* |
||||
* ~~~ |
||||
* $customers = Customer::find()->with('orders')->asArray()->all(); |
||||
* ~~~ |
||||
* |
||||
* @author Carsten Brandt <mail@cebe.cc> |
||||
* @since 2.0 |
||||
*/ |
||||
class ActiveQuery extends Query implements ActiveQueryInterface |
||||
{ |
||||
use ActiveQueryTrait; |
||||
|
||||
/** |
||||
* Creates a DB command that can be used to execute this query. |
||||
* @param Connection $db the DB connection used to create the DB command. |
||||
* If null, the DB connection returned by [[modelClass]] will be used. |
||||
* @return Command the created DB command instance. |
||||
*/ |
||||
public function createCommand($db = null) |
||||
{ |
||||
/** @var ActiveRecord $modelClass */ |
||||
$modelClass = $this->modelClass; |
||||
if ($db === null) { |
||||
$db = $modelClass::getDb(); |
||||
} |
||||
|
||||
if ($this->type === null) { |
||||
$this->type = $modelClass::type(); |
||||
} |
||||
if ($this->index === null) { |
||||
$this->index = $modelClass::index(); |
||||
$this->type = $modelClass::type(); |
||||
} |
||||
$commandConfig = $db->getQueryBuilder()->build($this); |
||||
return $db->createCommand($commandConfig); |
||||
} |
||||
|
||||
/** |
||||
* Executes query and returns all results as an array. |
||||
* @param Connection $db the DB connection used to create the DB command. |
||||
* If null, the DB connection returned by [[modelClass]] will be used. |
||||
* @return array the query results. If the query results in nothing, an empty array will be returned. |
||||
*/ |
||||
public function all($db = null) |
||||
{ |
||||
$result = $this->createCommand($db)->search(); |
||||
if (empty($result['hits']['hits'])) { |
||||
return []; |
||||
} |
||||
if ($this->fields !== null) { |
||||
foreach ($result['hits']['hits'] as &$row) { |
||||
$row['_source'] = isset($row['fields']) ? $row['fields'] : []; |
||||
unset($row['fields']); |
||||
} |
||||
unset($row); |
||||
} |
||||
if ($this->asArray && $this->indexBy) { |
||||
foreach ($result['hits']['hits'] as &$row) { |
||||
$row['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $row['_id']; |
||||
$row = $row['_source']; |
||||
} |
||||
} |
||||
$models = $this->createModels($result['hits']['hits']); |
||||
if ($this->asArray && !$this->indexBy) { |
||||
foreach($models as $key => $model) { |
||||
$model['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $model['_id']; |
||||
$models[$key] = $model['_source']; |
||||
} |
||||
} |
||||
if (!empty($this->with)) { |
||||
$this->findWith($this->with, $models); |
||||
} |
||||
return $models; |
||||
} |
||||
|
||||
/** |
||||
* Executes query and returns a single row of result. |
||||
* @param Connection $db the DB connection used to create the DB command. |
||||
* If null, the DB connection returned by [[modelClass]] will be used. |
||||
* @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], |
||||
* the query result may be either an array or an ActiveRecord object. Null will be returned |
||||
* if the query results in nothing. |
||||
*/ |
||||
public function one($db = null) |
||||
{ |
||||
if (($result = parent::one($db)) === false) { |
||||
return null; |
||||
} |
||||
if ($this->asArray) { |
||||
$model = $result['_source']; |
||||
$model[ActiveRecord::PRIMARY_KEY_NAME] = $result['_id']; |
||||
} else { |
||||
/** @var ActiveRecord $class */ |
||||
$class = $this->modelClass; |
||||
$model = $class::create($result); |
||||
} |
||||
if (!empty($this->with)) { |
||||
$models = [$model]; |
||||
$this->findWith($this->with, $models); |
||||
$model = $models[0]; |
||||
} |
||||
return $model; |
||||
} |
||||
|
||||
/** |
||||
* @inheritDocs |
||||
*/ |
||||
public function search($db = null, $options = []) |
||||
{ |
||||
$result = $this->createCommand($db)->search($options); |
||||
if (!empty($result['hits']['hits'])) { |
||||
$models = $this->createModels($result['hits']['hits']); |
||||
if ($this->asArray) { |
||||
foreach($models as $key => $model) { |
||||
$model['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $model['_id']; |
||||
$models[$key] = $model['_source']; |
||||
} |
||||
} |
||||
if (!empty($this->with)) { |
||||
$this->findWith($this->with, $models); |
||||
} |
||||
$result['hits']['hits'] = $models; |
||||
} |
||||
return $result; |
||||
} |
||||
|
||||
/** |
||||
* @inheritDocs |
||||
*/ |
||||
public function scalar($field, $db = null) |
||||
{ |
||||
$record = parent::one($db); |
||||
if ($record !== false) { |
||||
if ($field == ActiveRecord::PRIMARY_KEY_NAME) { |
||||
return $record['_id']; |
||||
} elseif (isset($record['_source'][$field])) { |
||||
return $record['_source'][$field]; |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* @inheritDocs |
||||
*/ |
||||
public function column($field, $db = null) |
||||
{ |
||||
if ($field == ActiveRecord::PRIMARY_KEY_NAME) { |
||||
$command = $this->createCommand($db); |
||||
$command->queryParts['fields'] = []; |
||||
$result = $command->search(); |
||||
if (empty($result['hits']['hits'])) { |
||||
return []; |
||||
} |
||||
$column = []; |
||||
foreach ($result['hits']['hits'] as $row) { |
||||
$column[] = $row['_id']; |
||||
} |
||||
return $column; |
||||
} |
||||
return parent::column($field, $db); |
||||
} |
||||
} |
@ -0,0 +1,474 @@
|
||||
<?php |
||||
/** |
||||
* @link http://www.yiiframework.com/ |
||||
* @copyright Copyright (c) 2008 Yii Software LLC |
||||
* @license http://www.yiiframework.com/license/ |
||||
*/ |
||||
|
||||
namespace yii\elasticsearch; |
||||
|
||||
use yii\base\InvalidCallException; |
||||
use yii\base\InvalidConfigException; |
||||
use yii\base\NotSupportedException; |
||||
use yii\helpers\Inflector; |
||||
use yii\helpers\Json; |
||||
use yii\helpers\StringHelper; |
||||
|
||||
/** |
||||
* ActiveRecord is the base class for classes representing relational data in terms of objects. |
||||
* |
||||
* This class implements the ActiveRecord pattern for the fulltext search and data storage |
||||
* [elasticsearch](http://www.elasticsearch.org/). |
||||
* |
||||
* For defining a record a subclass should at least implement the [[attributes()]] method to define |
||||
* attributes. |
||||
* The primary key (the `_id` field in elasticsearch terms) is represented by `getId()` and `setId()`. |
||||
* The primary key is not part of the attributes. |
||||
* |
||||
* The following is an example model called `Customer`: |
||||
* |
||||
* ```php |
||||
* class Customer extends \yii\elasticsearch\ActiveRecord |
||||
* { |
||||
* public function attributes() |
||||
* { |
||||
* return ['id', 'name', 'address', 'registration_date']; |
||||
* } |
||||
* } |
||||
* ``` |
||||
* |
||||
* You may override [[index()]] and [[type()]] to define the index and type this record represents. |
||||
* |
||||
* @author Carsten Brandt <mail@cebe.cc> |
||||
* @since 2.0 |
||||
*/ |
||||
class ActiveRecord extends \yii\db\ActiveRecord |
||||
{ |
||||
const PRIMARY_KEY_NAME = 'id'; |
||||
|
||||
private $_id; |
||||
private $_version; |
||||
|
||||
/** |
||||
* Returns the database connection used by this AR class. |
||||
* By default, the "elasticsearch" application component is used as the database connection. |
||||
* You may override this method if you want to use a different database connection. |
||||
* @return Connection the database connection used by this AR class. |
||||
*/ |
||||
public static function getDb() |
||||
{ |
||||
return \Yii::$app->getComponent('elasticsearch'); |
||||
} |
||||
|
||||
/** |
||||
* @inheritDoc |
||||
*/ |
||||
public static function find($q = null) |
||||
{ |
||||
$query = static::createQuery(); |
||||
if (is_array($q)) { |
||||
if (count($q) == 1 && (array_key_exists(ActiveRecord::PRIMARY_KEY_NAME, $q))) { |
||||
$pk = $q[ActiveRecord::PRIMARY_KEY_NAME]; |
||||
if (is_array($pk)) { |
||||
return static::mget($pk); |
||||
} else { |
||||
return static::get($pk); |
||||
} |
||||
} |
||||
return $query->where($q)->one(); |
||||
} elseif ($q !== null) { |
||||
return static::get($q); |
||||
} |
||||
return $query; |
||||
} |
||||
|
||||
/** |
||||
* Gets a record by its primary key. |
||||
* |
||||
* @param mixed $primaryKey the primaryKey value |
||||
* @param array $options options given in this parameter are passed to elasticsearch |
||||
* as request URI parameters. |
||||
* Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html) |
||||
* for more details on these options. |
||||
* @return static|null The record instance or null if it was not found. |
||||
*/ |
||||
public static function get($primaryKey, $options = []) |
||||
{ |
||||
if ($primaryKey === null) { |
||||
return null; |
||||
} |
||||
$command = static::getDb()->createCommand(); |
||||
$result = $command->get(static::index(), static::type(), $primaryKey, $options); |
||||
if ($result['exists']) { |
||||
return static::create($result); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Gets a list of records by its primary keys. |
||||
* |
||||
* @param array $primaryKeys an array of primaryKey values |
||||
* @param array $options options given in this parameter are passed to elasticsearch |
||||
* as request URI parameters. |
||||
* |
||||
* Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html) |
||||
* for more details on these options. |
||||
* @return static|null The record instance or null if it was not found. |
||||
*/ |
||||
|
||||
public static function mget($primaryKeys, $options = []) |
||||
{ |
||||
if (empty($primaryKeys)) { |
||||
return []; |
||||
} |
||||
$command = static::getDb()->createCommand(); |
||||
$result = $command->mget(static::index(), static::type(), $primaryKeys, $options); |
||||
$models = []; |
||||
foreach($result['docs'] as $doc) { |
||||
if ($doc['exists']) { |
||||
$models[] = static::create($doc); |
||||
} |
||||
} |
||||
return $models; |
||||
} |
||||
|
||||
// TODO add more like this feature http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-more-like-this.html |
||||
|
||||
// TODO add percolate functionality http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-percolate.html |
||||
|
||||
/** |
||||
* @inheritDoc |
||||
*/ |
||||
public static function createQuery() |
||||
{ |
||||
return new ActiveQuery(['modelClass' => get_called_class()]); |
||||
} |
||||
|
||||
/** |
||||
* @inheritDoc |
||||
*/ |
||||
public static function createActiveRelation($config = []) |
||||
{ |
||||
return new ActiveRelation($config); |
||||
} |
||||
|
||||
// TODO implement copy and move as pk change is not possible |
||||
|
||||
public function getId() |
||||
{ |
||||
return $this->_id; |
||||
} |
||||
|
||||
/** |
||||
* Sets the primary key |
||||
* @param mixed $value |
||||
* @throws \yii\base\InvalidCallException when record is not new |
||||
*/ |
||||
public function setId($value) |
||||
{ |
||||
if ($this->isNewRecord) { |
||||
$this->_id = $value; |
||||
} else { |
||||
throw new InvalidCallException('Changing the primaryKey of an already saved record is not allowed.'); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @inheritDoc |
||||
*/ |
||||
public function getPrimaryKey($asArray = false) |
||||
{ |
||||
if ($asArray) { |
||||
return [ActiveRecord::PRIMARY_KEY_NAME => $this->_id]; |
||||
} else { |
||||
return $this->_id; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @inheritDoc |
||||
*/ |
||||
public function getOldPrimaryKey($asArray = false) |
||||
{ |
||||
$id = $this->isNewRecord ? null : $this->_id; |
||||
if ($asArray) { |
||||
return [ActiveRecord::PRIMARY_KEY_NAME => $id]; |
||||
} else { |
||||
return $this->_id; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method defines the primary. |
||||
* |
||||
* The primaryKey for elasticsearch documents is always `primaryKey`. It can not be changed. |
||||
* |
||||
* @return string[] the primary keys of this record. |
||||
*/ |
||||
public static function primaryKey() |
||||
{ |
||||
return [ActiveRecord::PRIMARY_KEY_NAME]; |
||||
} |
||||
|
||||
/** |
||||
* Returns the list of all attribute names of the model. |
||||
* This method must be overridden by child classes to define available attributes. |
||||
* @return array list of attribute names. |
||||
*/ |
||||
public static function attributes() |
||||
{ |
||||
throw new InvalidConfigException('The attributes() method of elasticsearch ActiveRecord has to be implemented by child classes.'); |
||||
} |
||||
|
||||
/** |
||||
* @return string the name of the index this record is stored in. |
||||
*/ |
||||
public static function index() |
||||
{ |
||||
return Inflector::pluralize(Inflector::camel2id(StringHelper::basename(get_called_class()), '-')); |
||||
} |
||||
|
||||
/** |
||||
* @return string the name of the type of this record. |
||||
*/ |
||||
public static function type() |
||||
{ |
||||
return Inflector::camel2id(StringHelper::basename(get_called_class()), '-'); |
||||
} |
||||
|
||||
/** |
||||
* Creates an active record object using a row of data. |
||||
* This method is called by [[ActiveQuery]] to populate the query results |
||||
* into Active Records. It is not meant to be used to create new records. |
||||
* @param array $row attribute values (name => value) |
||||
* @return ActiveRecord the newly created active record. |
||||
*/ |
||||
public static function create($row) |
||||
{ |
||||
$row['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $row['_id']; |
||||
$record = parent::create($row['_source']); |
||||
return $record; |
||||
} |
||||
|
||||
/** |
||||
* Inserts a document into the associated index using the attribute values of this record. |
||||
* |
||||
* This method performs the following steps in order: |
||||
* |
||||
* 1. call [[beforeValidate()]] when `$runValidation` is true. If validation |
||||
* fails, it will skip the rest of the steps; |
||||
* 2. call [[afterValidate()]] when `$runValidation` is true. |
||||
* 3. call [[beforeSave()]]. If the method returns false, it will skip the |
||||
* rest of the steps; |
||||
* 4. insert the record into database. If this fails, it will skip the rest of the steps; |
||||
* 5. call [[afterSave()]]; |
||||
* |
||||
* In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], |
||||
* [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] |
||||
* will be raised by the corresponding methods. |
||||
* |
||||
* Only the [[dirtyAttributes|changed attribute values]] will be inserted into database. |
||||
* |
||||
* If the [[primaryKey|primary key]] is not set (null) during insertion, |
||||
* it will be populated with a |
||||
* [randomly generated value](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html#_automatic_id_generation) |
||||
* after insertion. |
||||
* |
||||
* For example, to insert a customer record: |
||||
* |
||||
* ~~~ |
||||
* $customer = new Customer; |
||||
* $customer->name = $name; |
||||
* $customer->email = $email; |
||||
* $customer->insert(); |
||||
* ~~~ |
||||
* |
||||
* @param boolean $runValidation whether to perform validation before saving the record. |
||||
* If the validation fails, the record will not be inserted into the database. |
||||
* @param array $attributes list of attributes that need to be saved. Defaults to null, |
||||
* meaning all attributes will be saved. |
||||
* @param array $options options given in this parameter are passed to elasticsearch |
||||
* as request URI parameters. These are among others: |
||||
* |
||||
* - `routing` define shard placement of this record. |
||||
* - `parent` by giving the primaryKey of another record this defines a parent-child relation |
||||
* - `timestamp` specifies the timestamp to store along with the document. Default is indexing time. |
||||
* |
||||
* Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html) |
||||
* for more details on these options. |
||||
* |
||||
* By default the `op_type` is set to `create`. |
||||
* @return boolean whether the attributes are valid and the record is inserted successfully. |
||||
*/ |
||||
public function insert($runValidation = true, $attributes = null, $options = ['op_type' => 'create']) |
||||
{ |
||||
if ($runValidation && !$this->validate($attributes)) { |
||||
return false; |
||||
} |
||||
if ($this->beforeSave(true)) { |
||||
$values = $this->getDirtyAttributes($attributes); |
||||
|
||||
$response = static::getDb()->createCommand()->insert( |
||||
static::index(), |
||||
static::type(), |
||||
$values, |
||||
$this->getPrimaryKey(), |
||||
$options |
||||
); |
||||
|
||||
if (!$response['ok']) { |
||||
return false; |
||||
} |
||||
$this->_id = $response['_id']; |
||||
$this->_version = $response['_version']; |
||||
$this->setOldAttributes($values); |
||||
$this->afterSave(true); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Updates all records whos primary keys are given. |
||||
* For example, to change the status to be 1 for all customers whose status is 2: |
||||
* |
||||
* ~~~ |
||||
* Customer::updateAll(array('status' => 1), array(2, 3, 4)); |
||||
* ~~~ |
||||
* |
||||
* @param array $attributes attribute values (name-value pairs) to be saved into the table |
||||
* @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. |
||||
* Please refer to [[ActiveQuery::where()]] on how to specify this parameter. |
||||
* @param array $params this parameter is ignored in elasticsearch implementation. |
||||
* @return integer the number of rows updated |
||||
*/ |
||||
public static function updateAll($attributes, $condition = [], $params = []) |
||||
{ |
||||
if (count($condition) == 1 && isset($condition[ActiveRecord::PRIMARY_KEY_NAME])) { |
||||
$primaryKeys = (array) $condition[ActiveRecord::PRIMARY_KEY_NAME]; |
||||
} else { |
||||
$primaryKeys = static::find()->where($condition)->column(ActiveRecord::PRIMARY_KEY_NAME); |
||||
} |
||||
if (empty($primaryKeys)) { |
||||
return 0; |
||||
} |
||||
$bulk = ''; |
||||
foreach((array) $primaryKeys as $pk) { |
||||
$action = Json::encode([ |
||||
"update" => [ |
||||
"_id" => $pk, |
||||
"_type" => static::type(), |
||||
"_index" => static::index(), |
||||
], |
||||
]); |
||||
$data = Json::encode(array( |
||||
"doc" => $attributes |
||||
)); |
||||
$bulk .= $action . "\n" . $data . "\n"; |
||||
} |
||||
|
||||
// TODO do this via command |
||||
$url = [static::index(), static::type(), '_bulk']; |
||||
$response = static::getDb()->post($url, [], $bulk); |
||||
$n=0; |
||||
foreach($response['items'] as $item) { |
||||
if ($item['update']['ok']) { |
||||
$n++; |
||||
} |
||||
} |
||||
return $n; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Deletes rows in the table using the provided conditions. |
||||
* WARNING: If you do not specify any condition, this method will delete ALL rows in the table. |
||||
* |
||||
* For example, to delete all customers whose status is 3: |
||||
* |
||||
* ~~~ |
||||
* Customer::deleteAll('status = 3'); |
||||
* ~~~ |
||||
* |
||||
* @param array $condition the conditions that will be put in the WHERE part of the DELETE SQL. |
||||
* Please refer to [[ActiveQuery::where()]] on how to specify this parameter. |
||||
* @param array $params this parameter is ignored in elasticsearch implementation. |
||||
* @return integer the number of rows deleted |
||||
*/ |
||||
public static function deleteAll($condition = [], $params = []) |
||||
{ |
||||
if (count($condition) == 1 && isset($condition[ActiveRecord::PRIMARY_KEY_NAME])) { |
||||
$primaryKeys = (array) $condition[ActiveRecord::PRIMARY_KEY_NAME]; |
||||
} else { |
||||
$primaryKeys = static::find()->where($condition)->column(ActiveRecord::PRIMARY_KEY_NAME); |
||||
} |
||||
if (empty($primaryKeys)) { |
||||
return 0; |
||||
} |
||||
$bulk = ''; |
||||
foreach((array) $primaryKeys as $pk) { |
||||
$bulk .= Json::encode([ |
||||
"delete" => [ |
||||
"_id" => $pk, |
||||
"_type" => static::type(), |
||||
"_index" => static::index(), |
||||
], |
||||
]) . "\n"; |
||||
} |
||||
|
||||
// TODO do this via command |
||||
$url = [static::index(), static::type(), '_bulk']; |
||||
$response = static::getDb()->post($url, [], $bulk); |
||||
$n=0; |
||||
foreach($response['items'] as $item) { |
||||
if ($item['delete']['found'] && $item['delete']['ok']) { |
||||
$n++; |
||||
} |
||||
} |
||||
return $n; |
||||
} |
||||
|
||||
/** |
||||
* @inheritdoc |
||||
*/ |
||||
public static function updateAllCounters($counters, $condition = null, $params = []) |
||||
{ |
||||
throw new NotSupportedException('Update Counters is not supported by elasticsearch ActiveRecord.'); |
||||
} |
||||
|
||||
/** |
||||
* @inheritdoc |
||||
*/ |
||||
public static function getTableSchema() |
||||
{ |
||||
throw new NotSupportedException('getTableSchema() is not supported by elasticsearch ActiveRecord.'); |
||||
} |
||||
|
||||
/** |
||||
* @inheritDoc |
||||
*/ |
||||
public static function tableName() |
||||
{ |
||||
return static::index() . '/' . static::type(); |
||||
} |
||||
|
||||
/** |
||||
* @inheritdoc |
||||
*/ |
||||
public static function findBySql($sql, $params = []) |
||||
{ |
||||
throw new NotSupportedException('findBySql() is not supported by elasticsearch ActiveRecord.'); |
||||
} |
||||
|
||||
/** |
||||
* Returns a value indicating whether the specified operation is transactional in the current [[scenario]]. |
||||
* This method will always return false as transactional operations are not supported by elasticsearch. |
||||
* @param integer $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]]. |
||||
* @return boolean whether the specified operation is transactional in the current [[scenario]]. |
||||
*/ |
||||
public function isTransactional($operation) |
||||
{ |
||||
return false; |
||||
} |
||||
} |
@ -0,0 +1,61 @@
|
||||
<?php |
||||
/** |
||||
* @link http://www.yiiframework.com/ |
||||
* @copyright Copyright (c) 2008 Yii Software LLC |
||||
* @license http://www.yiiframework.com/license/ |
||||
*/ |
||||
|
||||
namespace yii\elasticsearch; |
||||
|
||||
use yii\db\ActiveRelationInterface; |
||||
use yii\db\ActiveRelationTrait; |
||||
|
||||
/** |
||||
* ActiveRelation represents a relation between two Active Record classes. |
||||
* |
||||
* ActiveRelation instances are usually created by calling [[ActiveRecord::hasOne()]] and |
||||
* [[ActiveRecord::hasMany()]]. An Active Record class declares a relation by defining |
||||
* a getter method which calls one of the above methods and returns the created ActiveRelation object. |
||||
* |
||||
* A relation is specified by [[link]] which represents the association between columns |
||||
* of different tables; and the multiplicity of the relation is indicated by [[multiple]]. |
||||
* |
||||
* If a relation involves a pivot table, it may be specified by [[via()]] method. |
||||
* |
||||
* @author Carsten Brandt <mail@cebe.cc> |
||||
* @since 2.0 |
||||
*/ |
||||
class ActiveRelation extends ActiveQuery implements ActiveRelationInterface |
||||
{ |
||||
use ActiveRelationTrait; |
||||
|
||||
/** |
||||
* Creates a DB command that can be used to execute this query. |
||||
* @param Connection $db the DB connection used to create the DB command. |
||||
* If null, the DB connection returned by [[modelClass]] will be used. |
||||
* @return Command the created DB command instance. |
||||
*/ |
||||
public function createCommand($db = null) |
||||
{ |
||||
if ($this->primaryModel !== null) { |
||||
// lazy loading |
||||
if (is_array($this->via)) { |
||||
// via relation |
||||
/** @var ActiveRelation $viaQuery */ |
||||
list($viaName, $viaQuery) = $this->via; |
||||
if ($viaQuery->multiple) { |
||||
$viaModels = $viaQuery->all(); |
||||
$this->primaryModel->populateRelation($viaName, $viaModels); |
||||
} else { |
||||
$model = $viaQuery->one(); |
||||
$this->primaryModel->populateRelation($viaName, $model); |
||||
$viaModels = $model === null ? [] : [$model]; |
||||
} |
||||
$this->filterByModels($viaModels); |
||||
} else { |
||||
$this->filterByModels([$this->primaryModel]); |
||||
} |
||||
} |
||||
return parent::createCommand($db); |
||||
} |
||||
} |
@ -0,0 +1,403 @@
|
||||
<?php |
||||
/** |
||||
* @link http://www.yiiframework.com/ |
||||
* @copyright Copyright (c) 2008 Yii Software LLC |
||||
* @license http://www.yiiframework.com/license/ |
||||
*/ |
||||
|
||||
namespace yii\elasticsearch; |
||||
|
||||
use yii\base\Component; |
||||
use yii\helpers\Json; |
||||
|
||||
/** |
||||
* The Command class implements the API for accessing the elasticsearch REST API. |
||||
* |
||||
* Check the [elasticsearch guide](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/index.html) |
||||
* for details on these commands. |
||||
* |
||||
* @author Carsten Brandt <mail@cebe.cc> |
||||
* @since 2.0 |
||||
*/ |
||||
class Command extends Component |
||||
{ |
||||
/** |
||||
* @var Connection |
||||
*/ |
||||
public $db; |
||||
/** |
||||
* @var string|array the indexes to execute the query on. Defaults to null meaning all indexes |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search.html#search-multi-index |
||||
*/ |
||||
public $index; |
||||
/** |
||||
* @var string|array the types to execute the query on. Defaults to null meaning all types |
||||
*/ |
||||
public $type; |
||||
/** |
||||
* @var array list of arrays or json strings that become parts of a query |
||||
*/ |
||||
public $queryParts; |
||||
|
||||
public $options = []; |
||||
|
||||
/** |
||||
* @param array $options |
||||
* @return mixed |
||||
*/ |
||||
public function search($options = []) |
||||
{ |
||||
$query = $this->queryParts; |
||||
if (empty($query)) { |
||||
$query = '{}'; |
||||
} |
||||
if (is_array($query)) { |
||||
$query = Json::encode($query); |
||||
} |
||||
$url = [ |
||||
$this->index !== null ? $this->index : '_all', |
||||
$this->type !== null ? $this->type : '_all', |
||||
'_search' |
||||
]; |
||||
return $this->db->get($url, array_merge($this->options, $options), $query); |
||||
} |
||||
|
||||
/** |
||||
* Inserts a document into an index |
||||
* @param string $index |
||||
* @param string $type |
||||
* @param string|array $data json string or array of data to store |
||||
* @param null $id the documents id. If not specified Id will be automatically choosen |
||||
* @param array $options |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html |
||||
*/ |
||||
public function insert($index, $type, $data, $id = null, $options = []) |
||||
{ |
||||
$body = is_array($data) ? Json::encode($data) : $data; |
||||
|
||||
if ($id !== null) { |
||||
return $this->db->put([$index, $type, $id], $options, $body); |
||||
} else { |
||||
return $this->db->post([$index, $type], $options, $body); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* gets a document from the index |
||||
* @param $index |
||||
* @param $type |
||||
* @param $id |
||||
* @param array $options |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html |
||||
*/ |
||||
public function get($index, $type, $id, $options = []) |
||||
{ |
||||
return $this->db->get([$index, $type, $id], $options, null, [200, 404]); |
||||
} |
||||
|
||||
/** |
||||
* gets multiple documents from the index |
||||
* |
||||
* TODO allow specifying type and index + fields |
||||
* @param $index |
||||
* @param $type |
||||
* @param $ids |
||||
* @param array $options |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-multi-get.html |
||||
*/ |
||||
public function mget($index, $type, $ids, $options = []) |
||||
{ |
||||
$body = Json::encode(['ids' => array_values($ids)]); |
||||
return $this->db->get([$index, $type, '_mget'], $options, $body); |
||||
} |
||||
|
||||
/** |
||||
* gets a documents _source from the index (>=v0.90.1) |
||||
* @param $index |
||||
* @param $type |
||||
* @param $id |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html#_source |
||||
*/ |
||||
public function getSource($index, $type, $id) |
||||
{ |
||||
return $this->db->get([$index, $type, $id]); |
||||
} |
||||
|
||||
/** |
||||
* gets a document from the index |
||||
* @param $index |
||||
* @param $type |
||||
* @param $id |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html |
||||
*/ |
||||
public function exists($index, $type, $id) |
||||
{ |
||||
return $this->db->head([$index, $type, $id]); |
||||
} |
||||
|
||||
/** |
||||
* deletes a document from the index |
||||
* @param $index |
||||
* @param $type |
||||
* @param $id |
||||
* @param array $options |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete.html |
||||
*/ |
||||
public function delete($index, $type, $id, $options = []) |
||||
{ |
||||
return $this->db->delete([$index, $type, $id], $options); |
||||
} |
||||
|
||||
/** |
||||
* updates a document |
||||
* @param $index |
||||
* @param $type |
||||
* @param $id |
||||
* @param array $options |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-update.html |
||||
*/ |
||||
// public function update($index, $type, $id, $data, $options = []) |
||||
// { |
||||
// // TODO implement |
||||
//// return $this->db->delete([$index, $type, $id], $options); |
||||
// } |
||||
|
||||
// TODO bulk http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-bulk.html |
||||
|
||||
/** |
||||
* creates an index |
||||
* @param $index |
||||
* @param array $configuration |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-create-index.html |
||||
*/ |
||||
public function createIndex($index, $configuration = null) |
||||
{ |
||||
$body = $configuration !== null ? Json::encode($configuration) : null; |
||||
return $this->db->put([$index], $body); |
||||
} |
||||
|
||||
/** |
||||
* deletes an index |
||||
* @param $index |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-delete-index.html |
||||
*/ |
||||
public function deleteIndex($index) |
||||
{ |
||||
return $this->db->delete([$index]); |
||||
} |
||||
|
||||
/** |
||||
* deletes all indexes |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-delete-index.html |
||||
*/ |
||||
public function deleteAllIndexes() |
||||
{ |
||||
return $this->db->delete(['_all']); |
||||
} |
||||
|
||||
/** |
||||
* checks whether an index exists |
||||
* @param $index |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-exists.html |
||||
*/ |
||||
public function indexExists($index) |
||||
{ |
||||
return $this->db->head([$index]); |
||||
} |
||||
|
||||
/** |
||||
* @param $index |
||||
* @param $type |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-types-exists.html |
||||
*/ |
||||
public function typeExists($index, $type) |
||||
{ |
||||
return $this->db->head([$index, $type]); |
||||
} |
||||
|
||||
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-aliases.html |
||||
|
||||
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-update-settings.html |
||||
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-settings.html |
||||
|
||||
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-warmers.html |
||||
|
||||
/** |
||||
* @param $index |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html |
||||
*/ |
||||
public function openIndex($index) |
||||
{ |
||||
return $this->db->post([$index, '_open']); |
||||
} |
||||
|
||||
/** |
||||
* @param $index |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html |
||||
*/ |
||||
public function closeIndex($index) |
||||
{ |
||||
return $this->db->post([$index, '_close']); |
||||
} |
||||
|
||||
/** |
||||
* @param $index |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-status.html |
||||
*/ |
||||
public function getIndexStatus($index = '_all') |
||||
{ |
||||
return $this->db->get([$index, '_status']); |
||||
} |
||||
|
||||
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-stats.html |
||||
// http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-segments.html |
||||
|
||||
/** |
||||
* @param $index |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-clearcache.html |
||||
*/ |
||||
public function clearIndexCache($index) |
||||
{ |
||||
return $this->db->post([$index, '_cache', 'clear']); |
||||
} |
||||
|
||||
/** |
||||
* @param $index |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-flush.html |
||||
*/ |
||||
public function flushIndex($index = '_all') |
||||
{ |
||||
return $this->db->post([$index, '_flush']); |
||||
} |
||||
|
||||
/** |
||||
* @param $index |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-refresh.html |
||||
*/ |
||||
public function refreshIndex($index) |
||||
{ |
||||
return $this->db->post([$index, '_refresh']); |
||||
} |
||||
|
||||
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-optimize.html |
||||
|
||||
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-gateway-snapshot.html |
||||
|
||||
/** |
||||
* @param $index |
||||
* @param $type |
||||
* @param $mapping |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-put-mapping.html |
||||
*/ |
||||
public function setMapping($index, $type, $mapping) |
||||
{ |
||||
$body = $mapping !== null ? Json::encode($mapping) : null; |
||||
return $this->db->put([$index, $type, '_mapping'], $body); |
||||
} |
||||
|
||||
/** |
||||
* @param string $index |
||||
* @param string $type |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-mapping.html |
||||
*/ |
||||
public function getMapping($index = '_all', $type = '_all') |
||||
{ |
||||
return $this->db->get([$index, $type, '_mapping']); |
||||
} |
||||
|
||||
/** |
||||
* @param $index |
||||
* @param $type |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-put-mapping.html |
||||
*/ |
||||
public function deleteMapping($index, $type) |
||||
{ |
||||
return $this->db->delete([$index, $type]); |
||||
} |
||||
|
||||
/** |
||||
* @param $index |
||||
* @param string $type |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html |
||||
*/ |
||||
public function getFieldMapping($index, $type = '_all') |
||||
{ |
||||
return $this->db->put([$index, $type, '_mapping']); |
||||
} |
||||
|
||||
/** |
||||
* @param $options |
||||
* @param $index |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-analyze.html |
||||
*/ |
||||
// public function analyze($options, $index = null) |
||||
// { |
||||
// // TODO implement |
||||
//// return $this->db->put([$index]); |
||||
// } |
||||
|
||||
/** |
||||
* @param $name |
||||
* @param $pattern |
||||
* @param $settings |
||||
* @param $mappings |
||||
* @param int $order |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html |
||||
*/ |
||||
public function createTemplate($name, $pattern, $settings, $mappings, $order = 0) |
||||
{ |
||||
$body = Json::encode([ |
||||
'template' => $pattern, |
||||
'order' => $order, |
||||
'settings' => (object) $settings, |
||||
'mappings' => (object) $mappings, |
||||
]); |
||||
return $this->db->put(['_template', $name], $body); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* @param $name |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html |
||||
*/ |
||||
public function deleteTemplate($name) |
||||
{ |
||||
return $this->db->delete(['_template', $name]); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* @param $name |
||||
* @return mixed |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html |
||||
*/ |
||||
public function getTemplate($name) |
||||
{ |
||||
return $this->db->get(['_template', $name]); |
||||
} |
||||
} |
@ -0,0 +1,346 @@
|
||||
<?php |
||||
/** |
||||
* @link http://www.yiiframework.com/ |
||||
* @copyright Copyright (c) 2008 Yii Software LLC |
||||
* @license http://www.yiiframework.com/license/ |
||||
*/ |
||||
|
||||
namespace yii\elasticsearch; |
||||
|
||||
use Yii; |
||||
use yii\base\Component; |
||||
use yii\base\InvalidConfigException; |
||||
use yii\helpers\Json; |
||||
|
||||
/** |
||||
* elasticsearch Connection is used to connect to an elasticsearch cluster version 0.20 or higher |
||||
* |
||||
* @author Carsten Brandt <mail@cebe.cc> |
||||
* @since 2.0 |
||||
*/ |
||||
class Connection extends Component |
||||
{ |
||||
/** |
||||
* @event Event an event that is triggered after a DB connection is established |
||||
*/ |
||||
const EVENT_AFTER_OPEN = 'afterOpen'; |
||||
|
||||
/** |
||||
* @var bool whether to autodetect available cluster nodes on [[open()]] |
||||
*/ |
||||
public $autodetectCluster = true; |
||||
/** |
||||
* @var array cluster nodes |
||||
* This is populated with the result of a cluster nodes request when [[autodetectCluster]] is true. |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/cluster-nodes-info.html#cluster-nodes-info |
||||
*/ |
||||
public $nodes = [ |
||||
['http_address' => 'inet[/127.0.0.1:9200]'], |
||||
]; |
||||
/** |
||||
* @var array the active node. key of [[nodes]]. Will be randomly selected on [[open()]]. |
||||
*/ |
||||
public $activeNode; |
||||
|
||||
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/client/php-api/current/_configuration.html#_example_configuring_http_basic_auth |
||||
public $auth = []; |
||||
/** |
||||
* @var float timeout to use for connecting to an elasticsearch node. |
||||
* This value will be used to configure the curl `CURLOPT_CONNECTTIMEOUT` option. |
||||
* If not set, no explicit timeout will be set for curl. |
||||
*/ |
||||
public $connectionTimeout = null; |
||||
/** |
||||
* @var float timeout to use when reading the response from an elasticsearch node. |
||||
* This value will be used to configure the curl `CURLOPT_TIMEOUT` option. |
||||
* If not set, no explicit timeout will be set for curl. |
||||
*/ |
||||
public $dataTimeout = null; |
||||
|
||||
|
||||
public function init() |
||||
{ |
||||
foreach($this->nodes as $node) { |
||||
if (!isset($node['http_address'])) { |
||||
throw new InvalidConfigException('Elasticsearch node needs at least a http_address configured.'); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Closes the connection when this component is being serialized. |
||||
* @return array |
||||
*/ |
||||
public function __sleep() |
||||
{ |
||||
$this->close(); |
||||
return array_keys(get_object_vars($this)); |
||||
} |
||||
|
||||
/** |
||||
* Returns a value indicating whether the DB connection is established. |
||||
* @return boolean whether the DB connection is established |
||||
*/ |
||||
public function getIsActive() |
||||
{ |
||||
return $this->activeNode !== null; |
||||
} |
||||
|
||||
/** |
||||
* Establishes a DB connection. |
||||
* It does nothing if a DB connection has already been established. |
||||
* @throws Exception if connection fails |
||||
*/ |
||||
public function open() |
||||
{ |
||||
if ($this->activeNode !== null) { |
||||
return; |
||||
} |
||||
if (empty($this->nodes)) { |
||||
throw new InvalidConfigException('elasticsearch needs at least one node to operate.'); |
||||
} |
||||
if ($this->autodetectCluster) { |
||||
$node = reset($this->nodes); |
||||
$host = $node['http_address']; |
||||
if (strncmp($host, 'inet[/', 6) == 0) { |
||||
$host = substr($host, 6, -1); |
||||
} |
||||
$response = $this->httpRequest('GET', 'http://' . $host . '/_cluster/nodes'); |
||||
$this->nodes = $response['nodes']; |
||||
if (empty($this->nodes)) { |
||||
throw new Exception('cluster autodetection did not find any active node.'); |
||||
} |
||||
} |
||||
$this->selectActiveNode(); |
||||
Yii::trace('Opening connection to elasticsearch. Nodes in cluster: ' . count($this->nodes) |
||||
. ', active node: ' . $this->nodes[$this->activeNode]['http_address'], __CLASS__); |
||||
$this->initConnection(); |
||||
} |
||||
|
||||
/** |
||||
* select active node randomly |
||||
*/ |
||||
protected function selectActiveNode() |
||||
{ |
||||
$keys = array_keys($this->nodes); |
||||
$this->activeNode = $keys[rand(0, count($keys) - 1)]; |
||||
} |
||||
|
||||
/** |
||||
* Closes the currently active DB connection. |
||||
* It does nothing if the connection is already closed. |
||||
*/ |
||||
public function close() |
||||
{ |
||||
Yii::trace('Closing connection to elasticsearch. Active node was: ' |
||||
. $this->nodes[$this->activeNode]['http_address'], __CLASS__); |
||||
$this->activeNode = null; |
||||
} |
||||
|
||||
/** |
||||
* Initializes the DB connection. |
||||
* This method is invoked right after the DB connection is established. |
||||
* The default implementation triggers an [[EVENT_AFTER_OPEN]] event. |
||||
*/ |
||||
protected function initConnection() |
||||
{ |
||||
$this->trigger(self::EVENT_AFTER_OPEN); |
||||
} |
||||
|
||||
/** |
||||
* Returns the name of the DB driver for the current [[dsn]]. |
||||
* @return string name of the DB driver |
||||
*/ |
||||
public function getDriverName() |
||||
{ |
||||
return 'elasticsearch'; |
||||
} |
||||
|
||||
/** |
||||
* Creates a command for execution. |
||||
* @param array $config the configuration for the Command class |
||||
* @return Command the DB command |
||||
*/ |
||||
public function createCommand($config = []) |
||||
{ |
||||
$this->open(); |
||||
$config['db'] = $this; |
||||
$command = new Command($config); |
||||
return $command; |
||||
} |
||||
|
||||
public function getQueryBuilder() |
||||
{ |
||||
return new QueryBuilder($this); |
||||
} |
||||
|
||||
public function get($url, $options = [], $body = null) |
||||
{ |
||||
$this->open(); |
||||
return $this->httpRequest('GET', $this->createUrl($url, $options), $body); |
||||
} |
||||
|
||||
public function head($url, $options = [], $body = null) |
||||
{ |
||||
$this->open(); |
||||
return $this->httpRequest('HEAD', $this->createUrl($url, $options), $body); |
||||
} |
||||
|
||||
public function post($url, $options = [], $body = null) |
||||
{ |
||||
$this->open(); |
||||
return $this->httpRequest('POST', $this->createUrl($url, $options), $body); |
||||
} |
||||
|
||||
public function put($url, $options = [], $body = null) |
||||
{ |
||||
$this->open(); |
||||
return $this->httpRequest('PUT', $this->createUrl($url, $options), $body); |
||||
} |
||||
|
||||
public function delete($url, $options = [], $body = null) |
||||
{ |
||||
$this->open(); |
||||
return $this->httpRequest('DELETE', $this->createUrl($url, $options), $body); |
||||
} |
||||
|
||||
private function createUrl($path, $options = []) |
||||
{ |
||||
$url = implode('/', array_map(function($a) { |
||||
return urlencode(is_array($a) ? implode(',', $a) : $a); |
||||
}, $path)); |
||||
|
||||
if (!empty($options)) { |
||||
$url .= '?' . http_build_query($options); |
||||
} |
||||
return [$this->nodes[$this->activeNode]['http_address'], $url]; |
||||
} |
||||
|
||||
protected function httpRequest($method, $url, $requestBody = null) |
||||
{ |
||||
$method = strtoupper($method); |
||||
|
||||
// response body and headers |
||||
$headers = []; |
||||
$body = ''; |
||||
|
||||
$options = [ |
||||
CURLOPT_USERAGENT => 'Yii2 Framework ' . __CLASS__, |
||||
CURLOPT_RETURNTRANSFER => false, |
||||
CURLOPT_HEADER => false, |
||||
// http://www.php.net/manual/en/function.curl-setopt.php#82418 |
||||
CURLOPT_HTTPHEADER => ['Expect:'], |
||||
|
||||
CURLOPT_WRITEFUNCTION => function($curl, $data) use (&$body) { |
||||
$body .= $data; |
||||
return mb_strlen($data, '8bit'); |
||||
}, |
||||
CURLOPT_HEADERFUNCTION => function($curl, $data) use (&$headers) { |
||||
foreach(explode("\r\n", $data) as $row) { |
||||
if (($pos = strpos($row, ':')) !== false) { |
||||
$headers[strtolower(substr($row, 0, $pos))] = trim(substr($row, $pos + 1)); |
||||
} |
||||
} |
||||
return mb_strlen($data, '8bit'); |
||||
}, |
||||
CURLOPT_CUSTOMREQUEST => $method, |
||||
]; |
||||
if ($this->connectionTimeout !== null) { |
||||
$options[CURLOPT_CONNECTTIMEOUT] = $this->connectionTimeout; |
||||
} |
||||
if ($this->dataTimeout !== null) { |
||||
$options[CURLOPT_TIMEOUT] = $this->dataTimeout; |
||||
} |
||||
if ($requestBody !== null) { |
||||
$options[CURLOPT_POSTFIELDS] = $requestBody; |
||||
} |
||||
if ($method == 'HEAD') { |
||||
$options[CURLOPT_NOBODY] = true; |
||||
unset($options[CURLOPT_WRITEFUNCTION]); |
||||
} |
||||
|
||||
if (is_array($url)) { |
||||
list($host, $q) = $url; |
||||
if (strncmp($host, 'inet[/', 6) == 0) { |
||||
$host = substr($host, 6, -1); |
||||
} |
||||
$profile = $q . $requestBody; |
||||
$url = 'http://' . $host . '/' . $q; |
||||
} else { |
||||
$profile = false; |
||||
} |
||||
|
||||
Yii::trace("Sending request to elasticsearch node: $url\n$requestBody", __METHOD__); |
||||
if ($profile !== false) { |
||||
Yii::beginProfile($profile, __METHOD__); |
||||
} |
||||
|
||||
$curl = curl_init($url); |
||||
curl_setopt_array($curl, $options); |
||||
if (curl_exec($curl) === false) { |
||||
throw new Exception('Elasticsearch request failed: ' . curl_errno($curl) . ' - ' . curl_error($curl), [ |
||||
'requestMethod' => $method, |
||||
'requestUrl' => $url, |
||||
'requestBody' => $requestBody, |
||||
'responseHeaders' => $headers, |
||||
'responseBody' => $body, |
||||
]); |
||||
} |
||||
|
||||
$responseCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); |
||||
curl_close($curl); |
||||
|
||||
if ($profile !== false) { |
||||
Yii::endProfile($profile, __METHOD__); |
||||
} |
||||
|
||||
if ($responseCode >= 200 && $responseCode < 300) { |
||||
if ($method == 'HEAD') { |
||||
return true; |
||||
} else { |
||||
if (isset($headers['content-length']) && ($len = mb_strlen($body, '8bit')) < $headers['content-length']) { |
||||
throw new Exception("Incomplete data received from elasticsearch: $len < {$headers['content-length']}", [ |
||||
'requestMethod' => $method, |
||||
'requestUrl' => $url, |
||||
'requestBody' => $requestBody, |
||||
'responseCode' => $responseCode, |
||||
'responseHeaders' => $headers, |
||||
'responseBody' => $body, |
||||
]); |
||||
} |
||||
if (isset($headers['content-type']) && !strncmp($headers['content-type'], 'application/json', 16)) { |
||||
return Json::decode($body); |
||||
} |
||||
throw new Exception('Unsupported data received from elasticsearch: ' . $headers['content-type'], [ |
||||
'requestMethod' => $method, |
||||
'requestUrl' => $url, |
||||
'requestBody' => $requestBody, |
||||
'responseCode' => $responseCode, |
||||
'responseHeaders' => $headers, |
||||
'responseBody' => $body, |
||||
]); |
||||
} |
||||
} elseif ($responseCode == 404) { |
||||
return false; |
||||
} else { |
||||
throw new Exception("Elasticsearch request failed with code $responseCode.", [ |
||||
'requestMethod' => $method, |
||||
'requestUrl' => $url, |
||||
'requestBody' => $requestBody, |
||||
'responseCode' => $responseCode, |
||||
'responseHeaders' => $headers, |
||||
'responseBody' => $body, |
||||
]); |
||||
} |
||||
} |
||||
|
||||
public function getNodeInfo() |
||||
{ |
||||
return $this->get([]); |
||||
} |
||||
|
||||
public function getClusterState() |
||||
{ |
||||
return $this->get(['_cluster', 'state']); |
||||
} |
||||
} |
@ -0,0 +1,43 @@
|
||||
<?php |
||||
/** |
||||
* @link http://www.yiiframework.com/ |
||||
* @copyright Copyright (c) 2008 Yii Software LLC |
||||
* @license http://www.yiiframework.com/license/ |
||||
*/ |
||||
|
||||
namespace yii\elasticsearch; |
||||
|
||||
/** |
||||
* Exception represents an exception that is caused by elasticsearch-related operations. |
||||
* |
||||
* @author Carsten Brandt <mail@cebe.cc> |
||||
* @since 2.0 |
||||
*/ |
||||
class Exception extends \yii\db\Exception |
||||
{ |
||||
/** |
||||
* @var array additional information about the http request that caused the error. |
||||
*/ |
||||
public $errorInfo = []; |
||||
|
||||
/** |
||||
* Constructor. |
||||
* @param string $message error message |
||||
* @param array $errorInfo error info |
||||
* @param integer $code error code |
||||
* @param \Exception $previous The previous exception used for the exception chaining. |
||||
*/ |
||||
public function __construct($message, $errorInfo = [], $code = 0, \Exception $previous = null) |
||||
{ |
||||
$this->errorInfo = $errorInfo; |
||||
parent::__construct($message, $code, $previous); |
||||
} |
||||
|
||||
/** |
||||
* @return string the user-friendly name of this exception |
||||
*/ |
||||
public function getName() |
||||
{ |
||||
return \Yii::t('yii', 'Elasticsearch Database Exception'); |
||||
} |
||||
} |
@ -0,0 +1,32 @@
|
||||
The Yii framework is free software. It is released under the terms of |
||||
the following BSD License. |
||||
|
||||
Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com) |
||||
All rights reserved. |
||||
|
||||
Redistribution and use in source and binary forms, with or without |
||||
modification, are permitted provided that the following conditions |
||||
are met: |
||||
|
||||
* Redistributions of source code must retain the above copyright |
||||
notice, this list of conditions and the following disclaimer. |
||||
* Redistributions in binary form must reproduce the above copyright |
||||
notice, this list of conditions and the following disclaimer in |
||||
the documentation and/or other materials provided with the |
||||
distribution. |
||||
* Neither the name of Yii Software LLC nor the names of its |
||||
contributors may be used to endorse or promote products derived |
||||
from this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS |
||||
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE |
||||
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, |
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
||||
POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,506 @@
|
||||
<?php |
||||
/** |
||||
* @link http://www.yiiframework.com/ |
||||
* @copyright Copyright (c) 2008 Yii Software LLC |
||||
* @license http://www.yiiframework.com/license/ |
||||
*/ |
||||
|
||||
namespace yii\elasticsearch; |
||||
|
||||
use Yii; |
||||
use yii\base\Component; |
||||
use yii\base\NotSupportedException; |
||||
use yii\db\QueryInterface; |
||||
use yii\db\QueryTrait; |
||||
|
||||
/** |
||||
* Query represents a query to the search API of elasticsearch. |
||||
* |
||||
* Query provides a set of methods to facilitate the specification of different parameters of the query. |
||||
* These methods can be chained together. |
||||
* |
||||
* By calling [[createCommand()]], we can get a [[Command]] instance which can be further |
||||
* used to perform/execute the DB query against a database. |
||||
* |
||||
* For example, |
||||
* |
||||
* ~~~ |
||||
* $query = new Query; |
||||
* $query->fields('id, name') |
||||
* ->from('myindex', 'users') |
||||
* ->limit(10); |
||||
* // build and execute the query |
||||
* $command = $query->createCommand(); |
||||
* $rows = $command->search(); // this way you get the raw output of elasticsearch. |
||||
* ~~~ |
||||
* |
||||
* You would normally call `$query->search()` instead of creating a command as this method |
||||
* adds the `indexBy()` feature and also removes some inconsistencies from the response. |
||||
* |
||||
* Query also provides some methods to easier get some parts of the result only: |
||||
* |
||||
* - [[one()]]: returns a single record populated with the first row of data. |
||||
* - [[all()]]: returns all records based on the query results. |
||||
* - [[count()]]: returns the number of records. |
||||
* - [[scalar()]]: returns the value of the first column in the first row of the query result. |
||||
* - [[column()]]: returns the value of the first column in the query result. |
||||
* - [[exists()]]: returns a value indicating whether the query result has data or not. |
||||
* |
||||
* @author Carsten Brandt <mail@cebe.cc> |
||||
* @since 2.0 |
||||
*/ |
||||
class Query extends Component implements QueryInterface |
||||
{ |
||||
use QueryTrait; |
||||
|
||||
/** |
||||
* @var array the fields being retrieved from the documents. For example, `['id', 'name']`. |
||||
* If not set, it means retrieving all fields. An empty array will result in no fields being |
||||
* retrieved. This means that only the primaryKey of a record will be available in the result. |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-fields.html#search-request-fields |
||||
* @see fields() |
||||
*/ |
||||
public $fields; |
||||
/** |
||||
* @var string|array The index to retrieve data from. This can be a string representing a single index |
||||
* or a an array of multiple indexes. If this is not set, indexes are being queried. |
||||
* @see from() |
||||
*/ |
||||
public $index; |
||||
/** |
||||
* @var string|array The type to retrieve data from. This can be a string representing a single type |
||||
* or a an array of multiple types. If this is not set, all types are being queried. |
||||
* @see from() |
||||
*/ |
||||
public $type; |
||||
/** |
||||
* @var integer A search timeout, bounding the search request to be executed within the specified time value |
||||
* and bail with the hits accumulated up to that point when expired. Defaults to no timeout. |
||||
* @see timeout() |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3 |
||||
*/ |
||||
public $timeout; |
||||
/** |
||||
* @var array|string The query part of this search query. This is an array or json string that follows the format of |
||||
* the elasticsearch [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html). |
||||
*/ |
||||
public $query; |
||||
/** |
||||
* @var array|string The filter part of this search query. This is an array or json string that follows the format of |
||||
* the elasticsearch [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html). |
||||
*/ |
||||
public $filter; |
||||
|
||||
public $facets = []; |
||||
|
||||
public function init() |
||||
{ |
||||
parent::init(); |
||||
// setting the default limit according to elasticsearch defaults |
||||
// http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3 |
||||
if ($this->limit === null) { |
||||
$this->limit = 10; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Creates a DB command that can be used to execute this query. |
||||
* @param Connection $db the database connection used to execute the query. |
||||
* If this parameter is not given, the `elasticsearch` application component will be used. |
||||
* @return Command the created DB command instance. |
||||
*/ |
||||
public function createCommand($db = null) |
||||
{ |
||||
if ($db === null) { |
||||
$db = Yii::$app->getComponent('elasticsearch'); |
||||
} |
||||
|
||||
$commandConfig = $db->getQueryBuilder()->build($this); |
||||
return $db->createCommand($commandConfig); |
||||
} |
||||
|
||||
/** |
||||
* Executes the query and returns all results as an array. |
||||
* @param Connection $db the database connection used to execute the query. |
||||
* If this parameter is not given, the `elasticsearch` application component will be used. |
||||
* @return array the query results. If the query results in nothing, an empty array will be returned. |
||||
*/ |
||||
public function all($db = null) |
||||
{ |
||||
$result = $this->createCommand($db)->search(); |
||||
if (empty($result['hits']['hits'])) { |
||||
return []; |
||||
} |
||||
$rows = $result['hits']['hits']; |
||||
if ($this->indexBy === null && $this->fields === null) { |
||||
return $rows; |
||||
} |
||||
$models = []; |
||||
foreach ($rows as $key => $row) { |
||||
if ($this->fields !== null) { |
||||
$row['_source'] = isset($row['fields']) ? $row['fields'] : []; |
||||
unset($row['fields']); |
||||
} |
||||
if ($this->indexBy !== null) { |
||||
if (is_string($this->indexBy)) { |
||||
$key = $row['_source'][$this->indexBy]; |
||||
} else { |
||||
$key = call_user_func($this->indexBy, $row); |
||||
} |
||||
} |
||||
$models[$key] = $row; |
||||
} |
||||
return $models; |
||||
} |
||||
|
||||
/** |
||||
* Executes the query and returns a single row of result. |
||||
* @param Connection $db the database connection used to execute the query. |
||||
* If this parameter is not given, the `elasticsearch` application component will be used. |
||||
* @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query |
||||
* results in nothing. |
||||
*/ |
||||
public function one($db = null) |
||||
{ |
||||
$options['size'] = 1; |
||||
$result = $this->createCommand($db)->search($options); |
||||
if (empty($result['hits']['hits'])) { |
||||
return false; |
||||
} |
||||
$record = reset($result['hits']['hits']); |
||||
if ($this->fields !== null) { |
||||
$record['_source'] = isset($record['fields']) ? $record['fields'] : []; |
||||
unset($record['fields']); |
||||
} |
||||
return $record; |
||||
} |
||||
|
||||
/** |
||||
* Executes the query and returns the complete search result including e.g. hits, facets, totalCount. |
||||
* @param Connection $db the database connection used to execute the query. |
||||
* If this parameter is not given, the `elasticsearch` application component will be used. |
||||
* @param array $options The options given with this query. Possible options are: |
||||
* - [routing](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search.html#search-routing) |
||||
* - [search_type](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-search-type.html) |
||||
* @return array the query results. |
||||
*/ |
||||
public function search($db = null, $options = []) |
||||
{ |
||||
$result = $this->createCommand($db)->search($options); |
||||
if (!empty($result['hits']['hits']) && ($this->indexBy === null || $this->fields === null)) { |
||||
$rows = []; |
||||
foreach ($result['hits']['hits'] as $key => $row) { |
||||
if ($this->fields !== null) { |
||||
$row['_source'] = isset($row['fields']) ? $row['fields'] : []; |
||||
unset($row['fields']); |
||||
} |
||||
if ($this->indexBy !== null) { |
||||
if (is_string($this->indexBy)) { |
||||
$key = $row['_source'][$this->indexBy]; |
||||
} else { |
||||
$key = call_user_func($this->indexBy, $row); |
||||
} |
||||
} |
||||
$rows[$key] = $row; |
||||
} |
||||
$result['hits']['hits'] = $rows; |
||||
} |
||||
return $result; |
||||
} |
||||
|
||||
// TODO add query stats http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search.html#stats-groups |
||||
|
||||
// TODO add scroll/scan http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-search-type.html#scan |
||||
|
||||
/** |
||||
* Executes the query and deletes all matching documents. |
||||
* |
||||
* This will not run facet queries. |
||||
* |
||||
* @param Connection $db the database connection used to execute the query. |
||||
* If this parameter is not given, the `elasticsearch` application component will be used. |
||||
* @return array the query results. If the query results in nothing, an empty array will be returned. |
||||
*/ |
||||
public function delete($db = null) |
||||
{ |
||||
// TODO implement http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete-by-query.html |
||||
throw new NotSupportedException('Delete by query is not implemented yet.'); |
||||
} |
||||
|
||||
/** |
||||
* Returns the query result as a scalar value. |
||||
* The value returned will be the specified field in the first document of the query results. |
||||
* @param string $field name of the attribute to select |
||||
* @param Connection $db the database connection used to execute the query. |
||||
* If this parameter is not given, the `elasticsearch` application component will be used. |
||||
* @return string the value of the specified attribute in the first record of the query result. |
||||
* Null is returned if the query result is empty or the field does not exist. |
||||
*/ |
||||
public function scalar($field, $db = null) |
||||
{ |
||||
$record = self::one($db); // TODO limit fields to the one required |
||||
if ($record !== false && isset($record['_source'][$field])) { |
||||
return $record['_source'][$field]; |
||||
} else { |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Executes the query and returns the first column of the result. |
||||
* @param string $field the field to query over |
||||
* @param Connection $db the database connection used to execute the query. |
||||
* If this parameter is not given, the `elasticsearch` application component will be used. |
||||
* @return array the first column of the query result. An empty array is returned if the query results in nothing. |
||||
*/ |
||||
public function column($field, $db = null) |
||||
{ |
||||
$command = $this->createCommand($db); |
||||
$command->queryParts['fields'] = [$field]; |
||||
$result = $command->search(); |
||||
if (empty($result['hits']['hits'])) { |
||||
return []; |
||||
} |
||||
$column = []; |
||||
foreach ($result['hits']['hits'] as $row) { |
||||
$column[] = isset($row['fields'][$field]) ? $row['fields'][$field] : null; |
||||
} |
||||
return $column; |
||||
} |
||||
|
||||
/** |
||||
* Returns the number of records. |
||||
* @param string $q the COUNT expression. This parameter is ignored by this implementation. |
||||
* @param Connection $db the database connection used to execute the query. |
||||
* If this parameter is not given, the `elasticsearch` application component will be used. |
||||
* @return integer number of records |
||||
*/ |
||||
public function count($q = '*', $db = null) |
||||
{ |
||||
// TODO consider sending to _count api instead of _search for performance |
||||
// only when no facety are registerted. |
||||
// http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-count.html |
||||
|
||||
$options = []; |
||||
$options['search_type'] = 'count'; |
||||
$count = $this->createCommand($db)->search($options)['hits']['total']; |
||||
if ($this->limit === null && $this->offset === null) { |
||||
return $count; |
||||
} elseif ($this->offset !== null) { |
||||
$count = $this->offset < $count ? $count - $this->offset : 0; |
||||
} |
||||
return $this->limit === null ? $count : ($this->limit > $count ? $count : $this->limit); |
||||
} |
||||
|
||||
/** |
||||
* Returns a value indicating whether the query result contains any row of data. |
||||
* @param Connection $db the database connection used to execute the query. |
||||
* If this parameter is not given, the `elasticsearch` application component will be used. |
||||
* @return boolean whether the query result contains any row of data. |
||||
*/ |
||||
public function exists($db = null) |
||||
{ |
||||
return self::one($db) !== false; |
||||
} |
||||
|
||||
/** |
||||
* Adds a facet search to this query. |
||||
* @param string $name the name of this facet |
||||
* @param string $type the facet type. e.g. `terms`, `range`, `histogram`... |
||||
* @param string|array $options the configuration options for this facet. Can be an array or a json string. |
||||
* @return static |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-query-facet.html |
||||
*/ |
||||
public function addFacet($name, $type, $options) |
||||
{ |
||||
$this->facets[$name] = [$type => $options]; |
||||
return $this; |
||||
} |
||||
|
||||
/** |
||||
* The `terms facet` allow to specify field facets that return the N most frequent terms. |
||||
* @param string $name the name of this facet |
||||
* @param array $options additional option. Please refer to the elasticsearch documentation for details. |
||||
* @return static |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-terms-facet.html |
||||
*/ |
||||
public function addTermFacet($name, $options) |
||||
{ |
||||
return $this->addFacet($name, 'terms', $options); |
||||
} |
||||
|
||||
/** |
||||
* Range facet allows to specify a set of ranges and get both the number of docs (count) that fall |
||||
* within each range, and aggregated data either based on the field, or using another field. |
||||
* @param string $name the name of this facet |
||||
* @param array $options additional option. Please refer to the elasticsearch documentation for details. |
||||
* @return static |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-range-facet.html |
||||
*/ |
||||
public function addRangeFacet($name, $options) |
||||
{ |
||||
return $this->addFacet($name, 'range', $options); |
||||
} |
||||
|
||||
/** |
||||
* The histogram facet works with numeric data by building a histogram across intervals of the field values. |
||||
* Each value is "rounded" into an interval (or placed in a bucket), and statistics are provided per |
||||
* interval/bucket (count and total). |
||||
* @param string $name the name of this facet |
||||
* @param array $options additional option. Please refer to the elasticsearch documentation for details. |
||||
* @return static |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-histogram-facet.html |
||||
*/ |
||||
public function addHistogramFacet($name, $options) |
||||
{ |
||||
return $this->addFacet($name, 'histogram', $options); |
||||
} |
||||
|
||||
/** |
||||
* A specific histogram facet that can work with date field types enhancing it over the regular histogram facet. |
||||
* @param string $name the name of this facet |
||||
* @param array $options additional option. Please refer to the elasticsearch documentation for details. |
||||
* @return static |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-date-histogram-facet.html |
||||
*/ |
||||
public function addDateHistogramFacet($name, $options) |
||||
{ |
||||
return $this->addFacet($name, 'date_histogram', $options); |
||||
} |
||||
|
||||
/** |
||||
* A filter facet (not to be confused with a facet filter) allows you to return a count of the hits matching the filter. |
||||
* The filter itself can be expressed using the Query DSL. |
||||
* @param string $name the name of this facet |
||||
* @param string $filter the query in Query DSL |
||||
* @return static |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-filter-facet.html |
||||
*/ |
||||
public function addFilterFacet($name, $filter) |
||||
{ |
||||
return $this->addFacet($name, 'filter', $filter); |
||||
} |
||||
|
||||
/** |
||||
* A facet query allows to return a count of the hits matching the facet query. |
||||
* The query itself can be expressed using the Query DSL. |
||||
* @param string $name the name of this facet |
||||
* @param string $query the query in Query DSL |
||||
* @return static |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-query-facet.html |
||||
*/ |
||||
public function addQueryFacet($name, $query) |
||||
{ |
||||
return $this->addFacet($name, 'query', $query); |
||||
} |
||||
|
||||
/** |
||||
* Statistical facet allows to compute statistical data on a numeric fields. The statistical data include count, |
||||
* total, sum of squares, mean (average), minimum, maximum, variance, and standard deviation. |
||||
* @param string $name the name of this facet |
||||
* @param array $options additional option. Please refer to the elasticsearch documentation for details. |
||||
* @return static |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-statistical-facet.html |
||||
*/ |
||||
public function addStatisticalFacet($name, $options) |
||||
{ |
||||
return $this->addFacet($name, 'statistical', $options); |
||||
} |
||||
|
||||
/** |
||||
* The `terms_stats` facet combines both the terms and statistical allowing to compute stats computed on a field, |
||||
* per term value driven by another field. |
||||
* @param string $name the name of this facet |
||||
* @param array $options additional option. Please refer to the elasticsearch documentation for details. |
||||
* @return static |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-terms-stats-facet.html |
||||
*/ |
||||
public function addTermsStatsFacet($name, $options) |
||||
{ |
||||
return $this->addFacet($name, 'terms_stats', $options); |
||||
} |
||||
|
||||
/** |
||||
* The `geo_distance` facet is a facet providing information for ranges of distances from a provided `geo_point` |
||||
* including count of the number of hits that fall within each range, and aggregation information (like `total`). |
||||
* @param string $name the name of this facet |
||||
* @param array $options additional option. Please refer to the elasticsearch documentation for details. |
||||
* @return static |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-geo-distance-facet.html |
||||
*/ |
||||
public function addGeoDistanceFacet($name, $options) |
||||
{ |
||||
return $this->addFacet($name, 'geo_distance', $options); |
||||
} |
||||
|
||||
// TODO add suggesters http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-suggesters.html |
||||
|
||||
// TODO add validate query http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-validate.html |
||||
|
||||
// TODO support multi query via static method http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-multi-search.html |
||||
|
||||
/** |
||||
* Sets the querypart of this search query. |
||||
* @param string $query |
||||
* @return static |
||||
*/ |
||||
public function query($query) |
||||
{ |
||||
$this->query = $query; |
||||
return $this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the filter part of this search query. |
||||
* @param string $filter |
||||
* @return static |
||||
*/ |
||||
public function filter($filter) |
||||
{ |
||||
$this->filter = $filter; |
||||
return $this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the index and type to retrieve documents from. |
||||
* @param string|array $index The index to retrieve data from. This can be a string representing a single index |
||||
* or a an array of multiple indexes. If this is `null` it means that all indexes are being queried. |
||||
* @param string|array $type The type to retrieve data from. This can be a string representing a single type |
||||
* or a an array of multiple types. If this is `null` it means that all types are being queried. |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-search.html#search-multi-index-type |
||||
*/ |
||||
public function from($index, $type = null) |
||||
{ |
||||
$this->index = $index; |
||||
$this->type = $type; |
||||
} |
||||
|
||||
/** |
||||
* Sets the fields to retrieve from the documents. |
||||
* @param array $fields the fields to be selected. |
||||
* @return static the query object itself |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-fields.html |
||||
*/ |
||||
public function fields($fields) |
||||
{ |
||||
if (is_array($fields) || $fields === null) { |
||||
$this->fields = $fields; |
||||
} else { |
||||
$this->fields = func_get_args(); |
||||
} |
||||
return $this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the search timeout. |
||||
* @param integer $timeout A search timeout, bounding the search request to be executed within the specified time value |
||||
* and bail with the hits accumulated up to that point when expired. Defaults to no timeout. |
||||
* @return static the query object itself |
||||
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3 |
||||
*/ |
||||
public function timeout($timeout) |
||||
{ |
||||
$this->timeout = $timeout; |
||||
return $this; |
||||
} |
||||
} |
@ -0,0 +1,349 @@
|
||||
<?php |
||||
/** |
||||
* @link http://www.yiiframework.com/ |
||||
* @copyright Copyright (c) 2008 Yii Software LLC |
||||
* @license http://www.yiiframework.com/license/ |
||||
*/ |
||||
|
||||
namespace yii\elasticsearch; |
||||
|
||||
use yii\base\InvalidParamException; |
||||
use yii\base\NotSupportedException; |
||||
use yii\helpers\Json; |
||||
|
||||
/** |
||||
* QueryBuilder builds an elasticsearch query based on the specification given as a [[Query]] object. |
||||
* |
||||
* |
||||
* @author Carsten Brandt <mail@cebe.cc> |
||||
* @since 2.0 |
||||
*/ |
||||
class QueryBuilder extends \yii\base\Object |
||||
{ |
||||
/** |
||||
* @var Connection the database connection. |
||||
*/ |
||||
public $db; |
||||
|
||||
/** |
||||
* Constructor. |
||||
* @param Connection $connection the database connection. |
||||
* @param array $config name-value pairs that will be used to initialize the object properties |
||||
*/ |
||||
public function __construct($connection, $config = []) |
||||
{ |
||||
$this->db = $connection; |
||||
parent::__construct($config); |
||||
} |
||||
|
||||
/** |
||||
* Generates query from a [[Query]] object. |
||||
* @param Query $query the [[Query]] object from which the query will be generated |
||||
* @return array the generated SQL statement (the first array element) and the corresponding |
||||
* parameters to be bound to the SQL statement (the second array element). |
||||
*/ |
||||
public function build($query) |
||||
{ |
||||
$parts = []; |
||||
|
||||
if ($query->fields !== null) { |
||||
$parts['fields'] = (array) $query->fields; |
||||
} |
||||
if ($query->limit !== null && $query->limit >= 0) { |
||||
$parts['size'] = $query->limit; |
||||
} |
||||
if ($query->offset > 0) { |
||||
$parts['from'] = (int) $query->offset; |
||||
} |
||||
|
||||
if (empty($parts['query'])) { |
||||
$parts['query'] = ["match_all" => (object)[]]; |
||||
} |
||||
|
||||
$whereFilter = $this->buildCondition($query->where); |
||||
if (is_string($query->filter)) { |
||||
if (empty($whereFilter)) { |
||||
$parts['filter'] = $query->filter; |
||||
} else { |
||||
$parts['filter'] = '{"and": [' . $query->filter . ', ' . Json::encode($whereFilter) . ']}'; |
||||
} |
||||
} elseif ($query->filter !== null) { |
||||
if (empty($whereFilter)) { |
||||
$parts['filter'] = $query->filter; |
||||
} else { |
||||
$parts['filter'] = ['and' => [$query->filter, $whereFilter]]; |
||||
} |
||||
} elseif (!empty($whereFilter)) { |
||||
$parts['filter'] = $whereFilter; |
||||
} |
||||
|
||||
$sort = $this->buildOrderBy($query->orderBy); |
||||
if (!empty($sort)) { |
||||
$parts['sort'] = $sort; |
||||
} |
||||
|
||||
if (!empty($query->facets)) { |
||||
$parts['facets'] = $query->facets; |
||||
} |
||||
|
||||
$options = []; |
||||
if ($query->timeout !== null) { |
||||
$options['timeout'] = $query->timeout; |
||||
} |
||||
|
||||
return [ |
||||
'queryParts' => $parts, |
||||
'index' => $query->index, |
||||
'type' => $query->type, |
||||
'options' => $options, |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* adds order by condition to the query |
||||
*/ |
||||
public function buildOrderBy($columns) |
||||
{ |
||||
if (empty($columns)) { |
||||
return []; |
||||
} |
||||
$orders = []; |
||||
foreach ($columns as $name => $direction) { |
||||
if (is_string($direction)) { |
||||
$column = $direction; |
||||
$direction = SORT_ASC; |
||||
} else { |
||||
$column = $name; |
||||
} |
||||
if ($column == ActiveRecord::PRIMARY_KEY_NAME) { |
||||
$column = '_id'; |
||||
} |
||||
|
||||
// allow elasticsearch extended syntax as described in http://www.elasticsearch.org/guide/reference/api/search/sort/ |
||||
if (is_array($direction)) { |
||||
$orders[] = [$column => $direction]; |
||||
} else { |
||||
$orders[] = [$column => ($direction === SORT_DESC ? 'desc' : 'asc')]; |
||||
} |
||||
} |
||||
return $orders; |
||||
} |
||||
|
||||
/** |
||||
* Parses the condition specification and generates the corresponding SQL expression. |
||||
* @param string|array $condition the condition specification. Please refer to [[Query::where()]] |
||||
* on how to specify a condition. |
||||
* @param array $params the binding parameters to be populated |
||||
* @return string the generated SQL expression |
||||
* @throws \yii\db\Exception if the condition is in bad format |
||||
*/ |
||||
public function buildCondition($condition) |
||||
{ |
||||
static $builders = array( |
||||
'and' => 'buildAndCondition', |
||||
'or' => 'buildAndCondition', |
||||
'between' => 'buildBetweenCondition', |
||||
'not between' => 'buildBetweenCondition', |
||||
'in' => 'buildInCondition', |
||||
'not in' => 'buildInCondition', |
||||
'like' => 'buildLikeCondition', |
||||
'not like' => 'buildLikeCondition', |
||||
'or like' => 'buildLikeCondition', |
||||
'or not like' => 'buildLikeCondition', |
||||
); |
||||
|
||||
if (empty($condition)) { |
||||
return []; |
||||
} |
||||
if (!is_array($condition)) { |
||||
throw new NotSupportedException('String conditions in where() are not supported by elasticsearch.'); |
||||
} |
||||
if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... |
||||
$operator = strtolower($condition[0]); |
||||
if (isset($builders[$operator])) { |
||||
$method = $builders[$operator]; |
||||
array_shift($condition); |
||||
return $this->$method($operator, $condition); |
||||
} else { |
||||
throw new InvalidParamException('Found unknown operator in query: ' . $operator); |
||||
} |
||||
} else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... |
||||
return $this->buildHashCondition($condition); |
||||
} |
||||
} |
||||
|
||||
private function buildHashCondition($condition) |
||||
{ |
||||
$parts = []; |
||||
foreach($condition as $attribute => $value) { |
||||
if ($attribute == ActiveRecord::PRIMARY_KEY_NAME) { |
||||
if ($value == null) { // there is no null pk |
||||
$parts[] = ['script' => ['script' => '0==1']]; |
||||
} else { |
||||
$parts[] = ['ids' => ['values' => is_array($value) ? $value : [$value]]]; |
||||
} |
||||
} else { |
||||
if (is_array($value)) { // IN condition |
||||
$parts[] = ['in' => [$attribute => $value]]; |
||||
} else { |
||||
if ($value === null) { |
||||
$parts[] = ['missing' => ['field' => $attribute, 'existence' => true, 'null_value' => true]]; |
||||
} else { |
||||
$parts[] = ['term' => [$attribute => $value]]; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return count($parts) === 1 ? $parts[0] : ['and' => $parts]; |
||||
} |
||||
|
||||
private function buildAndCondition($operator, $operands) |
||||
{ |
||||
$parts = []; |
||||
foreach ($operands as $operand) { |
||||
if (is_array($operand)) { |
||||
$operand = $this->buildCondition($operand); |
||||
} |
||||
if (!empty($operand)) { |
||||
$parts[] = $operand; |
||||
} |
||||
} |
||||
if (!empty($parts)) { |
||||
return [$operator => $parts]; |
||||
} else { |
||||
return []; |
||||
} |
||||
} |
||||
|
||||
private function buildBetweenCondition($operator, $operands) |
||||
{ |
||||
if (!isset($operands[0], $operands[1], $operands[2])) { |
||||
throw new InvalidParamException("Operator '$operator' requires three operands."); |
||||
} |
||||
|
||||
list($column, $value1, $value2) = $operands; |
||||
if ($column == ActiveRecord::PRIMARY_KEY_NAME) { |
||||
throw new NotSupportedException('Between condition is not supported for primaryKey.'); |
||||
} |
||||
$filter = ['range' => [$column => ['gte' => $value1, 'lte' => $value2]]]; |
||||
if ($operator == 'not between') { |
||||
$filter = ['not' => $filter]; |
||||
} |
||||
return $filter; |
||||
} |
||||
|
||||
private function buildInCondition($operator, $operands) |
||||
{ |
||||
if (!isset($operands[0], $operands[1])) { |
||||
throw new InvalidParamException("Operator '$operator' requires two operands."); |
||||
} |
||||
|
||||
list($column, $values) = $operands; |
||||
|
||||
$values = (array)$values; |
||||
|
||||
if (empty($values) || $column === []) { |
||||
return $operator === 'in' ? ['script' => ['script' => '0==1']] : []; |
||||
} |
||||
|
||||
if (count($column) > 1) { |
||||
return $this->buildCompositeInCondition($operator, $column, $values, $params); |
||||
} elseif (is_array($column)) { |
||||
$column = reset($column); |
||||
} |
||||
$canBeNull = false; |
||||
foreach ($values as $i => $value) { |
||||
if (is_array($value)) { |
||||
$values[$i] = $value = isset($value[$column]) ? $value[$column] : null; |
||||
} |
||||
if ($value === null) { |
||||
$canBeNull = true; |
||||
unset($values[$i]); |
||||
} |
||||
} |
||||
if ($column == ActiveRecord::PRIMARY_KEY_NAME) { |
||||
if (empty($values) && $canBeNull) { // there is no null pk |
||||
$filter = ['script' => ['script' => '0==1']]; |
||||
} else { |
||||
$filter = ['ids' => ['values' => array_values($values)]]; |
||||
if ($canBeNull) { |
||||
$filter = ['or' => [$filter, ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]]]; |
||||
} |
||||
} |
||||
} else { |
||||
if (empty($values) && $canBeNull) { |
||||
$filter = ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]; |
||||
} else { |
||||
$filter = ['in' => [$column => array_values($values)]]; |
||||
if ($canBeNull) { |
||||
$filter = ['or' => [$filter, ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]]]; |
||||
} |
||||
} |
||||
} |
||||
if ($operator == 'not in') { |
||||
$filter = ['not' => $filter]; |
||||
} |
||||
return $filter; |
||||
} |
||||
|
||||
protected function buildCompositeInCondition($operator, $columns, $values) |
||||
{ |
||||
throw new NotSupportedException('composite in is not supported by elasticsearch.'); |
||||
$vss = array(); |
||||
foreach ($values as $value) { |
||||
$vs = array(); |
||||
foreach ($columns as $column) { |
||||
if (isset($value[$column])) { |
||||
$phName = self::PARAM_PREFIX . count($params); |
||||
$params[$phName] = $value[$column]; |
||||
$vs[] = $phName; |
||||
} else { |
||||
$vs[] = 'NULL'; |
||||
} |
||||
} |
||||
$vss[] = '(' . implode(', ', $vs) . ')'; |
||||
} |
||||
foreach ($columns as $i => $column) { |
||||
if (strpos($column, '(') === false) { |
||||
$columns[$i] = $this->db->quoteColumnName($column); |
||||
} |
||||
} |
||||
return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; |
||||
} |
||||
|
||||
private function buildLikeCondition($operator, $operands) |
||||
{ |
||||
throw new NotSupportedException('like conditions is not supported by elasticsearch.'); |
||||
if (!isset($operands[0], $operands[1])) { |
||||
throw new Exception("Operator '$operator' requires two operands."); |
||||
} |
||||
|
||||
list($column, $values) = $operands; |
||||
|
||||
$values = (array)$values; |
||||
|
||||
if (empty($values)) { |
||||
return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0==1' : ''; |
||||
} |
||||
|
||||
if ($operator === 'LIKE' || $operator === 'NOT LIKE') { |
||||
$andor = ' AND '; |
||||
} else { |
||||
$andor = ' OR '; |
||||
$operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; |
||||
} |
||||
|
||||
if (strpos($column, '(') === false) { |
||||
$column = $this->db->quoteColumnName($column); |
||||
} |
||||
|
||||
$parts = array(); |
||||
foreach ($values as $value) { |
||||
$phName = self::PARAM_PREFIX . count($params); |
||||
$params[$phName] = $value; |
||||
$parts[] = "$column $operator $phName"; |
||||
} |
||||
|
||||
return implode($andor, $parts); |
||||
} |
||||
} |
@ -0,0 +1,127 @@
|
||||
Elasticsearch Query and ActiveRecord for Yii 2 |
||||
============================================== |
||||
|
||||
This extension provides the [elasticsearch](http://www.elasticsearch.org/) integration for the Yii2 framework. |
||||
It includes basic querying/search support and also implements the `ActiveRecord` pattern that allows you to store active |
||||
records in elasticsearch. |
||||
|
||||
To use this extension, you have to configure the Connection class in your application configuration: |
||||
|
||||
```php |
||||
return [ |
||||
//.... |
||||
'components' => [ |
||||
'elasticsearch' => [ |
||||
'class' => 'yii\elasticsearch\Connection', |
||||
'hosts' => [ |
||||
['http_address' => '127.0.0.1:9200'], |
||||
// configure more hosts if you have a cluster |
||||
], |
||||
], |
||||
] |
||||
]; |
||||
``` |
||||
|
||||
|
||||
Installation |
||||
------------ |
||||
|
||||
The preferred way to install this extension is through [composer](http://getcomposer.org/download/). |
||||
|
||||
Either run |
||||
|
||||
``` |
||||
php composer.phar require yiisoft/yii2-elasticsearch "*" |
||||
``` |
||||
|
||||
or add |
||||
|
||||
```json |
||||
"yiisoft/yii2-elasticsearch": "*" |
||||
``` |
||||
|
||||
to the require section of your composer.json. |
||||
|
||||
|
||||
Using the Query |
||||
--------------- |
||||
|
||||
TBD |
||||
|
||||
Using the ActiveRecord |
||||
---------------------- |
||||
|
||||
For general information on how to use yii's ActiveRecord please refer to the [guide](https://github.com/yiisoft/yii2/blob/master/docs/guide/active-record.md). |
||||
|
||||
For defining an elasticsearch ActiveRecord class your record class needs to extend from `yii\elasticsearch\ActiveRecord` and |
||||
implement at least the `attributes()` method to define the attributes of the record. |
||||
The primary key (the `_id` field in elasticsearch terms) is represented by `getId()` and `setId()` and can not be changed. |
||||
The primary key is not part of the attributes. |
||||
|
||||
primary key can be defined via [[primaryKey()]] which defaults to `id` if not specified. |
||||
The primaryKey needs to be part of the attributes so make sure you have an `id` attribute defined if you do |
||||
not specify your own primary key. |
||||
|
||||
The following is an example model called `Customer`: |
||||
|
||||
```php |
||||
class Customer extends \yii\elasticsearch\ActiveRecord |
||||
{ |
||||
/** |
||||
* @return array the list of attributes for this record |
||||
*/ |
||||
public function attributes() |
||||
{ |
||||
return ['id', 'name', 'address', 'registration_date']; |
||||
} |
||||
|
||||
/** |
||||
* @return ActiveRelation defines a relation to the Order record (can be in other database, e.g. redis or sql) |
||||
*/ |
||||
public function getOrders() |
||||
{ |
||||
return $this->hasMany(Order::className(), ['customer_id' => 'id'])->orderBy('id'); |
||||
} |
||||
|
||||
/** |
||||
* Defines a scope that modifies the `$query` to return only active(status = 1) customers |
||||
*/ |
||||
public static function active($query) |
||||
{ |
||||
$query->andWhere(array('status' => 1)); |
||||
} |
||||
} |
||||
``` |
||||
|
||||
You may override [[index()]] and [[type()]] to define the index and type this record represents. |
||||
|
||||
The general usage of elasticsearch ActiveRecord is very similar to the database ActiveRecord as described in the |
||||
[guide](https://github.com/yiisoft/yii2/blob/master/docs/guide/active-record.md). |
||||
It supports the same interface and features except the following limitations and additions(*!*): |
||||
|
||||
- As elasticsearch does not support SQL, the query API does not support `join()`, `groupBy()`, `having()` and `union()`. |
||||
Sorting, limit, offset and conditional where are all supported. |
||||
- `from()` does not select the tables, but the [index](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/glossary.html#glossary-index) |
||||
and [type](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/glossary.html#glossary-type) to query against. |
||||
- `select()` has been replaced with `fields()` which basically does the same but `fields` is more elasticsearch terminology. |
||||
It defines the fields to retrieve from a document. |
||||
- `via`-relations can not be defined via a table as there are not tables in elasticsearch. You can only define relations via other records. |
||||
- As elasticsearch is a data storage and search engine there is of course support added for search your records. |
||||
TBD ... |
||||
- It is also possible to define relations from elasticsearch ActiveRecords to normal ActiveRecord classes and vice versa. |
||||
|
||||
Elasticsearch separates primary key from attributes. You need to set the `id` property of the record to set its primary key. |
||||
|
||||
Usage example: |
||||
|
||||
```php |
||||
$customer = new Customer(); |
||||
$customer->id = 1; |
||||
$customer->attributes = ['name' => 'test']; |
||||
$customer->save(); |
||||
|
||||
$customer = Customer::get(1); // get a record by pk |
||||
$customers = Customer::get([1,2,3]); // get a records multiple by pk |
||||
$customer = Customer::find()->where(['name' => 'test'])->one(); // find by query |
||||
$customer = Customer::find()->active()->all(); // find all by query (using the `active` scope) |
||||
``` |
@ -0,0 +1,28 @@
|
||||
{ |
||||
"name": "yiisoft/yii2-elasticsearch", |
||||
"description": "Elasticsearch integration and ActiveRecord for the Yii framework", |
||||
"keywords": ["yii", "elasticsearch", "active-record", "search", "fulltext"], |
||||
"type": "yii2-extension", |
||||
"license": "BSD-3-Clause", |
||||
"support": { |
||||
"issues": "https://github.com/yiisoft/yii2/issues?labels=ext%3Aelasticsearch", |
||||
"forum": "http://www.yiiframework.com/forum/", |
||||
"wiki": "http://www.yiiframework.com/wiki/", |
||||
"irc": "irc://irc.freenode.net/yii", |
||||
"source": "https://github.com/yiisoft/yii2" |
||||
}, |
||||
"authors": [ |
||||
{ |
||||
"name": "Carsten Brandt", |
||||
"email": "mail@cebe.cc" |
||||
} |
||||
], |
||||
"require": { |
||||
"yiisoft/yii2": "*", |
||||
"ext-curl": "*" |
||||
}, |
||||
"autoload": { |
||||
"psr-0": { "yii\\elasticsearch\\": "" } |
||||
}, |
||||
"target-dir": "yii/elasticsearch" |
||||
} |
@ -0,0 +1,32 @@
|
||||
<?php |
||||
/** |
||||
* @link http://www.yiiframework.com/ |
||||
* @copyright Copyright (c) 2008 Yii Software LLC |
||||
* @license http://www.yiiframework.com/license/ |
||||
*/ |
||||
|
||||
namespace yiiunit\data\ar\elasticsearch; |
||||
|
||||
/** |
||||
* ActiveRecord is ... |
||||
* |
||||
* @author Qiang Xue <qiang.xue@gmail.com> |
||||
* @since 2.0 |
||||
*/ |
||||
class ActiveRecord extends \yii\elasticsearch\ActiveRecord |
||||
{ |
||||
public static $db; |
||||
|
||||
/** |
||||
* @return \yii\elasticsearch\Connection |
||||
*/ |
||||
public static function getDb() |
||||
{ |
||||
return self::$db; |
||||
} |
||||
|
||||
public static function index() |
||||
{ |
||||
return 'yiitest'; |
||||
} |
||||
} |
@ -0,0 +1,43 @@
|
||||
<?php |
||||
namespace yiiunit\data\ar\elasticsearch; |
||||
|
||||
use yiiunit\extensions\elasticsearch\ActiveRecordTest; |
||||
|
||||
/** |
||||
* Class Customer |
||||
* |
||||
* @property integer $id |
||||
* @property string $name |
||||
* @property string $email |
||||
* @property string $address |
||||
* @property integer $status |
||||
*/ |
||||
class Customer extends ActiveRecord |
||||
{ |
||||
const STATUS_ACTIVE = 1; |
||||
const STATUS_INACTIVE = 2; |
||||
|
||||
public $status2; |
||||
|
||||
public static function attributes() |
||||
{ |
||||
return ['name', 'email', 'address', 'status']; |
||||
} |
||||
|
||||
public function getOrders() |
||||
{ |
||||
return $this->hasMany(Order::className(), array('customer_id' => ActiveRecord::PRIMARY_KEY_NAME))->orderBy('create_time'); |
||||
} |
||||
|
||||
public static function active($query) |
||||
{ |
||||
$query->andWhere(array('status' => 1)); |
||||
} |
||||
|
||||
public function afterSave($insert) |
||||
{ |
||||
ActiveRecordTest::$afterSaveInsert = $insert; |
||||
ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord; |
||||
parent::afterSave($insert); |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
<?php |
||||
|
||||
namespace yiiunit\data\ar\elasticsearch; |
||||
|
||||
/** |
||||
* Class Item |
||||
* |
||||
* @property integer $id |
||||
* @property string $name |
||||
* @property integer $category_id |
||||
*/ |
||||
class Item extends ActiveRecord |
||||
{ |
||||
public static function attributes() |
||||
{ |
||||
return ['name', 'category_id']; |
||||
} |
||||
} |
@ -0,0 +1,68 @@
|
||||
<?php |
||||
|
||||
namespace yiiunit\data\ar\elasticsearch; |
||||
|
||||
/** |
||||
* Class Order |
||||
* |
||||
* @property integer $id |
||||
* @property integer $customer_id |
||||
* @property integer $create_time |
||||
* @property string $total |
||||
*/ |
||||
class Order extends ActiveRecord |
||||
{ |
||||
public static function attributes() |
||||
{ |
||||
return ['customer_id', 'create_time', 'total']; |
||||
} |
||||
|
||||
public function getCustomer() |
||||
{ |
||||
return $this->hasOne(Customer::className(), [ActiveRecord::PRIMARY_KEY_NAME => 'customer_id']); |
||||
} |
||||
|
||||
public function getOrderItems() |
||||
{ |
||||
return $this->hasMany(OrderItem::className(), ['order_id' => ActiveRecord::PRIMARY_KEY_NAME]); |
||||
} |
||||
|
||||
public function getItems() |
||||
{ |
||||
return $this->hasMany(Item::className(), [ActiveRecord::PRIMARY_KEY_NAME => 'item_id']) |
||||
->via('orderItems')->orderBy('id'); |
||||
} |
||||
|
||||
public function getItemsInOrder1() |
||||
{ |
||||
return $this->hasMany(Item::className(), ['id' => 'item_id']) |
||||
->via('orderItems', function ($q) { |
||||
$q->orderBy(['subtotal' => SORT_ASC]); |
||||
})->orderBy('name'); |
||||
} |
||||
|
||||
public function getItemsInOrder2() |
||||
{ |
||||
return $this->hasMany(Item::className(), ['id' => 'item_id']) |
||||
->via('orderItems', function ($q) { |
||||
$q->orderBy(['subtotal' => SORT_DESC]); |
||||
})->orderBy('name'); |
||||
} |
||||
|
||||
// public function getBooks() |
||||
// { |
||||
// return $this->hasMany('Item', [ActiveRecord::PRIMARY_KEY_NAME => 'item_id']) |
||||
// ->viaTable('tbl_order_item', ['order_id' => ActiveRecord::PRIMARY_KEY_NAME]) |
||||
// ->where(['category_id' => 1]); |
||||
// } |
||||
|
||||
public function beforeSave($insert) |
||||
{ |
||||
if (parent::beforeSave($insert)) { |
||||
// $this->create_time = time(); |
||||
return true; |
||||
} else { |
||||
return false; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,29 @@
|
||||
<?php |
||||
|
||||
namespace yiiunit\data\ar\elasticsearch; |
||||
|
||||
/** |
||||
* Class OrderItem |
||||
* |
||||
* @property integer $order_id |
||||
* @property integer $item_id |
||||
* @property integer $quantity |
||||
* @property string $subtotal |
||||
*/ |
||||
class OrderItem extends ActiveRecord |
||||
{ |
||||
public static function attributes() |
||||
{ |
||||
return ['order_id', 'item_id', 'quantity', 'subtotal']; |
||||
} |
||||
|
||||
public function getOrder() |
||||
{ |
||||
return $this->hasOne(Order::className(), [ActiveRecord::PRIMARY_KEY_NAME => 'order_id']); |
||||
} |
||||
|
||||
public function getItem() |
||||
{ |
||||
return $this->hasOne(Item::className(), [ActiveRecord::PRIMARY_KEY_NAME => 'item_id']); |
||||
} |
||||
} |
@ -0,0 +1,495 @@
|
||||
<?php |
||||
|
||||
namespace yiiunit\extensions\elasticsearch; |
||||
|
||||
use yii\elasticsearch\Connection; |
||||
use yii\helpers\Json; |
||||
use yiiunit\framework\ar\ActiveRecordTestTrait; |
||||
use yiiunit\data\ar\elasticsearch\ActiveRecord; |
||||
use yiiunit\data\ar\elasticsearch\Customer; |
||||
use yiiunit\data\ar\elasticsearch\OrderItem; |
||||
use yiiunit\data\ar\elasticsearch\Order; |
||||
use yiiunit\data\ar\elasticsearch\Item; |
||||
|
||||
/** |
||||
* @group elasticsearch |
||||
*/ |
||||
class ActiveRecordTest extends ElasticSearchTestCase |
||||
{ |
||||
use ActiveRecordTestTrait; |
||||
|
||||
public function callCustomerFind($q = null) { return Customer::find($q); } |
||||
public function callOrderFind($q = null) { return Order::find($q); } |
||||
public function callOrderItemFind($q = null) { return OrderItem::find($q); } |
||||
public function callItemFind($q = null) { return Item::find($q); } |
||||
|
||||
public function getCustomerClass() { return Customer::className(); } |
||||
public function getItemClass() { return Item::className(); } |
||||
public function getOrderClass() { return Order::className(); } |
||||
public function getOrderItemClass() { return OrderItem::className(); } |
||||
|
||||
/** |
||||
* can be overridden to do things after save() |
||||
*/ |
||||
public function afterSave() |
||||
{ |
||||
$this->getConnection()->createCommand()->flushIndex('yiitest'); |
||||
} |
||||
|
||||
public function setUp() |
||||
{ |
||||
parent::setUp(); |
||||
|
||||
/** @var Connection $db */ |
||||
$db = ActiveRecord::$db = $this->getConnection(); |
||||
|
||||
// delete index |
||||
if ($db->createCommand()->indexExists('yiitest')) { |
||||
$db->createCommand()->deleteIndex('yiitest'); |
||||
} |
||||
|
||||
$db->post(['yiitest'], [], Json::encode([ |
||||
'mappings' => [ |
||||
"item" => [ |
||||
"_source" => [ "enabled" => true ], |
||||
"properties" => [ |
||||
// allow proper sorting by name |
||||
"name" => ["type" => "string", "index" => "not_analyzed"], |
||||
] |
||||
] |
||||
], |
||||
])); |
||||
|
||||
$customer = new Customer(); |
||||
$customer->id = 1; |
||||
$customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1], false); |
||||
$customer->save(false); |
||||
$customer = new Customer(); |
||||
$customer->id = 2; |
||||
$customer->setAttributes(['email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1], false); |
||||
$customer->save(false); |
||||
$customer = new Customer(); |
||||
$customer->id = 3; |
||||
$customer->setAttributes(['email' => 'user3@example.com', 'name' => 'user3', 'address' => 'address3', 'status' => 2], false); |
||||
$customer->save(false); |
||||
|
||||
// INSERT INTO tbl_category (name) VALUES ('Books'); |
||||
// INSERT INTO tbl_category (name) VALUES ('Movies'); |
||||
|
||||
$item = new Item(); |
||||
$item->id = 1; |
||||
$item->setAttributes(['name' => 'Agile Web Application Development with Yii1.1 and PHP5', 'category_id' => 1], false); |
||||
$item->save(false); |
||||
$item = new Item(); |
||||
$item->id = 2; |
||||
$item->setAttributes(['name' => 'Yii 1.1 Application Development Cookbook', 'category_id' => 1], false); |
||||
$item->save(false); |
||||
$item = new Item(); |
||||
$item->id = 3; |
||||
$item->setAttributes(['name' => 'Ice Age', 'category_id' => 2], false); |
||||
$item->save(false); |
||||
$item = new Item(); |
||||
$item->id = 4; |
||||
$item->setAttributes(['name' => 'Toy Story', 'category_id' => 2], false); |
||||
$item->save(false); |
||||
$item = new Item(); |
||||
$item->id = 5; |
||||
$item->setAttributes(['name' => 'Cars', 'category_id' => 2], false); |
||||
$item->save(false); |
||||
|
||||
$order = new Order(); |
||||
$order->id = 1; |
||||
$order->setAttributes(['customer_id' => 1, 'create_time' => 1325282384, 'total' => 110.0], false); |
||||
$order->save(false); |
||||
$order = new Order(); |
||||
$order->id = 2; |
||||
$order->setAttributes(['customer_id' => 2, 'create_time' => 1325334482, 'total' => 33.0], false); |
||||
$order->save(false); |
||||
$order = new Order(); |
||||
$order->id = 3; |
||||
$order->setAttributes(['customer_id' => 2, 'create_time' => 1325502201, 'total' => 40.0], false); |
||||
$order->save(false); |
||||
|
||||
$orderItem = new OrderItem(); |
||||
$orderItem->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0], false); |
||||
$orderItem->save(false); |
||||
$orderItem = new OrderItem(); |
||||
$orderItem->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 2, 'subtotal' => 40.0], false); |
||||
$orderItem->save(false); |
||||
$orderItem = new OrderItem(); |
||||
$orderItem->setAttributes(['order_id' => 2, 'item_id' => 4, 'quantity' => 1, 'subtotal' => 10.0], false); |
||||
$orderItem->save(false); |
||||
$orderItem = new OrderItem(); |
||||
$orderItem->setAttributes(['order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 15.0], false); |
||||
$orderItem->save(false); |
||||
$orderItem = new OrderItem(); |
||||
$orderItem->setAttributes(['order_id' => 2, 'item_id' => 3, 'quantity' => 1, 'subtotal' => 8.0], false); |
||||
$orderItem->save(false); |
||||
$orderItem = new OrderItem(); |
||||
$orderItem->setAttributes(['order_id' => 3, 'item_id' => 2, 'quantity' => 1, 'subtotal' => 40.0], false); |
||||
$orderItem->save(false); |
||||
|
||||
$db->createCommand()->flushIndex('yiitest'); |
||||
} |
||||
|
||||
public function testSearch() |
||||
{ |
||||
$customers = $this->callCustomerFind()->search()['hits']; |
||||
$this->assertEquals(3, $customers['total']); |
||||
$this->assertEquals(3, count($customers['hits'])); |
||||
$this->assertTrue($customers['hits'][0] instanceof Customer); |
||||
$this->assertTrue($customers['hits'][1] instanceof Customer); |
||||
$this->assertTrue($customers['hits'][2] instanceof Customer); |
||||
|
||||
// limit vs. totalcount |
||||
$customers = $this->callCustomerFind()->limit(2)->search()['hits']; |
||||
$this->assertEquals(3, $customers['total']); |
||||
$this->assertEquals(2, count($customers['hits'])); |
||||
|
||||
// asArray |
||||
$result = $this->callCustomerFind()->asArray()->search()['hits']; |
||||
$this->assertEquals(3, $result['total']); |
||||
$customers = $result['hits']; |
||||
$this->assertEquals(3, count($customers)); |
||||
$this->assertArrayHasKey('id', $customers[0]); |
||||
$this->assertArrayHasKey('name', $customers[0]); |
||||
$this->assertArrayHasKey('email', $customers[0]); |
||||
$this->assertArrayHasKey('address', $customers[0]); |
||||
$this->assertArrayHasKey('status', $customers[0]); |
||||
$this->assertArrayHasKey('id', $customers[1]); |
||||
$this->assertArrayHasKey('name', $customers[1]); |
||||
$this->assertArrayHasKey('email', $customers[1]); |
||||
$this->assertArrayHasKey('address', $customers[1]); |
||||
$this->assertArrayHasKey('status', $customers[1]); |
||||
$this->assertArrayHasKey('id', $customers[2]); |
||||
$this->assertArrayHasKey('name', $customers[2]); |
||||
$this->assertArrayHasKey('email', $customers[2]); |
||||
$this->assertArrayHasKey('address', $customers[2]); |
||||
$this->assertArrayHasKey('status', $customers[2]); |
||||
|
||||
// TODO test asArray() + fields() + indexBy() |
||||
|
||||
// find by attributes |
||||
$result = $this->callCustomerFind()->where(['name' => 'user2'])->search()['hits']; |
||||
$customer = reset($result['hits']); |
||||
$this->assertTrue($customer instanceof Customer); |
||||
$this->assertEquals(2, $customer->id); |
||||
|
||||
// TODO test query() and filter() |
||||
} |
||||
|
||||
public function testSearchFacets() |
||||
{ |
||||
$result = $this->callCustomerFind()->addStatisticalFacet('status_stats', ['field' => 'status'])->search(); |
||||
$this->assertArrayHasKey('facets', $result); |
||||
$this->assertEquals(3, $result['facets']['status_stats']['count']); |
||||
$this->assertEquals(4, $result['facets']['status_stats']['total']); // sum of values |
||||
$this->assertEquals(1, $result['facets']['status_stats']['min']); |
||||
$this->assertEquals(2, $result['facets']['status_stats']['max']); |
||||
} |
||||
|
||||
public function testGetDb() |
||||
{ |
||||
$this->mockApplication(['components' => ['elasticsearch' => Connection::className()]]); |
||||
$this->assertInstanceOf(Connection::className(), ActiveRecord::getDb()); |
||||
} |
||||
|
||||
public function testGet() |
||||
{ |
||||
$this->assertInstanceOf(Customer::className(), Customer::get(1)); |
||||
$this->assertNull(Customer::get(5)); |
||||
} |
||||
|
||||
public function testMget() |
||||
{ |
||||
$this->assertEquals([], Customer::mget([])); |
||||
|
||||
$records = Customer::mget([1]); |
||||
$this->assertEquals(1, count($records)); |
||||
$this->assertInstanceOf(Customer::className(), reset($records)); |
||||
|
||||
$records = Customer::mget([5]); |
||||
$this->assertEquals(0, count($records)); |
||||
|
||||
$records = Customer::mget([1,3,5]); |
||||
$this->assertEquals(2, count($records)); |
||||
$this->assertInstanceOf(Customer::className(), $records[0]); |
||||
$this->assertInstanceOf(Customer::className(), $records[1]); |
||||
} |
||||
|
||||
public function testFindLazy() |
||||
{ |
||||
/** @var $customer Customer */ |
||||
$customer = Customer::find(2); |
||||
$orders = $customer->orders; |
||||
$this->assertEquals(2, count($orders)); |
||||
|
||||
$orders = $customer->getOrders()->where(['between', 'create_time', 1325334000, 1325400000])->all(); |
||||
$this->assertEquals(1, count($orders)); |
||||
$this->assertEquals(2, $orders[0]->id); |
||||
} |
||||
|
||||
public function testFindEagerViaRelation() |
||||
{ |
||||
// this test is currently failing randomly because of https://github.com/yiisoft/yii2/issues/1310 |
||||
$orders = Order::find()->with('items')->orderBy('create_time')->all(); |
||||
$this->assertEquals(3, count($orders)); |
||||
$order = $orders[0]; |
||||
$this->assertEquals(1, $order->id); |
||||
$this->assertEquals(2, count($order->items)); |
||||
$this->assertEquals(1, $order->items[0]->id); |
||||
$this->assertEquals(2, $order->items[1]->id); |
||||
} |
||||
|
||||
public function testInsertNoPk() |
||||
{ |
||||
$this->assertEquals([ActiveRecord::PRIMARY_KEY_NAME], Customer::primaryKey()); |
||||
$pkName = ActiveRecord::PRIMARY_KEY_NAME; |
||||
|
||||
$customer = new Customer; |
||||
$customer->email = 'user4@example.com'; |
||||
$customer->name = 'user4'; |
||||
$customer->address = 'address4'; |
||||
|
||||
$this->assertNull($customer->primaryKey); |
||||
$this->assertNull($customer->oldPrimaryKey); |
||||
$this->assertNull($customer->$pkName); |
||||
$this->assertTrue($customer->isNewRecord); |
||||
|
||||
$customer->save(); |
||||
|
||||
$this->assertNotNull($customer->primaryKey); |
||||
$this->assertNotNull($customer->oldPrimaryKey); |
||||
$this->assertNotNull($customer->$pkName); |
||||
$this->assertEquals($customer->primaryKey, $customer->oldPrimaryKey); |
||||
$this->assertEquals($customer->primaryKey, $customer->$pkName); |
||||
$this->assertFalse($customer->isNewRecord); |
||||
} |
||||
|
||||
public function testInsertPk() |
||||
{ |
||||
$pkName = ActiveRecord::PRIMARY_KEY_NAME; |
||||
|
||||
$customer = new Customer; |
||||
$customer->$pkName = 5; |
||||
$customer->email = 'user5@example.com'; |
||||
$customer->name = 'user5'; |
||||
$customer->address = 'address5'; |
||||
|
||||
$this->assertTrue($customer->isNewRecord); |
||||
|
||||
$customer->save(); |
||||
|
||||
$this->assertEquals(5, $customer->primaryKey); |
||||
$this->assertEquals(5, $customer->oldPrimaryKey); |
||||
$this->assertEquals(5, $customer->$pkName); |
||||
$this->assertFalse($customer->isNewRecord); |
||||
} |
||||
|
||||
public function testUpdatePk() |
||||
{ |
||||
$pkName = ActiveRecord::PRIMARY_KEY_NAME; |
||||
|
||||
$pk = [$pkName => 2]; |
||||
$orderItem = Order::find($pk); |
||||
$this->assertEquals(2, $orderItem->primaryKey); |
||||
$this->assertEquals(2, $orderItem->oldPrimaryKey); |
||||
$this->assertEquals(2, $orderItem->$pkName); |
||||
|
||||
$this->setExpectedException('yii\base\InvalidCallException'); |
||||
$orderItem->$pkName = 13; |
||||
$orderItem->save(); |
||||
} |
||||
|
||||
public function testFindLazyVia2() |
||||
{ |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
/** @var Order $order */ |
||||
$orderClass = $this->getOrderClass(); |
||||
$pkName = ActiveRecord::PRIMARY_KEY_NAME; |
||||
|
||||
$order = new $orderClass(); |
||||
$order->$pkName = 100; |
||||
$this->assertEquals([], $order->items); |
||||
} |
||||
|
||||
public function testUpdateCounters() |
||||
{ |
||||
// Update Counters is not supported by elasticsearch |
||||
// $this->setExpectedException('yii\base\NotSupportedException'); |
||||
// ActiveRecordTestTrait::testUpdateCounters(); |
||||
} |
||||
|
||||
/** |
||||
* Some PDO implementations(e.g. cubrid) do not support boolean values. |
||||
* Make sure this does not affect AR layer. |
||||
*/ |
||||
public function testBooleanAttribute() |
||||
{ |
||||
$db = $this->getConnection(); |
||||
$db->createCommand()->deleteIndex('yiitest'); |
||||
$db->post(['yiitest'], [], Json::encode([ |
||||
'mappings' => [ |
||||
"customer" => [ |
||||
"_source" => [ "enabled" => true ], |
||||
"properties" => [ |
||||
// this is for the boolean test |
||||
"status" => ["type" => "boolean"], |
||||
] |
||||
] |
||||
], |
||||
])); |
||||
|
||||
$customerClass = $this->getCustomerClass(); |
||||
$customer = new $customerClass(); |
||||
$customer->name = 'boolean customer'; |
||||
$customer->email = 'mail@example.com'; |
||||
$customer->status = true; |
||||
$customer->save(false); |
||||
|
||||
$customer->refresh(); |
||||
$this->assertEquals(true, $customer->status); |
||||
|
||||
$customer->status = false; |
||||
$customer->save(false); |
||||
|
||||
$customer->refresh(); |
||||
$this->assertEquals(false, $customer->status); |
||||
|
||||
$customer = new Customer(); |
||||
$customer->setAttributes(['email' => 'user2b@example.com', 'name' => 'user2b', 'status' => true], false); |
||||
$customer->save(false); |
||||
$customer = new Customer(); |
||||
$customer->setAttributes(['email' => 'user3b@example.com', 'name' => 'user3b', 'status' => false], false); |
||||
$customer->save(false); |
||||
$this->afterSave(); |
||||
|
||||
$customers = $this->callCustomerFind()->where(['status' => true])->all(); |
||||
$this->assertEquals(1, count($customers)); |
||||
|
||||
$customers = $this->callCustomerFind()->where(['status' => false])->all(); |
||||
$this->assertEquals(2, count($customers)); |
||||
} |
||||
|
||||
public function testfindAsArrayFields() |
||||
{ |
||||
$customerClass = $this->getCustomerClass(); |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
// indexBy + asArray |
||||
$customers = $this->callCustomerFind()->asArray()->fields(['id', 'name'])->all(); |
||||
$this->assertEquals(3, count($customers)); |
||||
$this->assertArrayHasKey('id', $customers[0]); |
||||
$this->assertArrayHasKey('name', $customers[0]); |
||||
$this->assertArrayNotHasKey('email', $customers[0]); |
||||
$this->assertArrayNotHasKey('address', $customers[0]); |
||||
$this->assertArrayNotHasKey('status', $customers[0]); |
||||
$this->assertArrayHasKey('id', $customers[1]); |
||||
$this->assertArrayHasKey('name', $customers[1]); |
||||
$this->assertArrayNotHasKey('email', $customers[1]); |
||||
$this->assertArrayNotHasKey('address', $customers[1]); |
||||
$this->assertArrayNotHasKey('status', $customers[1]); |
||||
$this->assertArrayHasKey('id', $customers[2]); |
||||
$this->assertArrayHasKey('name', $customers[2]); |
||||
$this->assertArrayNotHasKey('email', $customers[2]); |
||||
$this->assertArrayNotHasKey('address', $customers[2]); |
||||
$this->assertArrayNotHasKey('status', $customers[2]); |
||||
} |
||||
|
||||
public function testfindIndexByFields() |
||||
{ |
||||
$customerClass = $this->getCustomerClass(); |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
// indexBy + asArray |
||||
$customers = $this->callCustomerFind()->indexBy('name')->fields('id', 'name')->all(); |
||||
$this->assertEquals(3, count($customers)); |
||||
$this->assertTrue($customers['user1'] instanceof $customerClass); |
||||
$this->assertTrue($customers['user2'] instanceof $customerClass); |
||||
$this->assertTrue($customers['user3'] instanceof $customerClass); |
||||
$this->assertNotNull($customers['user1']->id); |
||||
$this->assertNotNull($customers['user1']->name); |
||||
$this->assertNull($customers['user1']->email); |
||||
$this->assertNull($customers['user1']->address); |
||||
$this->assertNull($customers['user1']->status); |
||||
$this->assertNotNull($customers['user2']->id); |
||||
$this->assertNotNull($customers['user2']->name); |
||||
$this->assertNull($customers['user2']->email); |
||||
$this->assertNull($customers['user2']->address); |
||||
$this->assertNull($customers['user2']->status); |
||||
$this->assertNotNull($customers['user3']->id); |
||||
$this->assertNotNull($customers['user3']->name); |
||||
$this->assertNull($customers['user3']->email); |
||||
$this->assertNull($customers['user3']->address); |
||||
$this->assertNull($customers['user3']->status); |
||||
|
||||
// indexBy callable + asArray |
||||
$customers = $this->callCustomerFind()->indexBy(function ($customer) { |
||||
return $customer->id . '-' . $customer->name; |
||||
})->fields('id', 'name')->all(); |
||||
$this->assertEquals(3, count($customers)); |
||||
$this->assertTrue($customers['1-user1'] instanceof $customerClass); |
||||
$this->assertTrue($customers['2-user2'] instanceof $customerClass); |
||||
$this->assertTrue($customers['3-user3'] instanceof $customerClass); |
||||
$this->assertNotNull($customers['1-user1']->id); |
||||
$this->assertNotNull($customers['1-user1']->name); |
||||
$this->assertNull($customers['1-user1']->email); |
||||
$this->assertNull($customers['1-user1']->address); |
||||
$this->assertNull($customers['1-user1']->status); |
||||
$this->assertNotNull($customers['2-user2']->id); |
||||
$this->assertNotNull($customers['2-user2']->name); |
||||
$this->assertNull($customers['2-user2']->email); |
||||
$this->assertNull($customers['2-user2']->address); |
||||
$this->assertNull($customers['2-user2']->status); |
||||
$this->assertNotNull($customers['3-user3']->id); |
||||
$this->assertNotNull($customers['3-user3']->name); |
||||
$this->assertNull($customers['3-user3']->email); |
||||
$this->assertNull($customers['3-user3']->address); |
||||
$this->assertNull($customers['3-user3']->status); |
||||
} |
||||
|
||||
public function testfindIndexByAsArrayFields() |
||||
{ |
||||
$customerClass = $this->getCustomerClass(); |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
// indexBy + asArray |
||||
$customers = $this->callCustomerFind()->indexBy('name')->asArray()->fields('id', 'name')->all(); |
||||
$this->assertEquals(3, count($customers)); |
||||
$this->assertArrayHasKey('id', $customers['user1']); |
||||
$this->assertArrayHasKey('name', $customers['user1']); |
||||
$this->assertArrayNotHasKey('email', $customers['user1']); |
||||
$this->assertArrayNotHasKey('address', $customers['user1']); |
||||
$this->assertArrayNotHasKey('status', $customers['user1']); |
||||
$this->assertArrayHasKey('id', $customers['user2']); |
||||
$this->assertArrayHasKey('name', $customers['user2']); |
||||
$this->assertArrayNotHasKey('email', $customers['user2']); |
||||
$this->assertArrayNotHasKey('address', $customers['user2']); |
||||
$this->assertArrayNotHasKey('status', $customers['user2']); |
||||
$this->assertArrayHasKey('id', $customers['user3']); |
||||
$this->assertArrayHasKey('name', $customers['user3']); |
||||
$this->assertArrayNotHasKey('email', $customers['user3']); |
||||
$this->assertArrayNotHasKey('address', $customers['user3']); |
||||
$this->assertArrayNotHasKey('status', $customers['user3']); |
||||
|
||||
// indexBy callable + asArray |
||||
$customers = $this->callCustomerFind()->indexBy(function ($customer) { |
||||
return $customer['id'] . '-' . $customer['name']; |
||||
})->asArray()->fields('id', 'name')->all(); |
||||
$this->assertEquals(3, count($customers)); |
||||
$this->assertArrayHasKey('id', $customers['1-user1']); |
||||
$this->assertArrayHasKey('name', $customers['1-user1']); |
||||
$this->assertArrayNotHasKey('email', $customers['1-user1']); |
||||
$this->assertArrayNotHasKey('address', $customers['1-user1']); |
||||
$this->assertArrayNotHasKey('status', $customers['1-user1']); |
||||
$this->assertArrayHasKey('id', $customers['2-user2']); |
||||
$this->assertArrayHasKey('name', $customers['2-user2']); |
||||
$this->assertArrayNotHasKey('email', $customers['2-user2']); |
||||
$this->assertArrayNotHasKey('address', $customers['2-user2']); |
||||
$this->assertArrayNotHasKey('status', $customers['2-user2']); |
||||
$this->assertArrayHasKey('id', $customers['3-user3']); |
||||
$this->assertArrayHasKey('name', $customers['3-user3']); |
||||
$this->assertArrayNotHasKey('email', $customers['3-user3']); |
||||
$this->assertArrayNotHasKey('address', $customers['3-user3']); |
||||
$this->assertArrayNotHasKey('status', $customers['3-user3']); |
||||
} |
||||
|
||||
|
||||
} |
@ -0,0 +1,28 @@
|
||||
<?php |
||||
|
||||
namespace yiiunit\extensions\elasticsearch; |
||||
|
||||
use yii\elasticsearch\Connection; |
||||
|
||||
/** |
||||
* @group elasticsearch |
||||
*/ |
||||
class ElasticSearchConnectionTest extends ElasticSearchTestCase |
||||
{ |
||||
public function testOpen() |
||||
{ |
||||
$connection = new Connection(); |
||||
$connection->autodetectCluster; |
||||
$connection->nodes = [ |
||||
['http_address' => 'inet[/127.0.0.1:9200]'], |
||||
]; |
||||
$this->assertNull($connection->activeNode); |
||||
$connection->open(); |
||||
$this->assertNotNull($connection->activeNode); |
||||
$this->assertArrayHasKey('name', reset($connection->nodes)); |
||||
$this->assertArrayHasKey('hostname', reset($connection->nodes)); |
||||
$this->assertArrayHasKey('version', reset($connection->nodes)); |
||||
$this->assertArrayHasKey('http_address', reset($connection->nodes)); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,51 @@
|
||||
<?php |
||||
|
||||
namespace yiiunit\extensions\elasticsearch; |
||||
|
||||
use Yii; |
||||
use yii\elasticsearch\Connection; |
||||
use yiiunit\TestCase; |
||||
|
||||
Yii::setAlias('@yii/elasticsearch', __DIR__ . '/../../../../extensions/elasticsearch'); |
||||
|
||||
/** |
||||
* ElasticSearchTestCase is the base class for all elasticsearch related test cases |
||||
*/ |
||||
class ElasticSearchTestCase extends TestCase |
||||
{ |
||||
protected function setUp() |
||||
{ |
||||
$this->mockApplication(); |
||||
|
||||
$databases = $this->getParam('databases'); |
||||
$params = isset($databases['elasticsearch']) ? $databases['elasticsearch'] : null; |
||||
if ($params === null || !isset($params['dsn'])) { |
||||
$this->markTestSkipped('No elasticsearch server connection configured.'); |
||||
} |
||||
$dsn = explode('/', $params['dsn']); |
||||
$host = $dsn[2]; |
||||
if (strpos($host, ':')===false) { |
||||
$host .= ':9200'; |
||||
} |
||||
if(!@stream_socket_client($host, $errorNumber, $errorDescription, 0.5)) { |
||||
$this->markTestSkipped('No elasticsearch server running at ' . $params['dsn'] . ' : ' . $errorNumber . ' - ' . $errorDescription); |
||||
} |
||||
|
||||
parent::setUp(); |
||||
} |
||||
|
||||
/** |
||||
* @param bool $reset whether to clean up the test database |
||||
* @return Connection |
||||
*/ |
||||
public function getConnection($reset = true) |
||||
{ |
||||
$databases = $this->getParam('databases'); |
||||
$params = isset($databases['elasticsearch']) ? $databases['elasticsearch'] : array(); |
||||
$db = new Connection(); |
||||
if ($reset) { |
||||
$db->open(); |
||||
} |
||||
return $db; |
||||
} |
||||
} |
@ -0,0 +1,185 @@
|
||||
<?php |
||||
|
||||
namespace yiiunit\extensions\elasticsearch; |
||||
|
||||
use yii\elasticsearch\Query; |
||||
|
||||
/** |
||||
* @group elasticsearch |
||||
*/ |
||||
class QueryTest extends ElasticSearchTestCase |
||||
{ |
||||
protected function setUp() |
||||
{ |
||||
parent::setUp(); |
||||
|
||||
$command = $this->getConnection()->createCommand(); |
||||
|
||||
$command->deleteAllIndexes(); |
||||
|
||||
$command->insert('test', 'user', ['name' => 'user1', 'email' => 'user1@example.com', 'status' => 1], 1); |
||||
$command->insert('test', 'user', ['name' => 'user2', 'email' => 'user2@example.com', 'status' => 1], 2); |
||||
$command->insert('test', 'user', ['name' => 'user3', 'email' => 'user3@example.com', 'status' => 2], 3); |
||||
$command->insert('test', 'user', ['name' => 'user4', 'email' => 'user4@example.com', 'status' => 1], 4); |
||||
|
||||
$command->flushIndex(); |
||||
} |
||||
|
||||
public function testFields() |
||||
{ |
||||
$query = new Query; |
||||
$query->from('test', 'user'); |
||||
|
||||
$query->fields(['name', 'status']); |
||||
$this->assertEquals(['name', 'status'], $query->fields); |
||||
|
||||
$query->fields('name', 'status'); |
||||
$this->assertEquals(['name', 'status'], $query->fields); |
||||
|
||||
$result = $query->one($this->getConnection()); |
||||
$this->assertEquals(2, count($result['_source'])); |
||||
$this->assertArrayHasKey('status', $result['_source']); |
||||
$this->assertArrayHasKey('name', $result['_source']); |
||||
$this->assertArrayHasKey('_id', $result); |
||||
|
||||
$query->fields([]); |
||||
$this->assertEquals([], $query->fields); |
||||
|
||||
$result = $query->one($this->getConnection()); |
||||
$this->assertEquals([], $result['_source']); |
||||
$this->assertArrayHasKey('_id', $result); |
||||
|
||||
$query->fields(null); |
||||
$this->assertNull($query->fields); |
||||
|
||||
$result = $query->one($this->getConnection()); |
||||
$this->assertEquals(3, count($result['_source'])); |
||||
$this->assertArrayHasKey('status', $result['_source']); |
||||
$this->assertArrayHasKey('email', $result['_source']); |
||||
$this->assertArrayHasKey('name', $result['_source']); |
||||
$this->assertArrayHasKey('_id', $result); |
||||
} |
||||
|
||||
public function testOne() |
||||
{ |
||||
$query = new Query; |
||||
$query->from('test', 'user'); |
||||
|
||||
$result = $query->one($this->getConnection()); |
||||
$this->assertEquals(3, count($result['_source'])); |
||||
$this->assertArrayHasKey('status', $result['_source']); |
||||
$this->assertArrayHasKey('email', $result['_source']); |
||||
$this->assertArrayHasKey('name', $result['_source']); |
||||
$this->assertArrayHasKey('_id', $result); |
||||
|
||||
$result = $query->where(['name' => 'user1'])->one($this->getConnection()); |
||||
$this->assertEquals(3, count($result['_source'])); |
||||
$this->assertArrayHasKey('status', $result['_source']); |
||||
$this->assertArrayHasKey('email', $result['_source']); |
||||
$this->assertArrayHasKey('name', $result['_source']); |
||||
$this->assertArrayHasKey('_id', $result); |
||||
$this->assertEquals(1, $result['_id']); |
||||
|
||||
$result = $query->where(['name' => 'user5'])->one($this->getConnection()); |
||||
$this->assertFalse($result); |
||||
} |
||||
|
||||
public function testAll() |
||||
{ |
||||
$query = new Query; |
||||
$query->from('test', 'user'); |
||||
|
||||
$results = $query->all($this->getConnection()); |
||||
$this->assertEquals(4, count($results)); |
||||
$result = reset($results); |
||||
$this->assertEquals(3, count($result['_source'])); |
||||
$this->assertArrayHasKey('status', $result['_source']); |
||||
$this->assertArrayHasKey('email', $result['_source']); |
||||
$this->assertArrayHasKey('name', $result['_source']); |
||||
$this->assertArrayHasKey('_id', $result); |
||||
|
||||
$query = new Query; |
||||
$query->from('test', 'user'); |
||||
|
||||
$results = $query->where(['name' => 'user1'])->all($this->getConnection()); |
||||
$this->assertEquals(1, count($results)); |
||||
$result = reset($results); |
||||
$this->assertEquals(3, count($result['_source'])); |
||||
$this->assertArrayHasKey('status', $result['_source']); |
||||
$this->assertArrayHasKey('email', $result['_source']); |
||||
$this->assertArrayHasKey('name', $result['_source']); |
||||
$this->assertArrayHasKey('_id', $result); |
||||
$this->assertEquals(1, $result['_id']); |
||||
|
||||
// indexBy |
||||
$query = new Query; |
||||
$query->from('test', 'user'); |
||||
|
||||
$results = $query->indexBy('name')->all($this->getConnection()); |
||||
$this->assertEquals(4, count($results)); |
||||
ksort($results); |
||||
$this->assertEquals(['user1', 'user2', 'user3', 'user4'], array_keys($results)); |
||||
} |
||||
|
||||
public function testScalar() |
||||
{ |
||||
$query = new Query; |
||||
$query->from('test', 'user'); |
||||
|
||||
$result = $query->where(['name' => 'user1'])->scalar('name', $this->getConnection()); |
||||
$this->assertEquals('user1', $result); |
||||
$result = $query->where(['name' => 'user1'])->scalar('noname', $this->getConnection()); |
||||
$this->assertNull($result); |
||||
$result = $query->where(['name' => 'user5'])->scalar('name', $this->getConnection()); |
||||
$this->assertNull($result); |
||||
} |
||||
|
||||
public function testColumn() |
||||
{ |
||||
$query = new Query; |
||||
$query->from('test', 'user'); |
||||
|
||||
$result = $query->orderBy(['name' => SORT_ASC])->column('name', $this->getConnection()); |
||||
$this->assertEquals(['user1', 'user2', 'user3', 'user4'], $result); |
||||
$result = $query->column('noname', $this->getConnection()); |
||||
$this->assertEquals([null, null, null, null], $result); |
||||
$result = $query->where(['name' => 'user5'])->scalar('name', $this->getConnection()); |
||||
$this->assertNull($result); |
||||
|
||||
} |
||||
|
||||
// TODO test facets |
||||
|
||||
// TODO test complex where() every edge of QueryBuilder |
||||
|
||||
public function testOrder() |
||||
{ |
||||
$query = new Query; |
||||
$query->orderBy('team'); |
||||
$this->assertEquals(['team' => SORT_ASC], $query->orderBy); |
||||
|
||||
$query->addOrderBy('company'); |
||||
$this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->orderBy); |
||||
|
||||
$query->addOrderBy('age'); |
||||
$this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->orderBy); |
||||
|
||||
$query->addOrderBy(['age' => SORT_DESC]); |
||||
$this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->orderBy); |
||||
|
||||
$query->addOrderBy('age ASC, company DESC'); |
||||
$this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->orderBy); |
||||
} |
||||
|
||||
public function testLimitOffset() |
||||
{ |
||||
$query = new Query; |
||||
$query->limit(10)->offset(5); |
||||
$this->assertEquals(10, $query->limit); |
||||
$this->assertEquals(5, $query->offset); |
||||
} |
||||
|
||||
public function testUnion() |
||||
{ |
||||
} |
||||
} |
@ -0,0 +1,759 @@
|
||||
<?php |
||||
/** |
||||
* |
||||
* |
||||
* @author Carsten Brandt <mail@cebe.cc> |
||||
*/ |
||||
|
||||
namespace yiiunit\framework\ar; |
||||
|
||||
use yii\db\ActiveQueryInterface; |
||||
use yiiunit\TestCase; |
||||
use yiiunit\data\ar\Customer; |
||||
use yiiunit\data\ar\Order; |
||||
|
||||
/** |
||||
* This trait provides unit tests shared by the differen AR implementations |
||||
* |
||||
* @var TestCase $this |
||||
*/ |
||||
trait ActiveRecordTestTrait |
||||
{ |
||||
/** |
||||
* This method should call Customer::find($q) |
||||
* @param $q |
||||
* @return mixed |
||||
*/ |
||||
public abstract function callCustomerFind($q = null); |
||||
|
||||
/** |
||||
* This method should call Order::find($q) |
||||
* @param $q |
||||
* @return mixed |
||||
*/ |
||||
public abstract function callOrderFind($q = null); |
||||
|
||||
/** |
||||
* This method should call OrderItem::find($q) |
||||
* @param $q |
||||
* @return mixed |
||||
*/ |
||||
public abstract function callOrderItemFind($q = null); |
||||
|
||||
/** |
||||
* This method should call Item::find($q) |
||||
* @param $q |
||||
* @return mixed |
||||
*/ |
||||
public abstract function callItemFind($q = null); |
||||
|
||||
/** |
||||
* This method should return the classname of Customer class |
||||
* @return string |
||||
*/ |
||||
public abstract function getCustomerClass(); |
||||
|
||||
/** |
||||
* This method should return the classname of Order class |
||||
* @return string |
||||
*/ |
||||
public abstract function getOrderClass(); |
||||
|
||||
/** |
||||
* This method should return the classname of OrderItem class |
||||
* @return string |
||||
*/ |
||||
public abstract function getOrderItemClass(); |
||||
|
||||
/** |
||||
* This method should return the classname of Item class |
||||
* @return string |
||||
*/ |
||||
public abstract function getItemClass(); |
||||
|
||||
/** |
||||
* can be overridden to do things after save() |
||||
*/ |
||||
public function afterSave() |
||||
{ |
||||
} |
||||
|
||||
|
||||
public function testFind() |
||||
{ |
||||
$customerClass = $this->getCustomerClass(); |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
// find one |
||||
$result = $this->callCustomerFind(); |
||||
$this->assertTrue($result instanceof ActiveQueryInterface); |
||||
$customer = $result->one(); |
||||
$this->assertTrue($customer instanceof $customerClass); |
||||
|
||||
// find all |
||||
$customers = $this->callCustomerFind()->all(); |
||||
$this->assertEquals(3, count($customers)); |
||||
$this->assertTrue($customers[0] instanceof $customerClass); |
||||
$this->assertTrue($customers[1] instanceof $customerClass); |
||||
$this->assertTrue($customers[2] instanceof $customerClass); |
||||
|
||||
// find all asArray |
||||
$customers = $this->callCustomerFind()->asArray()->all(); |
||||
$this->assertEquals(3, count($customers)); |
||||
$this->assertArrayHasKey('id', $customers[0]); |
||||
$this->assertArrayHasKey('name', $customers[0]); |
||||
$this->assertArrayHasKey('email', $customers[0]); |
||||
$this->assertArrayHasKey('address', $customers[0]); |
||||
$this->assertArrayHasKey('status', $customers[0]); |
||||
$this->assertArrayHasKey('id', $customers[1]); |
||||
$this->assertArrayHasKey('name', $customers[1]); |
||||
$this->assertArrayHasKey('email', $customers[1]); |
||||
$this->assertArrayHasKey('address', $customers[1]); |
||||
$this->assertArrayHasKey('status', $customers[1]); |
||||
$this->assertArrayHasKey('id', $customers[2]); |
||||
$this->assertArrayHasKey('name', $customers[2]); |
||||
$this->assertArrayHasKey('email', $customers[2]); |
||||
$this->assertArrayHasKey('address', $customers[2]); |
||||
$this->assertArrayHasKey('status', $customers[2]); |
||||
|
||||
// find by a single primary key |
||||
$customer = $this->callCustomerFind(2); |
||||
$this->assertTrue($customer instanceof $customerClass); |
||||
$this->assertEquals('user2', $customer->name); |
||||
$customer = $this->callCustomerFind(5); |
||||
$this->assertNull($customer); |
||||
$customer = $this->callCustomerFind(['id' => [5, 6, 1]]); |
||||
$this->assertEquals(1, count($customer)); |
||||
$customer = $this->callCustomerFind()->where(['id' => [5, 6, 1]])->one(); |
||||
$this->assertNotNull($customer); |
||||
|
||||
// find by column values |
||||
$customer = $this->callCustomerFind(['id' => 2, 'name' => 'user2']); |
||||
$this->assertTrue($customer instanceof $customerClass); |
||||
$this->assertEquals('user2', $customer->name); |
||||
$customer = $this->callCustomerFind(['id' => 2, 'name' => 'user1']); |
||||
$this->assertNull($customer); |
||||
$customer = $this->callCustomerFind(['id' => 5]); |
||||
$this->assertNull($customer); |
||||
$customer = $this->callCustomerFind(['name' => 'user5']); |
||||
$this->assertNull($customer); |
||||
|
||||
// find by attributes |
||||
$customer = $this->callCustomerFind()->where(['name' => 'user2'])->one(); |
||||
$this->assertTrue($customer instanceof $customerClass); |
||||
$this->assertEquals(2, $customer->id); |
||||
|
||||
// scope |
||||
$this->assertEquals(2, count($this->callCustomerFind()->active()->all())); |
||||
$this->assertEquals(2, $this->callCustomerFind()->active()->count()); |
||||
|
||||
// asArray |
||||
$customer = $this->callCustomerFind()->where(['id' => 2])->asArray()->one(); |
||||
$this->assertEquals([ |
||||
'id' => '2', |
||||
'email' => 'user2@example.com', |
||||
'name' => 'user2', |
||||
'address' => 'address2', |
||||
'status' => '1', |
||||
], $customer); |
||||
} |
||||
|
||||
public function testFindScalar() |
||||
{ |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
// query scalar |
||||
$customerName = $this->callCustomerFind()->where(['id' => 2])->scalar('name'); |
||||
$this->assertEquals('user2', $customerName); |
||||
$customerName = $this->callCustomerFind()->where(['status' => 2])->scalar('name'); |
||||
$this->assertEquals('user3', $customerName); |
||||
$customerName = $this->callCustomerFind()->where(['status' => 2])->scalar('noname'); |
||||
$this->assertNull($customerName); |
||||
$customerId = $this->callCustomerFind()->where(['status' => 2])->scalar('id'); |
||||
$this->assertEquals(3, $customerId); |
||||
} |
||||
|
||||
public function testFindColumn() |
||||
{ |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
$this->assertEquals(['user1', 'user2', 'user3'], $this->callCustomerFind()->orderBy(['name' => SORT_ASC])->column('name')); |
||||
$this->assertEquals(['user3', 'user2', 'user1'], $this->callCustomerFind()->orderBy(['name' => SORT_DESC])->column('name')); |
||||
} |
||||
|
||||
public function testfindIndexBy() |
||||
{ |
||||
$customerClass = $this->getCustomerClass(); |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
// indexBy |
||||
$customers = $this->callCustomerFind()->indexBy('name')->orderBy('id')->all(); |
||||
$this->assertEquals(3, count($customers)); |
||||
$this->assertTrue($customers['user1'] instanceof $customerClass); |
||||
$this->assertTrue($customers['user2'] instanceof $customerClass); |
||||
$this->assertTrue($customers['user3'] instanceof $customerClass); |
||||
|
||||
// indexBy callable |
||||
$customers = $this->callCustomerFind()->indexBy(function ($customer) { |
||||
return $customer->id . '-' . $customer->name; |
||||
})->orderBy('id')->all(); |
||||
$this->assertEquals(3, count($customers)); |
||||
$this->assertTrue($customers['1-user1'] instanceof $customerClass); |
||||
$this->assertTrue($customers['2-user2'] instanceof $customerClass); |
||||
$this->assertTrue($customers['3-user3'] instanceof $customerClass); |
||||
} |
||||
|
||||
public function testfindIndexByAsArray() |
||||
{ |
||||
$customerClass = $this->getCustomerClass(); |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
// indexBy + asArray |
||||
$customers = $this->callCustomerFind()->asArray()->indexBy('name')->all(); |
||||
$this->assertEquals(3, count($customers)); |
||||
$this->assertArrayHasKey('id', $customers['user1']); |
||||
$this->assertArrayHasKey('name', $customers['user1']); |
||||
$this->assertArrayHasKey('email', $customers['user1']); |
||||
$this->assertArrayHasKey('address', $customers['user1']); |
||||
$this->assertArrayHasKey('status', $customers['user1']); |
||||
$this->assertArrayHasKey('id', $customers['user2']); |
||||
$this->assertArrayHasKey('name', $customers['user2']); |
||||
$this->assertArrayHasKey('email', $customers['user2']); |
||||
$this->assertArrayHasKey('address', $customers['user2']); |
||||
$this->assertArrayHasKey('status', $customers['user2']); |
||||
$this->assertArrayHasKey('id', $customers['user3']); |
||||
$this->assertArrayHasKey('name', $customers['user3']); |
||||
$this->assertArrayHasKey('email', $customers['user3']); |
||||
$this->assertArrayHasKey('address', $customers['user3']); |
||||
$this->assertArrayHasKey('status', $customers['user3']); |
||||
|
||||
// indexBy callable + asArray |
||||
$customers = $this->callCustomerFind()->indexBy(function ($customer) { |
||||
return $customer['id'] . '-' . $customer['name']; |
||||
})->asArray()->all(); |
||||
$this->assertEquals(3, count($customers)); |
||||
$this->assertArrayHasKey('id', $customers['1-user1']); |
||||
$this->assertArrayHasKey('name', $customers['1-user1']); |
||||
$this->assertArrayHasKey('email', $customers['1-user1']); |
||||
$this->assertArrayHasKey('address', $customers['1-user1']); |
||||
$this->assertArrayHasKey('status', $customers['1-user1']); |
||||
$this->assertArrayHasKey('id', $customers['2-user2']); |
||||
$this->assertArrayHasKey('name', $customers['2-user2']); |
||||
$this->assertArrayHasKey('email', $customers['2-user2']); |
||||
$this->assertArrayHasKey('address', $customers['2-user2']); |
||||
$this->assertArrayHasKey('status', $customers['2-user2']); |
||||
$this->assertArrayHasKey('id', $customers['3-user3']); |
||||
$this->assertArrayHasKey('name', $customers['3-user3']); |
||||
$this->assertArrayHasKey('email', $customers['3-user3']); |
||||
$this->assertArrayHasKey('address', $customers['3-user3']); |
||||
$this->assertArrayHasKey('status', $customers['3-user3']); |
||||
} |
||||
|
||||
public function testRefresh() |
||||
{ |
||||
$customerClass = $this->getCustomerClass(); |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
$customer = new $customerClass(); |
||||
$this->assertFalse($customer->refresh()); |
||||
|
||||
$customer = $this->callCustomerFind(1); |
||||
$customer->name = 'to be refreshed'; |
||||
$this->assertTrue($customer->refresh()); |
||||
$this->assertEquals('user1', $customer->name); |
||||
} |
||||
|
||||
public function testEquals() |
||||
{ |
||||
$customerClass = $this->getCustomerClass(); |
||||
$itemClass = $this->getItemClass(); |
||||
|
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
$customerA = new $customerClass(); |
||||
$customerB = new $customerClass(); |
||||
$this->assertFalse($customerA->equals($customerB)); |
||||
|
||||
$customerA = new $customerClass(); |
||||
$customerB = new $itemClass(); |
||||
$this->assertFalse($customerA->equals($customerB)); |
||||
|
||||
$customerA = $this->callCustomerFind(1); |
||||
$customerB = $this->callCustomerFind(2); |
||||
$this->assertFalse($customerA->equals($customerB)); |
||||
|
||||
$customerB = $this->callCustomerFind(1); |
||||
$this->assertTrue($customerA->equals($customerB)); |
||||
|
||||
$customerA = $this->callCustomerFind(1); |
||||
$customerB = $this->callItemFind(1); |
||||
$this->assertFalse($customerA->equals($customerB)); |
||||
} |
||||
|
||||
public function testFindCount() |
||||
{ |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
$this->assertEquals(3, $this->callCustomerFind()->count()); |
||||
// TODO should limit have effect on count() |
||||
// $this->assertEquals(1, $this->callCustomerFind()->limit(1)->count()); |
||||
// $this->assertEquals(2, $this->callCustomerFind()->limit(2)->count()); |
||||
// $this->assertEquals(1, $this->callCustomerFind()->offset(2)->limit(2)->count()); |
||||
} |
||||
|
||||
public function testFindLimit() |
||||
{ |
||||
if (getenv('TRAVIS') == 'true' && $this instanceof \yiiunit\extensions\elasticsearch\ActiveRecordTest) { |
||||
// https://github.com/yiisoft/yii2/issues/1317 |
||||
$this->markTestSkipped('This test is unreproduceable failing on travis-ci, locally it is passing.'); |
||||
} |
||||
|
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
// all() |
||||
$customers = $this->callCustomerFind()->all(); |
||||
$this->assertEquals(3, count($customers)); |
||||
|
||||
$customers = $this->callCustomerFind()->orderBy('id')->limit(1)->all(); |
||||
$this->assertEquals(1, count($customers)); |
||||
$this->assertEquals('user1', $customers[0]->name); |
||||
|
||||
$customers = $this->callCustomerFind()->orderBy('id')->limit(1)->offset(1)->all(); |
||||
$this->assertEquals(1, count($customers)); |
||||
$this->assertEquals('user2', $customers[0]->name); |
||||
|
||||
$customers = $this->callCustomerFind()->orderBy('id')->limit(1)->offset(2)->all(); |
||||
$this->assertEquals(1, count($customers)); |
||||
$this->assertEquals('user3', $customers[0]->name); |
||||
|
||||
$customers = $this->callCustomerFind()->orderBy('id')->limit(2)->offset(1)->all(); |
||||
$this->assertEquals(2, count($customers)); |
||||
$this->assertEquals('user2', $customers[0]->name); |
||||
$this->assertEquals('user3', $customers[1]->name); |
||||
|
||||
$customers = $this->callCustomerFind()->limit(2)->offset(3)->all(); |
||||
$this->assertEquals(0, count($customers)); |
||||
|
||||
// one() |
||||
$customer = $this->callCustomerFind()->orderBy('id')->one(); |
||||
$this->assertEquals('user1', $customer->name); |
||||
|
||||
$customer = $this->callCustomerFind()->orderBy('id')->offset(0)->one(); |
||||
$this->assertEquals('user1', $customer->name); |
||||
|
||||
$customer = $this->callCustomerFind()->orderBy('id')->offset(1)->one(); |
||||
$this->assertEquals('user2', $customer->name); |
||||
|
||||
$customer = $this->callCustomerFind()->orderBy('id')->offset(2)->one(); |
||||
$this->assertEquals('user3', $customer->name); |
||||
|
||||
$customer = $this->callCustomerFind()->offset(3)->one(); |
||||
$this->assertNull($customer); |
||||
|
||||
} |
||||
|
||||
public function testFindComplexCondition() |
||||
{ |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
$this->assertEquals(2, $this->callCustomerFind()->where(['OR', ['name' => 'user1'], ['name' => 'user2']])->count()); |
||||
$this->assertEquals(2, count($this->callCustomerFind()->where(['OR', ['name' => 'user1'], ['name' => 'user2']])->all())); |
||||
|
||||
$this->assertEquals(2, $this->callCustomerFind()->where(['name' => ['user1','user2']])->count()); |
||||
$this->assertEquals(2, count($this->callCustomerFind()->where(['name' => ['user1','user2']])->all())); |
||||
|
||||
$this->assertEquals(1, $this->callCustomerFind()->where(['AND', ['name' => ['user2','user3']], ['BETWEEN', 'status', 2, 4]])->count()); |
||||
$this->assertEquals(1, count($this->callCustomerFind()->where(['AND', ['name' => ['user2','user3']], ['BETWEEN', 'status', 2, 4]])->all())); |
||||
} |
||||
|
||||
public function testFindNullValues() |
||||
{ |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
$customer = $this->callCustomerFind(2); |
||||
$customer->name = null; |
||||
$customer->save(false); |
||||
$this->afterSave(); |
||||
|
||||
$result = $this->callCustomerFind()->where(['name' => null])->all(); |
||||
$this->assertEquals(1, count($result)); |
||||
$this->assertEquals(2, reset($result)->primaryKey); |
||||
} |
||||
|
||||
public function testExists() |
||||
{ |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
$this->assertTrue($this->callCustomerFind()->where(['id' => 2])->exists()); |
||||
$this->assertFalse($this->callCustomerFind()->where(['id' => 5])->exists()); |
||||
$this->assertTrue($this->callCustomerFind()->where(['name' => 'user1'])->exists()); |
||||
$this->assertFalse($this->callCustomerFind()->where(['name' => 'user5'])->exists()); |
||||
} |
||||
|
||||
public function testFindLazy() |
||||
{ |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
$customer = $this->callCustomerFind(2); |
||||
$this->assertFalse($customer->isRelationPopulated('orders')); |
||||
$orders = $customer->orders; |
||||
$this->assertTrue($customer->isRelationPopulated('orders')); |
||||
$this->assertEquals(2, count($orders)); |
||||
$this->assertEquals(1, count($customer->populatedRelations)); |
||||
|
||||
/** @var Customer $customer */ |
||||
$customer = $this->callCustomerFind(2); |
||||
$this->assertFalse($customer->isRelationPopulated('orders')); |
||||
$orders = $customer->getOrders()->where(['id' => 3])->all(); |
||||
$this->assertFalse($customer->isRelationPopulated('orders')); |
||||
$this->assertEquals(0, count($customer->populatedRelations)); |
||||
|
||||
$this->assertEquals(1, count($orders)); |
||||
$this->assertEquals(3, $orders[0]->id); |
||||
} |
||||
|
||||
public function testFindEager() |
||||
{ |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
$customers = $this->callCustomerFind()->with('orders')->indexBy('id')->all(); |
||||
ksort($customers); |
||||
$this->assertEquals(3, count($customers)); |
||||
$this->assertTrue($customers[1]->isRelationPopulated('orders')); |
||||
$this->assertTrue($customers[2]->isRelationPopulated('orders')); |
||||
$this->assertTrue($customers[3]->isRelationPopulated('orders')); |
||||
$this->assertEquals(1, count($customers[1]->orders)); |
||||
$this->assertEquals(2, count($customers[2]->orders)); |
||||
$this->assertEquals(0, count($customers[3]->orders)); |
||||
|
||||
$customer = $this->callCustomerFind()->where(['id' => 1])->with('orders')->one(); |
||||
$this->assertTrue($customer->isRelationPopulated('orders')); |
||||
$this->assertEquals(1, count($customer->orders)); |
||||
$this->assertEquals(1, count($customer->populatedRelations)); |
||||
} |
||||
|
||||
public function testFindLazyVia() |
||||
{ |
||||
if (getenv('TRAVIS') == 'true' && $this instanceof \yiiunit\extensions\elasticsearch\ActiveRecordTest) { |
||||
// https://github.com/yiisoft/yii2/issues/1317 |
||||
$this->markTestSkipped('This test is unreproduceable failing on travis-ci, locally it is passing.'); |
||||
} |
||||
|
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
/** @var Order $order */ |
||||
$order = $this->callOrderFind(1); |
||||
$this->assertEquals(1, $order->id); |
||||
$this->assertEquals(2, count($order->items)); |
||||
$this->assertEquals(1, $order->items[0]->id); |
||||
$this->assertEquals(2, $order->items[1]->id); |
||||
} |
||||
|
||||
public function testFindLazyVia2() |
||||
{ |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
/** @var Order $order */ |
||||
$order = $this->callOrderFind(1); |
||||
$order->id = 100; |
||||
$this->assertEquals([], $order->items); |
||||
} |
||||
|
||||
public function testFindEagerViaRelation() |
||||
{ |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
$orders = $this->callOrderFind()->with('items')->orderBy('id')->all(); |
||||
$this->assertEquals(3, count($orders)); |
||||
$order = $orders[0]; |
||||
$this->assertEquals(1, $order->id); |
||||
$this->assertTrue($order->isRelationPopulated('items')); |
||||
$this->assertEquals(2, count($order->items)); |
||||
$this->assertEquals(1, $order->items[0]->id); |
||||
$this->assertEquals(2, $order->items[1]->id); |
||||
} |
||||
|
||||
public function testFindNestedRelation() |
||||
{ |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
$customers = $this->callCustomerFind()->with('orders', 'orders.items')->indexBy('id')->all(); |
||||
ksort($customers); |
||||
$this->assertEquals(3, count($customers)); |
||||
$this->assertTrue($customers[1]->isRelationPopulated('orders')); |
||||
$this->assertTrue($customers[2]->isRelationPopulated('orders')); |
||||
$this->assertTrue($customers[3]->isRelationPopulated('orders')); |
||||
$this->assertEquals(1, count($customers[1]->orders)); |
||||
$this->assertEquals(2, count($customers[2]->orders)); |
||||
$this->assertEquals(0, count($customers[3]->orders)); |
||||
$this->assertTrue($customers[1]->orders[0]->isRelationPopulated('items')); |
||||
$this->assertTrue($customers[2]->orders[0]->isRelationPopulated('items')); |
||||
$this->assertTrue($customers[2]->orders[1]->isRelationPopulated('items')); |
||||
$this->assertEquals(2, count($customers[1]->orders[0]->items)); |
||||
$this->assertEquals(3, count($customers[2]->orders[0]->items)); |
||||
$this->assertEquals(1, count($customers[2]->orders[1]->items)); |
||||
} |
||||
|
||||
/** |
||||
* Ensure ActiveRelation does preserve order of items on find via() |
||||
* https://github.com/yiisoft/yii2/issues/1310 |
||||
*/ |
||||
public function testFindEagerViaRelationPreserveOrder() |
||||
{ |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
$orders = $this->callOrderFind()->with('itemsInOrder1')->orderBy('create_time')->all(); |
||||
$this->assertEquals(3, count($orders)); |
||||
|
||||
$order = $orders[0]; |
||||
$this->assertEquals(1, $order->id); |
||||
$this->assertTrue($order->isRelationPopulated('itemsInOrder1')); |
||||
$this->assertEquals(2, count($order->itemsInOrder1)); |
||||
$this->assertEquals(1, $order->itemsInOrder1[0]->id); |
||||
$this->assertEquals(2, $order->itemsInOrder1[1]->id); |
||||
|
||||
$order = $orders[1]; |
||||
$this->assertEquals(2, $order->id); |
||||
$this->assertTrue($order->isRelationPopulated('itemsInOrder1')); |
||||
$this->assertEquals(3, count($order->itemsInOrder1)); |
||||
$this->assertEquals(5, $order->itemsInOrder1[0]->id); |
||||
$this->assertEquals(3, $order->itemsInOrder1[1]->id); |
||||
$this->assertEquals(4, $order->itemsInOrder1[2]->id); |
||||
|
||||
$order = $orders[2]; |
||||
$this->assertEquals(3, $order->id); |
||||
$this->assertTrue($order->isRelationPopulated('itemsInOrder1')); |
||||
$this->assertEquals(1, count($order->itemsInOrder1)); |
||||
$this->assertEquals(2, $order->itemsInOrder1[0]->id); |
||||
} |
||||
|
||||
// different order in via table |
||||
public function testFindEagerViaRelationPreserveOrderB() |
||||
{ |
||||
$orders = $this->callOrderFind()->with('itemsInOrder2')->orderBy('create_time')->all(); |
||||
$this->assertEquals(3, count($orders)); |
||||
|
||||
$order = $orders[0]; |
||||
$this->assertEquals(1, $order->id); |
||||
$this->assertTrue($order->isRelationPopulated('itemsInOrder2')); |
||||
$this->assertEquals(2, count($order->itemsInOrder2)); |
||||
$this->assertEquals(1, $order->itemsInOrder2[0]->id); |
||||
$this->assertEquals(2, $order->itemsInOrder2[1]->id); |
||||
|
||||
$order = $orders[1]; |
||||
$this->assertEquals(2, $order->id); |
||||
$this->assertTrue($order->isRelationPopulated('itemsInOrder2')); |
||||
$this->assertEquals(3, count($order->itemsInOrder2)); |
||||
$this->assertEquals(5, $order->itemsInOrder2[0]->id); |
||||
$this->assertEquals(3, $order->itemsInOrder2[1]->id); |
||||
$this->assertEquals(4, $order->itemsInOrder2[2]->id); |
||||
|
||||
$order = $orders[2]; |
||||
$this->assertEquals(3, $order->id); |
||||
$this->assertTrue($order->isRelationPopulated('itemsInOrder2')); |
||||
$this->assertEquals(1, count($order->itemsInOrder2)); |
||||
$this->assertEquals(2, $order->itemsInOrder2[0]->id); |
||||
} |
||||
|
||||
public function testLink() |
||||
{ |
||||
$orderClass = $this->getOrderClass(); |
||||
$orderItemClass = $this->getOrderItemClass(); |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
$customer = $this->callCustomerFind(2); |
||||
$this->assertEquals(2, count($customer->orders)); |
||||
|
||||
// has many |
||||
$order = new $orderClass; |
||||
$order->total = 100; |
||||
$this->assertTrue($order->isNewRecord); |
||||
$customer->link('orders', $order); |
||||
$this->afterSave(); |
||||
$this->assertEquals(3, count($customer->orders)); |
||||
$this->assertFalse($order->isNewRecord); |
||||
$this->assertEquals(3, count($customer->getOrders()->all())); |
||||
$this->assertEquals(2, $order->customer_id); |
||||
|
||||
// belongs to |
||||
$order = new $orderClass; |
||||
$order->total = 100; |
||||
$this->assertTrue($order->isNewRecord); |
||||
$customer = $this->callCustomerFind(1); |
||||
$this->assertNull($order->customer); |
||||
$order->link('customer', $customer); |
||||
$this->assertFalse($order->isNewRecord); |
||||
$this->assertEquals(1, $order->customer_id); |
||||
$this->assertEquals(1, $order->customer->primaryKey); |
||||
|
||||
// via model |
||||
$order = $this->callOrderFind(1); |
||||
$this->assertEquals(2, count($order->items)); |
||||
$this->assertEquals(2, count($order->orderItems)); |
||||
$orderItem = $this->callOrderItemFind(['order_id' => 1, 'item_id' => 3]); |
||||
$this->assertNull($orderItem); |
||||
$item = $this->callItemFind(3); |
||||
$order->link('items', $item, ['quantity' => 10, 'subtotal' => 100]); |
||||
$this->afterSave(); |
||||
$this->assertEquals(3, count($order->items)); |
||||
$this->assertEquals(3, count($order->orderItems)); |
||||
$orderItem = $this->callOrderItemFind(['order_id' => 1, 'item_id' => 3]); |
||||
$this->assertTrue($orderItem instanceof $orderItemClass); |
||||
$this->assertEquals(10, $orderItem->quantity); |
||||
$this->assertEquals(100, $orderItem->subtotal); |
||||
} |
||||
|
||||
public function testUnlink() |
||||
{ |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
// has many |
||||
$customer = $this->callCustomerFind(2); |
||||
$this->assertEquals(2, count($customer->orders)); |
||||
$customer->unlink('orders', $customer->orders[1], true); |
||||
$this->afterSave(); |
||||
$this->assertEquals(1, count($customer->orders)); |
||||
$this->assertNull($this->callOrderFind(3)); |
||||
|
||||
// via model |
||||
$order = $this->callOrderFind(2); |
||||
$this->assertEquals(3, count($order->items)); |
||||
$this->assertEquals(3, count($order->orderItems)); |
||||
$order->unlink('items', $order->items[2], true); |
||||
$this->afterSave(); |
||||
$this->assertEquals(2, count($order->items)); |
||||
$this->assertEquals(2, count($order->orderItems)); |
||||
} |
||||
|
||||
public static $afterSaveNewRecord; |
||||
public static $afterSaveInsert; |
||||
|
||||
public function testInsert() |
||||
{ |
||||
$customerClass = $this->getCustomerClass(); |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
$customer = new $customerClass; |
||||
$customer->email = 'user4@example.com'; |
||||
$customer->name = 'user4'; |
||||
$customer->address = 'address4'; |
||||
|
||||
$this->assertNull($customer->id); |
||||
$this->assertTrue($customer->isNewRecord); |
||||
static::$afterSaveNewRecord = null; |
||||
static::$afterSaveInsert = null; |
||||
|
||||
$customer->save(); |
||||
$this->afterSave(); |
||||
|
||||
$this->assertNotNull($customer->id); |
||||
$this->assertFalse(static::$afterSaveNewRecord); |
||||
$this->assertTrue(static::$afterSaveInsert); |
||||
$this->assertFalse($customer->isNewRecord); |
||||
} |
||||
|
||||
public function testUpdate() |
||||
{ |
||||
$customerClass = $this->getCustomerClass(); |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
// save |
||||
$customer = $this->callCustomerFind(2); |
||||
$this->assertTrue($customer instanceof $customerClass); |
||||
$this->assertEquals('user2', $customer->name); |
||||
$this->assertFalse($customer->isNewRecord); |
||||
static::$afterSaveNewRecord = null; |
||||
static::$afterSaveInsert = null; |
||||
|
||||
$customer->name = 'user2x'; |
||||
$customer->save(); |
||||
$this->afterSave(); |
||||
$this->assertEquals('user2x', $customer->name); |
||||
$this->assertFalse($customer->isNewRecord); |
||||
$this->assertFalse(static::$afterSaveNewRecord); |
||||
$this->assertFalse(static::$afterSaveInsert); |
||||
$customer2 = $this->callCustomerFind(2); |
||||
$this->assertEquals('user2x', $customer2->name); |
||||
|
||||
// updateAll |
||||
$customer = $this->callCustomerFind(3); |
||||
$this->assertEquals('user3', $customer->name); |
||||
$ret = $customerClass::updateAll(['name' => 'temp'], ['id' => 3]); |
||||
$this->afterSave(); |
||||
$this->assertEquals(1, $ret); |
||||
$customer = $this->callCustomerFind(3); |
||||
$this->assertEquals('temp', $customer->name); |
||||
|
||||
$ret = $customerClass::updateAll(['name' => 'tempX']); |
||||
$this->afterSave(); |
||||
$this->assertEquals(3, $ret); |
||||
|
||||
$ret = $customerClass::updateAll(['name' => 'temp'], ['name' => 'user6']); |
||||
$this->afterSave(); |
||||
$this->assertEquals(0, $ret); |
||||
} |
||||
|
||||
public function testUpdateCounters() |
||||
{ |
||||
$orderItemClass = $this->getOrderItemClass(); |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
// updateCounters |
||||
$pk = ['order_id' => 2, 'item_id' => 4]; |
||||
$orderItem = $this->callOrderItemFind($pk); |
||||
$this->assertEquals(1, $orderItem->quantity); |
||||
$ret = $orderItem->updateCounters(['quantity' => -1]); |
||||
$this->afterSave(); |
||||
$this->assertTrue($ret); |
||||
$this->assertEquals(0, $orderItem->quantity); |
||||
$orderItem = $this->callOrderItemFind($pk); |
||||
$this->assertEquals(0, $orderItem->quantity); |
||||
|
||||
// updateAllCounters |
||||
$pk = ['order_id' => 1, 'item_id' => 2]; |
||||
$orderItem = $this->callOrderItemFind($pk); |
||||
$this->assertEquals(2, $orderItem->quantity); |
||||
$ret = $orderItemClass::updateAllCounters([ |
||||
'quantity' => 3, |
||||
'subtotal' => -10, |
||||
], $pk); |
||||
$this->afterSave(); |
||||
$this->assertEquals(1, $ret); |
||||
$orderItem = $this->callOrderItemFind($pk); |
||||
$this->assertEquals(5, $orderItem->quantity); |
||||
$this->assertEquals(30, $orderItem->subtotal); |
||||
} |
||||
|
||||
public function testDelete() |
||||
{ |
||||
$customerClass = $this->getCustomerClass(); |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
// delete |
||||
$customer = $this->callCustomerFind(2); |
||||
$this->assertTrue($customer instanceof $customerClass); |
||||
$this->assertEquals('user2', $customer->name); |
||||
$customer->delete(); |
||||
$this->afterSave(); |
||||
$customer = $this->callCustomerFind(2); |
||||
$this->assertNull($customer); |
||||
|
||||
// deleteAll |
||||
$customers = $this->callCustomerFind()->all(); |
||||
$this->assertEquals(2, count($customers)); |
||||
$ret = $customerClass::deleteAll(); |
||||
$this->afterSave(); |
||||
$this->assertEquals(2, $ret); |
||||
$customers = $this->callCustomerFind()->all(); |
||||
$this->assertEquals(0, count($customers)); |
||||
|
||||
$ret = $customerClass::deleteAll(); |
||||
$this->afterSave(); |
||||
$this->assertEquals(0, $ret); |
||||
} |
||||
|
||||
/** |
||||
* Some PDO implementations(e.g. cubrid) do not support boolean values. |
||||
* Make sure this does not affect AR layer. |
||||
*/ |
||||
public function testBooleanAttribute() |
||||
{ |
||||
$customerClass = $this->getCustomerClass(); |
||||
/** @var TestCase|ActiveRecordTestTrait $this */ |
||||
$customer = new $customerClass(); |
||||
$customer->name = 'boolean customer'; |
||||
$customer->email = 'mail@example.com'; |
||||
$customer->status = true; |
||||
$customer->save(false); |
||||
|
||||
$customer->refresh(); |
||||
$this->assertEquals(1, $customer->status); |
||||
|
||||
$customer->status = false; |
||||
$customer->save(false); |
||||
|
||||
$customer->refresh(); |
||||
$this->assertEquals(0, $customer->status); |
||||
|
||||
$customers = $this->callCustomerFind()->where(['status' => true])->all(); |
||||
$this->assertEquals(2, count($customers)); |
||||
|
||||
$customers = $this->callCustomerFind()->where(['status' => false])->all(); |
||||
$this->assertEquals(1, count($customers)); |
||||
} |
||||
} |
Loading…
Reference in new issue