-
-
Notifications
You must be signed in to change notification settings - Fork 571
[5.x] Add support for whereHas()
and whereRelation()
to entry and user query builders
#8476
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
Open
ryanmitchell
wants to merge
24
commits into
statamic:5.x
Choose a base branch
from
ryanmitchell:feature/where-has-relationships
base: 5.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 12 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
7068857
Support whereHas / whereRelation in Entry and User query builders
ryanmitchell aef4da1
Better comments
ryanmitchell eb3c22f
Fix return type
ryanmitchell cf20ade
Don't need these here anymore
ryanmitchell 3ea9f36
Add some test coverage
ryanmitchell 1913bd3
Support basic relation check when maxItems: 1
ryanmitchell b8cba41
:beer:
ryanmitchell e6b82b6
Move count check and rewrite error message
ryanmitchell 86ed319
Merge branch '4.x' into feature/where-has-relationships
ryanmitchell 86c1dcc
Don't need boolean here
ryanmitchell 3986056
Support taxonomies for @robdekort
ryanmitchell 2522fbf
Merge branch '4.x' into feature/where-has-relationships
ryanmitchell 3c94835
Tidy up
ryanmitchell 20bd209
Merge branch '4.x' into feature/where-has-relationships
ryanmitchell dadcca7
Merge branch '4.x' into feature/where-has-relationships
ryanmitchell 3e81f42
:beer:
ryanmitchell ce0c244
Merge branch '4.x' into feature/where-has-relationships
ryanmitchell b63a327
Merge branch '4.x' into feature/where-has-relationships
ryanmitchell 2167bc0
:beer:
ryanmitchell 8ff6044
Merge branch '5.x' into feature/where-has-relationships
ryanmitchell 0d39b65
ids have changed in tests
ryanmitchell 4e933e6
Merge branch '5.x' into feature/where-has-relationships
ryanmitchell 0f55edc
:beer:
ryanmitchell bb94e89
Merge branch '5.x' into feature/where-has-relationships
ryanmitchell File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,244 @@ | ||
<?php | ||
|
||
namespace Statamic\Query\Traits; | ||
|
||
use Closure; | ||
use InvalidArgumentException; | ||
use Statamic\Support\Str; | ||
|
||
trait QueriesRelationships | ||
{ | ||
/** | ||
* Add a relationship count / exists condition to the query. | ||
* | ||
* @param string $relation | ||
* @param string $operator | ||
* @param int $count | ||
* @param string $boolean | ||
* @return \Statamic\Query\Builder|static | ||
* | ||
* @throws \InvalidArgumentException | ||
*/ | ||
public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null) | ||
{ | ||
[$relationQueryBuilder, $relationField] = $this->getRelationQueryBuilderAndField($relation); | ||
|
||
$maxItems = $relationField->config()['max_items'] ?? 0; | ||
$negate = in_array($operator, ['!=', '<']); | ||
|
||
if (! $callback) { | ||
if ($maxItems == 1) { | ||
$method = $boolean == 'and' ? 'whereNull' : 'orWhereNull'; | ||
if (! $negate) { | ||
$method = str_replace('Null', 'NotNull', $method); | ||
} | ||
|
||
return $this->$method($relation); | ||
} | ||
|
||
return $this->{$boolean == 'and' ? 'whereJsonLength' : 'orWhereJsonLength'}($relation, $operator, $count); | ||
} | ||
|
||
if ($count != 1) { | ||
throw new InvalidArgumentException('Counting with subqueries in has clauses is not supported'); | ||
} | ||
|
||
$ids = $relationQueryBuilder | ||
->where($callback) | ||
->get(['id']) | ||
->map(fn ($item) => Str::after($item->id(), '::')) | ||
->all(); | ||
|
||
if ($maxItems == 1) { | ||
$method = $boolean == 'and' ? 'whereIn' : 'orWhereIn'; | ||
if ($negate) { | ||
$method = str_replace('here', 'hereNot', $method); | ||
} | ||
|
||
return $this->$method($relation, $ids); | ||
} | ||
|
||
if (empty($ids)) { | ||
return $this->{$boolean == 'and' ? 'whereJsonContains' : 'orWhereJsonContains'}($relation, ['']); | ||
} | ||
|
||
return $this->{$boolean == 'and' ? 'where' : 'orWhere'}(function ($subquery) use ($ids, $negate, $relation) { | ||
foreach ($ids as $count => $id) { | ||
$method = $count == 0 ? 'whereJsonContains' : 'orWhereJsonContains'; | ||
if ($negate) { | ||
$method = str_replace('Contains', 'DoesntContain', $method); | ||
} | ||
|
||
$subquery->$method($relation, [$id]); | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Add a relationship count / exists condition to the query with an "or". | ||
* | ||
* @param string $relation | ||
* @param string $operator | ||
* @param int $count | ||
* @return \Statamic\Query\Builder|static | ||
*/ | ||
public function orHas($relation, $operator = '>=', $count = 1) | ||
{ | ||
return $this->has($relation, $operator, $count, 'or'); | ||
} | ||
|
||
/** | ||
* Add a relationship count / exists condition to the query. | ||
* | ||
* @param string $relation | ||
* @param string $boolean | ||
* @return \Statamic\Query\Builder|static | ||
*/ | ||
public function doesntHave($relation, $boolean = 'and', Closure $callback = null) | ||
{ | ||
return $this->has($relation, '<', 1, $boolean, $callback); | ||
} | ||
|
||
/** | ||
* Add a relationship count / exists condition to the query with an "or". | ||
* | ||
* @param string $relation | ||
* @return \Statamic\Query\Builder|static | ||
*/ | ||
public function orDoesntHave($relation) | ||
{ | ||
return $this->doesntHave($relation, 'or'); | ||
} | ||
|
||
/** | ||
* Add a relationship count / exists condition to the query with where clauses. | ||
* | ||
* @param string $relation | ||
* @param string $operator | ||
* @param int $count | ||
* @return \Statamic\Query\Builder|static | ||
*/ | ||
public function whereHas($relation, Closure $callback = null, $operator = '>=', $count = 1) | ||
{ | ||
return $this->has($relation, $operator, $count, 'and', $callback); | ||
} | ||
|
||
/** | ||
* Add a relationship count / exists condition to the query with where clauses and an "or". | ||
* | ||
* @param string $relation | ||
* @param string $operator | ||
* @param int $count | ||
* @return \Statamic\Query\Builder|static | ||
*/ | ||
public function orWhereHas($relation, Closure $callback = null, $operator = '>=', $count = 1) | ||
{ | ||
return $this->has($relation, $operator, $count, 'or', $callback); | ||
} | ||
|
||
/** | ||
* Add a relationship count / exists condition to the query with where clauses. | ||
* | ||
* @param string $relation | ||
* @return \Statamic\Query\Builder|static | ||
*/ | ||
public function whereDoesntHave($relation, Closure $callback = null) | ||
{ | ||
return $this->doesntHave($relation, 'and', $callback); | ||
} | ||
|
||
/** | ||
* Add a relationship count / exists condition to the query with where clauses and an "or". | ||
* | ||
* @param string $relation | ||
* @return \Statamic\Query\Builder|static | ||
*/ | ||
public function orWhereDoesntHave($relation, Closure $callback = null) | ||
{ | ||
return $this->doesntHave($relation, 'or', $callback); | ||
} | ||
|
||
/** | ||
* Add a basic where clause to a relationship query. | ||
* | ||
* @param string $relation | ||
* @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column | ||
* @param mixed $operator | ||
* @param mixed $value | ||
* @return \Statamic\Query\Builder|static | ||
*/ | ||
public function whereRelation($relation, $column, $operator = null, $value = null) | ||
{ | ||
return $this->whereHas($relation, function ($query) use ($column, $operator, $value) { | ||
if ($column instanceof Closure) { | ||
$column($query); | ||
} else { | ||
$query->where($column, $operator, $value); | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Add an "or where" clause to a relationship query. | ||
* | ||
* @param string $relation | ||
* @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column | ||
* @param mixed $operator | ||
* @param mixed $value | ||
* @return \Statamic\Query\Builder|static | ||
*/ | ||
public function orWhereRelation($relation, $column, $operator = null, $value = null) | ||
{ | ||
return $this->orWhereHas($relation, function ($query) use ($column, $operator, $value) { | ||
if ($column instanceof Closure) { | ||
$column($query); | ||
} else { | ||
$query->where($column, $operator, $value); | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Get the blueprints available to this query builder | ||
* | ||
* @return \Illuminate\Support\Collection | ||
*/ | ||
protected function getBlueprintsForRelations() | ||
{ | ||
return collect(); | ||
} | ||
|
||
/** | ||
* Get the query builder and field for the relation we are querying (if they exist) | ||
* | ||
* @param string $relation | ||
* @return \Statamic\Query\Builder | ||
*/ | ||
protected function getRelationQueryBuilderAndField($relation) | ||
{ | ||
$relationField = $this->getBlueprintsForRelations() | ||
->flatMap(function ($blueprint) use ($relation) { | ||
return $blueprint->fields()->all()->map(function ($field) use ($relation) { | ||
if ($field->handle() == $relation && $field->fieldtype()->isRelationship()) { | ||
return $field; | ||
} | ||
}) | ||
->filter() | ||
->values(); | ||
}) | ||
->filter() | ||
->first(); | ||
|
||
if (! $relationField) { | ||
throw new InvalidArgumentException("Relation {$relation} does not exist"); | ||
} | ||
|
||
$queryBuilder = $relationField->fieldtype()->relationshipQueryBuilder(); | ||
|
||
if (! $queryBuilder) { | ||
throw new InvalidArgumentException("Relation {$relation} does not support subquerying"); | ||
} | ||
|
||
return [$queryBuilder, $relationField]; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.