Skip to content

Commit 7283a1e

Browse files
authored
Merge pull request #805 from DirectoryTree/bug-804
Fix nested filter grouping order sensitivity
2 parents a82c083 + ce8d5f2 commit 7283a1e

8 files changed

Lines changed: 159 additions & 91 deletions

File tree

src/Query/Builder.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,7 @@ public function andFilter(Closure $closure): static
764764
$query = $this->newNestedInstance($closure);
765765

766766
if ($filter = $query->getFilter()) {
767-
$this->addFilter(new AndGroup(
767+
$this->addFilter(AndGroup::nested(
768768
...$this->extractNestedFilters($filter)
769769
), wrap: false);
770770
}
@@ -780,7 +780,7 @@ public function orFilter(Closure $closure): static
780780
$query = $this->newNestedInstance($closure);
781781

782782
if ($filter = $query->getFilter()) {
783-
$this->addFilter(new OrGroup(
783+
$this->addFilter(OrGroup::nested(
784784
...$this->extractNestedFilters($filter)
785785
), wrap: false);
786786
}
@@ -1162,11 +1162,11 @@ public function addFilter(Filter $filter, string $boolean = 'and', bool $wrap =
11621162
// Flatten same-type groups to avoid deeply nested structures.
11631163
// Ex: AndGroup(AndGroup(a, b), c) becomes AndGroup(a, b, c)
11641164
if ($boolean === 'or') {
1165-
$this->filter = $this->filter instanceof OrGroup && $wrap
1165+
$this->filter = $this->filter instanceof OrGroup && ! $this->filter->isNested() && $wrap
11661166
? new OrGroup(...[...$this->filter->getFilters(), $filter])
11671167
: new OrGroup($this->filter, $filter);
11681168
} else {
1169-
$this->filter = $this->filter instanceof AndGroup && $wrap
1169+
$this->filter = $this->filter instanceof AndGroup && ! $this->filter->isNested() && $wrap
11701170
? new AndGroup(...[...$this->filter->getFilters(), $filter])
11711171
: new AndGroup($this->filter, $filter);
11721172
}

src/Query/ExtractsNestedFilters.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace LdapRecord\Query;
44

5+
use LdapRecord\Query\Filter\BooleanGroup;
56
use LdapRecord\Query\Filter\Filter;
67
use LdapRecord\Query\Filter\GroupFilter;
78

@@ -14,7 +15,11 @@ trait ExtractsNestedFilters
1415
*/
1516
protected function extractNestedFilters(Filter $filter): array
1617
{
17-
if (! $filter instanceof GroupFilter) {
18+
if (! $filter instanceof BooleanGroup) {
19+
return [$filter];
20+
}
21+
22+
if ($filter->isNested()) {
1823
return [$filter];
1924
}
2025

src/Query/Filter/AndGroup.php

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,54 +2,13 @@
22

33
namespace LdapRecord\Query\Filter;
44

5-
class AndGroup implements GroupFilter
5+
class AndGroup extends BooleanGroup
66
{
7-
/**
8-
* The filters in the group.
9-
*
10-
* @var Filter[]
11-
*/
12-
protected array $filters;
13-
14-
/**
15-
* Create a new AND group filter.
16-
*/
17-
public function __construct(Filter ...$filters)
18-
{
19-
$this->filters = $filters;
20-
}
21-
22-
/**
23-
* Get the filters in the group.
24-
*
25-
* @return Filter[]
26-
*/
27-
public function getFilters(): array
28-
{
29-
return $this->filters;
30-
}
31-
327
/**
338
* {@inheritdoc}
349
*/
3510
public function getOperator(): string
3611
{
3712
return '&';
3813
}
39-
40-
/**
41-
* {@inheritdoc}
42-
*/
43-
public function getRaw(): string
44-
{
45-
return '&'.implode($this->filters);
46-
}
47-
48-
/**
49-
* {@inheritdoc}
50-
*/
51-
public function __toString(): string
52-
{
53-
return '('.$this->getRaw().')';
54-
}
5514
}

src/Query/Filter/BooleanGroup.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace LdapRecord\Query\Filter;
4+
5+
abstract class BooleanGroup implements GroupFilter
6+
{
7+
/**
8+
* The filters in the group.
9+
*
10+
* @var Filter[]
11+
*/
12+
protected array $filters;
13+
14+
/**
15+
* Determine if this group should be preserved when nesting filters.
16+
*/
17+
protected bool $nested = false;
18+
19+
/**
20+
* Create a new boolean group filter.
21+
*/
22+
public function __construct(Filter ...$filters)
23+
{
24+
$this->filters = $filters;
25+
}
26+
27+
/**
28+
* Create a new nested boolean group filter.
29+
*/
30+
public static function nested(Filter ...$filters): static
31+
{
32+
$group = new static(...$filters);
33+
34+
$group->nested = true;
35+
36+
return $group;
37+
}
38+
39+
/**
40+
* Determine if this group should be preserved when nesting filters.
41+
*/
42+
public function isNested(): bool
43+
{
44+
return $this->nested;
45+
}
46+
47+
/**
48+
* Get the filters in the group.
49+
*
50+
* @return Filter[]
51+
*/
52+
public function getFilters(): array
53+
{
54+
return $this->filters;
55+
}
56+
57+
/**
58+
* {@inheritdoc}
59+
*/
60+
public function getRaw(): string
61+
{
62+
return $this->getOperator().implode($this->filters);
63+
}
64+
65+
/**
66+
* {@inheritdoc}
67+
*/
68+
public function __toString(): string
69+
{
70+
return '('.$this->getRaw().')';
71+
}
72+
}

src/Query/Filter/OrGroup.php

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,54 +2,13 @@
22

33
namespace LdapRecord\Query\Filter;
44

5-
class OrGroup implements GroupFilter
5+
class OrGroup extends BooleanGroup
66
{
7-
/**
8-
* The filters in the group.
9-
*
10-
* @var Filter[]
11-
*/
12-
protected array $filters;
13-
14-
/**
15-
* Create a new OR group filter.
16-
*/
17-
public function __construct(Filter ...$filters)
18-
{
19-
$this->filters = $filters;
20-
}
21-
22-
/**
23-
* Get the filters in the group.
24-
*
25-
* @return Filter[]
26-
*/
27-
public function getFilters(): array
28-
{
29-
return $this->filters;
30-
}
31-
327
/**
338
* {@inheritdoc}
349
*/
3510
public function getOperator(): string
3611
{
3712
return '|';
3813
}
39-
40-
/**
41-
* {@inheritdoc}
42-
*/
43-
public function getRaw(): string
44-
{
45-
return '|'.implode($this->filters);
46-
}
47-
48-
/**
49-
* {@inheritdoc}
50-
*/
51-
public function __toString(): string
52-
{
53-
return '('.$this->getRaw().')';
54-
}
5514
}

src/Query/Model/Builder.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,7 @@ public function andFilter(Closure $closure): static
730730
$query = $this->newNestedModelInstance($closure);
731731

732732
if ($filter = $query->getQuery()->getFilter()) {
733-
$this->query->addFilter(new AndGroup(
733+
$this->query->addFilter(AndGroup::nested(
734734
...$this->extractNestedFilters($filter)
735735
), wrap: false);
736736
}
@@ -746,7 +746,7 @@ public function orFilter(Closure $closure): static
746746
$query = $this->newNestedModelInstance($closure);
747747

748748
if ($filter = $query->getQuery()->getFilter()) {
749-
$this->query->addFilter(new OrGroup(
749+
$this->query->addFilter(OrGroup::nested(
750750
...$this->extractNestedFilters($filter)
751751
), wrap: false);
752752
}

tests/Unit/Query/BuilderTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,36 @@ public function test_nested_or_filter_with_where_equals()
786786
$this->assertEquals('(|(foo=1)(foo=2))', $query);
787787
}
788788

789+
public function test_nested_or_filter_preserves_nested_and_filter_when_followed_by_where()
790+
{
791+
$b = $this->newBuilder();
792+
793+
$query = $b->orFilter(function ($query) {
794+
$query->andFilter(function ($query) {
795+
$query->whereStartsWith('givenName', 'John');
796+
$query->whereStartsWith('sn', 'Smith');
797+
});
798+
$query->where('mail', '=', 'John Smith');
799+
})->getUnescapedQuery();
800+
801+
$this->assertEquals('(|(&(givenName=John*)(sn=Smith*))(mail=John Smith))', $query);
802+
}
803+
804+
public function test_nested_or_filter_preserves_nested_and_filter_when_preceded_by_where()
805+
{
806+
$b = $this->newBuilder();
807+
808+
$query = $b->orFilter(function ($query) {
809+
$query->where('mail', '=', 'John Smith');
810+
$query->andFilter(function ($query) {
811+
$query->whereStartsWith('givenName', 'John');
812+
$query->whereStartsWith('sn', 'Smith');
813+
});
814+
})->getUnescapedQuery();
815+
816+
$this->assertEquals('(|(mail=John Smith)(&(givenName=John*)(sn=Smith*)))', $query);
817+
}
818+
789819
public function test_nested_and_filter()
790820
{
791821
$b = $this->newBuilder();
@@ -816,6 +846,19 @@ public function test_nested_not_filter()
816846
$this->assertEquals('(!(&(one=one)(two=two)))', $query);
817847
}
818848

849+
public function test_nested_and_filter_preserves_not_filter()
850+
{
851+
$b = $this->newBuilder();
852+
853+
$query = $b->andFilter(function ($query) {
854+
$query->notFilter(function ($query) {
855+
$query->whereEquals('foo', 'bar');
856+
});
857+
})->getUnescapedQuery();
858+
859+
$this->assertEquals('(&(!(foo=bar)))', $query);
860+
}
861+
819862
public function test_nested_filters()
820863
{
821864
$b = $this->newBuilder();

tests/Unit/Query/Model/ActiveDirectoryBuilderTest.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,36 @@ public function test_or_filter_extracts_filters_from_nested_query()
178178
$this->assertEquals('(|(foo=1)(foo=2))', $query);
179179
}
180180

181+
public function test_or_filter_preserves_nested_and_filter_when_followed_by_where()
182+
{
183+
$b = $this->newBuilder();
184+
185+
$query = $b->orFilter(function ($query) {
186+
$query->andFilter(function ($query) {
187+
$query->whereStartsWith('givenName', 'John');
188+
$query->whereStartsWith('sn', 'Smith');
189+
});
190+
$query->where('mail', '=', 'John Smith');
191+
})->getUnescapedQuery();
192+
193+
$this->assertEquals('(|(&(givenName=John*)(sn=Smith*))(mail=John Smith))', $query);
194+
}
195+
196+
public function test_or_filter_preserves_nested_and_filter_when_preceded_by_where()
197+
{
198+
$b = $this->newBuilder();
199+
200+
$query = $b->orFilter(function ($query) {
201+
$query->where('mail', '=', 'John Smith');
202+
$query->andFilter(function ($query) {
203+
$query->whereStartsWith('givenName', 'John');
204+
$query->whereStartsWith('sn', 'Smith');
205+
});
206+
})->getUnescapedQuery();
207+
208+
$this->assertEquals('(|(mail=John Smith)(&(givenName=John*)(sn=Smith*)))', $query);
209+
}
210+
181211
public function test_and_filter_extracts_filters_from_nested_query()
182212
{
183213
$b = $this->newBuilder();

0 commit comments

Comments
 (0)