Skip to content

Commit cb0f067

Browse files
authored
PHPLIB-1001, PHPLIB-1010, PHPLIB-1011: Improvements to $$type operator (mongodb#991)
* PHPLIB-1001: Support "number" alias in IsBsonType constraint Also revises instantiation of deprecated BSON types and fixes an assertion message. * PHPLIB-1010: Relax acceptance of Int64 for $$type operators * PHPLIB-1011: Support asserting multiple types in legacy $$type operator This refactors DocumentsMatchConstraint to use the IsBsonType constraint from the unified test runner.
1 parent c2f1873 commit cb0f067

File tree

4 files changed

+79
-176
lines changed

4 files changed

+79
-176
lines changed

tests/SpecTests/DocumentsMatchConstraint.php

Lines changed: 16 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,11 @@
44

55
use ArrayObject;
66
use InvalidArgumentException;
7-
use MongoDB\BSON\BinaryInterface;
8-
use MongoDB\BSON\DBPointer;
9-
use MongoDB\BSON\Decimal128;
107
use MongoDB\BSON\Int64;
11-
use MongoDB\BSON\Javascript;
12-
use MongoDB\BSON\MaxKey;
13-
use MongoDB\BSON\MinKey;
14-
use MongoDB\BSON\ObjectId;
15-
use MongoDB\BSON\Regex;
16-
use MongoDB\BSON\Symbol;
17-
use MongoDB\BSON\Timestamp;
18-
use MongoDB\BSON\Undefined;
19-
use MongoDB\BSON\UTCDateTime;
208
use MongoDB\Model\BSONArray;
219
use MongoDB\Model\BSONDocument;
10+
use MongoDB\Tests\UnifiedSpecTests\Constraint\IsBsonType;
2211
use PHPUnit\Framework\Constraint\Constraint;
23-
use PHPUnit\Framework\Constraint\IsInstanceOf;
24-
use PHPUnit\Framework\Constraint\IsNull;
25-
use PHPUnit\Framework\Constraint\IsType;
26-
use PHPUnit\Framework\Constraint\LogicalAnd;
27-
use PHPUnit\Framework\Constraint\LogicalNot;
28-
use PHPUnit\Framework\Constraint\LogicalOr;
2912
use RuntimeException;
3013
use SebastianBergmann\Comparator\ComparisonFailure;
3114
use SebastianBergmann\Comparator\Factory;
@@ -39,7 +22,12 @@
3922
use function is_float;
4023
use function is_int;
4124
use function is_object;
42-
use function method_exists;
25+
use function PHPUnit\Framework\assertThat;
26+
use function PHPUnit\Framework\containsOnly;
27+
use function PHPUnit\Framework\isInstanceOf;
28+
use function PHPUnit\Framework\isType;
29+
use function PHPUnit\Framework\logicalAnd;
30+
use function PHPUnit\Framework\logicalOr;
4331
use function sprintf;
4432

4533
use const PHP_INT_SIZE;
@@ -132,143 +120,18 @@ private function doEvaluate($other, $description = '', $returnResult = false)
132120
}
133121

