Skip to content

Commit

Permalink
Merge branch 'relations'
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/ActiveQuery.php
#	tests/RelationTest.php
  • Loading branch information
simialbi committed Jul 8, 2021
2 parents 890a4db + 43d8b40 commit e7cd6dd
Show file tree
Hide file tree
Showing 4 changed files with 322 additions and 110 deletions.
339 changes: 229 additions & 110 deletions src/ActiveQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
namespace simialbi\yii2\rest;

use yii\base\InvalidConfigException;
use yii\base\NotSupportedException;
use yii\db\ActiveQueryInterface;
use yii\db\ActiveQueryTrait;
use yii\db\ActiveRelationTrait;
Expand Down Expand Up @@ -57,6 +56,126 @@ public function createCommand($db = null)
return parent::createCommand($db);
}

/**
* {@inheritdoc}
* @throws InvalidConfigException
*/
public function prepare($builder)
{
if (!empty($this->joinWith)) {
$this->buildJoinWith();
$this->joinWith = null;
}

if ($this->primaryModel === null) {
// eager loading
$query = Query::create($this);
} else {
// lazy loading of a relation
$where = $this->where;

if ($this->via instanceof self) {
// via junction table
$viaModels = $this->via->findJunctionRows([$this->primaryModel]);
$this->filterByModels($viaModels);
} elseif (is_array($this->via)) {
// via relation
/* @var $viaQuery ActiveQuery */
list($viaName, $viaQuery) = $this->via;
if ($viaQuery->multiple) {
if ($this->primaryModel->isRelationPopulated($viaName)) {
$viaModels = $this->primaryModel->$viaName;
} else {
$viaModels = $viaQuery->all();
$this->primaryModel->populateRelation($viaName, $viaModels);
}
} else {
if ($this->primaryModel->isRelationPopulated($viaName)) {
$model = $this->primaryModel->$viaName;
} else {
$model = $viaQuery->one();
$this->primaryModel->populateRelation($viaName, $model);
}
$viaModels = $model === null ? [] : [$model];
}
$this->filterByModels($viaModels);
} else {
$this->filterByModels([$this->primaryModel]);
}

$query = Query::create($this);
$this->andWhere($where);
}

return $query;
}

/**
* Builds join with clauses
*/
private function buildJoinWith()
{
$join = $this->join;
$this->join = [];
$model = new $this->modelClass();
foreach ($this->joinWith as $with) {
$this->joinWithRelations($model, $with);
foreach ($with as $name => $callback) {
$this->innerJoin(is_int($name) ? $callback : [$name => $callback]);
unset($with[$name]);
}
}
if (!empty($join)) {
// append explicit join to joinWith()
// https://github.com/yiisoft/yii2/issues/2880
$this->join = empty($this->join) ? $join : array_merge($this->join, $join);
}
}

/**
* Modifies the current query by adding join fragments based on the given relations.
* @param ActiveRecord $model the primary model
* @param array $with the relations to be joined
*/
protected function joinWithRelations($model, $with)
{
foreach ($with as $name => $callback) {
if (is_int($name)) {
$name = $callback;
$callback = null;
}
$primaryModel = $model;
$parent = $this;
if (!isset($relations[$name])) {
$relations[$name] = $relation = $primaryModel->getRelation($name);
/* @var $relation static */
if ($callback !== null) {
call_user_func($callback, $relation);
}
if (!empty($relation->joinWith)) {
$relation->buildJoinWith();
}
$this->joinWithRelation($parent, $relation);
}
}
}

/**
* Joins a parent query with a child query.
* The current query object will be modified accordingly.
*
* @param ActiveQuery $parent
* @param ActiveQuery $child
*/
private function joinWithRelation($parent, $child)
{
if (!empty($child->join)) {
foreach ($child->join as $join) {
$this->join[] = $join;
}
}
}

