diff --git a/mirzaev/yii2-arangodb/ActiveQuery.php b/mirzaev/yii2-arangodb/ActiveQuery.php index 3d27e28..4a7d114 100644 --- a/mirzaev/yii2-arangodb/ActiveQuery.php +++ b/mirzaev/yii2-arangodb/ActiveQuery.php @@ -20,7 +20,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface parent::__construct($config); } - protected function buildQuery($query = null, $params = []) + protected function genQuery($query = null, $params = []) { if ($this->primaryModel !== null) { // lazy loading @@ -46,7 +46,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface } } - return parent::buildQuery($query, $params); + return parent::genQuery($query, $params); } private function createModels($rows) @@ -104,6 +104,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface { $statement = $this->createCommand($db); $token = $this->getRawAql($statement); + Yii::info($token, 'mirzaev\yii2\arangodb\Query::query'); try { Yii::beginProfile($token, 'mirzaev\yii2\arangodb\Query::query'); @@ -114,6 +115,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::query'); throw new \Exception($ex->getMessage(), (int) $ex->getCode(), $ex); } + if (!empty($rows)) { $models = $this->createModels($rows); if (!empty($this->with)) { diff --git a/mirzaev/yii2-arangodb/ActiveRecord.php b/mirzaev/yii2-arangodb/ActiveRecord.php index c1e3a61..ed10516 100644 --- a/mirzaev/yii2-arangodb/ActiveRecord.php +++ b/mirzaev/yii2-arangodb/ActiveRecord.php @@ -107,8 +107,8 @@ abstract class ActiveRecord extends BaseActiveRecord public static function find() { /** @var ActiveQuery $query */ - $query = \Yii::createObject(ActiveQuery::className(), [get_called_class()]); - $query->from(static::collectionName())->select(static::collectionName()); + $query = Yii::createObject(ActiveQuery::class, [get_called_class()]); + $query->collection(static::collectionName())->select(static::collectionName()); return $query; } @@ -123,6 +123,14 @@ abstract class ActiveRecord extends BaseActiveRecord $row = $row->getAll(); } + if (!is_iterable($row)) { + // Если нельзя обработать полученные данные + // Например, если прочитан NULL, то далее будет ошибка при обработке в foreach + + // Реинициализация + $row = []; + } + parent::populateRecord($record, $row); } diff --git a/mirzaev/yii2-arangodb/AqlExpression.php b/mirzaev/yii2-arangodb/AqlExpression.php index 80e8a79..35c4a75 100644 --- a/mirzaev/yii2-arangodb/AqlExpression.php +++ b/mirzaev/yii2-arangodb/AqlExpression.php @@ -2,9 +2,9 @@ namespace mirzaev\yii2\arangodb; -use yii\base\Object; +use yii\base\BaseObject; -class AqlExpression extends Object +class AqlExpression extends BaseObject { /** * @var string the AQL expression represented by this object diff --git a/mirzaev/yii2-arangodb/Query.php b/mirzaev/yii2-arangodb/Query.php index 4e52373..cfadb87 100644 --- a/mirzaev/yii2-arangodb/Query.php +++ b/mirzaev/yii2-arangodb/Query.php @@ -19,16 +19,17 @@ use Exception; class Query extends Component implements QueryInterface { const PARAM_PREFIX = 'qp'; + const DEBUG = true; public $separator = " "; protected $conditionBuilders = [ - 'NOT' => 'buildNotCondition', - 'AND' => 'buildAndCondition', - 'OR' => 'buildAndCondition', - 'IN' => 'buildInCondition', - 'LIKE' => 'buildLikeCondition', - 'BETWEEN' => 'buildBetweenCondition', + 'NOT' => 'genNotCondition', + 'AND' => 'genAndCondition', + 'OR' => 'genAndCondition', + 'IN' => 'genInCondition', + 'LIKE' => 'genLikeCondition', + 'BETWEEN' => 'genBetweenCondition' ]; protected $conditionMap = [ @@ -41,9 +42,26 @@ class Query extends Component implements QueryInterface public $select = []; - public $from; + public string|array $for; - public $where; + public string|array $in; + + public string $collection; + + public array $vars = []; + + /** + * Массив коллекций вершин и направлений для их обхода + * + * [ + * ["INBOUND" => "collection1"], + * ["OUTBOUND" => "collection2"], + * ["ANY" => "collection2"] + * ] + */ + public array $traversals = []; + + public $where = []; public $limit; @@ -103,7 +121,7 @@ class Query extends Component implements QueryInterface */ public function createCommand($db = null, $options = []) { - list($aql, $params) = $this->buildQuery($this); + list($aql, $params) = $this->genQuery($this); $options = ArrayHelper::merge( $options, @@ -162,28 +180,120 @@ class Query extends Component implements QueryInterface * @param $collection * @return $this */ - public function from($collection) + public function collection(string $collection) { - $this->from = $collection; + $this->collection = $collection; return $this; } /** - * @param $collection - * @return string */ - protected function buildFrom($collection) + public function for(string|array $for) { - $collection = trim($collection); - return $collection ? "FOR $collection IN $collection" : ''; + $this->for = $for; + + return $this; } /** - * @param $name + */ + public function in(string|array $in) + { + $this->in = $in; + + return $this; + } + + + /** + * Обойти коллекцию вершин по направлению + * + * Генерация AQL выражения + * + * @see https://www.arangodb.com/docs/3.7/aql/operations-let.html + * + * @param mixed $vertex Коллекция вершин из которой требуется обход + * @param string $direction Направление ('INBOUND', 'OUTBOUND', 'ANY') + */ + public function traversal(string $vertex, string $direction = 'INBOUND'): static + { + $this->traversals[] = [ + match ($direction) { + 'INBOUND', 'OUTBOUND', 'ANY' => $direction, + default => null + } + => $vertex + ]; + + return $this; + } + + /** + * Генерация AQL конструкции "FOR" + * + * Примеры: + * 1. "FOR account" + * 2. "FOR account, account_edge_supply" + * + */ + protected static function genFor(string|array $for): string + { + if (is_array($for)) { + // Если передан массив, то конвертировать в строку + + // Очистка элементов через trim() + array_walk($for, 'trim'); + + // Конвертация + $for = implode(", ", $for); + } + + // Генерация + return "FOR $for"; + } + + /** + * Генерация AQL конструкции "IN" + * + * Примеры: + * 1. "IN account" + * 2. "IN INBOUND supply account_edge_supply" + */ + protected static function genIn(string|array|null $in, array $traversals = null): string + { + if (is_array($in)) { + // Если передан массив, то конвертировать в строку + + // Очистка элементов через trim() + array_walk($in, 'trim'); + + // Конвертация + $in = implode(", ", $in); + } + + $expression = ''; + foreach ($traversals as $traversal) { + foreach ($traversal as $direction => $vertex) { + if ($aql = static::genTraversal($direction, $vertex)) { + $expression .= $aql . ', '; + } + } + } + $expression = trim($expression, ', '); + + // Если сгенерированное выражение не пустое, то добавить пробел + $expression = !empty($expression) ? $expression . ' ' : null; + + // Генерация + return "IN $expression" . $in; + } + + /** + * @param string|array $name Название * @return string */ - public function quoteCollectionName($name) + public static function quoteCollectionName(string|array $name): string { if (strpos($name, '(') !== false || strpos($name, '{{') !== false) { return $name; @@ -203,7 +313,7 @@ class Query extends Component implements QueryInterface * @param $name * @return string */ - public function quoteColumnName($name) + public function quoteColumnName(string $name) { if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) { return $name; @@ -213,7 +323,7 @@ class Query extends Component implements QueryInterface $prefix = $this->quoteCollectionName($prefix) . '.'; $name = substr($name, $pos + 1); } else { - $prefix = $this->quoteCollectionName($this->from) . '.'; + $prefix = $this->quoteCollectionName($this->collection) . '.'; } return $prefix . $name; @@ -224,19 +334,21 @@ class Query extends Component implements QueryInterface * @param $params * @return string */ - protected function buildWhere($condition, &$params) + protected function genWhere($condition, &$params) { - $where = $this->buildCondition($condition, $params); + $where = $this->genCondition($condition, $params); - return $where === '' ? '' : 'FILTER ' . $where; + return $where ? 'FILTER ' . $where : ''; } /** * @param $condition * @param $params * @return string + * + * @todo Разобраться с этим говном */ - protected function buildCondition($condition, &$params) + protected function genCondition($condition, &$params) { if (!is_array($condition)) { return (string) $condition; @@ -254,7 +366,7 @@ class Query extends Component implements QueryInterface throw new InvalidParamException('Found unknown operator in query: ' . $operator); } } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... - return $this->buildHashCondition($condition, $params); + return $this->genHashCondition($condition, $params); } } @@ -264,13 +376,13 @@ class Query extends Component implements QueryInterface * @return string * @throws Exception */ - protected function buildHashCondition($condition, &$params) + protected function genHashCondition($condition, &$params) { $parts = []; foreach ($condition as $column => $value) { if (is_array($value) || $value instanceof Query) { // IN condition - $parts[] = $this->buildInCondition('IN', [$column, $value], $params); + $parts[] = $this->genInCondition('IN', [$column, $value], $params); } else { if (strpos($column, '(') === false) { $column = $this->quoteColumnName($column); @@ -293,12 +405,12 @@ class Query extends Component implements QueryInterface * @param $params * @return string */ - protected function buildAndCondition($operator, $operands, &$params) + protected function genAndCondition($operator, $operands, &$params) { $parts = []; foreach ($operands as $operand) { if (is_array($operand)) { - $operand = $this->buildCondition($operand, $params); + $operand = $this->genCondition($operand, $params); } if ($operand !== '') { $parts[] = $operand; @@ -317,7 +429,7 @@ class Query extends Component implements QueryInterface * @param $params * @return string */ - protected function buildNotCondition($operator, $operands, &$params) + protected function genNotCondition($operator, $operands, &$params) { if (count($operands) != 1) { throw new InvalidParamException("Operator '$operator' requires exactly one operand."); @@ -325,7 +437,7 @@ class Query extends Component implements QueryInterface $operand = reset($operands); if (is_array($operand)) { - $operand = $this->buildCondition($operand, $params); + $operand = $this->genCondition($operand, $params); } if ($operand === '') { return ''; @@ -341,7 +453,7 @@ class Query extends Component implements QueryInterface * @return string * @throws Exception */ - protected function buildInCondition($operator, $operands, &$params) + protected function genInCondition($operator, $operands, &$params) { if (!isset($operands[0], $operands[1])) { throw new Exception("Operator '$operator' requires two operands."); @@ -355,7 +467,7 @@ class Query extends Component implements QueryInterface if ($values instanceof Query) { // sub-query - list($sql, $params) = $this->buildQuery($values, $params); + list($sql, $params) = $this->genQuery($values, $params); $column = (array)$column; if (is_array($column)) { foreach ($column as $i => $col) { @@ -375,7 +487,7 @@ class Query extends Component implements QueryInterface $values = (array) $values; if (count($column) > 1) { - return $this->buildCompositeInCondition($operator, $column, $values, $params); + return $this->genCompositeInCondition($operator, $column, $values, $params); } if (is_array($column)) { @@ -412,7 +524,7 @@ class Query extends Component implements QueryInterface * @param $params * @return string */ - protected function buildCompositeInCondition($operator, $columns, $values, &$params) + protected function genCompositeInCondition($operator, $columns, $values, &$params) { $vss = []; foreach ($values as $value) { @@ -446,7 +558,7 @@ class Query extends Component implements QueryInterface * @return string the generated AQL expression * @throws InvalidParamException if wrong number of operands have been given. */ - public function buildBetweenCondition($operator, $operands, &$params) + public function genBetweenCondition($operator, $operands, &$params) { if (!isset($operands[0], $operands[1], $operands[2])) { throw new InvalidParamException("Operator '$operator' requires three operands."); @@ -471,7 +583,7 @@ class Query extends Component implements QueryInterface * @param $params * @return string */ - protected function buildLikeCondition($operator, $condition, &$params) + protected function genLikeCondition($operator, $condition, &$params) { if (!(isset($condition[0]) && isset($condition[1]))) { throw new InvalidParamException("You must set 'column' and 'pattern' params"); @@ -491,7 +603,7 @@ class Query extends Component implements QueryInterface * @param $columns * @return string */ - protected function buildOrderBy($columns) + protected function genOrderBy($columns) { if (empty($columns)) { return ''; @@ -527,7 +639,7 @@ class Query extends Component implements QueryInterface * @param $offset * @return string */ - protected function buildLimit($limit, $offset) + protected function genLimit($limit, $offset) { $aql = ''; if ($this->hasLimit($limit)) { @@ -542,22 +654,17 @@ class Query extends Component implements QueryInterface * @param $params * @return string */ - protected function buildSelect($columns, &$params) // А нахуй здесь params ещё и ссылкой? Потом проверить + protected function genSelect($columns, &$params) // А нахуй здесь params ещё и ссылкой? Потом проверить { if ($columns === null || empty($columns)) { - return 'RETURN ' . $this->from; + return 'RETURN ' . $this->collection; } if (!is_array($columns)) { return 'RETURN ' . $columns; } - $names = ''; - foreach ($columns as $name => $column) { - $names .= "\"$name\": $this->from.$column, "; - } - - return 'RETURN {' . trim($names, ', ') . '}'; + return 'RETURN ' . self::convertArray2Aql($columns, $this->collection); } /** @@ -565,24 +672,20 @@ class Query extends Component implements QueryInterface * @param array $params * @return array */ - protected function buildQuery($query = null, $params = []) + protected function genQuery($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), + static::genFor($query->for ?? $query->collection), + static::genIn($query->in ?? $query->collection, $query->traversals), + static::genLet($query->vars), + $this->genWhere($query->where, $params), + $this->genOrderBy($query->orderBy, $params), + $this->genLimit($query->limit, $query->offset, $params), + $this->genSelect($query->select, $params), ]; $aql = implode($query->separator, array_filter($clauses)); @@ -622,6 +725,7 @@ class Query extends Component implements QueryInterface { $statement = $this->createCommand($db); $token = $this->getRawAql($statement); + Yii::info($token, 'mirzaev\yii2\arangodb\Query::query'); try { Yii::beginProfile($token, 'mirzaev\yii2\arangodb\Query::query'); @@ -671,7 +775,7 @@ class Query extends Component implements QueryInterface $clauses = [ "INSERT $doc IN {$this->quoteCollectionName($collection)}", - $this->buildOptions(), + $this->genOptions(), ]; $aql = implode($this->separator, array_filter($clauses)); @@ -708,12 +812,13 @@ class Query extends Component implements QueryInterface */ public function update($collection, $columns, $condition = [], $params = [], $db = null) { - $this->from($collection); + $this->collection = $collection; $clauses = [ - $this->buildFrom($collection), - $this->buildWhere($condition, $params), - $this->buildUpdate($collection, $columns), - $this->buildOptions(), + $this->genFor($collection), + $this->genIn($collection), + $this->genWhere($condition, $params), + $this->genUpdate($collection, $columns), + $this->genOptions(), ]; $aql = implode($this->separator, array_filter($clauses)); @@ -744,31 +849,33 @@ class Query extends Component implements QueryInterface } /** - * @param $collection - * @param $columns - * @param array $condition + * Представление + * + * Работа с представлениями + * + * @see https://www.arangodb.com/docs/3.7/http/views.html + * + * @param string $collection + * @param string|array $vars + * @param array $expression + * @param string $type * @param array $params - * @param null $db - * @return bool - * @throws Exception + * @param Connection $db */ - public function search(string|array $collection, string|array $vars, array $expression, int $type = 0, $params = [], $db = null): array + public function view(string $collection, string|array|null $vars = null, array $expression = null, string $type = null, array $params = [], $db = null): array { - $this->from($collection); + $this->collection = $collection; $clauses = [ - $this->buildFrom($collection), - $this->buildSearch($expression, $type), - $this->buildLimit($this->limit, 0), - $this->buildOptions(), - $this->buildSelect($vars, $params) + $this->genFor($collection), + $this->genIn($collection), + $this->genSearch($expression, $type), + $this->genLimit($this->limit, 0), + $this->genOptions(), + $this->genSelect($vars, $params) ]; $aql = implode($this->separator, array_filter($clauses)); - $fp = fopen('debug.txt', 'a'); - fwrite($fp, print_r($aql, true) . PHP_EOL); - fclose($fp); - $params = ArrayHelper::merge( $params, [ @@ -791,6 +898,25 @@ class Query extends Component implements QueryInterface return $this->prepareResult($cursor->getAll()); } + /** + * Поиск ребра + * + * Работа с представлениями + */ + // public function searchEdge(string $collection, string $_from, string $_to, string|array|null $vars = null, string $direction = 'INBOUND', array $expression = null, array $params = [], $db = null) + // { + // $this->collection = $collection; + // $clauses = [ + // $this->genFor($_from), + // $this->genLet($collection, $this->genFor([$_from, $collection], [$_to, $collection], $direction), $params), + // $this->genLimit($this->limit, 0), + // $this->genOptions(), + // $this->genSelect($vars, $params) + // ]; + + // $aql = implode($this->separator, array_filter($clauses)); + // } + /** * @param $collection * @param array $condition @@ -801,12 +927,13 @@ class Query extends Component implements QueryInterface */ public function remove($collection, $condition = [], $params = [], $db = null) { - $this->from($collection); + $this->collection = $collection; $clauses = [ - $this->buildFrom($collection), - $this->buildWhere($condition, $params), - $this->buildRemove($collection), - $this->buildOptions(), + $this->genFor($collection), + $this->genIn($collection), + $this->genWhere($condition, $params), + $this->genRemove($collection), + $this->genOptions(), ]; $aql = implode($this->separator, array_filter($clauses)); @@ -841,7 +968,7 @@ class Query extends Component implements QueryInterface * @param $columns * @return string */ - protected function buildUpdate($collection, $columns) + protected function genUpdate($collection, $columns) { return 'UPDATE ' . $collection . ' WITH ' . Serializer::encode($columns) . ' IN ' @@ -852,7 +979,7 @@ class Query extends Component implements QueryInterface * @param $collection * @return string */ - protected function buildRemove($collection) + protected function genRemove($collection) { return 'REMOVE ' . $collection . ' IN ' . $collection; } @@ -862,24 +989,108 @@ class Query extends Component implements QueryInterface * @param $columns * @return string */ - protected function buildSearch(array $expression, int $type = 0): string + protected function genSearch(array $expression, string $type = 'START'): string { $query = 'SEARCH '; - return match ($type) { - 1 => $query . $this->filterStartsWith($expression), + return match (strtoupper($type)) { + 'START', 'STARTS', 'STARTS_WITH' => $query . $this->filterStartsWith($expression), + 'CONTAINS', 'LIKE' => $query . $this->filterContains($expression), default => $query . Serializer::encode($expression) }; } + /** + * Присвоение переменной значения + * + * Генерация AQL выражения + * + * @see https://www.arangodb.com/docs/3.7/aql/operations-let.html + * + * @param array $vars Ключ - переменная, значение - её значение + */ + protected static function genLet(array $vars): ?string + { + // Инициализация + $result = ''; + + // Конвертация + foreach ($vars as $name => $value) { + if ( + $value[0] === '(' + || $value[0] === '"' && substr($value, -1) === '"' + || $value[0] === "'" && substr($value, -1) === "'" + ) { + $condition = $value; + } else { + $condition = '"' . $value . '"'; + } + + $result .= 'LET ' . $name . ' = ' . $condition . ', '; + } + + return trim($result, ', '); + } + + /** + * Обойти коллекцию вершин по направлению + * + * Генерация AQL выражения + * + * @see https://www.arangodb.com/docs/3.7/aql/operations-let.html + * + * @param string $direction Направление + * @param mixed $vertex Коллекция вершин из которой требуется обход + */ + protected static function genTraversal(string $direction, string $vertex): ?string + { + return $direction . ' ' . $vertex; + } + /** * @return string */ - protected function buildOptions() + protected function genOptions() { return empty($this->options) ? '' : ' OPTIONS ' . Json::encode($this->options); } + /** + * Конвертер Array -> AQL (string) + * + * Примеры: + * 1. {"id": product_search._key, "catn": product_search.catn} + * 2. {"name": login, "phone": number} + * 3. {"users": users} + * 4. {} + * + * @param array $target Массив для конвертации + * @param string|null Название коллекции к которой привязывать значения массива + */ + protected static function convertArray2Aql(array $target, string|null $collection = null): string + { + // Инициализация + $result = ''; + + // Конвертация + foreach ($target as $name => $value) { + $result .= "\"$name\": "; + + if (is_null($collection)) { + // Коллекция не отправлена + + $result .= "$value, "; + } else { + // Коллекция отправлена + + $result .= "$collection.$value, "; + } + } + + return '{' . trim($result, ', ') . '}'; + } + + /** * @param Document[] $rows * @return array @@ -955,6 +1166,7 @@ class Query extends Component implements QueryInterface public function indexBy($column) { $this->indexBy = $column; + return $this; } @@ -963,10 +1175,19 @@ class Query extends Component implements QueryInterface * @param array $params * @return $this|static */ - public function where($condition, $params = []) + public function where($condition) { $this->where = $condition; - $this->addParams($params); + + return $this; + } + + /** + */ + public function let(string $name, mixed $value): static + { + $this->vars[$name] = $value; + return $this; } @@ -1007,7 +1228,7 @@ class Query extends Component implements QueryInterface * * This method is similar to [[where()]]. The main difference is that this method will * remove [[isEmpty()|empty query operands]]. As a result, this method is best suited - * for building query conditions based on filter values entered by users. + * for gening query conditions based on filter values entered by users. * * The following code shows the difference between this method and [[where()]]: * @@ -1046,9 +1267,26 @@ class Query extends Component implements QueryInterface // Генерация foreach ($expression as $key => $value) { if ($return) { - $return .= ' OR STARTS_WITH(' . $this->quoteCollectionName($this->from) . ".$key, \"$value\")"; + $return .= ' OR STARTS_WITH(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\")"; } else { - $return = 'STARTS_WITH(' . $this->quoteCollectionName($this->from) . ".$key, \"$value\")"; + $return = 'STARTS_WITH(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\")"; + } + } + + return $return; + } + + public function filterContains(array $expression): string + { + // Инициализация + $return = []; + + // Генерация + foreach ($expression as $key => $value) { + if ($return) { + $return .= ' OR LIKE(' . $this->quoteCollectionName($this->collection) . ".$key, \"%$value%\")"; + } else { + $return = 'LIKE(' . $this->quoteCollectionName($this->collection) . ".$key, \"%$value%\")"; } } @@ -1061,7 +1299,7 @@ class Query extends Component implements QueryInterface * * This method is similar to [[andWhere()]]. The main difference is that this method will * remove [[isEmpty()|empty query operands]]. As a result, this method is best suited - * for building query conditions based on filter values entered by users. + * for gening query conditions based on filter values entered by users. * * @param array $condition the new WHERE condition. Please refer to [[where()]] * on how to specify this parameter. @@ -1084,7 +1322,7 @@ class Query extends Component implements QueryInterface * * This method is similar to [[orWhere()]]. The main difference is that this method will * remove [[isEmpty()|empty query operands]]. As a result, this method is best suited - * for building query conditions based on filter values entered by users. + * for gening query conditions based on filter values entered by users. * * @param array $condition the new WHERE condition. Please refer to [[where()]] * on how to specify this parameter. @@ -1287,6 +1525,7 @@ class Query extends Component implements QueryInterface } } } + return $this; }