134122
/**
135-
* @param mixed $actualValue
123+
* @param string|string[] $expectedType
124+
* @param mixed $actualValue
136125
*/
137-
private function assertBSONType(string $expectedType, $actualValue): void
126+
private function assertBSONType($expectedType, $actualValue): void
138127
{
139-
switch ($expectedType) {
140-
case 'double':
141-
(new IsType('float'))->evaluate($actualValue);
128+
assertThat(
129+
$expectedType,
130+
logicalOr(isType('string'), logicalAnd(isInstanceOf(BSONArray::class), containsOnly('string'))),
131+
'$$type requires string or string[]'
132+
);
142133

143-
return;
144-
145-
case 'string':
146-
(new IsType('string'))->evaluate($actualValue);
147-
148-
return;
149-
150-
case 'object':
151-
$constraints = [
152-
new IsType('object'),
153-
new LogicalNot(new IsInstanceOf(BSONArray::class)),
154-
];
155-
156-
// LogicalAnd::fromConstraints was introduced in PHPUnit 6.5.0.
157-
// This check can be removed when the PHPUnit dependency is bumped to that version
158-
if (method_exists(LogicalAnd::class, 'fromConstraints')) {
159-
$constraint = LogicalAnd::fromConstraints(...$constraints);
160-
} else {
161-
$constraint = new LogicalAnd();
162-
$constraint->setConstraints($constraints);
163-
}
164-
165-
$constraint->evaluate($actualValue);
166-
167-
return;
168-
169-
case 'array':
170-
$constraints = [
171-
new IsType('array'),
172-
new IsInstanceOf(BSONArray::class),
173-
];
174-
175-
// LogicalOr::fromConstraints was introduced in PHPUnit 6.5.0.
176-
// This check can be removed when the PHPUnit dependency is bumped to that version
177-
if (method_exists(LogicalOr::class, 'fromConstraints')) {
178-
$constraint = LogicalOr::fromConstraints(...$constraints);
179-
} else {
180-
$constraint = new LogicalOr();
181-
$constraint->setConstraints($constraints);
182-
}
183-
184-
$constraint->evaluate($actualValue);
185-
186-
return;
187-
188-
case 'binData':
189-
(new IsInstanceOf(BinaryInterface::class))->evaluate($actualValue);
190-
191-
return;
192-
193-
case 'undefined':
194-
(new IsInstanceOf(Undefined::class))->evaluate($actualValue);
195-
196-
return;
197-
198-
case 'objectId':
199-
(new IsInstanceOf(ObjectId::class))->evaluate($actualValue);
200-
201-
return;
202-
203-
case 'boolean':
204-
(new IsType('bool'))->evaluate($actualValue);
205-
206-
return;
207-
208-
case 'date':
209-
(new IsInstanceOf(UTCDateTime::class))->evaluate($actualValue);
210-
211-
return;
212-
213-
case 'null':
214-
(new IsNull())->evaluate($actualValue);
215-
216-
return;
217-
218-
case 'regex':
219-
(new IsInstanceOf(Regex::class))->evaluate($actualValue);
220-
221-
return;
222-
223-
case 'dbPointer':
224-
(new IsInstanceOf(DBPointer::class))->evaluate($actualValue);
225-
226-
return;
227-
228-
case 'javascript':
229-
(new IsInstanceOf(Javascript::class))->evaluate($actualValue);
230-
231-
return;
232-
233-
case 'symbol':
234-
(new IsInstanceOf(Symbol::class))->evaluate($actualValue);
235-
236-
return;
237-
238-
case 'int':
239-
(new IsType('int'))->evaluate($actualValue);
240-
241-
return;
242-
243-
case 'timestamp':
244-
(new IsInstanceOf(Timestamp::class))->evaluate($actualValue);
245-
246-
return;
247-
248-
case 'long':
249-
if (PHP_INT_SIZE == 4) {
250-
(new IsInstanceOf(Int64::class))->evaluate($actualValue);
251-
} else {
252-
(new IsType('int'))->evaluate($actualValue);
253-
}
254-
255-
return;
256-
257-
case 'decimal':
258-
(new IsInstanceOf(Decimal128::class))->evaluate($actualValue);
259-
260-
return;
261-
262-
case 'minKey':
263-
(new IsInstanceOf(MinKey::class))->evaluate($actualValue);
264-
265-
return;
266-
267-
case 'maxKey':
268-
(new IsInstanceOf(MaxKey::class))->evaluate($actualValue);
269-
270-
return;
271-
}
134+
IsBsonType::anyOf(...(array) $expectedType)->evaluate($actualValue);
272135
}
273136

