Skip to content

Commit

Permalink
Refactor populateRelation() (#361)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tigrov authored Jul 4, 2024
1 parent e9708de commit 8b686a0
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 143 deletions.
2 changes: 1 addition & 1 deletion docs/create-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ final class User extends ActiveRecord
}
```

Now you can use `$user->getProfile()` and `$user->getOrders()` to access the relation.
Now you can use `$user->getProfile()` and `$user->getOrders()` to access the relations.

```php
use Yiisoft\ActiveRecord\ActiveQuery;
Expand Down
221 changes: 80 additions & 141 deletions src/ActiveRelationTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use function array_diff_key;
use function array_fill_keys;
use function array_filter;
use function array_flip;
use function array_intersect_key;
use function array_keys;
use function array_merge;
Expand Down Expand Up @@ -248,20 +249,18 @@ public function populateRelation(string $name, array &$primaryModels): array
}

if (!$this->multiple && count($primaryModels) === 1) {
$model = $this->onePopulate();
$models = [$this->onePopulate()];
$this->populateInverseRelation($models, $primaryModels);

$primaryModel = reset($primaryModels);

if ($primaryModel instanceof ActiveRecordInterface) {
$primaryModel->populateRelation($name, $model);
$primaryModel->populateRelation($name, $models[0]);
} else {
$primaryModels[key($primaryModels)][$name] = $model;
}

if ($this->inverseOf !== null) {
$this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf);
$primaryModels[key($primaryModels)][$name] = $models[0];
}

return [$model];
return $models;
}

/**
Expand All @@ -273,6 +272,8 @@ public function populateRelation(string $name, array &$primaryModels): array
$this->indexBy(null);
$models = $this->all();

$this->populateInverseRelation($models, $primaryModels);

if (isset($viaModels, $viaQuery)) {
$buckets = $this->buildBuckets($models, $viaModels, $viaQuery);
} else {
Expand All @@ -297,51 +298,7 @@ public function populateRelation(string $name, array &$primaryModels): array
$link = $this->link;
}

foreach ($primaryModels as $i => $primaryModel) {
$keys = null;

if ($this->multiple && count($link) === 1) {
$primaryModelKey = reset($link);

if ($primaryModel instanceof ActiveRecordInterface) {
$keys = $primaryModel->getAttribute($primaryModelKey);
} else {
$keys = $primaryModel[$primaryModelKey] ?? null;
}
}

if (is_array($keys)) {
$value = [];

foreach ($keys as $key) {
$key = (string) $key;

if (isset($buckets[$key])) {
$value[] = $buckets[$key];
}
}

if ($indexBy !== null) {
/** if indexBy is set, array_merge will cause renumbering of numeric array */
$value = array_replace(...$value);
} else {
$value = array_merge(...$value);
}
} else {
$key = $this->getModelKey($primaryModel, $link);
$value = $buckets[$key] ?? ($this->multiple ? [] : null);
}

if ($primaryModel instanceof ActiveRecordInterface) {
$primaryModel->populateRelation($name, $value);
} else {
$primaryModels[$i][$name] = $value;
}
}

if ($this->inverseOf !== null) {
$this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf);
}
$this->populateRelationFromBuckets($primaryModels, $buckets, $name, $link);

