Skip to content

Commit

Permalink
Fix casting when setting an attribute (#2653)
Browse files Browse the repository at this point in the history
  • Loading branch information
hans-thomas authored Oct 30, 2023
1 parent f5ed7bf commit bc209f7
Show file tree
Hide file tree
Showing 14 changed files with 525 additions and 23 deletions.
33 changes: 33 additions & 0 deletions src/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,22 @@

namespace MongoDB\Laravel\Eloquent;

use Brick\Math\BigDecimal;
use Brick\Math\Exception\MathException as BrickMathException;
use Brick\Math\RoundingMode;
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;
use MongoDB\BSON\Decimal128;
use MongoDB\BSON\ObjectID;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Laravel\Query\Builder as QueryBuilder;
Expand Down Expand Up @@ -211,6 +217,11 @@ public function setAttribute($key, $value)
{
$key = (string) $key;

//Add casts
if ($this->hasCast($key)) {
$value = $this->castAttribute($key, $value);
}

// Convert _id to ObjectID.
if ($key === '_id' && is_string($value)) {
$builder = $this->newBaseQueryBuilder();
Expand All @@ -237,6 +248,28 @@ public function setAttribute($key, $value)
return parent::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);
} catch (BrickMathException $e) {
throw new MathException('Unable to cast value to a decimal.', previous: $e);
}
}

/** @inheritdoc */
public function fromJson($value, $asObject = false)
{
if (! is_string($value)) {
$value = Json::encode($value ?? '');
}

return Json::decode($value ?? '', ! $asObject);
}

/** @inheritdoc */
public function attributesToArray()
{
Expand Down
12 changes: 6 additions & 6 deletions tests/Casts/BinaryUuidTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Generator;
use MongoDB\BSON\Binary;
use MongoDB\Laravel\Tests\Models\CastBinaryUuid;
use MongoDB\Laravel\Tests\Models\Casting;
use MongoDB\Laravel\Tests\TestCase;

use function hex2bin;
Expand All @@ -17,15 +17,15 @@ protected function setUp(): void
{
parent::setUp();

CastBinaryUuid::truncate();
Casting::truncate();
}

/** @dataProvider provideBinaryUuidCast */
public function testBinaryUuidCastModel(string $expectedUuid, string|Binary $saveUuid, Binary $queryUuid): void
{
CastBinaryUuid::create(['uuid' => $saveUuid]);
Casting::create(['uuid' => $saveUuid]);

$model = CastBinaryUuid::firstWhere('uuid', $queryUuid);
$model = Casting::firstWhere('uuid', $queryUuid);
$this->assertNotNull($model);
$this->assertSame($expectedUuid, $model->uuid);
}
Expand All @@ -43,9 +43,9 @@ public function testQueryByStringDoesNotCast(): void
{
$uuid = '0c103357-3806-48c9-a84b-867dcb625cfb';

CastBinaryUuid::create(['uuid' => $uuid]);
Casting::create(['uuid' => $uuid]);

$model = CastBinaryUuid::firstWhere('uuid', $uuid);
$model = Casting::firstWhere('uuid', $uuid);
$this->assertNull($model);
}
}
54 changes: 54 additions & 0 deletions tests/Casts/BooleanTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace MongoDB\Laravel\Tests\Casts;

use MongoDB\Laravel\Tests\Models\Casting;
use MongoDB\Laravel\Tests\TestCase;

class BooleanTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();

Casting::truncate();
}

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

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

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

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

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

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

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

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

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

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

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

self::assertIsBool($model->booleanValue);
self::assertSame(false, $model->booleanValue);
}
}
34 changes: 34 additions & 0 deletions tests/Casts/CollectionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace MongoDB\Laravel\Tests\Casts;

use Illuminate\Support\Collection;
use MongoDB\Laravel\Tests\Models\Casting;
use MongoDB\Laravel\Tests\TestCase;

use function collect;

class CollectionTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();

Casting::truncate();
}

public function testCollection(): void
{
$model = Casting::query()->create(['collectionValue' => ['g' => 'G-Eazy']]);

self::assertInstanceOf(Collection::class, $model->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::assertEquals(collect(['Dont let me go' => 'Even the longest of nights turn days']), $model->collectionValue);
}
}
64 changes: 64 additions & 0 deletions tests/Casts/DateTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace MongoDB\Laravel\Tests\Casts;

use DateTime;
use Illuminate\Support\Carbon;
use MongoDB\Laravel\Tests\Models\Casting;
use MongoDB\Laravel\Tests\TestCase;

use function now;

class DateTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();

Casting::truncate();
}

public function testDate(): void
{
$model = Casting::query()->create(['dateField' => now()]);

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

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

self::assertInstanceOf(Carbon::class, $model->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::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::assertEquals(now()->subDay()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField);
}

public function testDateAsString(): void
{
$model = Casting::query()->create(['dateField' => '2023-10-29']);

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

$model->update(['dateField' => '2023-10-28']);

self::assertInstanceOf(Carbon::class, $model->dateField);
self::assertEquals(
Carbon::createFromTimestamp(1698577443)->subDay()->startOfDay()->format('Y-m-d H:i:s'),
(string) $model->dateField,
);
}
}
53 changes: 53 additions & 0 deletions tests/Casts/DatetimeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace MongoDB\Laravel\Tests\Casts;

use Illuminate\Support\Carbon;
use MongoDB\Laravel\Tests\Models\Casting;
use MongoDB\Laravel\Tests\TestCase;

use function now;

class DatetimeTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();

Casting::truncate();
}

public function testDate(): void
{
$model = Casting::query()->create(['datetimeField' => now()]);

self::assertInstanceOf(Carbon::class, $model->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::assertEquals(now()->subDay()->format('Y-m-d H:i:s'), (string) $model->datetimeField);
}

public function testDateAsString(): void
{
$model = Casting::query()->create(['datetimeField' => '2023-10-29']);

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

$model->update(['datetimeField' => '2023-10-28 11:04:03']);

self::assertInstanceOf(Carbon::class, $model->datetimeField);
self::assertEquals(
Carbon::createFromTimestamp(1698577443)->subDay()->format('Y-m-d H:i:s'),
(string) $model->datetimeField,
);
}
}
45 changes: 45 additions & 0 deletions tests/Casts/DecimalTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace MongoDB\Laravel\Tests\Casts;

use MongoDB\BSON\Decimal128;
use MongoDB\Laravel\Tests\Models\Casting;
use MongoDB\Laravel\Tests\TestCase;

class DecimalTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();

Casting::truncate();
}

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

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

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

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

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

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

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

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

0 comments on commit bc209f7

Please sign in to comment.