Skip to content

Add Hybrid has and whereHas functionality #1195

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

Merged
merged 7 commits into from
Sep 5, 2017
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 1 addition & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@
},
"autoload": {
"psr-0": {
"Jenssegers\\Mongodb": "src/",
"Jenssegers\\Eloquent": "src/"
"Jenssegers\\Mongodb": "src/"
}
},
"autoload-dev": {
Expand Down
53 changes: 4 additions & 49 deletions src/Jenssegers/Mongodb/Eloquent/Builder.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<?php namespace Jenssegers\Mongodb\Eloquent;

use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Jenssegers\Mongodb\Helpers\QueriesRelationships;
use MongoDB\Driver\Cursor;
use MongoDB\Model\BSONDocument;

class Builder extends EloquentBuilder
{
use QueriesRelationships;
/**
* The methods that should be returned from query builder.
*
Expand Down Expand Up @@ -139,54 +140,6 @@ public function decrement($column, $amount = 1, array $extra = [])
return parent::decrement($column, $amount, $extra);
}

/**
* @inheritdoc
*/
protected function addHasWhere(EloquentBuilder $hasQuery, Relation $relation, $operator, $count, $boolean)
{
$query = $hasQuery->getQuery();

// Get the number of related objects for each possible parent.
$relations = $query->pluck($relation->getHasCompareKey());
$relationCount = array_count_values(array_map(function ($id) {
return (string) $id; // Convert Back ObjectIds to Strings
}, is_array($relations) ? $relations : $relations->flatten()->toArray()));

// 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.
if ($count == 0) {
return true;
}

switch ($operator) {
case '>=':
case '<':
return $counted >= $count;
case '>':
case '<=':
return $counted > $count;
case '=':
case '!=':
return $counted == $count;
}
});

// If the operator is <, <= or !=, we will use whereNotIn.
$not = in_array($operator, ['<', '<=', '!=']);

// If we are comparing to 0, we need an additional $not flip.
if ($count == 0) {
$not = ! $not;
}

// All related ids.
$relatedIds = array_keys($relationCount);

// Add whereIn to the query.
return $this->whereIn($this->model->getKeyName(), $relatedIds, $boolean, $not);
}

/**
* @inheritdoc
*/
Expand All @@ -198,10 +151,12 @@ public function raw($expression = null)
// Convert MongoCursor results to a collection of models.
if ($results instanceof Cursor) {
$results = iterator_to_array($results, false);

return $this->model->hydrate($results);
} // Convert Mongo BSONDocument to a single object.
elseif ($results instanceof BSONDocument) {
$results = $results->getArrayCopy();

return $this->model->newFromBuilder((array) $results);
} // The result is a single object.
elseif (is_array($results) and array_key_exists('_id', $results)) {
Expand Down
9 changes: 9 additions & 0 deletions src/Jenssegers/Mongodb/Eloquent/HybridRelations.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Support\Str;
use Jenssegers\Mongodb\Helpers\EloquentBuilder;
use Jenssegers\Mongodb\Relations\BelongsTo;
use Jenssegers\Mongodb\Relations\BelongsToMany;
use Jenssegers\Mongodb\Relations\HasMany;
Expand Down Expand Up @@ -265,4 +266,12 @@ protected function guessBelongsToManyRelation()

return parent::guessBelongsToManyRelation();
}

/**
* @inheritdoc
*/
public function newEloquentBuilder($query)
{
return new EloquentBuilder($query);
}
}
10 changes: 10 additions & 0 deletions src/Jenssegers/Mongodb/Helpers/EloquentBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Jenssegers\Mongodb\Helpers;

use Illuminate\Database\Eloquent\Builder;

class EloquentBuilder extends Builder
{
use QueriesRelationships;
}
164 changes: 164 additions & 0 deletions src/Jenssegers/Mongodb/Helpers/QueriesRelationships.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php

namespace Jenssegers\Mongodb\Helpers;

use Closure;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
use Jenssegers\Mongodb\Eloquent\Model;