return $models;
}
Expand All @@ -350,84 +307,59 @@ public function populateRelation(string $name, array &$primaryModels): array
* @throws \Yiisoft\Definitions\Exception\InvalidConfigException
*/
private function populateInverseRelation(
array &$primaryModels,
array $models,
string $primaryName,
string $name
array &$models,
array $primaryModels,
): void {
if (empty($models) || empty($primaryModels)) {
if ($this->inverseOf === null || empty($models) || empty($primaryModels)) {
return;
}

$name = $this->inverseOf;
$model = reset($models);

if ($model instanceof ActiveRecordInterface) {
$this->populateInverseRelationToModels($models, $primaryModels, $name);
return;
}
/** @var ActiveQuery $relation */
$relation = is_array($model)
? $this->getARInstance()->relationQuery($name)
: $model->relationQuery($name);

$primaryModel = reset($primaryModels);
$link = $relation->getLink();
$indexBy = $relation->getIndexBy();
$buckets = $relation->buildBuckets($primaryModels);

if ($primaryModel instanceof ActiveRecordInterface) {
if ($this->multiple) {
foreach ($primaryModels as $primaryModel) {
$models = $primaryModel->relation($primaryName);
if (!empty($models)) {
$this->populateInverseRelationToModels($models, $primaryModels, $name);
$primaryModel->populateRelation($primaryName, $models);
}
}
} else {
foreach ($primaryModels as $primaryModel) {
$model = $primaryModel->relation($primaryName);
if (!empty($model)) {
$models = [$model];
$this->populateInverseRelationToModels($models, $primaryModels, $name);
$primaryModel->populateRelation($primaryName, $models[0]);
}
}
}
} else {
if ($this->multiple) {
foreach ($primaryModels as &$primaryModel) {
if (!empty($primaryModel[$primaryName])) {
$this->populateInverseRelationToModels($primaryModel[$primaryName], $primaryModels, $name);
}
}
} else {
foreach ($primaryModels as &$primaryModel) {
if (!empty($primaryModel[$primaryName])) {
$models = [$primaryModel[$primaryName]];
$this->populateInverseRelationToModels($models, $primaryModels, $name);
$primaryModel[$primaryName] = $models[0];
}
}
}
if ($indexBy !== null && $relation->getMultiple()) {
$buckets = $this->indexBuckets($buckets, $indexBy);
}
}

private function populateInverseRelationToModels(array &$models, array $primaryModels, string $name): void
{
$model = reset($models);
$isArray = is_array($model);
$relation->populateRelationFromBuckets($models, $buckets, $name, $link);
}

/** @var ActiveQuery $relation */
$relation = $isArray ? $this->getARInstance()->relationQuery($name) : $model->relationQuery($name);
$buckets = $relation->buildBuckets($primaryModels);
$link = $relation->getLink();
$default = $relation->getMultiple() ? [] : null;
private function populateRelationFromBuckets(
array &$models,
array $buckets,
string $name,
array $link
): void {
$indexBy = $this->getIndexBy();
$default = $this->multiple ? [] : null;

foreach ($models as &$model) {
$keys = $this->getModelKeys($model, $link);

/** @psalm-suppress NamedArgumentNotAllowed */
$value = match (count($keys)) {
0 => $default,
1 => $buckets[$keys[0]] ?? $default,
default => !$this->multiple
? $default
: ($indexBy !== null
? array_replace(...array_intersect_key($buckets, array_flip($keys)))
: array_merge(...array_intersect_key($buckets, array_flip($keys)))),
};

if ($isArray) {
/** @var array $model */
foreach ($models as &$model) {
$key = $this->getModelKey($model, $link);
$model[$name] = $buckets[$key] ?? $default;
}
} else {
/** @var ActiveRecordInterface $model */
foreach ($models as $model) {
$key = $this->getModelKey($model, $link);
$model->populateRelation($name, $buckets[$key] ?? $default);
if ($model instanceof ActiveRecordInterface) {
$model->populateRelation($name, $value);
} else {
$model[$name] = $value;
}
}
}
Expand All @@ -445,9 +377,13 @@ private function buildBuckets(
$viaVia = null;

foreach ($viaModels as $viaModel) {
$key1 = $this->getModelKey($viaModel, $viaLinkKeys);
$key2 = $this->getModelKey($viaModel, $linkValues);
$map[$key2][$key1] = true;
$key1 = $this->getModelKeys($viaModel, $viaLinkKeys);
$key2 = $this->getModelKeys($viaModel, $linkValues);
$map[] = array_fill_keys($key2, array_fill_keys($key1, true));
}

if (!empty($map)) {
$map = array_replace_recursive(...$map);
}

if ($viaQuery !== null) {
Expand All @@ -473,18 +409,21 @@ private function buildBuckets(

if (isset($map)) {
foreach ($models as $model) {
$key = $this->getModelKey($model, $linkKeys);
if (isset($map[$key])) {
foreach (array_keys($map[$key]) as $key2) {
/** @psalm-suppress InvalidArrayOffset */
$buckets[$key2][] = $model;
}
$keys = $this->getModelKeys($model, $linkKeys);
/** @var bool[][] $filtered */
$filtered = array_intersect_key($map, array_fill_keys($keys, null));

foreach (array_keys(array_replace(...$filtered)) as $key2) {
$buckets[$key2][] = $model;
}
}
} else {
foreach ($models as $model) {
$key = $this->getModelKey($model, $linkKeys);
$buckets[$key][] = $model;
$keys = $this->getModelKeys($model, $linkKeys);

foreach ($keys as $key) {
$buckets[$key][] = $model;
}
}
}

Expand All @@ -503,11 +442,7 @@ private function mapVia(array $map, array $viaMap): array
$resultMap = [];

foreach ($map as $key => $linkKeys) {
$resultMap[$key] = [];
foreach (array_keys($linkKeys) as $linkKey) {
/** @psalm-suppress InvalidArrayOffset */
$resultMap[$key] += $viaMap[$linkKey];
}
$resultMap[$key] = array_replace(...array_intersect_key($viaMap, $linkKeys));
}

return $resultMap;
Expand Down Expand Up @@ -636,30 +571,34 @@ protected function filterByModels(array $models): void
$this->andWhere(['in', $attributes, $values]);
}

private function getModelKey(ActiveRecordInterface|array $activeRecord, array $attributes): string
private function getModelKeys(ActiveRecordInterface|array $activeRecord, array $attributes): array
{
$key = [];

if (is_array($activeRecord)) {
foreach ($attributes as $attribute) {
if (isset($activeRecord[$attribute])) {
$key[] = (string) $activeRecord[$attribute];
$key[] = is_array($activeRecord[$attribute])
? $activeRecord[$attribute]
: (string) $activeRecord[$attribute];
}
}
} else {
foreach ($attributes as $attribute) {
$value = $activeRecord->getAttribute($attribute);

if ($value !== null) {
$key[] = (string) $value;
$key[] = is_array($value)
? $value
: (string) $value;
}
}
}

return match (count($key)) {
0 => '',
1 => $key[0],
default => serialize($key),
0 => [],
1 => is_array($key[0]) ? $key[0] : [$key[0]],
default => [serialize($key)],
};
}

Expand Down
31 changes: 31 additions & 0 deletions tests/Driver/Pgsql/ActiveRecordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use ArrayAccess;
use Traversable;
use Yiisoft\ActiveRecord\ActiveQuery;
use Yiisoft\ActiveRecord\ArArrayHelper;
use Yiisoft\ActiveRecord\Tests\Driver\Pgsql\Stubs\Promotion;
use Yiisoft\ActiveRecord\Tests\Driver\Pgsql\Stubs\Type;
use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\ArrayAndJsonTypes;
use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Beta;
Expand Down Expand Up @@ -436,4 +438,33 @@ public function testToArrayWithClosure(): void
$customer->toArray(),
);
}

public function testRelationViaArray()
{
$this->checkFixture($this->db(), 'promotion');

$promotionQuery = new ActiveQuery(Promotion::class);
/** @var Promotion[] $promotions */
$promotions = $promotionQuery->with('items')->all();

$this->assertSame([1, 2], ArArrayHelper::getColumn($promotions[0]->getItems(), 'id'));
$this->assertSame([3, 4, 5], ArArrayHelper::getColumn($promotions[1]->getItems(), 'id'));
$this->assertSame([1, 3], ArArrayHelper::getColumn($promotions[2]->getItems(), 'id'));
$this->assertCount(0, $promotions[3]->getItems());

/** Test inverse relation */
foreach ($promotions as $promotion) {
foreach ($promotion->getItems() as $item) {
$this->assertTrue($item->isRelationPopulated('promotions'));
}
}

$this->assertSame([1, 3], ArArrayHelper::getColumn($promotions[0]->getItems()[0]->getPromotions(), 'id'));
$this->assertSame([1], ArArrayHelper::getColumn($promotions[0]->getItems()[1]->getPromotions(), 'id'));
$this->assertSame([2, 3], ArArrayHelper::getColumn($promotions[1]->getItems()[0]->getPromotions(), 'id'));
$this->assertSame([2], ArArrayHelper::getColumn($promotions[1]->getItems()[1]->getPromotions(), 'id'));
$this->assertSame([2], ArArrayHelper::getColumn($promotions[1]->getItems()[2]->getPromotions(), 'id'));
$this->assertSame([1, 3], ArArrayHelper::getColumn($promotions[2]->getItems()[0]->getPromotions(), 'id'));
$this->assertSame([2, 3], ArArrayHelper::getColumn($promotions[2]->getItems()[1]->getPromotions(), 'id'));
}
}
Loading

0 comments on commit 8b686a0

Please sign in to comment.