Skip to content
83 changes: 52 additions & 31 deletions src/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,11 @@

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;
Expand All @@ -22,10 +18,11 @@
use MongoDB\BSON\Binary;
use MongoDB\BSON\Decimal128;
use MongoDB\BSON\ObjectID;
use MongoDB\BSON\Type;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Laravel\Query\Builder as QueryBuilder;
use Stringable;

use function abs;
use function array_key_exists;
use function array_keys;
use function array_merge;
Expand All @@ -41,7 +38,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 +135,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 +240,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 ($castType) {
'decimal' => $this->fromDecimal($value, $castOptions),
default => $value,
};
}

// Convert _id to ObjectID.
Expand Down Expand Up @@ -281,26 +278,38 @@ public function setAttribute($key, $value)
return parent::setAttribute($key, $value);
}

/** @inheritdoc */
/**
* @param mixed $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);
// Convert BSON to string.
if ($this->isBSON($value)) {
if ($value instanceof Binary) {
$value = $value->getData();
} elseif ($value instanceof Stringable) {
$value = (string) $value;
} else {
throw new MathException('BSON type ' . $value::class . ' cannot be converted to string');
}
}

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 Expand Up @@ -707,4 +716,16 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt

return $attributes;
}

/**
* Is a value a BSON type?
*
* @param mixed $value
*
* @return bool
*/
protected function isBSON(mixed $value): bool
{
return $value instanceof Type;
}
}
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
Loading