Skip to content

Commit ae3e0d5

Browse files
GromNaNalcaeus
authored andcommitted
PHPORM-45 Add Query\Builder::toMql() to simplify comprehensive query tests (#6)
* PHPORM-45 Add Query\Builder::toMql() to simplify comprehensive query tests * Move Query/Builder unit tests to a dedicated test class
1 parent 19cf7a2 commit ae3e0d5

File tree

4 files changed

+158
-65
lines changed

4 files changed

+158
-65
lines changed

Diff for: composer.json

+1
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",

Diff for: src/Query/Builder.php

+70-62
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use MongoDB\BSON\ObjectID;
1616
use MongoDB\BSON\Regex;
1717
use MongoDB\BSON\UTCDateTime;
18+
use MongoDB\Driver\Cursor;
1819
use RuntimeException;
1920

2021
/**
@@ -215,27 +216,21 @@ public function cursor($columns = [])
215216
}
216217

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

233229
// Drop all columns if * is present, MongoDB does not work this way.
234-
if (in_array('*', $this->columns)) {
235-
$this->columns = [];
230+
if (in_array('*', $columns)) {
231+
$columns = [];
236232
}
237233

238-
// Compile wheres
239234
$wheres = $this->compileWheres();
240235

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

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

260255
$group[$key] = ['$last' => '$'.$column];
@@ -274,26 +269,10 @@ public function getFresh($columns = [], $returnLazy = false)
274269
$column = implode('.', $splitColumns);
275270
}
276271

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

281274
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);
275+
return ['count' => [$wheres, []]];
297276
} elseif ($function == 'count') {
298277
// Translate count into sum.
299278
$group['aggregate'] = ['$sum' => 1];
@@ -348,34 +327,23 @@ public function getFresh($columns = [], $returnLazy = false)
348327

349328
$options = $this->inheritConnectionOptions($options);
350329

351-
// Execute aggregation
352-
$results = iterator_to_array($this->collection->aggregate($pipeline, $options));
353-
354-
// Return results
355-
return new Collection($results);
330+
return ['aggregate' => [$pipeline, $options]];
356331
} // Distinct query
357332
elseif ($this->distinct) {
358333
// Return distinct results directly
359-
$column = isset($this->columns[0]) ? $this->columns[0] : '_id';
334+
$column = isset($columns[0]) ? $columns[0] : '_id';
360335

361336
$options = $this->inheritConnectionOptions();
362337

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

376344
// Add custom projections.
377345
if ($this->projections) {
378-
$columns = array_merge($columns, $this->projections);
346+
$projection = array_merge($projection, $this->projections);
379347
}
380348
$options = [];
381349

@@ -395,8 +363,8 @@ public function getFresh($columns = [], $returnLazy = false)
395363
if ($this->hint) {
396364
$options['hint'] = $this->hint;
397365
}
398-
if ($columns) {
399-
$options['projection'] = $columns;
366+
if ($projection) {
367+
$options['projection'] = $projection;
400368
}
401369

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

410378
$options = $this->inheritConnectionOptions($options);
411379

412-
// Execute query and get MongoCursor
413-
$cursor = $this->collection->find($wheres, $options);
380+
return ['find' => [$wheres, $options]];
381+
}
382+
}
414383

415-
if ($returnLazy) {
416-
return LazyCollection::make(function () use ($cursor) {
417-
foreach ($cursor as $item) {
418-
yield $item;
419-
}
420-
});
421-
}
384+
/**
385+
* Execute the query as a fresh "select" statement.
386+
*
387+
* @param array $columns
388+
* @param bool $returnLazy
389+
* @return array|static[]|Collection|LazyCollection
390+
*/
391+
public function getFresh($columns = [], $returnLazy = false)
392+
{
393+
// If no columns have been specified for the select statement, we will set them
394+
// here to either the passed columns, or the standard default of retrieving
395+
// all of the columns on the table using the "wildcard" column character.
396+
if ($this->columns === null) {
397+
$this->columns = $columns;
398+
}
399+
400+
// Drop all columns if * is present, MongoDB does not work this way.
401+
if (in_array('*', $this->columns)) {
402+
$this->columns = [];
403+
}
404+
405+
$command = $this->toMql($columns);
406+
assert(count($command) >= 1, 'At least one method call is required to execute a query');
407+
408+
$result = $this->collection;
409+
foreach ($command as $method => $arguments) {
410+
$result = call_user_func_array([$result, $method], $arguments);
411+
}
412+
413+
// countDocuments method returns int, wrap it to the format expected by the framework
414+
if (is_int($result)) {
415+
$result = [
416+
[
417+
'_id' => null,
418+
'aggregate' => $result,
419+
],
420+
];
421+
}
422422

423-
// Return results as an array with numeric keys
424-
$results = iterator_to_array($cursor, false);
423+
if ($returnLazy) {
424+
return LazyCollection::make(function () use ($result) {
425+
foreach ($result as $item) {
426+
yield $item;
427+
}
428+
});
429+
}
425430

426-
return new Collection($results);
431+
if ($result instanceof Cursor) {
432+
$result = $result->toArray();
427433
}
434+
435+
return new Collection($result);
428436
}
429437

430438
/**

Diff for: tests/Query/BuilderTest.php

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Jenssegers\Mongodb\Tests\Query;
6+
7+
use DateTimeImmutable;
8+
use Jenssegers\Mongodb\Connection;
9+
use Jenssegers\Mongodb\Query\Builder;
10+
use Jenssegers\Mongodb\Query\Processor;
11+
use Mockery as m;
12+
use MongoDB\BSON\UTCDateTime;
13+
use PHPUnit\Framework\TestCase;
14+
15+
class BuilderTest extends TestCase
16+
{
17+
/**
18+
* @dataProvider provideQueryBuilderToMql
19+
*/
20+
public function testMql(array $expected, \Closure $build): void
21+
{
22+
$builder = $build(self::getBuilder());
23+
$this->assertInstanceOf(Builder::class, $builder);
24+
$mql = $builder->toMql();
25+
26+
// Operations that return a Cursor expect a "typeMap" option.
27+
if (isset($expected['find'][1])) {
28+
$expected['find'][1]['typeMap'] = ['root' => 'array', 'document' => 'array'];
29+
}
30+
if (isset($expected['aggregate'][1])) {
31+
$expected['aggregate'][1]['typeMap'] = ['root' => 'array', 'document' => 'array'];
32+
}
33+
34+
// Compare with assertEquals because the query can contain BSON objects.
35+
$this->assertEquals($expected, $mql, var_export($mql, true));
36+
}
37+
38+
public static function provideQueryBuilderToMql(): iterable
39+
{
40+
/**
41+
* Builder::aggregate() and Builder::count() cannot be tested because they return the result,
42+
* without modifying the builder.
43+
*/
44+
$date = new DateTimeImmutable('2016-07-12 15:30:00');
45+
46+
yield 'find' => [
47+
['find' => [['foo' => 'bar'], []]],
48+
fn (Builder $builder) => $builder->where('foo', 'bar'),
49+
];
50+
51+
yield 'find > date' => [
52+
['find' => [['foo' => ['$gt' => new UTCDateTime($date)]], []]],
53+
fn (Builder $builder) => $builder->where('foo', '>', $date),
54+
];
55+
56+
yield 'find in array' => [
57+
['find' => [['foo' => ['$in' => ['bar', 'baz']]], []]],
58+
fn (Builder $builder) => $builder->whereIn('foo', ['bar', 'baz']),
59+
];
60+
61+
yield 'find limit offset select' => [
62+
['find' => [[], ['limit' => 10, 'skip' => 5, 'projection' => ['foo' => 1, 'bar' => 1]]]],
63+
fn (Builder $builder) => $builder->limit(10)->offset(5)->select('foo', 'bar'),
64+
];
65+
66+
yield 'distinct' => [
67+
['distinct' => ['foo', [], []]],
68+
fn (Builder $builder) => $builder->distinct('foo'),
69+
];
70+
71+
yield 'groupBy' => [
72+
['aggregate' => [[['$group' => ['_id' => ['foo' => '$foo'], 'foo' => ['$last' => '$foo']]]], []]],
73+
fn (Builder $builder) => $builder->groupBy('foo'),
74+
];
75+
}
76+
77+
private static function getBuilder(): Builder
78+
{
79+
$connection = m::mock(Connection::class);
80+
$processor = m::mock(Processor::class);
81+
$connection->shouldReceive('getSession')->andReturn(null);
82+
83+
return new Builder($connection, $processor);
84+
}
85+
}

Diff for: tests/QueryBuilderTest.php

+2-3
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,7 @@ public function testFindWithTimeout()
144144
{
145145
$id = DB::collection('users')->insertGetId(['name' => 'John Doe']);
146146

147-
$subscriber = new class implements CommandSubscriber
148-
{
147+
$subscriber = new class implements CommandSubscriber {
149148
public function commandStarted(CommandStartedEvent $event)
150149
{
151150
if ($event->getCommandName() !== 'find') {
@@ -830,7 +829,7 @@ public function testValue()
830829
public function testHintOptions()
831830
{
832831
DB::collection('items')->insert([
833-
['name' => 'fork', 'tags' => ['sharp', 'pointy']],
832+
['name' => 'fork', 'tags' => ['sharp', 'pointy']],
834833
['name' => 'spork', 'tags' => ['sharp', 'pointy', 'round', 'bowl']],
835834
['name' => 'spoon', 'tags' => ['round', 'bowl']],
836835
]);

0 commit comments

Comments
 (0)