Skip to content
58 changes: 27 additions & 31 deletions src/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,14 @@

namespace MongoDB\Laravel\Eloquent;

use Brick\Math\BigDecimal;
use Brick\Math\Exception\MathException as BrickMathException;
use Brick\Math\RoundingMode;
use Carbon\CarbonInterface;
use DateTimeInterface;
use Illuminate\Contracts\Queue\QueueableCollection;
use Illuminate\Contracts\Queue\QueueableEntity;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Casts\Json;
use Illuminate\Database\Eloquent\Model as BaseModel;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Arr;
use Illuminate\Support\Exceptions\MathException;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Str;
use MongoDB\BSON\Binary;
Expand All @@ -25,7 +20,6 @@
use MongoDB\BSON\UTCDateTime;
use MongoDB\Laravel\Query\Builder as QueryBuilder;

use function abs;
use function array_key_exists;
use function array_keys;
use function array_merge;
Expand All @@ -41,7 +35,6 @@
use function is_string;
use function ltrim;
use function method_exists;
use function sprintf;
use function str_contains;
use function str_starts_with;
use function strcmp;
Expand Down Expand Up @@ -139,15 +132,9 @@ public function fromDateTime($value)
/** @inheritdoc */
protected function asDateTime($value)
{
// Convert UTCDateTime instances.
// Convert UTCDateTime instances to Carbon.
if ($value instanceof UTCDateTime) {
$date = $value->toDateTime();

$seconds = $date->format('U');
$milliseconds = abs((int) $date->format('v'));
$timestampMs = sprintf('%d%03d', $seconds, $milliseconds);

return Date::createFromTimestampMs($timestampMs);
return Date::instance($value->toDateTime());
}

return parent::asDateTime($value);
Expand Down Expand Up @@ -250,9 +237,16 @@ public function setAttribute($key, $value)
{
$key = (string) $key;

// Add casts
if ($this->hasCast($key)) {
$value = $this->castAttribute($key, $value);
$casts = $this->getCasts();
if (array_key_exists($key, $casts)) {
$castType = $this->getCastType($key);
$castOptions = Str::after($casts[$key], ':');

// Can add more native mongo type casts here.
$value = match (true) {
$castType === 'decimal' => $this->fromDecimal($value, $castOptions),
default => $value,
};
}

// Convert _id to ObjectID.
Expand Down Expand Up @@ -284,23 +278,25 @@ public function setAttribute($key, $value)
/** @inheritdoc */
protected function asDecimal($value, $decimals)
{
try {
$value = (string) BigDecimal::of((string) $value)->toScale((int) $decimals, RoundingMode::HALF_UP);

return new Decimal128($value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Good catch, the method asDecimal must return a string, as mandated in the phpdoc of the parent method.

} catch (BrickMathException $e) {
throw new MathException('Unable to cast value to a decimal.', previous: $e);
if ($value instanceof Decimal128) {
// Convert it to a string to round, want to make it act exactly like we expect.
$value = (string) $value;
}

return parent::asDecimal($value, $decimals);
}

/** @inheritdoc */
public function fromJson($value, $asObject = false)
/**
* Change to mongo native for decimal cast.
*
* @param mixed $value
* @param int $decimals
*
* @return Decimal128
*/
protected function fromDecimal($value, $decimals)
{
if (! is_string($value)) {
$value = Json::encode($value);
}

return Json::decode($value, ! $asObject);
return new Decimal128($this->asDecimal($value, $decimals));
}

/** @inheritdoc */
Expand Down
37 changes: 37 additions & 0 deletions tests/Casts/BooleanTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,42 @@ public function testBoolAsString(): void

self::assertIsBool($model->booleanValue);
self::assertSame(false, $model->booleanValue);

$model->update(['booleanValue' => 'false']);

self::assertIsBool($model->booleanValue);
self::assertSame(true, $model->booleanValue);

$model->update(['booleanValue' => '0.0']);

self::assertIsBool($model->booleanValue);
self::assertSame(true, $model->booleanValue);

$model->update(['booleanValue' => 'true']);

self::assertIsBool($model->booleanValue);
self::assertSame(true, $model->booleanValue);
}

public function testBoolAsNumber(): void
{
$model = Casting::query()->create(['booleanValue' => 1]);

self::assertIsBool($model->booleanValue);
self::assertSame(true, $model->booleanValue);

$model->update(['booleanValue' => 0]);

self::assertIsBool($model->booleanValue);
self::assertSame(false, $model->booleanValue);

$model->update(['booleanValue' => 1.79]);

self::assertIsBool($model->booleanValue);
self::assertSame(true, $model->booleanValue);

$model->update(['booleanValue' => 0.0]);
self::assertIsBool($model->booleanValue);
self::assertSame(false, $model->booleanValue);
}
}
8 changes: 8 additions & 0 deletions tests/Casts/CollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,19 @@ public function testCollection(): void
$model = Casting::query()->create(['collectionValue' => ['g' => 'G-Eazy']]);

self::assertInstanceOf(Collection::class, $model->collectionValue);
self::assertIsString($model->getRawOriginal('collectionValue'));
self::assertEquals(collect(['g' => 'G-Eazy']), $model->collectionValue);

$model->update(['collectionValue' => ['Dont let me go' => 'Even the longest of nights turn days']]);

self::assertInstanceOf(Collection::class, $model->collectionValue);
self::assertIsString($model->getRawOriginal('collectionValue'));
self::assertEquals(collect(['Dont let me go' => 'Even the longest of nights turn days']), $model->collectionValue);

$model->update(['collectionValue' => [['Dont let me go' => 'Even the longest of nights turn days']]]);

self::assertInstanceOf(Collection::class, $model->collectionValue);
self::assertIsString($model->getRawOriginal('collectionValue'));
self::assertEquals(collect([['Dont let me go' => 'Even the longest of nights turn days']]), $model->collectionValue);
}
}
10 changes: 10 additions & 0 deletions tests/Casts/DateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Carbon\CarbonImmutable;
use DateTime;
use Illuminate\Support\Carbon;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Laravel\Tests\Models\Casting;
use MongoDB\Laravel\Tests\TestCase;

Expand All @@ -31,17 +32,26 @@ public function testDate(): void
$model->update(['dateField' => now()->subDay()]);

self::assertInstanceOf(Carbon::class, $model->dateField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('dateField'));
self::assertEquals(now()->subDay()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField);

$model->update(['dateField' => new DateTime()]);

self::assertInstanceOf(Carbon::class, $model->dateField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('dateField'));
self::assertEquals(now()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField);

$model->update(['dateField' => (new DateTime())->modify('-1 day')]);

self::assertInstanceOf(Carbon::class, $model->dateField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('dateField'));
self::assertEquals(now()->subDay()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField);

$refetchedModel = Casting::query()->find($model->getKey());

self::assertInstanceOf(Carbon::class, $refetchedModel->dateField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('dateField'));
self::assertEquals(now()->subDay()->startOfDay()->format('Y-m-d H:i:s'), (string) $refetchedModel->dateField);
}

