Skip to content

Commit 266305c

Browse files
committed
add whereHas() support for hybrid relations
1 parent 3276bac commit 266305c

File tree

8 files changed

+280
-56
lines changed

8 files changed

+280
-56
lines changed

src/Jenssegers/Mongodb/Eloquent/Builder.php

Lines changed: 6 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
<?php namespace Jenssegers\Mongodb\Eloquent;
22

33
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
4-
use Illuminate\Database\Eloquent\Relations\Relation;
4+
use Jenssegers\Mongodb\Helpers\QueriesRelationships;
55
use MongoDB\Driver\Cursor;
66
use MongoDB\Model\BSONDocument;
77

88
class Builder extends EloquentBuilder
99
{
10+
use QueriesRelationships;
1011
/**
1112
* The methods that should be returned from query builder.
1213
*
@@ -139,54 +140,6 @@ public function decrement($column, $amount = 1, array $extra = [])
139140
return parent::decrement($column, $amount, $extra);
140141
}
141142

142-
/**
143-
* @inheritdoc
144-
*/
145-
protected function addHasWhere(EloquentBuilder $hasQuery, Relation $relation, $operator, $count, $boolean)
146-
{
147-
$query = $hasQuery->getQuery();
148-
149-
// Get the number of related objects for each possible parent.
150-
$relations = $query->pluck($relation->getHasCompareKey());
151-
$relationCount = array_count_values(array_map(function ($id) {
152-
return (string) $id; // Convert Back ObjectIds to Strings
153-
}, is_array($relations) ? $relations : $relations->flatten()->toArray()));
154-
155-
// Remove unwanted related objects based on the operator and count.
156-
$relationCount = array_filter($relationCount, function ($counted) use ($count, $operator) {
157-
// If we are comparing to 0, we always need all results.
158-
if ($count == 0) {
159-
return true;
160-
}
161-
162-
switch ($operator) {
163-
case '>=':
164-
case '<':
165-
return $counted >= $count;
166-
case '>':
167-
case '<=':
168-
return $counted > $count;
169-
case '=':
170-
case '!=':
171-
return $counted == $count;
172-
}
173-
});
174-
175-
// If the operator is <, <= or !=, we will use whereNotIn.
176-
$not = in_array($operator, ['<', '<=', '!=']);
177-
178-
// If we are comparing to 0, we need an additional $not flip.
179-
if ($count == 0) {
180-
$not = ! $not;
181-
}
182-
183-
// All related ids.
184-
$relatedIds = array_keys($relationCount);
185-
186-
// Add whereIn to the query.
187-
return $this->whereIn($this->model->getKeyName(), $relatedIds, $boolean, $not);
188-
}
189-
190143
/**
191144
* @inheritdoc
192145
*/
@@ -198,14 +151,16 @@ public function raw($expression = null)
198151
// Convert MongoCursor results to a collection of models.
199152
if ($results instanceof Cursor) {
200153
$results = iterator_to_array($results, false);
154+
201155
return $this->model->hydrate($results);
202156
} // Convert Mongo BSONDocument to a single object.
203157
elseif ($results instanceof BSONDocument) {
204158
$results = $results->getArrayCopy();
205-
return $this->model->newFromBuilder((array) $results);
159+
160+
return $this->model->newFromBuilder((array)$results);
206161
} // The result is a single object.
207162
elseif (is_array($results) and array_key_exists('_id', $results)) {
208-
return $this->model->newFromBuilder((array) $results);
163+
return $this->model->newFromBuilder((array)$results);
209164
}
210165

211166
return $results;

src/Jenssegers/Mongodb/Eloquent/HybridRelations.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use Illuminate\Database\Eloquent\Relations\MorphMany;
44
use Illuminate\Database\Eloquent\Relations\MorphOne;
55
use Illuminate\Support\Str;
6+
use Jenssegers\Mongodb\Helpers\EloquentBuilder;
67
use Jenssegers\Mongodb\Relations\BelongsTo;
78
use Jenssegers\Mongodb\Relations\BelongsToMany;
89
use Jenssegers\Mongodb\Relations\HasMany;
@@ -265,4 +266,12 @@ protected function guessBelongsToManyRelation()
265266

266267
return parent::guessBelongsToManyRelation();
267268
}
269+
270+
/**
271+
* @inheritdoc
272+
*/
273+
public function newEloquentBuilder($query)
274+
{
275+
return new EloquentBuilder($query);
276+
}
268277
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Jenssegers\Mongodb\Helpers;
4+
5+
use Illuminate\Database\Eloquent\Builder;
6+
7+
class EloquentBuilder extends Builder
8+
{
9+
use QueriesRelationships;
10+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
<?php
2+
3+
namespace Jenssegers\Mongodb\Helpers;
4+
5+
use Closure;
6+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
7+
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
8+
use Illuminate\Database\Eloquent\Relations\Relation;
9+
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
10+
11+
trait QueriesRelationships
12+
{
13+
/**
14+
* Add a relationship count / exists condition to the query.
15+
*
16+
* @param string $relation
17+
* @param string $operator
18+
* @param int $count
19+
* @param string $boolean
20+
* @param \Closure|null $callback
21+
* @return \Illuminate\Database\Eloquent\Builder|static
22+
*/
23+
public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null)
24+
{
25+
if (strpos($relation, '.') !== false) {
26+
return $this->hasNested($relation, $operator, $count, $boolean, $callback);
27+
}
28+
29+
$relation = $this->getRelationWithoutConstraints($relation);
30+
31+
// If this is a hybrid relation then we can not use an existence query
32+
// We need to use a `whereIn` query
33+
if ($relation->getParent()->getConnectionName() !== $relation->getRelated()->getConnectionName()) {
34+
return $this->addHybridHas($relation, $operator, $count, $boolean, $callback);
35+
}
36+
37+
// If we only need to check for the existence of the relation, then we can optimize
38+
// the subquery to only run a "where exists" clause instead of this full "count"
39+
// clause. This will make these queries run much faster compared with a count.
40+
$method = $this->canUseExistsForExistenceCheck($operator, $count)
41+
? 'getRelationExistenceQuery'
42+
: 'getRelationExistenceCountQuery';
43+
44+
$hasQuery = $relation->{$method}(
45+
$relation->getRelated()->newQuery(), $this
46+
);
47+
48+
// Next we will call any given callback as an "anonymous" scope so they can get the
49+
// proper logical grouping of the where clauses if needed by this Eloquent query
50+
// builder. Then, we will be ready to finalize and return this query instance.
51+
if ($callback) {
52+
$hasQuery->callScope($callback);
53+
}
54+
55+
return $this->addHasWhere(
56+
$hasQuery, $relation, $operator, $count, $boolean
57+
);
58+
}
59+
60+
/**
61+
* Compare across databases
62+
* @param $relation
63+
* @param string $operator
64+
* @param int $count
65+
* @param string $boolean
66+
* @param Closure|null $callback
67+
* @return mixed
68+
*/
69+
public function addHybridHas($relation, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null)
70+
{
71+
$hasQuery = $relation->getQuery();
72+
if ($callback) {
73+
$hasQuery->callScope($callback);
74+
}
75+
76+
$relations = $hasQuery->pluck($this->getHasCompareKey($relation));
77+
$constraintKey = $this->getRelatedConstraintKey($relation);
78+
79+
return $this->addRelatedCountConstraint($constraintKey, $relations, $operator, $count, $boolean);
80+
}
81+
82+
83+
/**
84+
* Returns key we are constraining this parent model's query witth
85+
* @param $relation
86+
* @return string
87+
* @throws \Exception
88+
*/
89+
protected function getRelatedConstraintKey($relation)
90+
{
91+
if ($relation instanceof HasOneOrMany) {
92+
return $relation->getQualifiedParentKeyName();
93+
}
94+
95+
if ($relation instanceof BelongsTo) {
96+
return $relation->getForeignKey();
97+
}
98+
99+
throw new \Exception(class_basename($relation).' Is Not supported for hybrid query constraints!');
100+
}
101+
102+
/**
103+
* @param $relation
104+
* @return string
105+
*/
106+
protected function getHasCompareKey($relation)
107+
{
108+
if ($relation instanceof HasOneOrMany) {
109+
return $relation->getForeignKeyName();
110+
}
111+
112+
$keyMethods = ['getOwnerKey', 'getHasCompareKey'];
113+
foreach ($keyMethods as $method) {
114+
if (method_exists($relation, $method)) {
115+
return $relation->$method();
116+
}
117+
}
118+
}
119+
120+
/**
121+
* Add the "has" condition where clause to the query.
122+
*
123+
* @param \Illuminate\Database\Eloquent\Builder $hasQuery
124+
* @param \Illuminate\Database\Eloquent\Relations\Relation $relation
125+
* @param string $operator
126+
* @param int $count
127+
* @param string $boolean
128+
* @return \Illuminate\Database\Eloquent\Builder|static
129+
*/
130+
protected function addHasWhere(EloquentBuilder $hasQuery, Relation $relation, $operator, $count, $boolean)
131+
{
132+
$query = $hasQuery->getQuery();
133+
// Get the number of related objects for each possible parent.
134+
$relations = $query->pluck($relation->getHasCompareKey());
135+
136+
return $this->addRelatedCountConstraint($this->model->getKeyName(), $relations, $operator, $count, $boolean);
137+
}
138+
139+
/**
140+
* Consta
141+
* @param $key
142+
* @param $relations
143+
* @param $operator
144+
* @param $count
145+
* @param $boolean
146+
* @return mixed
147+
*/
148+
protected function addRelatedCountConstraint($key, $relations, $operator, $count, $boolean)
149+
{
150+
$relationCount = array_count_values(array_map(function ($id) {
151+
return (string)$id; // Convert Back ObjectIds to Strings
152+
}, is_array($relations) ? $relations : $relations->flatten()->toArray()));
153+
// Remove unwanted related objects based on the operator and count.
154+
$relationCount = array_filter($relationCount, function ($counted) use ($count, $operator) {
155+
// If we are comparing to 0, we always need all results.
156+
if ($count == 0) {
157+
return true;
158+
}
159+
switch ($operator) {
160+
case '>=':
161+
case '<':
162+
return $counted >= $count;
163+
case '>':
164+
case '<=':
165+
return $counted > $count;
166+
case '=':
167+
case '!=':
168+
return $counted == $count;
169+
}
170+
});
171+
172+
// If the operator is <, <= or !=, we will use whereNotIn.
173+
$not = in_array($operator, ['<', '<=', '!=']);
174+
// If we are comparing to 0, we need an additional $not flip.
175+
if ($count == 0) {
176+
$not = ! $not;
177+
}
178+
// All related ids.
179+
$relatedIds = array_keys($relationCount);
180+
181+
// Add whereIn to the query.
182+
return $this->whereIn($key, $relatedIds, $boolean, $not);
183+
}
184+
}

