From 4bb94c60c6cf4df2244c5171bb9f8af8528dfa33 Mon Sep 17 00:00:00 2001 From: evgen-d Date: Tue, 29 Jul 2014 09:33:21 +0400 Subject: [PATCH] arango recode --- ActiveQuery.php | 166 ++++++ ActiveRecord.php | 440 ++++++++++++++++ ArangoDbConnection.php | 81 +-- ArangoModel.php | 109 ---- ArangoProvider.php | 142 ----- Connection.php | 100 +++- Exception.php | 14 + Migration.php | 89 ++++ Query.php | 606 ++++++++++++++++++++++ composer.json | 8 + console/controllers/MigrateController.php | 159 ++++++ views/migration.php | 24 + 12 files changed, 1607 insertions(+), 331 deletions(-) create mode 100644 ActiveQuery.php create mode 100644 ActiveRecord.php delete mode 100644 ArangoModel.php delete mode 100644 ArangoProvider.php create mode 100644 Exception.php create mode 100644 Migration.php create mode 100644 Query.php create mode 100644 console/controllers/MigrateController.php create mode 100644 views/migration.php diff --git a/ActiveQuery.php b/ActiveQuery.php new file mode 100644 index 0000000..963e61d --- /dev/null +++ b/ActiveQuery.php @@ -0,0 +1,166 @@ +modelClass = $modelClass; + parent::__construct($config); + } + + protected function buildQuery($query = null, $params = []) + { + if ($this->primaryModel !== null) { + // lazy loading + if ($this->via instanceof self) { + // via pivot collection + $viaModels = $this->via->findPivotRows([$this->primaryModel]); + $this->filterByModels($viaModels); + } elseif (is_array($this->via)) { + // via relation + /* @var $viaQuery ActiveQuery */ + 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::buildQuery($query, $params); + } + + private function createModels($rows) + { + $models = []; + if ($this->asArray) { + array_walk( + $rows, + function (&$doc) { + if ($doc instanceof Document) { + $doc = $doc->getAll(); + } + } + ); + if ($this->indexBy === null) { + return $rows; + } + foreach ($rows as $row) { + if (is_string($this->indexBy)) { + $key = $row[$this->indexBy]; + } else { + $key = call_user_func($this->indexBy, $row); + } + $models[$key] = $row; + } + } else { + /* @var $class ActiveRecord */ + $class = $this->modelClass; + if ($this->indexBy === null) { + foreach ($rows as $row) { + $model = $class::instantiate($row); + $class::populateRecord($model, $row); + $model->setIsNewRecord(false); + $models[] = $model; + } + } else { + foreach ($rows as $row) { + $model = $class::instantiate($row); + $class::populateRecord($model, $row); + $model->setIsNewRecord(false); + if (is_string($this->indexBy)) { + $key = $model->{$this->indexBy}; + } else { + $key = call_user_func($this->indexBy, $model); + } + $models[$key] = $model; + } + } + } + + return $models; + } + + private function prefixKeyColumns($attributes) + { + if ($this instanceof ActiveQuery) { + /* @var $modelClass ActiveRecord */ + $modelClass = $this->modelClass; + foreach ($attributes as $i => $attribute) { + $attributes[$i] = "{$modelClass::collectionName()}.$attribute"; + } + } + return $attributes; + } + + public function all($db = null) + { + $statement = $this->createCommand(); + $cursor = $statement->execute(); + $rows = $cursor->getAll(); + if (!empty($rows)) { + $models = $this->createModels($rows); + if (!empty($this->with)) { + $this->findWith($this->with, $models); + } + if (!$this->asArray) { + foreach ($models as $model) { + $model->afterFind(); + } + } + + return $models; + } else { + return []; + } + } + + public function one($db = null) + { + $row = parent::one($db); + if ($row !== false) { + if ($this->asArray) { + $model = $row; + } else { + /* @var $class ActiveRecord */ + $class = $this->modelClass; + $model = $class::instantiate($row); + $class::populateRecord($model, $row); + $model->setIsNewRecord(false); + } + if (!empty($this->with)) { + $models = [$model]; + $this->findWith($this->with, $models); + $model = $models[0]; + } + if (!$this->asArray) { + $model->afterFind(); + } + + return $model; + } else { + return null; + } + } +} diff --git a/ActiveRecord.php b/ActiveRecord.php new file mode 100644 index 0000000..f52c3a5 --- /dev/null +++ b/ActiveRecord.php @@ -0,0 +1,440 @@ +document = new Document(); + + parent::__construct($config); + } + + public function mergeAttribute($name, $value) + { + $newValue = $this->getAttribute($name); + if (!is_array($newValue)) { + $newValue === null ? [] : [$newValue]; + } + + if (is_array($value)) { + $this->setAttribute($name, ArrayHelper::merge($newValue, $value)); + } else { + $newValue[] = $value; + $this->setAttribute($name, $newValue); + } + } + + public static function collectionName() + { + return Inflector::camel2id(StringHelper::basename(get_called_class()), '_'); + } + + public function setAttribute($name, $value) + { + $this->document->set($name, $value); + parent::setAttribute($name, $value); + } + + /** + * Returns the primary key **name(s)** for this AR class. + * + * Note that an array should be returned even when the record only has a single primary key. + * + * For the primary key **value** see [[getPrimaryKey()]] instead. + * + * @return string[] the primary key name(s) for this AR class. + */ + public static function primaryKey() + { + return ['_key']; + } + + /** + * Creates an [[ActiveQueryInterface|ActiveQuery]] instance for query purpose. + * + * The returned [[ActiveQueryInterface|ActiveQuery]] instance can be further customized by calling + * methods defined in [[ActiveQueryInterface]] before `one()` or `all()` is called to return + * populated ActiveRecord instances. For example, + * + * ```php + * // find the customer whose ID is 1 + * $customer = Customer::find()->where(['id' => 1])->one(); + * + * // find all active customers and order them by their age: + * $customers = Customer::find() + * ->where(['status' => 1]) + * ->orderBy('age') + * ->all(); + * ``` + * + * This method is also called by [[BaseActiveRecord::hasOne()]] and [[BaseActiveRecord::hasMany()]] to + * create a relational query. + * + * You may override this method to return a customized query. For example, + * + * ```php + * class Customer extends ActiveRecord + * { + * public static function find() + * { + * // use CustomerQuery instead of the default ActiveQuery + * return new CustomerQuery(get_called_class()); + * } + * } + * ``` + * + * The following code shows how to apply a default condition for all queries: + * + * ```php + * class Customer extends ActiveRecord + * { + * public static function find() + * { + * return parent::find()->where(['deleted' => false]); + * } + * } + * + * // Use andWhere()/orWhere() to apply the default condition + * // SELECT FROM customer WHERE `deleted`=:deleted AND age>30 + * $customers = Customer::find()->andWhere('age>30')->all(); + * + * // Use where() to ignore the default condition + * // SELECT FROM customer WHERE age>30 + * $customers = Customer::find()->where('age>30')->all(); + * + * @return ActiveQueryInterface the newly created [[ActiveQueryInterface|ActiveQuery]] instance. + */ + public static function find() + { + /** @var ActiveQuery $query */ + $query = \Yii::createObject(ActiveQuery::className(), [get_called_class()]); + $query->from(static::collectionName())->select(static::collectionName()); + + return $query; + } + + /** + * @param ActiveRecord $record + * @param Document $row + */ + public static function populateRecord($record, $row) + { + $record->document = $row; + parent::populateRecord($record, $record->document->getAll()); + } + + public function attributes() + { + $class = new \ReflectionClass($this); + $names = []; + foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { + if (!$property->isStatic()) { + $names[] = $property->getName(); + } + } + + return $names; + } + + /** + * Inserts the record into the database using the attribute values of this record. + * + * Usage example: + * + * ```php + * $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 that are loaded from DB will be saved. + * @return boolean whether the attributes are valid and the record is inserted successfully. + */ + public function insert($runValidation = true, $attributes = null) + { + if ($runValidation && !$this->validate($attributes)) { + return false; + } + $result = $this->insertInternal($attributes); + + return $result; + } + + protected function insertInternal($attributes = null) + { + if (!$this->beforeSave(true)) { + return false; + } + $values = $this->getDirtyAttributes($attributes); + if (empty($values)) { + $currentAttributes = $this->getAttributes(); + foreach ($this->primaryKey() as $key) { + $values[$key] = isset($currentAttributes[$key]) ? $currentAttributes[$key] : null; + } + } + + $newId = static::getDb()->getDocumentHandler()->save(static::collectionName(), $values); + static::populateRecord($this, static::getDb()->getDocument(static::collectionName(), $newId)); + $this->setIsNewRecord(false); + + $changedAttributes = array_fill_keys(array_keys($values), null); + $this->setOldAttributes($this->document->getAll()); + $this->afterSave(true, $changedAttributes); + + return true; + } + + protected function updateInternal($attributes = null) + { + if (!$this->beforeSave(false)) { + return false; + } + $values = $this->getDirtyAttributes($attributes); + if (empty($values)) { + $this->afterSave(false, $values); + return 0; + } + $condition = $this->getOldPrimaryKey(true); + $lock = $this->optimisticLock(); + if ($lock !== null) { + if (!isset($values[$lock])) { + $values[$lock] = $this->$lock + 1; + } + $condition[$lock] = $this->$lock; + } + + foreach ($values as $key => $attribute) { + $this->setAttribute($key, $attribute); + } + + $rows = static::getDb()->getDocumentHandler()->updateById( + static::collectionName(), + $this->getOldAttribute('_key'), + Document::createFromArray($values) + ); + + if ($lock !== null && !$rows) { + throw new StaleObjectException('The object being updated is outdated.'); + } + + $changedAttributes = []; + foreach ($values as $name => $value) { + $changedAttributes[$name] = $this->getOldAttribute($name); + $this->setOldAttribute($name, $value); + } + $this->afterSave(false, $changedAttributes); + + return $rows; + } + + /** + * Returns the connection used by this AR class. + * @return Connection the database connection used by this AR class. + */ + public static function getDb() + { + return \Yii::$app->get('arangodb'); + } + + protected static function findByCondition($condition, $one) + { + /** @var ActiveQuery $query */ + $query = static::find(); + + if (!ArrayHelper::isAssociative($condition)) { + // query by primary key + $primaryKey = static::primaryKey(); + if (isset($primaryKey[0])) { + $collection = static::collectionName(); + $condition = ["{$collection}.{$primaryKey[0]}" => $condition]; + } else { + throw new InvalidConfigException(get_called_class() . ' must have a primary key.'); + } + } + + return $one ? $query->andWhere($condition)->one() : $query->andWhere($condition)->all(); + } + + /** + * Updates records using the provided attribute values and conditions. + * For example, to change the status to be 1 for all customers whose status is 2: + * + * ~~~ + * Customer::updateAll(['status' => 1], ['status' => '2']); + * ~~~ + * + * @param array $attributes attribute values (name-value pairs) to be saved for the record. + * Unlike [[update()]] these are not going to be validated. + * @param array $condition the condition that matches the records that should get updated. + * Please refer to [[QueryInterface::where()]] on how to specify this parameter. + * An empty condition will match all records. + * @return integer the number of rows updated + */ + public static function updateAll($attributes, $condition = []) + { + $docs = static::findAll($condition); + + $count = 0; + foreach ($docs as $doc) { + foreach ($attributes as $key => $attribute) { + $doc->setAttribute($key, $attribute); + $doc->document->set($key, $attribute); + } + if (static::getDb()->getDocumentHandler()->update($doc->document)) { + $count++; + } + } + + return $count; + } + + /** + * Deletes records 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 condition that matches the records that should get deleted. + * Please refer to [[QueryInterface::where()]] on how to specify this parameter. + * An empty condition will match all records. + * @return integer the number of rows deleted + */ + public static function deleteAll($condition = null) + { + /** @var Document[] $docs */ + $records = static::findAll($condition); + + $count = 0; + foreach ($records as $record) { + if (static::getDb()->getDocumentHandler()->remove($record->document)) { + $count++; + } + } + + return $count; + } + + public static function truncate() + { + return static::getDb()->getCollectionHandler()->truncate(static::collectionName()); + } + + /** + * Saves the current record. + * + * This method will call [[insert()]] when [[getIsNewRecord()|isNewRecord]] is true, or [[update()]] + * when [[getIsNewRecord()|isNewRecord]] is false. + * + * For example, to save a customer record: + * + * ~~~ + * $customer = new Customer; // or $customer = Customer::findOne($id); + * $customer->name = $name; + * $customer->email = $email; + * $customer->save(); + * ~~~ + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be saved to database. `false` will be returned + * in this case. + * @param array $attributeNames list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the saving succeeds + */ + public function save($runValidation = true, $attributeNames = null) + { + if ($this->getIsNewRecord()) { + return $this->insert($runValidation, $attributeNames); + } else { + return $this->update($runValidation, $attributeNames) !== false; + } + } + + /** + * Deletes the record from the database. + * + * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. + * Note that it is possible that the number of rows deleted is 0, even though the deletion execution is successful. + */ + public function delete() + { + $result = false; + if ($this->beforeDelete()) { + $result = $this->deleteInternal(); + $this->afterDelete(); + } + + return $result; + } + + /** + * @see ActiveRecord::delete() + * @throws StaleObjectException + */ + protected function deleteInternal() + { + $condition = $this->getOldPrimaryKey(); + $lock = $this->optimisticLock(); + if ($lock !== null) { + $condition[$lock] = $this->$lock; + } + $result = static::getDb()->getDocumentHandler()->removeById(static::collectionName(), $condition); + if ($lock !== null && !$result) { + throw new StaleObjectException('The object being deleted is outdated.'); + } + $this->setOldAttributes(null); + + return $result; + } + + /** + * Returns a value indicating whether the current record is new (not saved in the database). + * @return boolean whether the record is new and should be inserted when calling [[save()]]. + */ + public function getIsNewRecord() + { + return $this->document->getIsNew(); + } + + public function setIsNewRecord($value) + { + $this->document->setIsNew($value); + } + + public function init() + { + parent::init(); + $this->setAttributes($this->defaultValues()); + } + + public function defaultValues() + { + return []; + } +} diff --git a/ArangoDbConnection.php b/ArangoDbConnection.php index 37d7d76..26c79f0 100644 --- a/ArangoDbConnection.php +++ b/ArangoDbConnection.php @@ -2,75 +2,18 @@ namespace devgroup\arangodb; -use yii\helpers\ArrayHelper; -use yii\base\Object; +use \triagens\ArangoDb\ConnectionOptions; -// set up some aliases for less typing later - -use triagens\ArangoDb\ConnectionOptions as ArangoConnectionOptions; -use triagens\ArangoDb\DocumentHandler as ArangoDocumentHandler; - -use triagens\ArangoDb\Document as ArangoDocument; -use triagens\ArangoDb\Exception as ArangoException; -use triagens\ArangoDb\ConnectException as ArangoConnectException; -use triagens\ArangoDb\ClientException as ArangoClientException; -use triagens\ArangoDb\ServerException as ArangoServerException; -use triagens\ArangoDb\UpdatePolicy as ArangoUpdatePolicy; -use triagens\ArangoDb\Statement as Statement; - -class ArangoDbConnection extends Object { - private $_connection = null; - public $connectionOptions = [ - // server endpoint to connect to - ArangoConnectionOptions::OPTION_ENDPOINT => 'tcp://127.0.0.1:8529', - // authorization type to use (currently supported: 'Basic') - ArangoConnectionOptions::OPTION_AUTH_TYPE => 'Basic', - // user for basic authorization - ArangoConnectionOptions::OPTION_AUTH_USER => 'root', - // password for basic authorization - ArangoConnectionOptions::OPTION_AUTH_PASSWD => '', - // connection persistence on server. can use either 'Close' (one-time connections) or 'Keep-Alive' (re-used connections) - ArangoConnectionOptions::OPTION_CONNECTION => 'Close', - // connect timeout in seconds - ArangoConnectionOptions::OPTION_TIMEOUT => 3, - // whether or not to reconnect when a keep-alive connection has timed out on server - ArangoConnectionOptions::OPTION_RECONNECT => true, - // optionally create new collections when inserting documents - ArangoConnectionOptions::OPTION_CREATE => true, - // optionally create new collections when inserting documents - ArangoConnectionOptions::OPTION_UPDATE_POLICY => ArangoUpdatePolicy::LAST, - ]; - - private $_collectionHandler = null; - private $_documentHandler = null; - - public function __construct($config=[]) +class ArangoDbConnection extends \triagens\ArangoDb\Connection +{ + public function json_encode_wrapper($data, $options = null) { - parent::__construct($config); + if ($this->getOption(ConnectionOptions::OPTION_CHECK_UTF8_CONFORM) === true) { + self::check_encoding($data); + } + + $response = json_encode($data, $options | JSON_FORCE_OBJECT); + + return $response; } - - public function init() - { - parent::init(); - - $this->_connection = new Connection($this->connectionOptions); - $this->_collectionHandler = new \triagens\ArangoDb\CollectionHandler($this->_connection); - $this->_documentHandler = new \triagens\ArangoDb\DocumentHandler($this->_connection); - } - - public function getDocument($collection, $id) { - return $this->documentHandler()->get($collection, $id); - } - - public function documentHandler() { - return $this->_documentHandler; - } - - public function statement($options=[]) { - return new Statement($this->_connection, $options); - } - - public function collectionHandler() { - return $this->_collectionHandler; - } -} \ No newline at end of file +} diff --git a/ArangoModel.php b/ArangoModel.php deleted file mode 100644 index 58d411c..0000000 --- a/ArangoModel.php +++ /dev/null @@ -1,109 +0,0 @@ -setDocument(Yii::$app->arango->getDocument(static::class_to_collection(get_called_class()), $id)) - ->setIsNewRecord(false); - - return $model; - } - - public function getAttributes($names=null, $except=['_id']){ - return parent::getAttributes($names, $except); - } - - /** - * @todo функция должна возвращать true/false в зависимости от результата - * Но аранга возвращает различный тип данных. Надо написать код - * - */ - public function save() - { - if ($this->_isNewRecord) { - // добавляем запись - $this->_doc = Document::createFromArray($this->getAttributes()); - - $result = intval(Yii::$app->arango->documentHandler()->add(static::class_to_collection(get_called_class()), $this->_doc)) > 0; - if ($result) { - $this->_isNewRecord = false; - } - return $result; - } else { - // патчим! - $doc_attributes = array_keys($this->_doc->getAll()); - - $attributes = $this->getAttributes(); - foreach ($attributes as $k=>$v) { - $this->_doc->set($k, $v); - unset($doc_attributes[$k]); - } - foreach ($doc_attributes as $key) { - if ($key != '_key') - unset($this->_doc->$key); - } - return Yii::$app->arango->documentHandler()->update($this->_doc); - } - } - - private static function class_to_collection($class) - { - $parts = explode("\\", $class); - return end($parts); - } - private static function id_to_int($class) - { - $parts = explode("/", $class); - return end($parts); - } - - public function setIsNewRecord($state) - { - $this->_isNewRecord = $state; - return $this; - } - - public function setDocument($doc) - { - $this->_doc = $doc; - $all = $this->_doc->getAll(); - $this->_id = $this->_doc->getInternalId(); - $this->setAttributes($all, false); - - return $this; - } - - public function delete() - { - - Yii::$app->arango->documentHandler()->deleteById( - static::class_to_collection(get_called_class()), - static::id_to_int($this->_doc->getInternalId()) - ); - } -} \ No newline at end of file diff --git a/ArangoProvider.php b/ArangoProvider.php deleted file mode 100644 index 2d33500..0000000 --- a/ArangoProvider.php +++ /dev/null @@ -1,142 +0,0 @@ -arango = Instance::ensure($this->arango, \devgroup\arangodb\ArangoDbConnection::className()); - if ($this->collection === null) { - throw new InvalidConfigException('The "collection" property must be set.'); - } - } - - /** - * @inheritdoc - */ - protected function prepareKeys($models) - { - - return array_keys($models); - - } - - /** - * @inheritdoc - */ - protected function prepareModels() - { - // $sql = $this->sql; - // $qb = $this->db->getQueryBuilder(); - // if (($sort = $this->getSort()) !== false) { - // $orderBy = $qb->buildOrderBy($sort->getOrders()); - // if (!empty($orderBy)) { - // $orderBy = substr($orderBy, 9); - // if (preg_match('/\s+order\s+by\s+[\w\s,\.]+$/i', $sql)) { - // $sql .= ', ' . $orderBy; - // } else { - // $sql .= ' ORDER BY ' . $orderBy; - // } - // } - // } - - // if (($pagination = $this->getPagination()) !== false) { - // $pagination->totalCount = $this->getTotalCount(); - // $sql .= ' ' . $qb->buildLimit($pagination->getLimit(), $pagination->getOffset()); - // } - - // return $this->db->createCommand($sql, $this->params)->queryAll(); - $statement = $this->getBaseStatement(); - - if (($pagination = $this->getPagination()) !== false) { - $pagination->totalCount = $this->getTotalCount(); - $statement->setQuery($statement->getQuery() . "\n LIMIT " . $pagination->getOffset() . ", " . $pagination->getLimit()); - } - - - $statement->setQuery($statement->getQuery()."\n RETURN a"); - $cursor = $statement->execute(); - $data = $cursor->getAll(); - $result = []; - foreach ($data as $doc) { - $item = $doc->getAll(); - foreach ($item as $k=>$v) { - if (is_array($item[$k]) || is_object($item[$k])) { - $item[$k] = json_encode($v, true); - } - } - $result[$item['_key']] = $item; - } - if (is_object($pagination)) { - $pagination->totalCount = $cursor->getFullCount(); - } - - return $result; - } - - public function getTotalCount() { - $statement = $this->getBaseStatement(); - $statement->setQuery($statement->getQuery(). "\n LIMIT 1 \n RETURN a"); - - - $cursor = $statement->execute(); - return $cursor->getFullCount(); - } - - private function getBaseStatement() { - $query = "FOR a in @@collection\n"; - - $filter = []; - $bindings = ['@collection' => $this->collection]; - $counter = 0; - foreach ($this->params as $k => $v) { - $filter[] = " a.@filter_field_$counter == @filter_value_$counter "; - $bindings["filter_field_$counter"] = $k; - $bindings["filter_value_$counter"] = $v; - $counter++; - } - if (count($filter)>0){ - $query .= "\nFILTER ".implode(" && ", $filter)."\n"; - } - - if ($this->sort) { - $query .= "\n SORT a." . $this->sort; - } - - $statement = $this->arango->statement([ - 'query' => $query, - 'count' => true, - 'bindVars' => $bindings, - 'fullCount' => true, - ]); - return $statement; - } - - /** - * @inheritdoc - */ - protected function prepareTotalCount() - { - return 0; - } -} \ No newline at end of file diff --git a/Connection.php b/Connection.php index 1868ea1..e19f7e8 100644 --- a/Connection.php +++ b/Connection.php @@ -2,18 +2,96 @@ namespace devgroup\arangodb; -use \triagens\ArangoDb\ConnectionOptions; +use triagens\ArangoDb\CollectionHandler; +use triagens\ArangoDb\ConnectionOptions; +use triagens\ArangoDb\Document; +use triagens\ArangoDb\DocumentHandler; +use triagens\ArangoDb\Statement; +use triagens\ArangoDb\UpdatePolicy; -class Connection extends \triagens\ArangoDb\Connection { - public function json_encode_wrapper($data, $options = null) +use yii\base\Object; + +class Connection extends Object +{ + private $connection = null; + + public $connectionOptions = [ + // server endpoint to connect to + ConnectionOptions::OPTION_ENDPOINT => 'tcp://127.0.0.1:8529', + // authorization type to use (currently supported: 'Basic') + ConnectionOptions::OPTION_AUTH_TYPE => 'Basic', + // user for basic authorization + ConnectionOptions::OPTION_AUTH_USER => 'root', + // password for basic authorization + ConnectionOptions::OPTION_AUTH_PASSWD => '', + // connection persistence on server. can use either 'Close' + // (one-time connections) or 'Keep-Alive' (re-used connections) + ConnectionOptions::OPTION_CONNECTION => 'Close', + // connect timeout in seconds + ConnectionOptions::OPTION_TIMEOUT => 3, + // whether or not to reconnect when a keep-alive connection has timed out on server + ConnectionOptions::OPTION_RECONNECT => true, + // optionally create new collections when inserting documents + ConnectionOptions::OPTION_CREATE => true, + // optionally create new collections when inserting documents + ConnectionOptions::OPTION_UPDATE_POLICY => UpdatePolicy::LAST, + ]; + + /** @var null|CollectionHandler $collectionHandler */ + private $collectionHandler = null; + /** @var null|DocumentHandler $documentHandler */ + private $documentHandler = null; + + public function init() { - if ($this->getOption(ConnectionOptions::OPTION_CHECK_UTF8_CONFORM) === true) { - self::check_encoding($data); - } + parent::init(); - - $response = json_encode($data, $options | JSON_FORCE_OBJECT); - - return $response; + $this->connection = new ArangoDbConnection($this->connectionOptions); + $this->collectionHandler = new CollectionHandler($this->connection); + $this->documentHandler = new DocumentHandler($this->connection); } -} \ No newline at end of file + + /** + * @return null|CollectionHandler + */ + public function getCollectionHandler() + { + return $this->collectionHandler; + } + + /** + * @param $collectionId + * @return \triagens\ArangoDb\Collection + */ + public function getCollection($collectionId) + { + return $this->getCollectionHandler()->get($collectionId); + } + + /** + * @return null|DocumentHandler + */ + public function getDocumentHandler() + { + return $this->documentHandler; + } + + /** + * @param $collectionId + * @param $documentId + * @return Document + */ + public function getDocument($collectionId, $documentId) + { + return $this->getDocumentHandler()->get($collectionId, $documentId); + } + + /** + * @param array $options + * @return Statement + */ + public function getStatement($options = []) + { + return new Statement($this->connection, $options); + } +} diff --git a/Exception.php b/Exception.php new file mode 100644 index 0000000..6e0b4ad --- /dev/null +++ b/Exception.php @@ -0,0 +1,14 @@ +db = Instance::ensure($this->db, Connection::className()); + } + + public function execute($aql, $bindValues = [], $params = []) + { + echo " > execute AQL: $aql ..."; + $time = microtime(true); + $options = [ + 'query' => $aql, + 'bindValues' => $bindValues, + ]; + $options = ArrayHelper::merge($params, $options); + $statement = $this->db->getStatement($options); + $statement->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + public function insert($collection, $columns) + { + echo " > insert into $collection ..."; + $time = microtime(true); + $this->db->getDocumentHandler()->save($collection, $columns); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + public function update($collection, $columns, $condition = '', $params = []) + { + echo " > update $collection ..."; + $time = microtime(true); + (new Query())->update($collection, $columns, $condition, $params)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + public function delete($collection, $condition = '', $params = []) + { + echo " > delete from $collection ..."; + $time = microtime(true); + (new Query())->remove($collection, $condition, $params)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + public function createCollection($collection, $options = []) + { + echo " > create collection $collection ..."; + $time = microtime(true); + $this->db->getCollectionHandler()->create($collection, $options); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + public function dropCollection($collection) + { + echo " > drop collection $collection ..."; + $time = microtime(true); + $this->db->getCollectionHandler()->drop($collection); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + public function truncateCollection($collection) + { + echo " > truncate collection $collection ..."; + $time = microtime(true); + $this->db->getCollectionHandler()->truncate($collection); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } +} diff --git a/Query.php b/Query.php new file mode 100644 index 0000000..1ed575e --- /dev/null +++ b/Query.php @@ -0,0 +1,606 @@ + 'buildNotCondition', + 'AND' => 'buildAndCondition', + 'OR' => 'buildAndCondition', + 'IN' => 'buildInCondition', + 'LIKE' => 'buildLikeCondition', + ]; + + protected $conditionMap = [ + 'NOT' => '!', + 'AND' => '&&', + 'OR' => '||', + 'IN' => 'in', + 'LIKE' => 'LIKE', + ]; + + public $select = []; + + public $from; + + public $where; + + public $limit; + + public $offset; + + public $orderBy; + + public $indexBy; + + public $params = []; + + /** + * @param array $options + * @param null|Connection $db + * @return null|Statement + */ + private function getStatement($options = [], $db = null) + { + if ($db === null) { + $db = Yii::$app->get('arangodb'); + } + + return $db->getStatement($options); + } + + public function createCommand($options = []) + { + list ($aql, $params) = $this->buildQuery($this); + + $options = ArrayHelper::merge( + $options, + [ + 'query' => $aql, + 'bindVars' => $params, + ] + ); + + return $this->getStatement($options); + } + + public function update($collection, $columns, $condition, $params) + { + $clauses = [ + $this->buildFrom($collection), + $this->buildWhere($condition, $params), + $this->buildUpdate($collection, $columns), + ]; + + $aql = implode($this->separator, array_filter($clauses)); + + $options = ArrayHelper::merge( + $params, + [ + 'query' => $aql, + 'bindVars' => $params, + ] + ); + + return $this->getStatement($options); + } + + public function remove($collection, $condition, $params) + { + $clauses = [ + $this->buildFrom($collection), + $this->buildWhere($condition, $params), + $this->buildRemove($collection), + ]; + + $aql = implode($this->separator, array_filter($clauses)); + + $options = ArrayHelper::merge( + $params, + [ + 'query' => $aql, + 'bindVars' => $params, + ] + ); + + return $this->getStatement($options); + } + + protected function buildUpdate($collection, $columns) + { + return 'UPDATE ' . $collection . 'WITH ' . Json::encode($columns) . ' IN ' . $collection; + } + + protected function buildRemove($collection) + { + return 'REMOVE ' . $collection . ' IN ' . $collection; + } + + /** + * @param $fields + * @return $this + */ + public function select($fields) + { + $this->select = $fields; + + return $this; + } + + public function from($collection) + { + $this->from = $collection; + + return $this; + } + + protected function buildFrom($collection) + { + $collection = trim($collection); + return $collection ? "FOR $collection IN $collection" : ''; + } + + public function quoteCollectionName($name) + { + if (strpos($name, '(') !== false || strpos($name, '{{') !== false) { + return $name; + } + if (strpos($name, '.') === false) { + return $name; + } + $parts = explode('.', $name); + foreach ($parts as $i => $part) { + $parts[$i] = $part; + } + + return implode('.', $parts); + + } + + public function quoteColumnName($name) + { + if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) { + return $name; + } + if (($pos = strrpos($name, '.')) !== false) { + $prefix = substr($name, 0, $pos); + $prefix = $this->quoteCollectionName($prefix) . '.'; + $name = substr($name, $pos + 1); + } else { + $prefix = $this->quoteCollectionName($this->from) . '.'; + } + + return $prefix . $name; + } + + protected function buildWhere($condition, &$params) + { + $where = $this->buildCondition($condition, $params); + + return $where === '' ? '' : 'FILTER ' . $where; + } + + protected function buildCondition($condition, &$params) + { + if (!is_array($condition)) { + return (string) $condition; + } elseif (empty($condition)) { + return ''; + } + + if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... + $operator = strtoupper($condition[0]); + if (isset($this->conditionBuilders[$operator])) { + $method = $this->conditionBuilders[$operator]; + array_shift($condition); + return $this->$method($operator, $condition, $params); + } else { + throw new InvalidParamException('Found unknown operator in query: ' . $operator); + } + } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... + return $this->buildHashCondition($condition, $params); + } + } + + protected function buildHashCondition($condition, &$params) + { + $parts = []; + foreach ($condition as $column => $value) { + if (is_array($value) || $value instanceof Query) { + // IN condition + $parts[] = $this->buildInCondition('IN', [$column, $value], $params); + } else { + if (strpos($column, '(') === false) { + $column = $this->quoteColumnName($column); + } + if ($value === null) { + $parts[] = "$column == null"; + } else { + $phName = self::PARAM_PREFIX . count($params); + $parts[] = "$column==@$phName"; + $params[$phName] = $value; + } + } + } + return count($parts) === 1 ? $parts[0] : '(' . implode(') && (', $parts) . ')'; + } + + protected function buildAndCondition($operator, $operands, &$params) + { + $parts = []; + foreach ($operands as $operand) { + if (is_array($operand)) { + $operand = $this->buildCondition($operand, $params); + } + if ($operand !== '') { + $parts[] = $operand; + } + } + if (!empty($parts)) { + return '(' . implode(") {$this->conditionMap[$operator]} (", $parts) . ')'; + } else { + return ''; + } + } + + protected function buildNotCondition($operator, $operands, &$params) + { + if (count($operands) != 1) { + throw new InvalidParamException("Operator '$operator' requires exactly one operand."); + } + + $operand = reset($operands); + if (is_array($operand)) { + $operand = $this->buildCondition($operand, $params); + } + if ($operand === '') { + return ''; + } + + return "{$this->conditionMap[$operator]} ($operand)"; + } + + protected function buildInCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1])) { + throw new Exception("Operator '$operator' requires two operands."); + } + + list($column, $values) = $operands; + + if ($values === [] || $column === []) { + return $operator === 'IN' ? '0=1' : ''; + } + + if ($values instanceof Query) { + // sub-query + list($sql, $params) = $this->buildQuery($values, $params); + $column = (array)$column; + if (is_array($column)) { + foreach ($column as $i => $col) { + if (strpos($col, '(') === false) { + $column[$i] = $this->quoteColumnName($col); + } + } + return '(' . implode(', ', $column) . ") {$this->conditionMap[$operator]} ($sql)"; + } else { + if (strpos($column, '(') === false) { + $column = $this->quoteColumnName($column); + } + return "$column {$this->conditionMap[$operator]} ($sql)"; + } + } + + $values = (array) $values; + + if (count($column) > 1) { + return $this->buildCompositeInCondition($operator, $column, $values, $params); + } + + if (is_array($column)) { + $column = reset($column); + } + foreach ($values as $i => $value) { + if (is_array($value)) { + $value = isset($value[$column]) ? $value[$column] : null; + } + if ($value === null) { + $values[$i] = 'null'; + } else { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value; + $values[$i] = "@$phName"; + } + } + if (strpos($column, '(') === false) { + $column = $this->quoteColumnName($column); + } + + if (count($values) > 1) { + return "$column {$this->conditionMap[$operator]} [" . implode(', ', $values) . ']'; + } else { + $operator = $operator === 'IN' ? '==' : '!='; + return $column . $operator . reset($values); + } + } + + protected function buildCompositeInCondition($operator, $columns, $values, &$params) + { + $vss = []; + foreach ($values as $value) { + $vs = []; + 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->quoteColumnName($column); + } + } + + return '(' . implode(', ', $columns) . ") {$this->conditionMap[$operator]} [" . implode(', ', $vss) . ']'; + } + + protected function buildLikeCondition($operator, $condition, &$params) + { + if (!(isset($condition[0]) && isset($condition[1]))) { + throw new InvalidParamException("You must set 'column' and 'pattern' params"); + } + $caseInsensitive = isset($condition[2]) ? (bool)$condition[2] : false; + return $this->conditionMap[$operator] + . '(' + . $this->quoteColumnName($condition[0]) + . ', "' + . $condition[1] + . '", ' + . ($caseInsensitive ? 'TRUE' : 'FALSE') + . ')'; + } + + protected function buildOrderBy($columns) + { + if (empty($columns)) { + return ''; + } + $orders = []; + foreach ($columns as $name => $direction) { + $orders[] = $this->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : ''); + } + + return 'SORT ' . implode(', ', $orders); + } + + protected function hasLimit($limit) + { + return is_string($limit) && ctype_digit($limit) || is_integer($limit) && $limit >= 0; + } + + protected function hasOffset($offset) + { + return is_integer($offset) && $offset > 0 || is_string($offset) && ctype_digit($offset) && $offset !== '0'; + } + + protected function buildLimit($limit, $offset) + { + $aql = ''; + if ($this->hasLimit($limit)) { + $aql = 'LIMIT ' . $limit; + if ($this->hasOffset($offset)) { + $aql .= ', ' . $offset; + } + } + + return $aql; + } + + protected function buildSelect($columns, &$params) + { + if ($columns == null || empty($columns)) { + return 'RETURN ' . $this->from; + } + + if (!is_array($columns)) { + return 'RETURN ' . $columns; + } + + $names = ''; + foreach ($columns as $name => $column) { + if (is_int($name)) { + $names .= $column . ', '; + } else { + $names .= "\"$name\": $this->from.$column, "; + } + } + + return 'RETURN {' . trim($names, ', ') . '}'; + } + + protected function buildQuery($query = null, $params = []) + { + $query = isset($query) ? $query : $this; + + if ($query->where === null) { + $where = []; + } else { + $where = $query->where; + } + + $params = empty($params) ? $query->params : array_merge($params, $query->params); + + $clauses = [ + $this->buildFrom($query->from), + $this->buildWhere($where, $params), + $this->buildOrderBy($query->orderBy, $params), + $this->buildLimit($query->limit, $query->offset, $params), + $this->buildSelect($query->select, $params), + ]; + + $aql = implode($query->separator, array_filter($clauses)); + + return [$aql, $params]; + } + + public function all($db = null) + { + $statement = $this->createCommand(); + $cursor = $statement->execute(); + return $cursor->getAll(); + } + + public function one($db = null) + { + $this->limit(1); + $statement = $this->createCommand(); + $cursor = $statement->execute(); + $result = $cursor->getAll(); + return empty($result) ? false : $result[0]; + } + + public function prepareResult($rows) + { + if ($this->indexBy === null) { + return $rows; + } + $result = []; + foreach ($rows as $row) { + if (is_string($this->indexBy)) { + $key = $row[$this->indexBy]; + } else { + $key = call_user_func($this->indexBy, $row); + } + $result[$key] = $row; + } + return $result; + } + + public function count($q = '*', $db = null) + { + $statement = $this->createCommand(); + $statement->setCount(true); + $cursor = $statement->execute(); + return $cursor->getCount(); + } + + public function exists($db = null) + { + $record = $this->one($db); + return !empty($record); + } + + public function indexBy($column) + { + $this->indexBy = $column; + return $this; + } + + public function where($condition) + { + $this->where = $condition; + return $this; + } + + public function andWhere($condition) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = ['AND', $this->where, $condition]; + } + return $this; + } + + public function orWhere($condition) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = ['OR', $this->where, $condition]; + } + return $this; + } + + public function filterWhere(array $condition) + { + // TODO: Implement filterWhere() method. + } + + public function andFilterWhere(array $condition) + { + // TODO: Implement andFilterWhere() method. + } + + public function orFilterWhere(array $condition) + { + // TODO: Implement orFilterWhere() method. + } + + public function orderBy($columns) + { + $this->orderBy = $this->normalizeOrderBy($columns); + return $this; + } + + public function addOrderBy($columns) + { + $columns = $this->normalizeOrderBy($columns); + if ($this->orderBy === null) { + $this->orderBy = $columns; + } else { + $this->orderBy = array_merge($this->orderBy, $columns); + } + return $this; + } + + public function limit($limit) + { + $this->limit = $limit; + return $this; + } + + public function offset($offset) + { + $this->offset = $offset; + return $this; + } + + protected function normalizeOrderBy($columns) + { + if (is_array($columns)) { + return $columns; + } else { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + $result = []; + foreach ($columns as $column) { + if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) { + $result[$matches[1]] = strcasecmp($matches[2], 'desc') ? SORT_ASC : SORT_DESC; + } else { + $result[$column] = SORT_ASC; + } + } + return $result; + } + } +} diff --git a/composer.json b/composer.json index 3d913d1..ce3d828 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,14 @@ { "name": "Alexander Kozhevnikov", "email": "b37hr3z3n@gmail.com" + }, + { + "name": "Evgeny Dubovitsky", + "email": "flynn068@gmail.com" + }, + { + "name": "Pavel Fedotov", + "email": "fps.06@mail.ru" } ], "require": { diff --git a/console/controllers/MigrateController.php b/console/controllers/MigrateController.php new file mode 100644 index 0000000..abd886c --- /dev/null +++ b/console/controllers/MigrateController.php @@ -0,0 +1,159 @@ +id !== 'create') { + if (is_string($this->db)) { + $this->db = \Yii::$app->get($this->db); + } + if (!$this->db instanceof Connection) { + throw new Exception("The 'db' option must refer to the application component ID of a ArangoDB connection."); + } + } + return true; + } else { + return false; + } + } + + /** + * Creates a new migration instance. + * @param string $class the migration class name + * @return Migration the migration instance + */ + protected function createMigration($class) + { + $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; + require_once($file); + + return new $class(['db' => $this->db]); + } + + /** + * @inheritdoc + */ + protected function getMigrationHistory($limit) + { + try { + $history = $this->getHistory($limit); + } catch (ServerException $ex) { + if ($ex->getServerCode() == 1203) { + $this->createMigrationHistoryCollection(); + $history = $this->getHistory($limit); + } else { + throw $ex; + } + } + unset($history[self::BASE_MIGRATION]); + + return $history; + } + + private function getHistory($limit) + { + $query = new Query; + $rows = $query->select(['version' => 'version', 'apply_time' => 'apply_time']) + ->from($this->migrationCollection) + ->orderBy('version DESC') + ->limit($limit) + ->all($this->db); + $history = ArrayHelper::map($rows, 'version', 'apply_time'); + unset($history[self::BASE_MIGRATION]); + + return $history; + } + + protected function createMigrationHistoryCollection() + { + echo "Creating migration history collection \"$this->migrationCollection\"..."; + $this->db->getCollectionHandler()->create($this->migrationCollection); + $this->db->getDocumentHandler()->save( + $this->migrationCollection, + [ + 'version' => self::BASE_MIGRATION, + 'apply_time' => time(), + ] + ); + echo "done.\n"; + } + + /** + * @inheritdoc + */ + protected function addMigrationHistory($version) + { + $this->db->getDocumentHandler()->save( + $this->migrationCollection, + [ + 'version' => $version, + 'apply_time' => time(), + ] + ); + } + + /** + * @inheritdoc + */ + protected function removeMigrationHistory($version) + { + $this->db->getCollectionHandler()->removeByExample( + $this->migrationCollection, + [ + 'version' => $version, + ] + ); + } +} diff --git a/views/migration.php b/views/migration.php new file mode 100644 index 0000000..1c0ca21 --- /dev/null +++ b/views/migration.php @@ -0,0 +1,24 @@ + + +class extends \app\components\arangodb\Migration +{ + public function up() + { + + } + + public function down() + { + echo " cannot be reverted.\n"; + + return false; + } +}