Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/Helpers/QueriesRelationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use function collect;
use function in_array;
use function is_array;
use function is_object;
use function is_string;
use function method_exists;
use function str_contains;
Expand Down Expand Up @@ -200,9 +201,19 @@ protected function getHasCompareKey(Relation $relation)
*/
protected function getConstrainedRelatedIds($relations, $operator, $count)
{
$ids = is_array($relations) ? $relations : $relations->flatten()->toArray();

$originalIds = [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you rename this $idsByStringKey for accuracy?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed, and switched the key itself. A string only map still merged int 1 with string "1", which is the integer edge case in the tests you added. idsByTypedKey keeps those two separate and still restores the original scalar for Mongo $in.

foreach ($ids as $id) {
$key = (string) $id;
if (! isset($originalIds[$key])) {
$originalIds[$key] = is_object($id) ? $key : $id;
}
}

$relationCount = array_count_values(array_map(function ($id) {
return (string) $id; // Convert Back ObjectIds to Strings
}, is_array($relations) ? $relations : $relations->flatten()->toArray()));
}, $ids));
// Remove unwanted related objects based on the operator and count.
$relationCount = array_filter($relationCount, function ($counted) use ($count, $operator) {
// If we are comparing to 0, we always need all results.
Expand All @@ -224,7 +235,10 @@ protected function getConstrainedRelatedIds($relations, $operator, $count)
});

// All related ids.
return array_keys($relationCount);
return array_map(
static fn ($key) => $originalIds[(string) $key],
array_keys($relationCount),
);
}

/**
Expand Down
5 changes: 5 additions & 0 deletions tests/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ public function items()
return $this->hasMany(Item::class);
}

public function itemsWithCustomKey()
{
return $this->hasMany(Item::class, 'parent_key', 'custom_key');
}

public function role()
{
return $this->hasOne(Role::class);
Expand Down
59 changes: 59 additions & 0 deletions tests/RelationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,65 @@ public function testHasManyHas(): void
$this->assertCount(1, $authors);
}

public function testHasManyHasWithNumericStringCustomKeys(): void
{
$parent = User::create(['name' => 'Numeric Parent', 'custom_key' => '1']);
User::create(['name' => 'No Children', 'custom_key' => '2']);
Item::create(['type' => 'child', 'parent_key' => '1']);

$this->assertCount(1, $parent->itemsWithCustomKey);
$this->assertSame(1, $parent->itemsWithCustomKey()->count());

$parents = User::has('itemsWithCustomKey')->get();
$this->assertCount(1, $parents);
$this->assertEquals('Numeric Parent', $parents[0]->name);

$parents = User::whereHas('itemsWithCustomKey')->get();
$this->assertCount(1, $parents);
$this->assertEquals('Numeric Parent', $parents[0]->name);
}

public function testHasManyHasWithIntegerCustomKeys(): void
{
$parent = User::create(['name' => 'Integer Parent', 'custom_key' => 7]);
User::create(['name' => 'No Children', 'custom_key' => 8]);
Item::create(['type' => 'child', 'parent_key' => 7]);

$this->assertCount(1, $parent->itemsWithCustomKey);
$this->assertSame(1, $parent->itemsWithCustomKey()->count());

$parents = User::has('itemsWithCustomKey')->get();
$this->assertCount(1, $parents);
$this->assertSame('Integer Parent', $parents[0]->name);

$parents = User::whereHas('itemsWithCustomKey')->get();
$this->assertCount(1, $parents);
$this->assertSame('Integer Parent', $parents[0]->name);
}

public function testHasManyHasWithMixedIntegerAndStringCustomKeys(): void
{
User::create(['name' => 'String Parent', 'custom_key' => '1']);
User::create(['name' => 'Integer Parent', 'custom_key' => 1]);
Item::create(['type' => 'child', 'parent_key' => '1']);
Item::create(['type' => 'child', 'parent_key' => 1]);

$names = User::has('itemsWithCustomKey')->get()->pluck('name')->sort()->values()->all();

$this->assertSame(['Integer Parent', 'String Parent'], $names);
}

public function testHasManyHasCountIgnoresRelatedIdsOfAnotherType(): void
{
$parent = User::create(['name' => 'String Parent', 'custom_key' => '1']);
Item::create(['type' => 'child', 'parent_key' => '1']);
Item::create(['type' => 'child', 'parent_key' => 1]);

$this->assertSame(1, $parent->itemsWithCustomKey()->count());
$this->assertCount(0, User::has('itemsWithCustomKey', '>=', 2)->get());
$this->assertCount(1, User::has('itemsWithCustomKey', '=', 1)->get());
}

public function testHasOneHas(): void
{
$user1 = User::create(['name' => 'John Doe']);
Expand Down
Loading