149 changed files with 5475 additions and 1226 deletions
			
			
		| @ -0,0 +1,24 @@ | ||||
| <?php | ||||
| use yii\helpers\Html; | ||||
| use yii\mail\BaseMessage; | ||||
| 
 | ||||
| /** | ||||
|  * @var \yii\web\View $this | ||||
|  * @var BaseMessage $content | ||||
|  */ | ||||
| ?> | ||||
| <?php $this->beginPage() ?> | ||||
| <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> | ||||
| <html xmlns="http://www.w3.org/1999/xhtml"> | ||||
| <head> | ||||
| 	<meta http-equiv="Content-Type" content="text/html; charset=<?= Yii::$app->charset ?>" />
 | ||||
| 	<title><?= Html::encode($this->title) ?></title>
 | ||||
| 	<?php $this->head() ?> | ||||
| </head> | ||||
| <body> | ||||
| 	<?php $this->beginBody() ?> | ||||
| 	<?= $content ?> | ||||
| 	<?php $this->endBody() ?> | ||||
| </body> | ||||
| </html> | ||||
| <?php $this->endPage() ?> | ||||
| @ -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; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * {@inheritdoc} | ||||
| 	 */ | ||||
| 	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; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * {@inheritdoc} | ||||
| 	 */ | ||||
| 	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; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * {@inheritdoc} | ||||
| 	 */ | ||||
| 	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 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 = '_uid'; | ||||
| 			} | ||||
| 
 | ||||