public function testDateAsString(): void
Expand Down
6 changes: 6 additions & 0 deletions tests/Casts/DatetimeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Carbon\CarbonImmutable;
use DateTime;
use Illuminate\Support\Carbon;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Laravel\Tests\Models\Casting;
use MongoDB\Laravel\Tests\TestCase;

Expand All @@ -27,11 +28,13 @@ public function testDatetime(): void
$model = Casting::query()->create(['datetimeField' => now()]);

self::assertInstanceOf(Carbon::class, $model->datetimeField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('datetimeField'));
self::assertEquals(now()->format('Y-m-d H:i:s'), (string) $model->datetimeField);

$model->update(['datetimeField' => now()->subDay()]);

self::assertInstanceOf(Carbon::class, $model->datetimeField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('datetimeField'));
self::assertEquals(now()->subDay()->format('Y-m-d H:i:s'), (string) $model->datetimeField);
}

Expand All @@ -40,6 +43,7 @@ public function testDatetimeAsString(): void
$model = Casting::query()->create(['datetimeField' => '2023-10-29']);

self::assertInstanceOf(Carbon::class, $model->datetimeField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('datetimeField'));
self::assertEquals(
Carbon::createFromTimestamp(1698577443)->startOfDay()->format('Y-m-d H:i:s'),
(string) $model->datetimeField,
Expand All @@ -48,6 +52,7 @@ public function testDatetimeAsString(): void
$model->update(['datetimeField' => '2023-10-28 11:04:03']);

self::assertInstanceOf(Carbon::class, $model->datetimeField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('datetimeField'));
self::assertEquals(
Carbon::createFromTimestamp(1698577443)->subDay()->format('Y-m-d H:i:s'),
(string) $model->datetimeField,
Expand Down Expand Up @@ -82,6 +87,7 @@ public function testImmutableDatetime(): void
$model->update(['immutableDatetimeField' => '2023-10-28 11:04:03']);

self::assertInstanceOf(CarbonImmutable::class, $model->immutableDatetimeField);
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('immutableDatetimeField'));
self::assertEquals(
Carbon::createFromTimestamp(1698577443)->subDay()->format('Y-m-d H:i:s'),
(string) $model->immutableDatetimeField,
Expand Down
39 changes: 35 additions & 4 deletions tests/Casts/DecimalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,56 @@ public function testDecimal(): void
{
$model = Casting::query()->create(['decimalNumber' => 100.99]);

self::assertInstanceOf(Decimal128::class, $model->decimalNumber);
self::assertIsString($model->decimalNumber);
self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber'));
self::assertEquals('100.99', $model->decimalNumber);

$model->update(['decimalNumber' => 9999.9]);

self::assertInstanceOf(Decimal128::class, $model->decimalNumber);
self::assertIsString($model->decimalNumber);
self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber'));
self::assertEquals('9999.90', $model->decimalNumber);

$model->update(['decimalNumber' => 9999.00000009]);

self::assertIsString($model->decimalNumber);
self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber'));
self::assertEquals('9999.00', $model->decimalNumber);
}

public function testDecimalAsString(): void
{
$model = Casting::query()->create(['decimalNumber' => '120.79']);

self::assertInstanceOf(Decimal128::class, $model->decimalNumber);
self::assertIsString($model->decimalNumber);
self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber'));
self::assertEquals('120.79', $model->decimalNumber);

$model->update(['decimalNumber' => '795']);

self::assertInstanceOf(Decimal128::class, $model->decimalNumber);
self::assertIsString($model->decimalNumber);
self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber'));
self::assertEquals('795.00', $model->decimalNumber);

$model->update(['decimalNumber' => '1234.99999999999']);

self::assertIsString($model->decimalNumber);
self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber'));
self::assertEquals('1235.00', $model->decimalNumber);
}

public function testDecimalAsDecimal128(): void
{
$model = Casting::query()->create(['decimalNumber' => new Decimal128('100.99')]);

self::assertIsString($model->decimalNumber);
self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber'));
self::assertEquals('100.99', $model->decimalNumber);

$model->update(['decimalNumber' => new Decimal128('9999.9')]);

self::assertIsString($model->decimalNumber);
self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber'));
self::assertEquals('9999.90', $model->decimalNumber);
}
}
Loading