Skip to content

Commit 710d7f7

Browse files
authored
Merge pull request #791 from DirectoryTree/and-or-behaviour
Fix `orFilter/andFilter` behaviour with nested clauses
2 parents d51ca5e + 5ae40e4 commit 710d7f7

File tree

2 files changed

+78
-12
lines changed

2 files changed

+78
-12
lines changed

src/Query/Builder.php

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use LdapRecord\Query\Filter\AndGroup;
1818
use LdapRecord\Query\Filter\Factory;
1919
use LdapRecord\Query\Filter\Filter;
20+
use LdapRecord\Query\Filter\GroupFilter;
2021
use LdapRecord\Query\Filter\Not;
2122
use LdapRecord\Query\Filter\OrGroup;
2223
use LdapRecord\Query\Filter\Raw;
@@ -763,7 +764,9 @@ public function andFilter(Closure $closure): static
763764
$query = $this->newNestedInstance($closure);
764765

765766
if ($filter = $query->getFilter()) {
766-
$this->addFilter(new AndGroup($filter));
767+
$this->addFilter(new AndGroup(
768+
...$this->extractNestedFilters($filter)
769+
), wrap: false);
767770
}
768771

769772
return $this;
@@ -777,12 +780,38 @@ public function orFilter(Closure $closure): static
777780
$query = $this->newNestedInstance($closure);
778781

779782
if ($filter = $query->getFilter()) {
780-
$this->addFilter(new OrGroup($filter), 'or');
783+
$this->addFilter(new OrGroup(
784+
...$this->extractNestedFilters($filter)
785+
), wrap: false);
781786
}
782787

783788
return $this;
784789
}
785790

791+
/**
792+
* Extract filters from a nested group filter for re-wrapping, preserving nested groups.
793+
*
794+
* @return array<Filter>
795+
*/
796+
protected function extractNestedFilters(Filter $filter): array
797+
{
798+
if (! $filter instanceof GroupFilter) {
799+
return [$filter];
800+
}
801+
802+
$children = $filter->getFilters();
803+
804+
// If any child is a group, preserve the structure
805+
foreach ($children as $child) {
806+
if ($child instanceof GroupFilter) {
807+
return $children;
808+
}
809+
}
810+
811+
// All children are non-groups, it's safe to unwrap.
812+
return $children;
813+
}
814+
786815
/**
787816
* Add a nested 'not' filter to the query.
788817
*/
@@ -1146,7 +1175,7 @@ public function orWhereNotEndsWith(string $attribute, string $value): static
11461175
/**
11471176
* Add a filter to the query.
11481177
*/
1149-
public function addFilter(Filter $filter, string $boolean = 'and'): static
1178+
public function addFilter(Filter $filter, string $boolean = 'and', bool $wrap = true): static
11501179
{
11511180
if (is_null($this->filter)) {
11521181
$this->filter = $filter;
@@ -1157,11 +1186,11 @@ public function addFilter(Filter $filter, string $boolean = 'and'): static
11571186
// Flatten same-type groups to avoid deeply nested structures.
11581187
// Ex: AndGroup(AndGroup(a, b), c) becomes AndGroup(a, b, c)
11591188
if ($boolean === 'or') {
1160-
$this->filter = $this->filter instanceof OrGroup
1189+
$this->filter = $this->filter instanceof OrGroup && $wrap
11611190
? new OrGroup(...[...$this->filter->getFilters(), $filter])
11621191
: new OrGroup($this->filter, $filter);
11631192
} else {
1164-
$this->filter = $this->filter instanceof AndGroup
1193+
$this->filter = $this->filter instanceof AndGroup && $wrap
11651194
? new AndGroup(...[...$this->filter->getFilters(), $filter])
11661195
: new AndGroup($this->filter, $filter);
11671196
}

tests/Unit/Query/BuilderTest.php

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -762,31 +762,43 @@ public function test_nested_or_filter()
762762
{
763763
$b = $this->newBuilder();
764764

765-
// orFilter wraps the nested query in an OrGroup
766-
// Since where() creates an AndGroup, the result is OrGroup(AndGroup(...))
765+
// orFilter combines the filters inside with OR
767766
$query = $b->orFilter(function ($query) {
768767
$query->where([
769768
'one' => 'one',
770769
'two' => 'two',
771770
]);
772771
})->getUnescapedQuery();
773772

774-
$this->assertEquals('(|(&(one=one)(two=two)))', $query);
773+
$this->assertEquals('(|(one=one)(two=two))', $query);
774+
}
775+
776+
public function test_nested_or_filter_with_where_equals()
777+
{
778+
$b = $this->newBuilder();
779+
780+
// orFilter with whereEquals should combine with OR, not AND
781+
$query = $b->orFilter(function ($query) {
782+
$query->whereEquals('foo', '1');
783+
$query->whereEquals('foo', '2');
784+
})->getUnescapedQuery();
785+
786+
$this->assertEquals('(|(foo=1)(foo=2))', $query);
775787
}
776788

777789
public function test_nested_and_filter()
778790
{
779791
$b = $this->newBuilder();
780792

781-
// andFilter wraps the nested query in an AndGroup
793+
// andFilter combines the filters inside with AND
782794
$query = $b->andFilter(function ($query) {
783795
$query->where([
784796
'one' => 'one',
785797
'two' => 'two',
786798
]);
787799
})->getUnescapedQuery();
788800

789-
$this->assertEquals('(&(&(one=one)(two=two)))', $query);
801+
$this->assertEquals('(&(one=one)(two=two))', $query);
790802
}
791803

792804
public function test_nested_not_filter()
@@ -820,7 +832,7 @@ public function test_nested_filters()
820832
]);
821833
})->getUnescapedQuery();
822834

823-
$this->assertEquals('(&(|(&(one=one)(two=two)))(&(&(one=one)(two=two))))', $query);
835+
$this->assertEquals('(&(|(one=one)(two=two))(&(one=one)(two=two)))', $query);
824836
}
825837

826838
public function test_nested_filters_with_non_nested()
@@ -842,7 +854,32 @@ public function test_nested_filters_with_non_nested()
842854
'six' => 'six',
843855
])->getUnescapedQuery();
844856

845-
$this->assertEquals('(&(|(&(one=one)(two=two)))(&(&(three=three)(four=four)))(five=five)(six=six))', $query);
857+
$this->assertEquals('(&(|(one=one)(two=two))(&(three=three)(four=four))(five=five)(six=six))', $query);
858+
}
859+
860+
public function test_deeply_nested_filters()
861+
{
862+
$b = $this->newBuilder();
863+
864+
// Complex query: enabled AND (user with verified OR service with trusted)
865+
$query = $b->andFilter(function ($query) {
866+
$query->whereEquals('enabled', 'true');
867+
$query->orFilter(function ($q) {
868+
$q->andFilter(function ($inner) {
869+
$inner->whereEquals('type', 'user');
870+
$inner->whereEquals('verified', 'true');
871+
});
872+
$q->andFilter(function ($inner) {
873+
$inner->whereEquals('type', 'service');
874+
$inner->whereEquals('trusted', 'true');
875+
});
876+
});
877+
})->getUnescapedQuery();
878+
879+
$this->assertEquals(
880+
'(&(enabled=true)(|(&(type=user)(verified=true))(&(type=service)(trusted=true))))',
881+
$query
882+
);
846883
}
847884

848885
public function test_nested_builder_is_nested()

0 commit comments

Comments
 (0)