Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor populateRelation() #361

Merged
merged 10 commits into from
Jul 4, 2024
Merged
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 @@
}

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 @@
$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 @@
$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 @@
* @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);

Check warning on line 330 in src/ActiveRelationTrait.php

View check run for this annotation

Codecov / codecov/patch

src/ActiveRelationTrait.php#L330

Added line #L330 was not covered by tests
}
}

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

Check warning on line 353 in src/ActiveRelationTrait.php

View check run for this annotation

Codecov / codecov/patch

src/ActiveRelationTrait.php#L353

Added line #L353 was not covered by tests
: ($indexBy !== null
? array_replace(...array_intersect_key($buckets, array_flip($keys)))

Check warning on line 355 in src/ActiveRelationTrait.php

View check run for this annotation

Codecov / codecov/patch

src/ActiveRelationTrait.php#L355

Added line #L355 was not covered by tests
: 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 @@
$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 @@

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 @@
$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 @@
$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]

Check warning on line 582 in src/ActiveRelationTrait.php

View check run for this annotation

Codecov / codecov/patch

src/ActiveRelationTrait.php#L582

Added line #L582 was not covered by tests
: (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
Loading