Skip to content

Commit e7cd6dd

Browse files
author
Simon Karlen
committed
Merge branch 'relations'
# Conflicts: # src/ActiveQuery.php # tests/RelationTest.php
2 parents 890a4db + 43d8b40 commit e7cd6dd

File tree

4 files changed

+322
-110
lines changed

4 files changed

+322
-110
lines changed

src/ActiveQuery.php

Lines changed: 229 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
namespace simialbi\yii2\rest;
1010

1111
use yii\base\InvalidConfigException;
12-
use yii\base\NotSupportedException;
1312
use yii\db\ActiveQueryInterface;
1413
use yii\db\ActiveQueryTrait;
1514
use yii\db\ActiveRelationTrait;
@@ -57,6 +56,126 @@ public function createCommand($db = null)
5756
return parent::createCommand($db);
5857
}
5958

59+
/**
60+
* {@inheritdoc}
61+
* @throws InvalidConfigException
62+
*/
63+
public function prepare($builder)
64+
{
65+
if (!empty($this->joinWith)) {
66+
$this->buildJoinWith();
67+
$this->joinWith = null;
68+
}
69+
70+
if ($this->primaryModel === null) {
71+
// eager loading
72+
$query = Query::create($this);
73+
} else {
74+
// lazy loading of a relation
75+
$where = $this->where;
76+
77+
if ($this->via instanceof self) {
78+
// via junction table
79+
$viaModels = $this->via->findJunctionRows([$this->primaryModel]);
80+
$this->filterByModels($viaModels);
81+
} elseif (is_array($this->via)) {
82+
// via relation
83+
/* @var $viaQuery ActiveQuery */
84+
list($viaName, $viaQuery) = $this->via;
85+
if ($viaQuery->multiple) {
86+
if ($this->primaryModel->isRelationPopulated($viaName)) {
87+
$viaModels = $this->primaryModel->$viaName;
88+
} else {
89+
$viaModels = $viaQuery->all();
90+
$this->primaryModel->populateRelation($viaName, $viaModels);
91+
}
92+
} else {
93+
if ($this->primaryModel->isRelationPopulated($viaName)) {
94+
$model = $this->primaryModel->$viaName;
95+
} else {
96+
$model = $viaQuery->one();
97+
$this->primaryModel->populateRelation($viaName, $model);
98+
}
99+
$viaModels = $model === null ? [] : [$model];
100+
}
101+
$this->filterByModels($viaModels);
102+
} else {
103+
$this->filterByModels([$this->primaryModel]);
104+
}
105+
106+
$query = Query::create($this);
107+
$this->andWhere($where);
108+
}
109+
110+
return $query;
111+
}
112+
113+
/**
114+
* Builds join with clauses
115+
*/
116+
private function buildJoinWith()
117+
{
118+
$join = $this->join;
119+
$this->join = [];
120+
$model = new $this->modelClass();
121+
foreach ($this->joinWith as $with) {
122+
$this->joinWithRelations($model, $with);
123+
foreach ($with as $name => $callback) {
124+
$this->innerJoin(is_int($name) ? $callback : [$name => $callback]);
125+
unset($with[$name]);
126+
}
127+
}
128+
if (!empty($join)) {
129+
// append explicit join to joinWith()
130+
// https://github.com/yiisoft/yii2/issues/2880
131+
$this->join = empty($this->join) ? $join : array_merge($this->join, $join);
132+
}
133+
}
134+
135+
/**
136+
* Modifies the current query by adding join fragments based on the given relations.
137+
* @param ActiveRecord $model the primary model
138+
* @param array $with the relations to be joined
139+
*/
140+
protected function joinWithRelations($model, $with)
141+
{
142+
foreach ($with as $name => $callback) {
143+
if (is_int($name)) {
144+
$name = $callback;
145+
$callback = null;
146+
}
147+
$primaryModel = $model;
148+
$parent = $this;
149+
if (!isset($relations[$name])) {
150+
$relations[$name] = $relation = $primaryModel->getRelation($name);
151+
/* @var $relation static */
152+
if ($callback !== null) {
153+
call_user_func($callback, $relation);
154+
}
155+
if (!empty($relation->joinWith)) {
156+
$relation->buildJoinWith();
157+
}
158+
$this->joinWithRelation($parent, $relation);
159+
}
160+
}
161+
}
162+
163+
/**
164+
* Joins a parent query with a child query.
165+
* The current query object will be modified accordingly.
166+
*
167+
* @param ActiveQuery $parent
168+
* @param ActiveQuery $child
169+
*/
170+
private function joinWithRelation($parent, $child)
171+
{
172+
if (!empty($child->join)) {
173+
foreach ($child->join as $join) {
174+
$this->join[] = $join;
175+
}
176+
}
177+
}
178+
60179
/**
61180
* {@inheritdoc}
62181
*/
@@ -72,7 +191,7 @@ public function all($db = null)
72191
public function one($db = null)
73192
{
74193
$row = parent::one($db);
75-
if (!empty($row)) {
194+
if ($row !== false) {
76195
$models = $this->populate(isset($row[0]) ? $row : [$row]);
77196

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

203+
204+
/**
205+
* {@inheritdoc}
206+
* @throws InvalidConfigException
207+
*/
208+
public function populate($rows)
209+
{
210+
if (empty($rows)) {
211+
return [];
212+
}
213+
214+
$models = $this->createModels($rows);
215+
if (!empty($this->join) && $this->indexBy === null) {
216+
$models = $this->removeDuplicatedModels($models);
217+
}
218+
if (!empty($this->with)) {
219+
$this->findWith($this->with, $models);
220+
}
221+
if (!$this->asArray) {
222+
foreach ($models as $model) {
223+
$model->afterFind();
224+
}
225+
} elseif ($this->indexBy !== null) {
226+
$models = ArrayHelper::index($models, $this->indexBy);
227+
}
228+
229+
return $models;
230+
}
231+
84232
/**
85233
* {@inheritDoc}
86-
* @throws NotSupportedException
87234
*/
88-
public function via($relationName, callable $callable = null)
235+
protected function createModels($rows)
89236
{
90-
throw new NotSupportedException('Via relations are not supported in rest applications');
237+
if ($this->asArray) {
238+
return $rows;
239+
} else {
240+
$models = [];
241+
/* @var $class ActiveRecord */
242+
$class = $this->modelClass;
243+
foreach ($rows as $row) {
244+
$model = $class::instantiate($row);
245+
/** @var $modelClass ActiveRecord */
246+
$modelClass = get_class($model);
247+
$modelClass::populateRecord($model, $row);
248+
if (!empty($this->join)) {
249+
foreach ($this->join as $join) {
250+
if (isset($join[1], $row[$join[1]])) {
251+
$relation = $model->getRelation($join[1]);
252+
$rows = (ArrayHelper::isAssociative($row[$join[1]])) ? [$row[$join[1]]] : $row[$join[1]];
253+
$relations = $relation->populate($rows);
254+
$model->populateRelation($join[1], $relation->multiple ? $relations : $relations[0]);
255+
}
256+
}
257+
}
258+
$models[] = $model;
259+
}
260+
return $models;
261+
}
91262
}
92263

93264
/**
94-
* {@inheritdoc}
95-
* @throws InvalidConfigException
265+
* Removes duplicated models by checking their primary key values.
266+
* This method is mainly called when a join query is performed, which may cause duplicated rows being returned.
267+
*
268+
* @param array $models the models to be checked
269+
*
270+
* @return array the distinctive models
271+
* @throws InvalidConfigException if model primary key is empty
96272
*/
97-
public function prepare($builder)
273+
private function removeDuplicatedModels($models)
98274
{
99-
return Query::create($this);
275+
$hash = [];
276+
/* @var $class ActiveRecord */
277+
$class = $this->modelClass;
278+
$pks = $class::primaryKey();
279+
280+
if (count($pks) > 1) {
281+
// composite primary key
282+
foreach ($models as $i => $model) {
283+
$key = [];
284+
foreach ($pks as $pk) {
285+
if (!isset($model[$pk])) {
286+
// do not continue if the primary key is not part of the result set
287+
break 2;
288+
}
289+
$key[] = $model[$pk];
290+
}
291+
$key = serialize($key);
292+
if (isset($hash[$key])) {
293+
unset($models[$i]);
294+
} else {
295+
$hash[$key] = true;
296+
}
297+
}
298+
} elseif (empty($pks)) {
299+
/** @var $class string */
300+
throw new InvalidConfigException("Primary key of '{$class}' can not be empty.");
301+
} else {
302+
// single column primary key
303+
$pk = reset($pks);
304+
foreach ($models as $i => $model) {
305+
if (!isset($model[$pk])) {
306+
// do not continue if the primary key is not part of the result set
307+
break;
308+
}
309+
$key = $model[$pk];
310+
if (isset($hash[$key])) {
311+
unset($models[$i]);
312+
} elseif ($key !== null) {
313+
$hash[$key] = true;
314+
}
315+
}
316+
}
317+
318+
return array_values($models);
100319
}
101320

102321
/**
@@ -135,107 +354,7 @@ public function prepare($builder)
135354
*/
136355
public function joinWith($with)
137356
{
138-
if (is_array($with)) {
139-
$this->join = array_merge((array)$this->join, $with);
140-
} else {
141-
$this->join[] = $with;
142-
}
143-
144-
return $this;
145-
}
146-
147-
/**
148-
* {@inheritDoc}
149-
*/
150-
public function with()
151-
{
152-
$this->joinWith(func_get_args());
153-
357+
$this->joinWith[] = (array)$with;
154358
return $this;
155359
}
156-
157-
/**
158-
* {@inheritdoc}
159-
* @throws InvalidConfigException
160-
*/
161-
public function populate($rows)
162-
{
163-
if (empty($rows)) {
164-
return [];
165-
}
166-
167-
if ($this->asArray) {
168-
if ($this->indexBy) {
169-
return ArrayHelper::index($rows, $this->indexBy);
170-
}
171-
172-
return $rows;
173-
}
174-
175-
$models = $this->createModels($rows);
176-
foreach ($models as $model) {
177-
$model->afterFind();
178-
}
179-
180-
return $models;
181-
}
182-
183-
/**
184-
* {@inheritDoc}
185-
*/
186-
protected function createModels($rows)
187-
{
188-
$models = [];
189-
/** @var ActiveRecord $class */
190-
$class = $this->modelClass;
191-
$namespace = null;
192-
foreach ($rows as $row) {
193-
$model = $class::instantiate($row);
194-
$relations = $model->getRelations();
195-
/** @var ActiveRecord $modelClass */
196-
$modelClass = get_class($model);
197-
$modelClass::populateRecord($model, $row);
198-
$models[] = $model;
199-
if (is_array($this->join)) {
200-
foreach ($this->join as $join) {
201-
$relationRows = ArrayHelper::remove($row, $join, []);
202-
if (empty($relationRows)) {
203-
continue;
204-
}
205-
if (isset($relations[$join])) {
206-
$relationClass = $relations[$join];
207-
if (!class_exists($relationClass)) {
208-
if (null === $namespace && strpos($relationClass, '\\') === false) {
209-
$r = new \ReflectionClass($this->modelClass);
210-
$namespace = $r->getNamespaceName();
211-
}
212-
213-
if (class_exists($namespace . '\\' . $relationClass)) {
214-
$relationClass = $namespace . '\\' . $relationClass;
215-
}
216-
}
217-
if (class_exists($relationClass)) {
218-
/** @var ActiveRecord $relationClass */
219-
if (ArrayHelper::isAssociative($relationRows)) {
220-
$relationModel = $relationClass::instantiate($relationRows);
221-
$relationClass::populateRecord($relationModel, $relationRows);
222-
$model->populateRelation($join, $relationModel);
223-
} else {
224-
$populatedRows = [];
225-
foreach ($relationRows as $relationRow) {
226-
$relationModel = $relationClass::instantiate($relationRow);
227-
$relationClass::populateRecord($relationModel, $relationRow);
228-
$populatedRows[] = $relationModel;
229-
}
230-
$model->populateRelation($join, $populatedRows);
231-
}
232-
}
233-
} else {
234-
$model->populateRelation($join, ArrayHelper::remove($row, $join, []));
235-
}
236-
}
237-
}
238-
}
239-
return $models;
240-
}
241360
}

0 commit comments

Comments
 (0)