Skip to content

Commit f729baa

Browse files
GromNaNalcaeus
authored andcommitted
PHPORM-47 Improve Builder::whereBetween to support CarbonPeriod and reject invalid array (#10)
The Query\Builder::whereBetween() method can be used like this: whereBetween('date_field', [min, max]) whereBetween('date_field', collect([min, max])) whereBetween('date_field', CarbonPeriod) Laravel allows other formats: the $values array is flatten and the builder assumes there are at least 2 elements and ignore the others. It's a design that can lead to misunderstandings. I prefer to raise an exception when we have incorrect values, rather than trying to guess what the developer would like to do. Support for CarbonPeriod was fixed in Laravel 10: laravel/framework#46720 because the query builder was taking the 1st 2 values of the iterator instead of the start & end dates.
1 parent f90bf78 commit f729baa

File tree

2 files changed

+184
-6
lines changed

2 files changed

+184
-6
lines changed

src/Query/Builder.php

+22-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Jenssegers\Mongodb\Query;
44

5+
use Carbon\CarbonPeriod;
56
use Closure;
67
use DateTimeInterface;
78
use Illuminate\Database\Query\Builder as BaseBuilder;
@@ -554,11 +555,20 @@ public function whereAll($column, array $values, $boolean = 'and', $not = false)
554555

555556
/**
556557
* @inheritdoc
558+
* @param list{mixed, mixed}|CarbonPeriod $values
557559
*/
558560
public function whereBetween($column, iterable $values, $boolean = 'and', $not = false)
559561
{
560562
$type = 'between';
561563

564+
if ($values instanceof Collection) {
565+
$values = $values->all();
566+
}
567+
568+
if (is_array($values) && (! array_is_list($values) || count($values) !== 2)) {
569+
throw new \InvalidArgumentException('Between $values must be a list with exactly two elements: [min, max]');
570+
}
571+
562572
$this->wheres[] = compact('column', 'type', 'boolean', 'values', 'not');
563573

564574
return $this;
@@ -995,11 +1005,18 @@ protected function compileWheres(): array
9951005
}
9961006
}
9971007
} elseif (isset($where['values'])) {
998-
array_walk_recursive($where['values'], function (&$item, $key) {
999-
if ($item instanceof DateTimeInterface) {
1000-
$item = new UTCDateTime($item);
1001-
}
1002-
});
1008+
if (is_array($where['values'])) {
1009+
array_walk_recursive($where['values'], function (&$item, $key) {
1010+
if ($item instanceof DateTimeInterface) {
1011+
$item = new UTCDateTime($item);
1012+
}
1013+
});
1014+
} elseif ($where['values'] instanceof CarbonPeriod) {
1015+
$where['values'] = [
1016+
new UTCDateTime($where['values']->getStartDate()),
1017+
new UTCDateTime($where['values']->getEndDate()),
1018+
];
1019+
}
10031020
}
10041021

10051022
// The next item in a "chain" of wheres devices the boolean of the

tests/Query/BuilderTest.php

+162-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Jenssegers\Mongodb\Tests\Query;
66

77
use DateTimeImmutable;
8+
use Illuminate\Database\Eloquent\Collection;
89
use Illuminate\Tests\Database\DatabaseQueryBuilderTest;
910
use Jenssegers\Mongodb\Connection;
1011
use Jenssegers\Mongodb\Query\Builder;
@@ -124,13 +125,142 @@ function (Builder $builder) {
124125
->orderBy('score', ['$meta' => 'textScore']),
125126
];
126127