| 			// 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,148 @@ | ||||
| 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', | ||||
|             'nodes' => [ | ||||
|                 ['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. | ||||
|   There are `query()`, `filter()` and `addFacets()` methods that allows to compose an elasticsearch query. | ||||
|   See the usage example below on how they work and check out the [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html) | ||||
|   on how to compose `query` and `filter` parts. | ||||
| - 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 | ||||
| $customers = Customer::find()->active()->all(); // find all by query (using the `active` scope) | ||||
| 
 | ||||
| // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-field-query.html | ||||
| $result = Article::find()->query(["field" => ["title" => "yii"]])->all(); // articles whose title contains "yii" | ||||
| 
 | ||||
| // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-flt-query.html | ||||
| $query = Article::find()->query([ | ||||
| 	"fuzzy_like_this" => [ | ||||
| 		"fields" => ["title", "description"], | ||||
| 		"like_text" => "This query will return articles that are similar to this text :-)", | ||||
|         "max_query_terms" : 12 | ||||
| 	] | ||||
| ]); | ||||
| 
 | ||||
| $query->all(); // gives you all the documents | ||||
| // you can add facets to your search: | ||||
| $query->addStatisticalFacet('click_stats', ['field' => 'visit_count']); | ||||
| $query->search(); // gives you all the records + stats about the visit_count field. e.g. mean, sum, min, max etc... | ||||
| ``` | ||||
| 
 | ||||
| And there is so much more in it. "it’s endless what you can build"[¹](http://www.elasticsearch.org/) | ||||
| @ -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,157 @@ | ||||
| <?php | ||||
| /** | ||||
|  * @link http://www.yiiframework.com/ | ||||
|  * @copyright Copyright (c) 2008 Yii Software LLC | ||||
|  * @license http://www.yiiframework.com/license/ | ||||
|  */ | ||||
| 
 | ||||
| namespace yii\redis; | ||||
| 
 | ||||
| use Yii; | ||||
| use yii\base\InvalidConfigException; | ||||
| 
 | ||||
| /** | ||||
|  * Redis Session implements a session component using [redis](http://redis.io/) as the storage medium. | ||||
|  * | ||||
|  * Redis Session requires redis version 2.6.12 or higher to work properly. | ||||
|  * | ||||
|  * It needs to be configured with a redis [[Connection]] that is also configured as an application component. | ||||
|  * By default it will use the `redis` application component. | ||||
|  * | ||||
|  * To use redis Session as the session application component, configure the application as follows, | ||||
|  * | ||||
|  * ~~~ | ||||
|  * [ | ||||
|  *     'components' => [ | ||||
|  *         'session' => [ | ||||
|  *             'class' => 'yii\redis\Session', | ||||
|  *             'redis' => [ | ||||
|  *                 'hostname' => 'localhost', | ||||
|  *                 'port' => 6379, | ||||
|  *                 'database' => 0, | ||||
|  *             ] | ||||
|  *         ], | ||||
|  *     ], | ||||
|  * ] | ||||
|  * ~~~ | ||||
|  * | ||||
|  * Or if you have configured the redis [[Connection]] as an application component, the following is sufficient: | ||||
|  * | ||||
|  * ~~~ | ||||
|  * [ | ||||
|  *     'components' => [ | ||||
|  *         'session' => [ | ||||
|  *             'class' => 'yii\redis\Session', | ||||
|  *             // 'redis' => 'redis' // id of the connection application component | ||||
|  *         ], | ||||
|  *     ], | ||||
|  * ] | ||||
|  * ~~~ | ||||
|  * | ||||
|  * @property boolean $useCustomStorage Whether to use custom storage. This property is read-only. | ||||
|  * | ||||
|  * @author Carsten Brandt <mail@cebe.cc> | ||||
|  * @since 2.0 | ||||
|  */ | ||||
| class Session extends \yii\web\Session | ||||
| { | ||||
| 	/** | ||||
| 	 * @var Connection|string|array the Redis [[Connection]] object or the application component ID of the Redis [[Connection]]. | ||||
| 	 * This can also be an array that is used to create a redis [[Connection]] instance in case you do not want do configure | ||||
| 	 * redis connection as an application component. | ||||
| 	 * After the Session object is created, if you want to change this property, you should only assign it | ||||
| 	 * with a Redis [[Connection]] object. | ||||
| 	 */ | ||||
| 	public $redis = 'redis'; | ||||
| 	/** | ||||
| 	 * @var string a string prefixed to every cache key so that it is unique. If not set, | ||||
| 	 * it will use a prefix generated from [[Application::id]]. You may set this property to be an empty string | ||||
| 	 * if you don't want to use key prefix. It is recommended that you explicitly set this property to some | ||||
| 	 * static value if the cached data needs to be shared among multiple applications. | ||||
| 	 * | ||||
| 	 * To ensure interoperability, only alphanumeric characters should be used. | ||||
| 	 */ | ||||
| 	public $keyPrefix; | ||||
| 
 | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Initializes the redis Session component. | ||||
| 	 * This method will initialize the [[redis]] property to make sure it refers to a valid redis connection. | ||||
| 	 * @throws InvalidConfigException if [[redis]] is invalid. | ||||
| 	 */ | ||||
| 	public function init() | ||||
| 	{ | ||||
| 		if (is_string($this->redis)) { | ||||
| 			$this->redis = Yii::$app->getComponent($this->redis); | ||||
| 		} else if (is_array($this->redis)) { | ||||
| 			if (!isset($this->redis['class'])) { | ||||
| 				$this->redis['class'] = Connection::className(); | ||||
| 			} | ||||
| 			$this->redis = Yii::createObject($this->redis); | ||||
| 		} | ||||
| 		if (!$this->redis instanceof Connection) { | ||||
| 			throw new InvalidConfigException("Session::redis must be either a Redis connection instance or the application component ID of a Redis connection."); | ||||
| 		} | ||||
| 		if ($this->keyPrefix === null) { | ||||
| 			$this->keyPrefix = substr(md5(Yii::$app->id), 0, 5); | ||||
| 		} elseif (!ctype_alnum($this->keyPrefix)) { | ||||
| 			throw new InvalidConfigException(get_class($this) . '::keyPrefix should only contain alphanumeric characters.'); | ||||
| 		} | ||||
| 		parent::init(); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Returns a value indicating whether to use custom session storage. | ||||
| 	 * This method overrides the parent implementation and always returns true. | ||||
| 	 * @return boolean whether to use custom storage. | ||||
| 	 */ | ||||
| 	public function getUseCustomStorage() | ||||
| 	{ | ||||
| 		return true; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Session read handler. | ||||
| 	 * Do not call this method directly. | ||||
| 	 * @param string $id session ID | ||||
| 	 * @return string the session data | ||||
| 	 */ | ||||
| 	public function readSession($id) | ||||
| 	{ | ||||
| 		$data = $this->redis->executeCommand('GET', [$this->calculateKey($id)]); | ||||
| 		return $data === false ? '' : $data; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Session write handler. | ||||
| 	 * Do not call this method directly. | ||||
| 	 * @param string $id session ID | ||||
| 	 * @param string $data session data | ||||
| 	 * @return boolean whether session write is successful | ||||
| 	 */ | ||||
| 	public function writeSession($id, $data) | ||||
| 	{ | ||||
| 		return (bool) $this->redis->executeCommand('SET', [$this->calculateKey($id), $data, 'EX', $this->getTimeout()]); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Session destroy handler. | ||||
| 	 * Do not call this method directly. | ||||
| 	 * @param string $id session ID | ||||
| 	 * @return boolean whether session is destroyed successfully | ||||
| 	 */ | ||||
| 	public function destroySession($id) | ||||
| 	{ | ||||
| 		return (bool) $this->redis->executeCommand('DEL', [$this->calculateKey($id)]); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Generates a unique key used for storing session data in cache. | ||||
| 	 * @param string $id session variable name | ||||
| 	 * @return string a safe cache key associated with the session variable name | ||||
| 	 */ | ||||
| 	protected function calculateKey($id) | ||||
| 	{ | ||||
| 		return $this->keyPrefix . md5(json_encode([__CLASS__, $id])); | ||||
| 	} | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
					Loading…
					
					
				
		Reference in new issue