Skip to content

Commit a8988d4

Browse files
committed
WIP
1 parent 3a273b2 commit a8988d4

File tree

4 files changed

+162
-27
lines changed

4 files changed

+162
-27
lines changed

src/Models/Model.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -399,9 +399,13 @@ public function registerGlobalScopes(Builder $builder): Builder
399399
*/
400400
public function applyObjectClassScopes(Builder $query): void
401401
{
402-
foreach (static::$objectClasses as $objectClass) {
403-
$query->where('objectclass', '=', $objectClass);
404-
}
402+
$query->withGlobalScope('objectClasses', function (Builder $query) {
403+
$query->where(function (Builder $query) {
404+
foreach (static::$objectClasses as $objectClass) {
405+
$query->where('objectclass', $objectClass);
406+
}
407+
});
408+
});
405409
}
406410

407411
/**

src/Query/Builder.php

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class Builder
4848
'and' => [],
4949
'or' => [],
5050
'raw' => [],
51+
'nested' => [],
5152
];
5253

5354
/**
@@ -764,6 +765,8 @@ public function rawFilter(array|string $filters = []): static
764765

765766
/**
766767
* Add a nested 'and' filter to the query.
768+
*
769+
* @deprecated Use where() with a closure instead for better Laravel Eloquent compatibility
767770
*/
768771
public function andFilter(Closure $closure): static
769772
{
@@ -776,6 +779,8 @@ public function andFilter(Closure $closure): static
776779

777780
/**
778781
* Add a nested 'or' filter to the query.
782+
*
783+
* @deprecated Use orWhere() with a closure instead for better Laravel Eloquent compatibility
779784
*/
780785
public function orFilter(Closure $closure): static
781786
{
@@ -803,8 +808,12 @@ public function notFilter(Closure $closure): static
803808
*
804809
* @throws InvalidArgumentException
805810
*/
806-
public function where(array|string $attribute, mixed $operator = null, mixed $value = null, string $boolean = 'and', bool $raw = false): static
811+
public function where(Closure|array|string $attribute, mixed $operator = null, mixed $value = null, string $boolean = 'and', bool $raw = false): static
807812
{
813+
if ($attribute instanceof Closure) {
814+
return $this->whereNested($attribute, $boolean);
815+
}
816+
808817
if (is_array($attribute)) {
809818
return $this->addArrayOfWheres($attribute, $boolean, $raw);
810819
}
@@ -1009,11 +1018,39 @@ public function hasControl(string $oid): bool
10091018
return array_key_exists($oid, $this->controls);
10101019
}
10111020

1021+
/**
1022+
* Add a nested where clause to the query.
1023+
*/
1024+
public function whereNested(Closure $callback, string $boolean = 'and'): static
1025+
{
1026+
$query = $this->newNestedInstance();
1027+
1028+
$callback($query);
1029+
1030+
return $this->addNestedWhereQuery($query, $boolean);
1031+
}
1032+
1033+
/**
1034+
* Add another query builder as a nested where to the query builder.
1035+
*/
1036+
public function addNestedWhereQuery(Builder $query, string $boolean = 'and'): static
1037+
{
1038+
if (count($query->filters['and']) || count($query->filters['or']) || count($query->filters['raw']) || count($query->filters['nested'])) {
1039+
$this->addFilter('nested', compact('query', 'boolean'));
1040+
}
1041+
1042+
return $this;
1043+
}
1044+
10121045
/**
10131046
* Add an 'or where' clause to the query.
10141047
*/
1015-
public function orWhere(array|string $attribute, ?string $operator = null, ?string $value = null): static
1048+
public function orWhere(Closure|array|string $attribute, ?string $operator = null, ?string $value = null): static
10161049
{
1050+
if ($attribute instanceof Closure) {
1051+
return $this->whereNested($attribute, 'or');
1052+
}
1053+
10171054
[$value, $operator] = $this->prepareValueAndOperator(
10181055
$value, $operator, func_num_args() === 2 && ! $this->operatorRequiresValue($operator)
10191056
);
@@ -1130,6 +1167,13 @@ public function addFilter(string $type, array $bindings): static
11301167
throw new InvalidArgumentException("Filter type: [$type] is invalid.");
11311168
}
11321169

1170+
// Nested filters have different validation requirements
1171+
if ($type === 'nested') {
1172+
$this->filters[$type][] = $bindings;
1173+
1174+
return $this;
1175+
}
1176+
11331177
// Each filter clause require key bindings to be set. We
11341178
// will validate this here to ensure all of them have
11351179
// been provided, or throw an exception otherwise.

src/Query/Grammar.php

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ protected function compileFilters(Builder $query): string
6161
{
6262
return $this->compileRaws($query)
6363
.$this->compileWheres($query)
64-
.$this->compileOrWheres($query);
64+
.$this->compileOrWheres($query)
65+
.$this->compileNestedWheres($query);
6566
}
6667

6768
/**
@@ -98,7 +99,7 @@ protected function hasMultipleFilterTypes(Builder $query): bool
9899
{
99100
$filterCount = 0;
100101

101-
foreach (['and', 'or', 'raw'] as $type) {
102+
foreach (['and', 'or', 'raw', 'nested'] as $type) {
102103
if (! empty($query->filters[$type])) {
103104
$filterCount++;
104105
}
@@ -115,6 +116,14 @@ protected function hasMultipleAndConditions(Builder $query): bool
115116
return count($query->filters['and'] ?? []) > 1;
116117
}
117118

119+
/**
120+
* Determine if the query has multiple OR conditions.
121+
*/
122+
protected function hasMultipleOrConditions(Builder $query): bool
123+
{
124+
return count($query->filters['or'] ?? []) > 1;
125+
}
126+
118127
/**
119128
* Determine if the query has multiple raw filters.
120129
*/
@@ -128,11 +137,22 @@ protected function hasMultipleRawFilters(Builder $query): bool
128137
*/
129138
protected function shouldWrapEntireQueryInOr(Builder $query): bool
130139
{
131-
// If we have exactly one AND condition and one or more OR conditions, wrap the
140+
// If we have AND conditions and OR conditions (regardless of count), wrap the
132141
// entire query in OR to treat all conditions as alternatives. This handles
133-
// the common case where a single where() is followed by orWhere() calls.
134-
return count($query->filters['and'] ?? []) === 1
135-
&& ! empty($query->filters['or'])
142+
// both single conditions and array-based conditions.
143+
$hasOrConditions = ! empty($query->filters['or']);
144+
145+
// Check if we have nested OR conditions
146+
$hasNestedOrConditions = false;
147+
foreach ($query->filters['nested'] ?? [] as $nested) {
148+
if ($nested['boolean'] === 'or') {
149+
$hasNestedOrConditions = true;
150+
break;
151+
}
152+
}
153+
154+
return ! empty($query->filters['and'])
155+
&& ($hasOrConditions || $hasNestedOrConditions)
136156
&& empty($query->filters['raw']);
137157
}
138158

@@ -160,7 +180,15 @@ protected function compileRaws(Builder $query): string
160180
*/
161181
protected function compileWheres(Builder $query): string
162182
{
163-
return $this->compileFilterType($query, 'and');
183+
$filter = $this->compileFilterType($query, 'and');
184+
185+
// If we have multiple AND conditions and OR conditions exist,
186+
// wrap the AND conditions in their own AND statement
187+
if (! empty($filter) && $this->hasMultipleAndConditions($query) && ! empty($query->filters['or'])) {
188+
return $this->compileAnd($filter);
189+
}
190+
191+
return $filter;
164192
}
165193

166194
/**
@@ -170,6 +198,10 @@ protected function compileOrWheres(Builder $query): string
170198
{
171199
$filter = $this->compileFilterType($query, 'or');
172200

201+
if (! empty($filter) && $this->hasMultipleOrConditions($query) && ! empty($query->filters['and'])) {
202+
return $this->compileAnd($filter);
203+
}
204+
173205
// If we're going to wrap the entire query in OR, don't wrap OR clauses separately
174206
if ($this->shouldWrapEntireQueryInOr($query)) {
175207
return $filter;
@@ -184,6 +216,45 @@ protected function compileOrWheres(Builder $query): string
184216
return $filter;
185217
}
186218

219+
/**
220+
* Assembles all nested where clauses.
221+
*/
222+
protected function compileNestedWheres(Builder $query): string
223+
{
224+
$filter = '';
225+
226+
foreach ($query->filters['nested'] ?? [] as $nested) {
227+
$nestedQuery = $this->compileFilters($nested['query']);
228+
229+
if ($nestedQuery) {
230+
// Always wrap nested queries in AND if they have multiple conditions,
231+
// regardless of the boolean type. The boolean type determines how
232+
// this nested group relates to other conditions at the parent level.
233+
if ($this->shouldWrapNestedAnd($nested['query'])) {
234+
$filter .= $this->compileAnd($nestedQuery);
235+
} else {
236+
$filter .= $nestedQuery;
237+
}
238+
}
239+
}
240+
241+
return $filter;
242+
}
243+
244+
/**
245+
* Determine if a nested AND query should be wrapped.
246+
*/
247+
protected function shouldWrapNestedAnd(Builder $query): bool
248+
{
249+
$conditionCount = 0;
250+
251+
foreach (['and', 'or', 'raw', 'nested'] as $type) {
252+
$conditionCount += count($query->filters[$type] ?? []);
253+
}
254+
255+
return $conditionCount > 1;
256+
}
257+
187258
/**
188259
* Compile filters of a specific type.
189260
*/

src/Query/Model/Builder.php

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ public function addSelect(array|string $select): static
590590
public function where(Closure|array|string $attribute, mixed $operator = null, mixed $value = null, string $boolean = 'and', bool $raw = false): static
591591
{
592592
if ($attribute instanceof Closure) {
593-
return $this->nestedFilter($attribute, $boolean);
593+
return $this->whereNested($attribute, $boolean);
594594
}
595595

596596
if (is_array($attribute)) {
@@ -636,13 +636,33 @@ public function whereRaw(array|string $attribute, ?string $operator = null, mixe
636636
return $this;
637637
}
638638

639+
/**
640+
* Add a nested where clause to the query.
641+
*/
642+
public function whereNested(Closure $callback, string $boolean = 'and'): static
643+
{
644+
$query = $this->newNestedModelInstance($callback);
645+
646+
return $this->addNestedWhereQuery($query, $boolean);
647+
}
648+
649+
/**
650+
* Add another query builder as a nested where to the query builder.
651+
*/
652+
public function addNestedWhereQuery(self $query, string $boolean = 'and'): static
653+
{
654+
$this->query->addNestedWhereQuery($query->getQuery(), $boolean);
655+
656+
return $this;
657+
}
658+
639659
/**
640660
* Add an or where clause to the query.
641661
*/
642662
public function orWhere(Closure|array|string $attribute, ?string $operator = null, ?string $value = null): static
643663
{
644664
if ($attribute instanceof Closure) {
645-
return $this->nestedFilter($attribute, 'or');
665+
return $this->whereNested($attribute, 'or');
646666
}
647667

648668
[$value, $operator] = $this->query->prepareValueAndOperator(
@@ -704,36 +724,32 @@ public function whereNotHas(string $attribute): static
704724

705725
/**
706726
* Add a nested where clause to the query.
727+
*
728+
* @deprecated Use whereNested() instead for better Laravel Eloquent compatibility
707729
*/
708730
public function nestedFilter(Closure $closure, string $boolean = 'and'): static
709731
{
710-
return $boolean === 'or'
711-
? $this->orFilter($closure)
712-
: $this->andFilter($closure);
732+
return $this->whereNested($closure, $boolean);
713733
}
714734

715735
/**
716736
* Adds a nested 'and' filter to the current query.
737+
*
738+
* @deprecated Use where() with a closure instead for better Laravel Eloquent compatibility
717739
*/
718740
public function andFilter(Closure $closure): static
719741
{
720-
$query = $this->newNestedModelInstance($closure);
721-
722-
return $this->rawFilter(
723-
$this->query->getGrammar()->compileAnd($query->getQuery()->getQuery())
724-
);
742+
return $this->whereNested($closure, 'and');
725743
}
726744

727745
/**
728746
* Adds a nested 'or' filter to the current query.
747+
*
748+
* @deprecated Use orWhere() with a closure instead for better Laravel Eloquent compatibility
729749
*/
730750
public function orFilter(Closure $closure): static
731751
{
732-
$query = $this->newNestedModelInstance($closure);
733-
734-
return $this->rawFilter(
735-
$this->query->getGrammar()->compileOr($query->getQuery()->getQuery())
736-
);
752+
return $this->whereNested($closure, 'or');
737753
}
738754

739755
/**

0 commit comments

Comments
 (0)