Skip to content

Commit b0fdb1f

Browse files
committed
Improve casting from BSON to decimal
1 parent c55b2af commit b0fdb1f

File tree

2 files changed

+84
-4
lines changed

2 files changed

+84
-4
lines changed

src/Eloquent/Model.php

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
use Illuminate\Database\Eloquent\Model as BaseModel;
1313
use Illuminate\Database\Eloquent\Relations\Relation;
1414
use Illuminate\Support\Arr;
15+
use Illuminate\Support\Exceptions\MathException;
1516
use Illuminate\Support\Facades\Date;
1617
use Illuminate\Support\Str;
1718
use MongoDB\BSON\Binary;
1819
use MongoDB\BSON\Decimal128;
1920
use MongoDB\BSON\ObjectID;
21+
use MongoDB\BSON\Type;
2022
use MongoDB\BSON\UTCDateTime;
2123
use MongoDB\Laravel\Query\Builder as QueryBuilder;
24+
use Stringable;
2225

2326
use function array_key_exists;
2427
use function array_keys;
@@ -275,12 +278,22 @@ public function setAttribute($key, $value)
275278
return parent::setAttribute($key, $value);
276279
}
277280

278-
/** @inheritdoc */
281+
/**
282+
* @param mixed $value
283+
*
284+
* @inheritdoc
285+
*/
279286
protected function asDecimal($value, $decimals)
280287
{
281-
if ($value instanceof Decimal128) {
282-
// Convert it to a string to round, want to make it act exactly like we expect.
283-
$value = (string) $value;
288+
// Convert BSON to string.
289+
if ($this->isBSON($value)) {
290+
if ($value instanceof Binary) {
291+
$value = $value->getData();
292+
} elseif ($value instanceof Stringable) {
293+
$value = (string) $value;
294+
} else {
295+
throw new MathException('BSON type ' . $value::class . ' cannot be converted to string');
296+
}
284297
}
285298

286299
return parent::asDecimal($value, $decimals);
@@ -703,4 +716,16 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt
703716

704717
return $attributes;
705718
}
719+
720+
/**
721+
* Is a value a BSON type?
722+
*
723+
* @param mixed $value
724+
*
725+
* @return bool
726+
*/
727+
protected function isBSON(mixed $value): bool
728+
{
729+
return $value instanceof Type;
730+
}
706731
}

tests/Casts/DecimalTest.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,18 @@
44

55
namespace MongoDB\Laravel\Tests\Casts;
66

7+
use Illuminate\Support\Exceptions\MathException;
8+
use MongoDB\BSON\Binary;
79
use MongoDB\BSON\Decimal128;
10+
use MongoDB\BSON\Int64;
11+
use MongoDB\BSON\Javascript;
12+
use MongoDB\BSON\UTCDateTime;
13+
use MongoDB\Laravel\Collection;
814
use MongoDB\Laravel\Tests\Models\Casting;
915
use MongoDB\Laravel\Tests\TestCase;
1016

17+
use function now;
18+
1119
class DecimalTest extends TestCase
1220
{
1321
protected function setUp(): void
@@ -73,4 +81,51 @@ public function testDecimalAsDecimal128(): void
7381
self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber'));
7482
self::assertEquals('9999.90', $model->decimalNumber);
7583
}
84+
85+
public function testOtherBSONTypes(): void
86+
{
87+
$modelId = $this->setBSONType(new Int64(100));
88+
$model = Casting::query()->find($modelId);
89+
90+
self::assertIsString($model->decimalNumber);
91+
self::assertIsInt($model->getRawOriginal('decimalNumber'));
92+
self::assertEquals('100.00', $model->decimalNumber);
93+
94+
// Update decimalNumber to a Binary type
95+
$this->setBSONType(new Binary('100.1234', Binary::TYPE_GENERIC), $modelId);
96+
$model->refresh();
97+
98+
self::assertIsString($model->decimalNumber);
99+
self::assertInstanceOf(Binary::class, $model->getRawOriginal('decimalNumber'));
100+
self::assertEquals('100.12', $model->decimalNumber);
101+
102+
$this->setBSONType(new Javascript('function() { return 100; }'), $modelId);
103+
$model->refresh();
104+
self::expectException(MathException::class);
105+
self::expectExceptionMessage('Unable to cast value to a decimal.');
106+
$model->decimalNumber;
107+
self::assertInstanceOf(Javascript::class, $model->getRawOriginal('decimalNumber'));
108+
109+
$this->setBSONType(new UTCDateTime(now()), $modelId);
110+
$model->refresh();
111+
self::expectException(MathException::class);
112+
self::expectExceptionMessage('Unable to cast value to a decimal.');
113+
$model->decimalNumber;
114+
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('decimalNumber'));
115+
}
116+
117+
private function setBSONType($value, $id = null)
118+
{
119+
// Do a raw insert/update, so we can enforce the type we want
120+
return Casting::raw(function (Collection $collection) use ($id, $value) {
121+
if (! empty($id)) {
122+
return $collection->updateOne(
123+
['_id' => $id],
124+
['$set' => ['decimalNumber' => $value]],
125+
);
126+
}
127+
128+
return $collection->insertOne(['decimalNumber' => $value])->getInsertedId();
129+
});
130+
}
76131
}

0 commit comments

Comments
 (0)