274137
/**

tests/SpecTests/DocumentsMatchConstraintTest.php

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,34 +84,58 @@ public function testBSONTypeAssertions($type, $value): void
8484

8585
public function provideBSONTypes()
8686
{
87-
$undefined = toPHP(fromJSON('{ "undefined": {"$undefined": true} }'));
88-
$symbol = toPHP(fromJSON('{ "symbol": {"$symbol": "test"} }'));
89-
$dbPointer = toPHP(fromJSON('{ "dbPointer": {"$dbPointer": {"$ref": "phongo.test", "$id" : { "$oid" : "5a2e78accd485d55b405ac12" } }} }'));
87+
$undefined = toPHP(fromJSON('{ "x": {"$undefined": true} }'))->x;
88+
$symbol = toPHP(fromJSON('{ "x": {"$symbol": "test"} }'))->x;
89+
$dbPointer = toPHP(fromJSON('{ "x": {"$dbPointer": {"$ref": "db.coll", "$id" : { "$oid" : "5a2e78accd485d55b405ac12" } }} }'))->x;
90+
$int64 = unserialize('C:18:"MongoDB\BSON\Int64":28:{a:1:{s:7:"integer";s:1:"1";}}');
91+
$long = PHP_INT_SIZE == 4 ? unserialize('C:18:"MongoDB\BSON\Int64":38:{a:1:{s:7:"integer";s:10:"4294967296";}}') : 4294967296;
9092

9193
return [
9294
'double' => ['double', 1.4],
9395
'string' => ['string', 'foo'],
9496
'object' => ['object', new BSONDocument()],
9597
'array' => ['array', ['foo']],
9698
'binData' => ['binData', new Binary('', 0)],
97-
'undefined' => ['undefined', $undefined->undefined],
99+
'undefined' => ['undefined', $undefined],
98100
'objectId' => ['objectId', new ObjectId()],
99-
'boolean' => ['boolean', true],
101+
'bool' => ['bool', true],
100102
'date' => ['date', new UTCDateTime()],
101103
'null' => ['null', null],
102104
'regex' => ['regex', new Regex('.*')],
103-
'dbPointer' => ['dbPointer', $dbPointer->dbPointer],
105+
'dbPointer' => ['dbPointer', $dbPointer],
104106
'javascript' => ['javascript', new Javascript('foo = 1;')],
105-
'symbol' => ['symbol', $symbol->symbol],
107+
'symbol' => ['symbol', $symbol],
106108
'int' => ['int', 1],
107109
'timestamp' => ['timestamp', new Timestamp(0, 0)],
108-
'long' => ['long', PHP_INT_SIZE == 4 ? unserialize('C:18:"MongoDB\BSON\Int64":38:{a:1:{s:7:"integer";s:10:"4294967296";}}') : 4294967296],
110+
'long(int64)' => ['long', $int64],
111+
'long(long)' => ['long', $long],
109112
'decimal' => ['decimal', new Decimal128('18446744073709551616')],
110113
'minKey' => ['minKey', new MinKey()],
111114
'maxKey' => ['maxKey', new MaxKey()],
115+
'number(double)' => ['number', 1.4],
116+
'number(decimal)' => ['number', new Decimal128('18446744073709551616')],
117+
'number(int)' => ['number', 1],
118+
'number(int64)' => ['number', $int64],
119+
'number(long)' => ['number', $long],
112120
];
113121
}
114122

123+
public function testBSONTypeAssertionsWithMultipleTypes(): void
124+
{
125+
$c1 = new DocumentsMatchConstraint(['x' => ['$$type' => ['double', 'int']]]);
126+
127+
$this->assertResult(true, $c1, ['x' => 1], 'int is double or int');
128+
$this->assertResult(true, $c1, ['x' => 1.4], 'double is double or int');
129+
$this->assertResult(false, $c1, ['x' => 'foo'], 'string is not double or int');
130+
131+
$c2 = new DocumentsMatchConstraint(['x' => ['$$type' => ['number', 'string']]]);
132+
133+
$this->assertResult(true, $c2, ['x' => 1], 'int is number or string');
134+
$this->assertResult(true, $c2, ['x' => 1.4], 'double is number or string');
135+
$this->assertResult(true, $c2, ['x' => 'foo'], 'string is number or string');
136+
$this->assertResult(false, $c2, ['x' => true], 'bool is not number or string');
137+
}
138+
115139
/**
116140
* @dataProvider errorMessageProvider
117141
*/

tests/UnifiedSpecTests/Constraint/IsBsonType.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@
3838
use function range;
3939
use function sprintf;
4040

41-
use const PHP_INT_SIZE;
42-
4341
final class IsBsonType extends Constraint
4442
{
4543
use ConstraintTrait;
@@ -67,6 +65,7 @@ final class IsBsonType extends Constraint
6765
'decimal',
6866
'minKey',
6967
'maxKey',
68+
'number',
7069
];
7170

7271
/** @var string */
@@ -152,11 +151,7 @@ private function doMatches($other): bool
152151
return $other instanceof TimestampInterface;
153152

154153
case 'long':
155-
if (PHP_INT_SIZE == 4) {
156-
return $other instanceof Int64;
157-
}
158-
159-
return is_int($other);
154+
return is_int($other) || $other instanceof Int64;
160155

161156
case 'decimal':
162157
return $other instanceof Decimal128Interface;
@@ -167,6 +162,9 @@ private function doMatches($other): bool
167162
case 'maxKey':
168163
return $other instanceof MaxKeyInterface;
169164

165+
case 'number':
166+
return is_int($other) || $other instanceof Int64 || is_float($other) || $other instanceof Decimal128Interface;
167+
170168
default:
171169
// This should already have been caught in the constructor
172170
throw new LogicException('Unsupported type: ' . $this->type);

