Skip to content
This repository was archived by the owner on Aug 22, 2023. It is now read-only.

Commit 46028c7

Browse files
committed
PHPORM-45 Add Query\Builder::toMql() to simplify comprehensive query tests
1 parent bc364b5 commit 46028c7

File tree

3 files changed

+133
-60
lines changed

3 files changed

+133
-60
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
],
2020
"license": "MIT",
2121
"require": {
22+
"ext-mongodb": "^1.15",
2223
"illuminate/support": "^10.0",
2324
"illuminate/container": "^10.0",
2425
"illuminate/database": "^10.0",

src/Query/Builder.php

Lines changed: 59 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -215,27 +215,27 @@ public function cursor($columns = [])
215215
}
216216

217217
/**
218-
* Execute the query as a fresh "select" statement.
218+
* Return the MongoDB query to be run in the form of an element array like ['method' => [arguments]].
219219
*
220-
* @param array $columns
221-
* @param bool $returnLazy
222-
* @return array|static[]|Collection|LazyCollection
220+
* Example: ['find' => [['name' => 'John Doe'], ['projection' => ['birthday' => 1]]]]
221+
*
222+
* @param $columns
223+
* @return array<string, mixed[]>
223224
*/
224-
public function getFresh($columns = [], $returnLazy = false)
225+
public function toMql($columns = []): array
225226
{
226227
// If no columns have been specified for the select statement, we will set them
227228
// here to either the passed columns, or the standard default of retrieving
228229
// all of the columns on the table using the "wildcard" column character.
229-
if ($this->columns === null) {
230-
$this->columns = $columns;
230+
if ($this->columns !== null) {
231+
$columns = $this->columns;
231232
}
232233

233234
// Drop all columns if * is present, MongoDB does not work this way.
234-
if (in_array('*', $this->columns)) {
235-
$this->columns = [];
235+
if (in_array('*', $columns)) {
236+
$columns = [];
236237
}
237238

238-
// Compile wheres
239239
$wheres = $this->compileWheres();
240240

241241
// Use MongoDB's aggregation framework when using grouping or aggregation functions.
@@ -254,7 +254,7 @@ public function getFresh($columns = [], $returnLazy = false)
254254
}
255255

256256
// Do the same for other columns that are selected.
257-
foreach ($this->columns as $column) {
257+
foreach ($columns as $column) {
258258
$key = str_replace('.', '_', $column);
259259

260260
$group[$key] = ['$last' => '$'.$column];
@@ -274,26 +274,10 @@ public function getFresh($columns = [], $returnLazy = false)
274274
$column = implode('.', $splitColumns);
275275
}
276276

277-
// Null coalense only > 7.2
278-
279277
$aggregations = blank($this->aggregate['columns']) ? [] : $this->aggregate['columns'];
280278

281279
if (in_array('*', $aggregations) && $function == 'count') {
282-
// When ORM is paginating, count doesnt need a aggregation, just a cursor operation
283-
// elseif added to use this only in pagination
284-
// https://docs.mongodb.com/manual/reference/method/cursor.count/
285-
// count method returns int
286-
287-
$totalResults = $this->collection->count($wheres);
288-
// Preserving format expected by framework
289-
$results = [
290-
[
291-
'_id' => null,
292-
'aggregate' => $totalResults,
293-
],
294-
];
295-
296-
return new Collection($results);
280+
return ['countDocuments' => [$wheres, []]];
297281
} elseif ($function == 'count') {
298282
// Translate count into sum.
299283
$group['aggregate'] = ['$sum' => 1];
@@ -348,34 +332,23 @@ public function getFresh($columns = [], $returnLazy = false)
348332

349333
$options = $this->inheritConnectionOptions($options);
350334

351-
// Execute aggregation
352-
$results = iterator_to_array($this->collection->aggregate($pipeline, $options));
353-
354-
// Return results
355-
return new Collection($results);
335+
return ['aggregate' => [$pipeline, $options]];
356336
} // Distinct query
357337
elseif ($this->distinct) {
358338
// Return distinct results directly
359-
$column = isset($this->columns[0]) ? $this->columns[0] : '_id';
339+
$column = isset($columns[0]) ? $columns[0] : '_id';
360340

361341
$options = $this->inheritConnectionOptions();
362342

363-
// Execute distinct
364-
$result = $this->collection->distinct($column, $wheres ?: [], $options);
365-
366-
return new Collection($result);
343+
return ['distinct' => [$column, $wheres ?: [], $options]];
367344
} // Normal query
368345
else {
369-
$columns = [];
370-
371346
// Convert select columns to simple projections.
372-
foreach ($this->columns as $column) {
373-
$columns[$column] = true;
374-
}
347+
$projection = array_fill_keys($columns, true);
375348

376349
// Add custom projections.
377350
if ($this->projections) {
378-
$columns = array_merge($columns, $this->projections);
351+
$projection = array_merge($projection, $this->projections);
379352
}
380353
$options = [];
381354

@@ -395,8 +368,8 @@ public function getFresh($columns = [], $returnLazy = false)
395368
if ($this->hint) {
396369
$options['hint'] = $this->hint;
397370
}
398-
if ($columns) {
399-
$options['projection'] = $columns;
371+
if ($projection) {
372+
$options['projection'] = $projection;
400373
}
401374

402375
// Fix for legacy support, converts the results to arrays instead of objects.
@@ -409,22 +382,50 @@ public function getFresh($columns = [], $returnLazy = false)
409382

410383
$options = $this->inheritConnectionOptions($options);
411384

412-
// Execute query and get MongoCursor
413-
$cursor = $this->collection->find($wheres, $options);
385+
return ['find' => [$wheres, $options]];
386+
}
387+
}
414388

415-
if ($returnLazy) {
416-
return LazyCollection::make(function () use ($cursor) {
417-
foreach ($cursor as $item) {
418-
yield $item;
419-
}
420-
});
421-
}
389+
/**
390+
* Execute the query as a fresh "select" statement.
391+
*
392+
* @param array $columns
393+
* @param bool $returnLazy
394+
* @return array|static[]|Collection|LazyCollection
395+
*/
396+
public function getFresh($columns = [], $returnLazy = false)
397+
{
398+
$command = $this->toMql($columns);
399+
assert(count($command) >= 1, 'At least one method call is required to execute a query');
400+
401+
$result = $this->collection;
402+
foreach ($command as $method => $arguments) {
403+
$result = call_user_func_array([$result, $method], $arguments);
404+
}
422405

423-
// Return results as an array with numeric keys
424-
$results = iterator_to_array($cursor, false);
406+
// countDocuments method returns int, wrap it to the format expected by the framework
407+
if (is_int($result)) {
408+
$result = [
409+
[
410+
'_id' => null,
411+
'aggregate' => $result,
412+
],
413+
];
414+
}
425415

426-
return new Collection($results);
416+
if ($returnLazy) {
417+
return LazyCollection::make(function () use ($result) {
418+
foreach ($result as $item) {
419+
yield $item;
420+
}
421+
});
422+
}
423+
424+
if ($result instanceof Cursor) {
425+
$result = $result->toArray();
427426
}
427+
428+
return new Collection($result);
428429
}
429430

430431
/**

tests/QueryBuilderTest.php

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
use Illuminate\Support\LazyCollection;
1212
use Illuminate\Testing\Assert;
1313
use Jenssegers\Mongodb\Collection;
14+
use Jenssegers\Mongodb\Connection;
1415
use Jenssegers\Mongodb\Query\Builder;
16+
use Jenssegers\Mongodb\Query\Processor;
1517
use Jenssegers\Mongodb\Tests\Models\Item;
1618
use Jenssegers\Mongodb\Tests\Models\User;
19+
use Mockery as m;
1720
use MongoDB\BSON\ObjectId;
1821
use MongoDB\BSON\Regex;
1922
use MongoDB\BSON\UTCDateTime;
@@ -31,6 +34,66 @@ public function tearDown(): void
3134
DB::collection('items')->truncate();
3235
}
3336

37+
/**
38+
* @dataProvider provideQueryBuilderToMql
39+
*/
40+
public function testMql(array $expected, \Closure $build): void
41+
{
42+
$builder = $build(self::getBuilder());
43+
$this->assertInstanceOf(Builder::class, $builder);
44+
$mql = $builder->toMql();
45+
46+
// Operations that return a Cursor expect a "typeMap" option.
47+
if (isset($expected['find'][1])) {
48+
$expected['find'][1]['typeMap'] = ['root' => 'array', 'document' => 'array'];
49+
}
50+
if (isset($expected['aggregate'][1])) {
51+
$expected['aggregate'][1]['typeMap'] = ['root' => 'array', 'document' => 'array'];
52+
}
53+
54+
// Compare with assertEquals because the query can contain BSON objects.
55+
$this->assertEquals($expected, $mql, var_export($mql, true));
56+
}
57+
58+
public static function provideQueryBuilderToMql(): iterable
59+
{
60+
/**
61+
* Builder::aggregate() and Builder::count() cannot be tested because they return the result,
62+
* without modifying the builder.
63+
*/
64+
$date = new DateTimeImmutable('2016-07-12 15:30:00');
65+
66+
yield [
67+
['find' => [['foo' => 'bar'], []]],
68+
fn (Builder $builder) => $builder->where('foo', 'bar'),
69+
];
70+
71+
yield [
72+
['find' => [['foo' => ['$gt' => new UTCDateTime($date)]], []]],
73+
fn (Builder $builder) => $builder->where('foo', '>', $date),
74+
];
75+
76+
yield [
77+
['find' => [['foo' => ['$in' => ['bar', 'baz']]], []]],
78+
fn (Builder $builder) => $builder->whereIn('foo', ['bar', 'baz']),
79+
];
80+
81+
yield [
82+
['find' => [[], ['limit' => 10, 'skip' => 5, 'projection' => ['foo' => 1, 'bar' => 1]]]],
83+
fn (Builder $builder) => $builder->limit(10)->offset(5)->select('foo', 'bar'),
84+
];
85+
86+
yield [
87+
['distinct' => ['foo', [], []]],
88+
fn (Builder $builder) => $builder->distinct('foo'),
89+
];
90+
91+
yield [
92+
['aggregate' => [[['$group' => ['_id' => ['foo' => '$foo'], 'foo' => ['$last' => '$foo']]]], []]],
93+
fn (Builder $builder) => $builder->groupBy('foo'),
94+
];
95+
}
96+
3497
public function testDeleteWithId()
3598
{
3699
$user = DB::collection('users')->insertGetId([
@@ -144,8 +207,7 @@ public function testFindWithTimeout()
144207
{
145208
$id = DB::collection('users')->insertGetId(['name' => 'John Doe']);
146209

147-
$subscriber = new class implements CommandSubscriber
148-
{
210+
$subscriber = new class implements CommandSubscriber {
149211
public function commandStarted(CommandStartedEvent $event)
150212
{
151213
if ($event->getCommandName() !== 'find') {
@@ -864,4 +926,13 @@ public function testCursor()
864926
$this->assertEquals($data[$i]['name'], $result['name']);
865927
}
866928
}
929+
930+
private static function getBuilder(): Builder
931+
{
932+
$connection = m::mock(Connection::class);
933+
$processor = m::mock(Processor::class);
934+
$connection->shouldReceive('getSession')->andReturn(null);
935+
936+
return new Builder($connection, $processor);
937+
}
867938
}

0 commit comments

Comments
 (0)