trait QueriesRelationships
{
/**
* Add a relationship count / exists condition to the query.
*
* @param string $relation
* @param string $operator
* @param int $count
* @param string $boolean
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null)
{
if (strpos($relation, '.') !== false) {
return $this->hasNested($relation, $operator, $count, $boolean, $callback);
}

$relation = $this->getRelationWithoutConstraints($relation);

// If this is a hybrid relation then we can not use a normal whereExists() query that relies on a subquery
// We need to use a `whereIn` query
if ($this->getModel() instanceof Model || $this->isAcrossConnections($relation)) {
return $this->addHybridHas($relation, $operator, $count, $boolean, $callback);
}

// If we only need to check for the existence of the relation, then we can optimize
// the subquery to only run a "where exists" clause instead of this full "count"
// clause. This will make these queries run much faster compared with a count.
$method = $this->canUseExistsForExistenceCheck($operator, $count)
? 'getRelationExistenceQuery'
: 'getRelationExistenceCountQuery';

$hasQuery = $relation->{$method}(
$relation->getRelated()->newQuery(), $this
);

// Next we will call any given callback as an "anonymous" scope so they can get the
// proper logical grouping of the where clauses if needed by this Eloquent query
// builder. Then, we will be ready to finalize and return this query instance.
if ($callback) {
$hasQuery->callScope($callback);
}

return $this->addHasWhere(
$hasQuery, $relation, $operator, $count, $boolean
);
}

/**
* @param $relation
* @return bool
*/
protected function isAcrossConnections($relation)
{
return $relation->getParent()->getConnectionName() !== $relation->getRelated()->getConnectionName();
}

/**
* Compare across databases
* @param $relation
* @param string $operator
* @param int $count
* @param string $boolean
* @param Closure|null $callback
* @return mixed
* @throws \Exception
*/
public function addHybridHas($relation, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null)
{
$hasQuery = $relation->getQuery();
if ($callback) {
$hasQuery->callScope($callback);
}

// If the operator is <, <= or !=, we will use whereNotIn.
$not = in_array($operator, ['<', '<=', '!=']);
// If we are comparing to 0, we need an additional $not flip.
if ($count == 0) {
$not = ! $not;
}

$relations = $hasQuery->pluck($this->getHasCompareKey($relation));

$relatedIds = $this->getConstrainedRelatedIds($relations, $operator, $count);

return $this->whereIn($this->getRelatedConstraintKey($relation), $relatedIds, $boolean, $not);
}

/**
* Returns key we are constraining this parent model's query with
* @param $relation
* @return string
* @throws \Exception
*/
protected function getRelatedConstraintKey($relation)
{
if ($relation instanceof HasOneOrMany) {
return $this->model->getKeyName();
}

if ($relation instanceof BelongsTo) {
return $relation->getForeignKey();
}

throw new \Exception(class_basename($relation).' Is Not supported for hybrid query constraints!');
}

/**
* @param $relation
* @return string
*/
protected function getHasCompareKey($relation)
{
if (method_exists($relation, 'getHasCompareKey')) {
return $relation->getHasCompareKey();
}

return $relation instanceof HasOneOrMany ? $relation->getForeignKeyName() : $relation->getOwnerKey();
}

/**
* @param $relations
* @param $operator
* @param $count
* @return array
*/
protected function getConstrainedRelatedIds($relations, $operator, $count)
{
$relationCount = array_count_values(array_map(function ($id) {
return (string) $id; // Convert Back ObjectIds to Strings
}, is_array($relations) ? $relations : $relations->flatten()->toArray()));
// 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.
if ($count == 0) {
return true;
}
switch ($operator) {
case '>=':
case '<':
return $counted >= $count;
case '>':
case '<=':
return $counted > $count;
case '=':
case '!=':
return $counted == $count;
}
});

// All related ids.
return array_keys($relationCount);
}
}
10 changes: 10 additions & 0 deletions src/Jenssegers/Mongodb/Relations/BelongsTo.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@

class BelongsTo extends \Illuminate\Database\Eloquent\Relations\BelongsTo
{
/**
* Get the key for comparing against the parent key in "has" query.
*
* @return string
*/
public function getHasCompareKey()
{
return $this->getOwnerKey();
}

/**
* @inheritdoc
*/
Expand Down
Loading