Skip to content

Commit 9a8ad5b

Browse files
authored
Validate duplicate operators are not used in CombinedFieldQuery (mongodb#3)
1 parent 49e266d commit 9a8ad5b

File tree

2 files changed

+135
-20
lines changed

2 files changed

+135
-20
lines changed

src/Builder/Type/CombinedFieldQuery.php

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,78 @@
44

55
namespace MongoDB\Builder\Type;
66

7-
use MongoDB\BSON\Document;
8-
use MongoDB\BSON\Serializable;
7+
use MongoDB\BSON\Decimal128;
8+
use MongoDB\BSON\Int64;
9+
use MongoDB\BSON\Regex;
910
use MongoDB\Exception\InvalidArgumentException;
1011
use stdClass;
1112

13+
use function array_is_list;
14+
use function array_key_exists;
15+
use function array_key_first;
16+
use function array_merge;
17+
use function array_reduce;
18+
use function count;
1219
use function get_debug_type;
20+
use function get_object_vars;
1321
use function is_array;
22+
use function is_string;
1423
use function sprintf;
24+
use function str_starts_with;
1525

1626
/**
17-
* List of filters that apply to the same field path.
27+
* List of field queries that apply to the same field path.
1828
*/
1929
class CombinedFieldQuery implements FieldQueryInterface
2030
{
21-
public function __construct(
22-
/** @var list<FieldQueryInterface|Serializable|array|stdClass> $fieldQueries */
23-
public readonly array $fieldQueries,
24-
) {
25-
foreach ($fieldQueries as $fieldQuery) {
26-
if (! $fieldQuery instanceof FieldQueryInterface && ! $fieldQuery instanceof Serializable && ! is_array($fieldQuery) && ! $fieldQuery instanceof stdClass) {
27-
throw new InvalidArgumentException(sprintf('Expected filters to be a list of %s, %s, array or stdClass, %s given.', FieldQueryInterface::class, Document::class, get_debug_type($fieldQuery)));
31+
/** @var list<QueryInterface|FieldQueryInterface|Decimal128|Int64|Regex|stdClass|array|bool|float|int|string|null> $fieldQueries */
32+
public readonly array $fieldQueries;
33+
34+
/** @param list<QueryInterface|FieldQueryInterface|Decimal128|Int64|Regex|stdClass|array|bool|float|int|string|null> $fieldQueries */
35+
public function __construct(array $fieldQueries)
36+
{
37+
if (! array_is_list($fieldQueries)) {
38+
throw new InvalidArgumentException('Expected filters to be a list, invalid array given.');
39+
}
40+
41+
// Flatten nested CombinedFieldQuery
42+
$this->fieldQueries = array_reduce($fieldQueries, static function (array $fieldQueries, QueryInterface|FieldQueryInterface|Decimal128|Int64|Regex|stdClass|array|bool|float|int|string|null $fieldQuery): array {
43+
if ($fieldQuery instanceof CombinedFieldQuery) {
44+
return array_merge($fieldQueries, $fieldQuery->fieldQueries);
45+
}
46+
47+
$fieldQueries[] = $fieldQuery;
48+
49+
return $fieldQueries;
50+
}, []);
51+
52+
// Validate FieldQuery types and non-duplicate operators
53+
$seenOperators = [];
54+
foreach ($this->fieldQueries as $fieldQuery) {
55+
if ($fieldQuery instanceof stdClass) {
56+
$fieldQuery = get_object_vars($fieldQuery);
2857
}
58+
59+
if ($fieldQuery instanceof FieldQueryInterface && $fieldQuery instanceof OperatorInterface) {
60+
$operator = $fieldQuery->getOperator();
61+
} elseif (is_array($fieldQuery)) {
62+
if (count($fieldQuery) !== 1) {
63+
throw new InvalidArgumentException(sprintf('Operator must contain exactly one key, %d given', count($fieldQuery)));
64+
}
65+
66+
$operator = array_key_first($fieldQuery);
67+
if (! is_string($operator) || ! str_starts_with($operator, '$')) {
68+
throw new InvalidArgumentException(sprintf('Operator must contain exactly one key starting with $, "%s" given', $operator));
69+
}
70+
} else {
71+
throw new InvalidArgumentException(sprintf('Expected filters to be a list of field query operators, array or stdClass, %s given', get_debug_type($fieldQuery)));
72+
}
73+
74+
if (array_key_exists($operator, $seenOperators)) {
75+
throw new InvalidArgumentException(sprintf('Duplicate operator "%s" detected', $operator));
76+
}
77+
78+
$seenOperators[$operator] = true;
2979
}
3080
}
3181
}

tests/Builder/Type/CombinedFieldQueryTest.php

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,44 +5,109 @@
55
namespace MongoDB\Tests\Builder\Type;
66

77
use Generator;
8+
use MongoDB\Builder\Query\EqOperator;
9+
use MongoDB\Builder\Query\GtOperator;
810
use MongoDB\Builder\Type\CombinedFieldQuery;
911
use MongoDB\Exception\InvalidArgumentException;
1012
use PHPUnit\Framework\TestCase;
1113

1214
class CombinedFieldQueryTest extends TestCase
1315
{
14-
public function testEmptyFieldQueries(): void
16+
public function testEmpty(): void
1517
{
1618
$fieldQueries = new CombinedFieldQuery([]);
1719

1820
$this->assertSame([], $fieldQueries->fieldQueries);
1921
}
2022

21-
public function testFieldQueries(): void
23+
public function testSupportedTypes(): void
2224
{
2325
$fieldQueries = new CombinedFieldQuery([
24-
$this->createMock(CombinedFieldQuery::class),
26+
new EqOperator(1),
2527
['$gt' => 1],
26-
new CombinedFieldQuery([]),
28+
(object) ['$lt' => 1],
29+
]);
30+
31+
$this->assertCount(3, $fieldQueries->fieldQueries);
32+
}
33+
34+
public function testFlattenCombinedFieldQueries(): void
35+
{
36+
$fieldQueries = new CombinedFieldQuery([
37+
new CombinedFieldQuery([
38+
new CombinedFieldQuery([
39+
['$lt' => 1],
40+
new CombinedFieldQuery([]),
41+
]),
42+
['$gt' => 1],
43+
]),
44+
['$gte' => 1],
2745
]);
2846

2947
$this->assertCount(3, $fieldQueries->fieldQueries);
3048
}
3149

3250
/** @dataProvider provideInvalidFieldQuery */
33-
public function testRejectInvalidFieldQueries($invalidQuery): void
51+
public function testRejectInvalidFieldQueries(mixed $invalidQuery, string $message = '-'): void
3452
{
3553
$this->expectException(InvalidArgumentException::class);
54+
$this->expectExceptionMessage($message);
3655

3756
new CombinedFieldQuery([$invalidQuery]);
3857
}
3958

4059
public static function provideInvalidFieldQuery(): Generator
4160
{
42-
yield 'int' => [1];
43-
yield 'float' => [1.1];
44-
yield 'string' => ['foo'];
45-
yield 'bool' => [true];
46-
yield 'null' => [null];
61+
yield 'int' => [1, 'Expected filters to be a list of field query operators, array or stdClass, int given'];
62+
yield 'float' => [1.1, 'Expected filters to be a list of field query operators, array or stdClass, float given'];
63+
yield 'string' => ['foo', 'Expected filters to be a list of field query operators, array or stdClass, string given'];
64+
yield 'bool' => [true, 'Expected filters to be a list of field query operators, array or stdClass, bool given'];
65+
yield 'null' => [null, 'Expected filters to be a list of field query operators, array or stdClass, null given'];
66+
yield 'empty array' => [[], 'Operator must contain exactly one key, 0 given'];
67+
yield 'array with two keys' => [['$eq' => 1, '$ne' => 2], 'Operator must contain exactly one key, 2 given'];
68+
yield 'array key without $' => [['eq' => 1], 'Operator must contain exactly one key starting with $, "eq" given'];
69+
yield 'empty object' => [(object) [], 'Operator must contain exactly one key, 0 given'];
70+
yield 'object with two keys' => [(object) ['$eq' => 1, '$ne' => 2], 'Operator must contain exactly one key, 2 given'];
71+
yield 'object key without $' => [(object) ['eq' => 1], 'Operator must contain exactly one key starting with $, "eq" given'];
72+
}
73+
74+
/**
75+
* @param array<mixed> $fieldQueries
76+
*
77+
* @dataProvider provideDuplicateOperator
78+
*/
79+
public function testRejectDuplicateOperator(array $fieldQueries): void
80+
{
81+
$this->expectException(InvalidArgumentException::class);
82+
$this->expectExceptionMessage('Duplicate operator "$eq" detected');
83+
84+
new CombinedFieldQuery([
85+
['$eq' => 1],
86+
new EqOperator(2),
87+
]);
88+
}
89+
90+
public function provideDuplicateOperator(): Generator
91+
{
92+
yield 'array and FieldQuery' => [
93+
[
94+
['$eq' => 1],
95+
new EqOperator(2),
96+
],
97+
];
98+
99+
yield 'object and FieldQuery' => [
100+
[
101+
(object) ['$gt' => 1],
102+
new GtOperator(2),
103+
],
104+
];
105+
106+
yield 'object and array' => [
107+
[
108+
(object) ['$ne' => 1],
109+
['$ne' => 2],
110+
],
111+
];
47112
}
48113
}

0 commit comments

Comments
 (0)