tests/UnifiedSpecTests/Constraint/IsBsonTypeTest.php

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ public function testConstraint($type, $value): void
3838

3939
public function provideTypes()
4040
{
41-
$undefined = toPHP(fromJSON('{ "undefined": {"$undefined": true} }'));
42-
$symbol = toPHP(fromJSON('{ "symbol": {"$symbol": "test"} }'));
43-
$dbPointer = toPHP(fromJSON('{ "dbPointer": {"$dbPointer": {"$ref": "phongo.test", "$id" : { "$oid" : "5a2e78accd485d55b405ac12" } }} }'));
41+
$undefined = toPHP(fromJSON('{ "x": {"$undefined": true} }'))->x;
42+
$symbol = toPHP(fromJSON('{ "x": {"$symbol": "test"} }'))->x;
43+
$dbPointer = toPHP(fromJSON('{ "x": {"$dbPointer": {"$ref": "db.coll", "$id" : { "$oid" : "5a2e78accd485d55b405ac12" } }} }'))->x;
44+
$int64 = unserialize('C:18:"MongoDB\BSON\Int64":28:{a:1:{s:7:"integer";s:1:"1";}}');
45+
$long = PHP_INT_SIZE == 4 ? unserialize('C:18:"MongoDB\BSON\Int64":38:{a:1:{s:7:"integer";s:10:"4294967296";}}') : 4294967296;
4446

4547
return [
4648
'double' => ['double', 1.4],
@@ -52,22 +54,28 @@ public function provideTypes()
5254
'array(indexed array)' => ['array', ['foo']],
5355
'array(BSONArray)' => ['array', new BSONArray()],
5456
'binData' => ['binData', new Binary('', 0)],
55-
'undefined' => ['undefined', $undefined->undefined],
57+
'undefined' => ['undefined', $undefined],
5658
'objectId' => ['objectId', new ObjectId()],
5759
'bool' => ['bool', true],
5860
'date' => ['date', new UTCDateTime()],
5961
'null' => ['null', null],
6062
'regex' => ['regex', new Regex('.*')],
61-
'dbPointer' => ['dbPointer', $dbPointer->dbPointer],
63+
'dbPointer' => ['dbPointer', $dbPointer],
6264
'javascript' => ['javascript', new Javascript('foo = 1;')],
63-
'symbol' => ['symbol', $symbol->symbol],
65+
'symbol' => ['symbol', $symbol],
6466
'javascriptWithScope' => ['javascriptWithScope', new Javascript('foo = 1;', ['x' => 1])],
6567
'int' => ['int', 1],
6668
'timestamp' => ['timestamp', new Timestamp(0, 0)],
67-
'long' => ['long', PHP_INT_SIZE == 4 ? unserialize('C:18:"MongoDB\BSON\Int64":38:{a:1:{s:7:"integer";s:10:"4294967296";}}') : 4294967296],
69+
'long(int64)' => ['long', $int64],
70+
'long(long)' => ['long', $long],
6871
'decimal' => ['decimal', new Decimal128('18446744073709551616')],
6972
'minKey' => ['minKey', new MinKey()],
7073
'maxKey' => ['maxKey', new MaxKey()],
74+
'number(double)' => ['number', 1.4],
75+
'number(decimal)' => ['number', new Decimal128('18446744073709551616')],
76+
'number(int)' => ['number', 1],
77+
'number(int64)' => ['number', $int64],
78+
'number(long)' => ['number', $long],
7179
];
7280
}
7381

@@ -89,10 +97,20 @@ public function testAnyOf(): void
8997
$c = IsBsonType::anyOf('double', 'int');
9098

9199
$this->assertResult(true, $c, 1, 'int is double or int');
92-
$this->assertResult(true, $c, 1.4, 'int is double or int');
100+
$this->assertResult(true, $c, 1.4, 'double is double or int');
93101
$this->assertResult(false, $c, 'foo', 'string is not double or int');
94102
}
95103

104+
public function testAnyOfWithNumberAlias(): void
105+
{
106+
$c = IsBsonType::anyOf('number', 'string');
107+
108+
$this->assertResult(true, $c, 1, 'int is number or string');
109+
$this->assertResult(true, $c, 1.4, 'double is number or string');
110+
$this->assertResult(true, $c, 'foo', 'string is number or string');
111+
$this->assertResult(false, $c, true, 'bool is not number or string');
112+
}
113+
96114
public function testErrorMessage(): void
97115
{
98116
$c = new IsBsonType('string');

0 commit comments

Comments
 (0)