/**
* {@inheritdoc}
*/
Expand All @@ -72,7 +191,7 @@ public function all($db = null)
public function one($db = null)
{
$row = parent::one($db);
if (!empty($row)) {
if ($row !== false) {
$models = $this->populate(isset($row[0]) ? $row : [$row]);

return reset($models) ?: null;
Expand All @@ -81,22 +200,122 @@ public function one($db = null)
return null;
}


/**
* {@inheritdoc}
* @throws InvalidConfigException
*/
public function populate($rows)
{
if (empty($rows)) {
return [];
}

$models = $this->createModels($rows);
if (!empty($this->join) && $this->indexBy === null) {
$models = $this->removeDuplicatedModels($models);
}
if (!empty($this->with)) {
$this->findWith($this->with, $models);
}
if (!$this->asArray) {
foreach ($models as $model) {
$model->afterFind();
}
} elseif ($this->indexBy !== null) {
$models = ArrayHelper::index($models, $this->indexBy);
}

return $models;
}

/**
* {@inheritDoc}
* @throws NotSupportedException
*/
public function via($relationName, callable $callable = null)
protected function createModels($rows)
{
throw new NotSupportedException('Via relations are not supported in rest applications');
if ($this->asArray) {
return $rows;
} else {
$models = [];
/* @var $class ActiveRecord */
$class = $this->modelClass;
foreach ($rows as $row) {
$model = $class::instantiate($row);
/** @var $modelClass ActiveRecord */
$modelClass = get_class($model);
$modelClass::populateRecord($model, $row);
if (!empty($this->join)) {
foreach ($this->join as $join) {
if (isset($join[1], $row[$join[1]])) {
$relation = $model->getRelation($join[1]);
$rows = (ArrayHelper::isAssociative($row[$join[1]])) ? [$row[$join[1]]] : $row[$join[1]];
$relations = $relation->populate($rows);
$model->populateRelation($join[1], $relation->multiple ? $relations : $relations[0]);
}
}
}
$models[] = $model;
}
return $models;
}
}

/**
* {@inheritdoc}
* @throws InvalidConfigException
* Removes duplicated models by checking their primary key values.
* This method is mainly called when a join query is performed, which may cause duplicated rows being returned.
*
* @param array $models the models to be checked
*
* @return array the distinctive models
* @throws InvalidConfigException if model primary key is empty
*/
public function prepare($builder)
private function removeDuplicatedModels($models)
{
return Query::create($this);
$hash = [];
/* @var $class ActiveRecord */
$class = $this->modelClass;
$pks = $class::primaryKey();

if (count($pks) > 1) {
// composite primary key
foreach ($models as $i => $model) {
$key = [];
foreach ($pks as $pk) {
if (!isset($model[$pk])) {
// do not continue if the primary key is not part of the result set
break 2;
}
$key[] = $model[$pk];
}
$key = serialize($key);
if (isset($hash[$key])) {
unset($models[$i]);
} else {
$hash[$key] = true;
}
}
} elseif (empty($pks)) {
/** @var $class string */
throw new InvalidConfigException("Primary key of '{$class}' can not be empty.");
} else {
// single column primary key
$pk = reset($pks);
foreach ($models as $i => $model) {
if (!isset($model[$pk])) {
// do not continue if the primary key is not part of the result set
break;
}
$key = $model[$pk];
if (isset($hash[$key])) {
unset($models[$i]);
} elseif ($key !== null) {
$hash[$key] = true;
}
}
}

return array_values($models);
}

/**
Expand Down Expand Up @@ -135,107 +354,7 @@ public function prepare($builder)
*/
public function joinWith($with)
{
if (is_array($with)) {
$this->join = array_merge((array)$this->join, $with);
} else {
$this->join[] = $with;
}

return $this;
}

/**
* {@inheritDoc}
*/
public function with()
{
$this->joinWith(func_get_args());

$this->joinWith[] = (array)$with;
return $this;
}

/**
* {@inheritdoc}
* @throws InvalidConfigException
*/
public function populate($rows)
{
if (empty($rows)) {
return [];
}

if ($this->asArray) {
if ($this->indexBy) {
return ArrayHelper::index($rows, $this->indexBy);
}

return $rows;
}

$models = $this->createModels($rows);
foreach ($models as $model) {
$model->afterFind();
}

return $models;
}

/**
* {@inheritDoc}
*/
protected function createModels($rows)
{
$models = [];
/** @var ActiveRecord $class */
$class = $this->modelClass;
$namespace = null;
foreach ($rows as $row) {
$model = $class::instantiate($row);
$relations = $model->getRelations();
/** @var ActiveRecord $modelClass */
$modelClass = get_class($model);
$modelClass::populateRecord($model, $row);
$models[] = $model;
if (is_array($this->join)) {
foreach ($this->join as $join) {
$relationRows = ArrayHelper::remove($row, $join, []);
if (empty($relationRows)) {
continue;
}
if (isset($relations[$join])) {
$relationClass = $relations[$join];
if (!class_exists($relationClass)) {
if (null === $namespace && strpos($relationClass, '\\') === false) {
$r = new \ReflectionClass($this->modelClass);
$namespace = $r->getNamespaceName();
}

if (class_exists($namespace . '\\' . $relationClass)) {
$relationClass = $namespace . '\\' . $relationClass;
}
}
if (class_exists($relationClass)) {
/** @var ActiveRecord $relationClass */
if (ArrayHelper::isAssociative($relationRows)) {
$relationModel = $relationClass::instantiate($relationRows);
$relationClass::populateRecord($relationModel, $relationRows);
$model->populateRelation($join, $relationModel);
} else {
$populatedRows = [];
foreach ($relationRows as $relationRow) {
$relationModel = $relationClass::instantiate($relationRow);
$relationClass::populateRecord($relationModel, $relationRow);
$populatedRows[] = $relationModel;
}
$model->populateRelation($join, $populatedRows);
}
}
} else {
$model->populateRelation($join, ArrayHelper::remove($row, $join, []));
}
}
}
}
return $models;
}
}
Loading

0 comments on commit e7cd6dd

Please sign in to comment.