128+
/** @see DatabaseQueryBuilderTest::testWhereBetweens() */
129+
yield 'whereBetween array of numbers' => [
130+
['find' => [['id' => ['$gte' => 1, '$lte' => 2]], []]],
131+
fn (Builder $builder) => $builder->whereBetween('id', [1, 2]),
132+
];
133+
134+
yield 'whereBetween nested array of numbers' => [
135+
['find' => [['id' => ['$gte' => [1], '$lte' => [2, 3]]], []]],
136+
fn (Builder $builder) => $builder->whereBetween('id', [[1], [2, 3]]),
137+
];
138+
139+
$period = now()->toPeriod(now()->addMonth());
140+
yield 'whereBetween CarbonPeriod' => [
141+
['find' => [
142+
['created_at' => ['$gte' => new UTCDateTime($period->start), '$lte' => new UTCDateTime($period->end)]],
143+
[], // options
144+
]],
145+
fn (Builder $builder) => $builder->whereBetween('created_at', $period),
146+
];
147+
148+
yield 'whereBetween collection' => [
149+
['find' => [['id' => ['$gte' => 1, '$lte' => 2]], []]],
150+
fn (Builder $builder) => $builder->whereBetween('id', collect([1, 2])),
151+
];
152+
153+
/** @see DatabaseQueryBuilderTest::testOrWhereBetween() */
154+
yield 'orWhereBetween array of numbers' => [
155+
['find' => [
156+
['$or' => [
157+
['id' => 1],
158+
['id' => ['$gte' => 3, '$lte' => 5]],
159+
]],
160+
[], // options
161+
]],
162+
fn (Builder $builder) => $builder
163+
->where('id', '=', 1)
164+
->orWhereBetween('id', [3, 5]),
165+
];
166+
167+
/** @link https://www.mongodb.com/docs/manual/reference/bson-type-comparison-order/#arrays */
168+
yield 'orWhereBetween nested array of numbers' => [
169+
['find' => [
170+
['$or' => [
171+
['id' => 1],
172+
['id' => ['$gte' => [4], '$lte' => [6, 8]]],
173+
]],
174+
[], // options
175+
]],
176+
fn (Builder $builder) => $builder
177+
->where('id', '=', 1)
178+
->orWhereBetween('id', [[4], [6, 8]]),
179+
];
180+
181+
yield 'orWhereBetween collection' => [
182+
['find' => [
183+
['$or' => [
184+
['id' => 1],
185+
['id' => ['$gte' => 3, '$lte' => 4]],
186+
]],
187+
[], // options
188+
]],
189+
fn (Builder $builder) => $builder
190+
->where('id', '=', 1)
191+
->orWhereBetween('id', collect([3, 4])),
192+
];
193+
194+
yield 'whereNotBetween array of numbers' => [
195+
['find' => [
196+
['$or' => [
197+
['id' => ['$lte' => 1]],
198+
['id' => ['$gte' => 2]],
199+
]],
200+
[], // options
201+
]],
202+
fn (Builder $builder) => $builder->whereNotBetween('id', [1, 2]),
203+
];
204+
205+
/** @see DatabaseQueryBuilderTest::testOrWhereNotBetween() */
206+
yield 'orWhereNotBetween array of numbers' => [
207+
['find' => [
208+
['$or' => [
209+
['id' => 1],
210+
['$or' => [
211+
['id' => ['$lte' => 3]],
212+
['id' => ['$gte' => 5]],
213+
]],
214+
]],
215+
[], // options
216+
]],
217+
fn (Builder $builder) => $builder
218+
->where('id', '=', 1)
219+
->orWhereNotBetween('id', [3, 5]),
220+
];
221+
222+
yield 'orWhereNotBetween nested array of numbers' => [
223+
['find' => [
224+
['$or' => [
225+
['id' => 1],
226+
['$or' => [
227+
['id' => ['$lte' => [2, 3]]],
228+
['id' => ['$gte' => [5]]],
229+
]],
230+
]],
231+
[], // options
232+
]],
233+
fn (Builder $builder) => $builder
234+
->where('id', '=', 1)
235+
->orWhereNotBetween('id', [[2, 3], [5]]),
236+
];
237+
238+
yield 'orWhereNotBetween collection' => [
239+
['find' => [
240+
['$or' => [
241+
['id' => 1],
242+
['$or' => [
243+
['id' => ['$lte' => 3]],
244+
['id' => ['$gte' => 4]],
245+
]],
246+
]],
247+
[], // options
248+
]],
249+
fn (Builder $builder) => $builder
250+
->where('id', '=', 1)
251+
->orWhereNotBetween('id', collect([3, 4])),
252+
];
253+
127254
yield 'distinct' => [
128255
['distinct' => ['foo', [], []]],
129256
fn (Builder $builder) => $builder->distinct('foo'),
130257
];
131258

132259
yield 'groupBy' => [
133-
['aggregate' => [[['$group' => ['_id' => ['foo' => '$foo'], 'foo' => ['$last' => '$foo']]]], []]],
260+
['aggregate' => [
261+
[['$group' => ['_id' => ['foo' => '$foo'], 'foo' => ['$last' => '$foo']]]],
262+
[], // options
263+
]],
134264
fn (Builder $builder) => $builder->groupBy('foo'),
135265
];
136266
}
@@ -154,6 +284,37 @@ public static function provideExceptions(): iterable
154284
'Order direction must be "asc" or "desc"',
155285
fn (Builder $builder) => $builder->orderBy('_id', 'dasc'),
156286
];
287+
288+
/** @see DatabaseQueryBuilderTest::testWhereBetweens */
289+
yield 'whereBetween array too short' => [
290+
\InvalidArgumentException::class,
291+
'Between $values must be a list with exactly two elements: [min, max]',
292+
fn (Builder $builder) => $builder->whereBetween('id', [1]),
293+
];
294+
295+
yield 'whereBetween array too short (nested)' => [
296+
\InvalidArgumentException::class,
297+
'Between $values must be a list with exactly two elements: [min, max]',
298+
fn (Builder $builder) => $builder->whereBetween('id', [[1, 2]]),
299+
];
300+
301+
yield 'whereBetween array too long' => [
302+
\InvalidArgumentException::class,
303+
'Between $values must be a list with exactly two elements: [min, max]',
304+
fn (Builder $builder) => $builder->whereBetween('id', [1, 2, 3]),
305+
];
306+
307+
yield 'whereBetween collection too long' => [
308+
\InvalidArgumentException::class,
309+
'Between $values must be a list with exactly two elements: [min, max]',
310+
fn (Builder $builder) => $builder->whereBetween('id', new Collection([1, 2, 3])),
311+
];
312+
313+
yield 'whereBetween array is not a list' => [
314+
\InvalidArgumentException::class,
315+
'Between $values must be a list with exactly two elements: [min, max]',
316+
fn (Builder $builder) => $builder->whereBetween('id', ['min' => 1, 'max' => 2]),
317+
];
157318
}
158319

159320
/** @dataProvider getEloquentMethodsNotSupported */

0 commit comments

Comments
 (0)