src/Jenssegers/Mongodb/Relations/BelongsTo.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44

55
class BelongsTo extends \Illuminate\Database\Eloquent\Relations\BelongsTo
66
{
7+
/**
8+
* Get the key for comparing against the parent key in "has" query.
9+
*
10+
* @return string
11+
*/
12+
public function getHasCompareKey()
13+
{
14+
return $this->getOwnerKey();
15+
}
16+
717
/**
818
* @inheritdoc
919
*/

tests/MysqlRelationsTest.php renamed to tests/HybridRelationsTest.php

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
class MysqlRelationsTest extends TestCase
3+
class HybridRelationsTest extends TestCase
44
{
55
public function setUp()
66
{
@@ -74,4 +74,59 @@ public function testMysqlRelations()
7474
$role = $user->mysqlRole()->first(); // refetch
7575
$this->assertEquals('John Doe', $role->user->name);
7676
}
77+
78+
79+
public function testRelationConstraints()
80+
{
81+
$user = new MysqlUser;
82+
$otherUser = new MysqlUser;
83+
$this->assertInstanceOf('MysqlUser', $user);
84+
$this->assertInstanceOf('Illuminate\Database\MySqlConnection', $user->getConnection());
85+
$this->assertInstanceOf('MysqlUser', $otherUser);
86+
$this->assertInstanceOf('Illuminate\Database\MySqlConnection', $otherUser->getConnection());
87+
88+
//MySql User
89+
$user->name = "John Doe";
90+
$user->id = 2;
91+
$user->save();
92+
// Other user
93+
$otherUser->name = 'Other User';
94+
$otherUser->id = 3;
95+
$otherUser->save();
96+
// Make sure they are created
97+
$this->assertTrue(is_int($user->id));
98+
$this->assertTrue(is_int($otherUser->id));
99+
// Clear to start
100+
$user->books()->truncate();
101+
$otherUser->books()->truncate();
102+
// Create books
103+
$otherUser->books()->saveMany([
104+
new Book(['title' => 'Harry Plants']),
105+
new Book(['title' => 'Harveys']),
106+
]);
107+
// SQL has many
108+
$user->books()->saveMany([
109+
new Book(['title' => 'Game of Thrones']),
110+
new Book(['title' => 'Harry Potter']),
111+
new Book(['title' => 'Harry Planter']),
112+
]);
113+
114+
$users = MysqlUser::whereHas('books', function ($query) {
115+
return $query->where('title', 'LIKE', 'Har%');
116+
})->get();
117+
118+
$this->assertEquals(2, $users->count());
119+
120+
$users = MysqlUser::whereHas('books', function ($query) {
121+
return $query->where('title', 'LIKE', 'Harry%');
122+
}, '>=', 2)->get();
123+
124+
$this->assertEquals(1, $users->count());
125+
126+
$books = Book::whereHas('mysqlAuthor', function ($query) {
127+
return $query->where('name', 'LIKE', 'Other%');
128+
})->get();
129+
130+
$this->assertEquals(2, $books->count());
131+
}
77132
}

0 commit comments

Comments
 (0)