Skip to content

Commit 39f6516

Browse files
authored
Merge pull request #1195 from Tucker-Eric/master
Add Hybrid `has` and `whereHas` functionality
2 parents c4a5264 + ea3be77 commit 39f6516

11 files changed

+399
-129
lines changed

composer.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@
2525
},
2626
"autoload": {
2727
"psr-0": {
28-
"Jenssegers\\Mongodb": "src/",
29-
"Jenssegers\\Eloquent": "src/"
28+
"Jenssegers\\Mongodb": "src/"
3029
}
3130
},
3231
"autoload-dev": {

src/Jenssegers/Mongodb/Eloquent/Builder.php

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

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
*/

0 commit comments

Comments
 (0)