arango recode

This commit is contained in:
evgen-d 2014-07-29 09:33:21 +04:00
parent 12c3862177
commit 4bb94c60c6
12 changed files with 1607 additions and 331 deletions

166
ActiveQuery.php Normal file
View File

@ -0,0 +1,166 @@
<?php
namespace devgroup\arangodb;
use yii\base\InvalidConfigException;
use yii\db\ActiveQueryInterface;
use yii\db\ActiveQueryTrait;
use yii\db\ActiveRecordInterface;
use yii\db\ActiveRelationTrait;
use yii\helpers\VarDumper;
use triagens\ArangoDb\Document;
class ActiveQuery extends Query implements ActiveQueryInterface
{
use ActiveQueryTrait;
use ActiveRelationTrait;
public function __construct($modelClass, $config = [])
{
$this->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;
}
}
}

440
ActiveRecord.php Normal file
View File

@ -0,0 +1,440 @@
<?php
namespace devgroup\arangodb;
use Yii;
use yii\base\ArrayableTrait;
use yii\base\InvalidConfigException;
use yii\base\Model;
use yii\db\ActiveQueryInterface;
use yii\db\BaseActiveRecord;
use yii\db\StaleObjectException;
use yii\helpers\ArrayHelper;
use yii\helpers\Inflector;
use yii\helpers\StringHelper;
use triagens\ArangoDb\Document;
abstract class ActiveRecord extends BaseActiveRecord
{
/** @var Document $document */
private $document;
public function __construct($config = [])
{
$this->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 [];
}
}

View File

@ -2,75 +2,18 @@
namespace devgroup\arangodb; namespace devgroup\arangodb;
use yii\helpers\ArrayHelper; use \triagens\ArangoDb\ConnectionOptions;
use yii\base\Object;
// set up some aliases for less typing later class ArangoDbConnection extends \triagens\ArangoDb\Connection
{
use triagens\ArangoDb\ConnectionOptions as ArangoConnectionOptions; public function json_encode_wrapper($data, $options = null)
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=[])
{ {
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;
}
}

View File

@ -1,109 +0,0 @@
<?php
namespace devgroup\arangodb;
use yii;
use triagens\ArangoDb\Document;
class ArangoModel extends \yii\base\Model {
private $_isNewRecord = true;
private $_doc = null;
public $_id = null;
public static function findById($id)
{
$parts = explode("\\", $id);
if (count($parts)==2) {
$id = $parts[1]; // для формата "Collection\1237643123"
} else {
$parts = explode("/", $id); // для формата "Collection/123123321"
if (count($parts)==2) {
$id = $parts[1];
}
}
$model = new static;
$model
->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())
);
}
}

View File

@ -1,142 +0,0 @@
<?php
namespace devgroup\arangodb;
use yii;
use yii\di\Instance;
use triagens\ArangoDb\Document;
class ArangoProvider extends yii\data\ActiveDataProvider
{
public $arango = 'arango';
public $collection;
/**
* @var array parameters for example
*/
public $params = [];
public $sort = '';
public function init()
{
parent::init();
$this->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;
}
}

View File

@ -2,18 +2,96 @@
namespace devgroup\arangodb; 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 { use yii\base\Object;
public function json_encode_wrapper($data, $options = null)
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) { parent::init();
self::check_encoding($data);
}
$this->connection = new ArangoDbConnection($this->connectionOptions);
$response = json_encode($data, $options | JSON_FORCE_OBJECT); $this->collectionHandler = new CollectionHandler($this->connection);
$this->documentHandler = new DocumentHandler($this->connection);
return $response;
} }
}
/**
* @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);
}
}

14
Exception.php Normal file
View File

@ -0,0 +1,14 @@
<?php
namespace devgroup\arangodb;
class Exception extends \yii\base\Exception
{
/**
* @return string the user-friendly name of this exception
*/
public function getName()
{
return 'ArangoDB Exception';
}
}

89
Migration.php Normal file
View File

@ -0,0 +1,89 @@
<?php
namespace devgroup\arangodb;
use yii\base\Component;
use yii\db\MigrationInterface;
use yii\di\Instance;
use yii\helpers\ArrayHelper;
abstract class Migration extends Component implements MigrationInterface
{
/**
* @var Connection|string the DB connection object or the application component ID of the DB connection
* that this migration should work with.
*/
public $db = 'arangodb';
/**
* Initializes the migration.
* This method will set [[db]] to be the 'db' application component, if it is null.
*/
public function init()
{
parent::init();
$this->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";
}
}

606
Query.php Normal file
View File

@ -0,0 +1,606 @@
<?php
namespace devgroup\arangodb;
use Yii;
use yii\base\Component;
use yii\base\InvalidParamException;
use yii\db\QueryInterface;
use yii\helpers\ArrayHelper;
use yii\helpers\Json;
use yii\helpers\VarDumper;
use triagens\ArangoDb\Statement;
class Query extends Component implements QueryInterface
{
const PARAM_PREFIX = 'qp';
public $separator = " ";
protected $conditionBuilders = [
'NOT' => '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;
}
}
}

View File

@ -8,6 +8,14 @@
{ {
"name": "Alexander Kozhevnikov", "name": "Alexander Kozhevnikov",
"email": "b37hr3z3n@gmail.com" "email": "b37hr3z3n@gmail.com"
},
{
"name": "Evgeny Dubovitsky",
"email": "flynn068@gmail.com"
},
{
"name": "Pavel Fedotov",
"email": "fps.06@mail.ru"
} }
], ],
"require": { "require": {

View File

@ -0,0 +1,159 @@
<?php
namespace devgroup\arangodb\console\controllers;
use devgroup\arangodb\Connection;
use devgroup\arangodb\Exception;
use devgroup\arangodb\Migration;
use devgroup\arangodb\Query;
use yii;
use yii\console\controllers\BaseMigrateController;
use yii\helpers\ArrayHelper;
use triagens\ArangoDb\ServerException;
class MigrateController extends BaseMigrateController
{
/**
* @var string the name of the collection for keeping applied migration information.
*/
public $migrationCollection = 'migration';
/**
* @var string the directory storing the migration classes. This can be either
* a path alias or a directory.
*/
public $migrationPath = '@app/migrations/arangodb';
/**
* @inheritdoc
*/
public $templateFile = '@devgroup/arangodb/views/migration.php';
/**
* @var Connection|string the DB connection object or the application
* component ID of the DB connection.
*/
public $db = 'arangodb';
/**
* @inheritdoc
*/
public function options($actionId)
{
return array_merge(
parent::options($actionId),
['migrationCollection', 'db'] // global for all actions
);
}
/**
* This method is invoked right before an action is to be executed (after all possible filters.)
* It checks the existence of the [[migrationPath]].
* @param yii\base\Action $action the action to be executed.
* @throws Exception if db component isn't configured
* @return boolean whether the action should continue to be executed.
*/
public function beforeAction($action)
{
if (parent::beforeAction($action)) {
if ($action->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,
]
);
}
}

24
views/migration.php Normal file
View File

@ -0,0 +1,24 @@
<?php
/**
* This view is used by console/controllers/MigrateController.php
* The following variables are available in this view:
*/
/* @var $className string the new migration class name */
echo "<?php\n";
?>
class <?= $className ?> extends \app\components\arangodb\Migration
{
public function up()
{
}
public function down()
{
echo "<?= $className ?> cannot be reverted.\n";
return false;
}
}