Skip to content

Commit

Permalink
feat: add datetime casts
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielgomes94 committed Oct 10, 2023
1 parent cd3cbce commit 54f76a4
Show file tree
Hide file tree
Showing 7 changed files with 322 additions and 0 deletions.
53 changes: 53 additions & 0 deletions docs/docs/casting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
sidebar_position: 4
---

# Casting attributes

## Casting to DateTime


With Mongolid, you can define attributes to be cast to `DateTime` or `DateTimeImmutable` using `$casts` property in your models.

```php
class Person extends \Mongolid\Model\AbstractModel {
protected $casts = [
'expires_at' => \DateTime::class,
'birthdate' => \DateTimeImmutable::class
];
}
```

When you define an attribute to be cast as `DateTime` or `DateTimeImmutable`, Mongolid will load it from database will do its trick to return an `DateTime` instance(or `DateTimeImmutable`) anytime you try to access it with property accessor operator (`->`).

If you need to manipulate its original value on MongoDB, then you can access it through `getOriginalDocumentAttributes()` method

To write a value on an attribute with `DateTime` cast, you can use both an `\MongoDB\BSON\UTCDateTime`, `\DateTime` or `\DateTimeImmutable` instance.
Internally, Mongolid will manage to set the property as an UTCDateTime, because it is the datetime format accepted by MongoDB.

Check out some usages and examples:

```php

$user = Person::first();
$user->birthdate; // Returns birthdate as a DateTimeImmutable instance
$user->expires_at; // Returns expires_at as DateTime instance

$user->getOriginalDocumentAttributes()['birthdate']; // Returns birthdate as an \MongoDB\BSON\UTCDateTime instance

// To set a new birthdate, you can pass both UTCDateTime or native's PHP DateTime
$user->birthdate = new \MongoDB\BSON\UTCDateTime($anyDateTime);
$user->birthdate = DateTime::createFromFormat('d/m/Y', '01/03/1970');


```

As an alternative, you can use Eloquent's syntax to define a cast:
```php
class Person extends \Mongolid\Model\AbstractModel {
protected $casts = [
'expires_at' => 'datetime',
'birthdate' => 'immutable_datetime',
];
}
```
67 changes: 67 additions & 0 deletions src/Model/Casts/DateTimeCast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace Mongolid\Model\Casts;

use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use MongoDB\BSON\UTCDateTime;
use Mongolid\Util\LocalDateTime;

class DateTimeCast
{
public static array $validCasts = [
'datetime',
'immutable_datetime',
DateTime::class,
DateTimeImmutable::class,
];

public static function castToDateTime(
string $attribute,
array $castsConfig,
mixed $value
): null|DateTime|DateTimeImmutable
{
if (!$value instanceof UTCDateTime) {
return null;
}

if (
in_array(
$castsConfig[$attribute],
[DateTime::class, 'datetime']
)
) {
return LocalDateTime::get($value);
}

if (
in_array(
$castsConfig[$attribute],
[DateTimeImmutable::class, 'immutable_datetime']
)
) {
return DateTimeImmutable::createFromMutable(
LocalDateTime::get($value)
);
}

return null;
}

public static function castToMongoUTCDateTime(
null|UTCDateTime|DateTimeInterface $value
): ?UTCDateTime
{
if ($value instanceof UTCDateTime) {
return $value;
}

if ($value instanceof DateTimeInterface) {
return new UTCDateTime($value);
}

return null;
}
}
35 changes: 35 additions & 0 deletions src/Model/HasAttributesTrait.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<?php
namespace Mongolid\Model;

use DateTimeInterface;
use Exception;
use Illuminate\Support\Str;
use Mongolid\Container\Container;
use Mongolid\Model\Casts\DateTimeCast;
use stdClass;

/**
Expand Down Expand Up @@ -65,6 +67,14 @@ trait HasAttributesTrait
*/
private $originalAttributes = [];

/**
* Attributes that are cast to another types when fetched from database.
*
* @var array
*/
protected $casts = [];


/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -123,6 +133,19 @@ public function &getDocumentAttribute(string $key)
return $this->mutableCache[$key];
}

if (
$this->shouldCastDateTime($key)
&& !$this->attributes[$key] instanceof DateTimeInterface
) {
$this->attributes[$key] = DateTimeCast::castToDateTime(
$key,
$this->casts,
$this->attributes[$key]
);

return $this->attributes[$key];
}

if (array_key_exists($key, $this->attributes)) {
return $this->attributes[$key];
}
Expand Down Expand Up @@ -171,6 +194,12 @@ public function setDocumentAttribute(string $key, $value)
$value = $this->{$this->buildMutatorMethod($key, 'set')}($value);
}

if ($this->shouldCastDateTime($key)) {
$this->attributes[$key] = DateTimeCast::castToMongoUTCDateTime($value);

return $this->attributes[$key];
}

if (null === $value) {
$this->cleanDocumentAttribute($key);

Expand Down Expand Up @@ -235,4 +264,10 @@ protected function buildMutatorMethod(string $key, string $prefix): string
{
return $prefix.Str::studly($key).'DocumentAttribute';
}

private function shouldCastDateTime(string $key): bool
{
return array_key_exists($key, $this->casts)
&& in_array($this->casts[$key], DateTimeCast::$validCasts);
}
}
48 changes: 48 additions & 0 deletions tests/Integration/DateTimeCastTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Integration;

use DateTime;
use MongoDB\BSON\UTCDateTime;
use Mongolid\Tests\Integration\IntegrationTestCase;
use Mongolid\Tests\Stubs\ExpirablePrice;

class DateTimeCastTest extends IntegrationTestCase
{
public function testShouldCreateAndSavePricesWithCastedAttributes(): void
{
// Set
$price = new ExpirablePrice();
$price->value = '100.0';
$price->expires_at = DateTime::createFromFormat('d/m/Y', '02/10/2025');

// Actions
$price->save();

// Assertions
$this->assertInstanceOf(DateTime::class, $price->expires_at);
$this->assertInstanceOf(UTCDateTime::class, $price->getOriginalDocumentAttributes()['expires_at']);

$price = ExpirablePrice::first($price->_id);
$this->assertSame('02/10/2025', $price->expires_at->format('d/m/Y'));
$this->assertSame(
'02/10/2025',
$price->getOriginalDocumentAttributes()['expires_at']->toDateTime()->format('d/m/Y')
);
}

public function testShouldUpdatePriceWithCastedAttributes(): void
{
// Set
$price = new ExpirablePrice();
$price->value = '100.0';
$price->expires_at = DateTime::createFromFormat('d/m/Y', '02/10/2025');

// Actions
$price->expires_at = DateTime::createFromFormat('d/m/Y', '02/10/2030');

// Assertions
$this->assertInstanceOf(DateTime::class, $price->expires_at);
$this->assertSame('02/10/2030', $price->expires_at->format('d/m/Y'));
}
}
12 changes: 12 additions & 0 deletions tests/Stubs/ExpirablePrice.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Mongolid\Tests\Stubs;

use DateTime;

class ExpirablePrice extends Price
{
protected $casts = [
'expires_at' => DateTime::class,
];
}
59 changes: 59 additions & 0 deletions tests/Unit/Model/Casts/DateTimeCastTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Mongolid\Model\Casts;

use DateTime;
use DateTimeImmutable;
use MongoDB\BSON\UTCDateTime;
use Mongolid\TestCase;

class DateTimeCastTest extends TestCase
{
public function testCastToDateTime(): void
{
// Set
$casts = [
'expires_at' => DateTime::class,
'birthdate' => DateTimeImmutable::class,
'validated_at' => 'datetime',
'created_at' => 'immutable_datetime',
'deleted_at' => 'invalid',
];
$timestamp = new UTCDateTime(
DateTime::createFromFormat('d/m/Y H:i:s', '08/10/2025 12:30:45')
);

// Actions
$revoked_at = DateTimeCast::castToDateTime('revoked_at', $casts, new DateTime());
$birthdate = DateTimeCast::castToDateTime('birthdate', $casts, $timestamp);
$expires_at = DateTimeCast::castToDateTime('expires_at', $casts, $timestamp);
$deleted_at = DateTimeCast::castToDateTime('deleted_at', $casts, $timestamp);
$validated_at = DateTimeCast::castToDateTime('validated_at', $casts, $timestamp);
$created_at = DateTimeCast::castToDateTime('created_at', $casts, $timestamp);

// Assertions
$this->assertNull($revoked_at);
$this->assertInstanceOf(DateTime::class, $expires_at);
$this->assertInstanceOf(DateTimeImmutable::class, $birthdate);
$this->assertNull($deleted_at);
$this->assertInstanceOf(DateTime::class, $validated_at);
$this->assertInstanceOf(DateTimeImmutable::class, $created_at);
}

public function testCastToMongoUTCDateTime(): void
{
// Set
$dateInDateTime = DateTime::createFromFormat('d/m/Y H:i:s', '08/10/2025 12:30:45');
$dateInUTC = new UTCDateTime($dateInDateTime);

// Actions
$revoked_at = DateTimeCast::castToMongoUTCDateTime($dateInUTC);
$expires_at = DateTimeCast::castToMongoUTCDateTime($dateInDateTime);
$nulled_at = DateTimeCast::castToMongoUTCDateTime(null);

// Assertions
$this->assertInstanceOf(UTCDateTime::class, $revoked_at);
$this->assertInstanceOf(UTCDateTime::class, $expires_at);
$this->assertNull($nulled_at);
}
}
48 changes: 48 additions & 0 deletions tests/Unit/Model/HasAttributesTraitTest.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<?php
namespace Mongolid\Model;

use DateTime;
use DateTimeImmutable;
use MongoDB\BSON\ObjectId;
use MongoDB\BSON\UTCDateTime;
use Mongolid\TestCase;
use Mongolid\Tests\Stubs\PolymorphedReferencedUser;
use Mongolid\Tests\Stubs\ReferencedUser;
Expand Down Expand Up @@ -381,6 +384,51 @@ public function getNameDocumentAttribute()
$this->assertFalse(isset($model->nonexistant));
}

public function testShouldCastAttributeToDateTimeWhenLoadingFromDatabase(): void
{
// Set
$model = new class() extends AbstractModel
{
protected $casts = [
'expires_at' => DateTime::class,
'birthdate' => DateTimeImmutable::class,
];

};
$model->expires_at = new UTCDateTime(
DateTime::createFromFormat('d/m/Y H:i:s', '08/10/2025 12:30:45')
);
$model->birthdate = new UTCDateTime(
DateTimeImmutable::createFromFormat('d/m/Y', '02/04/1990')
);

// Assertions
$this->assertInstanceOf(DateTime::class, $model->expires_at);
$this->assertSame('08/10/2025 12:30:45', $model->expires_at->format('d/m/Y H:i:s'));

$this->assertInstanceOf(DateTimeImmutable::class, $model->birthdate);
$this->assertSame('02/04/1990', $model->birthdate->format('d/m/Y'));
}

public function testShouldCastAttributeToUTCDateTimeWhenSettingAttributes(): void
{
// Set
$model = new class() extends AbstractModel
{
protected $casts = [
'expires_at' => DateTime::class,
'birthdate' => DateTimeImmutable::class,
];
};

// Actions
$model->expires_at = DateTime::createFromFormat('d/m/Y H:i:s', '08/10/2025 12:30:45');

// Assertions
$this->assertInstanceOf(UTCDateTime::class, $model->getDocumentAttributes()['expires_at']);
$this->assertInstanceOf(DateTime::class, $model->expires_at);
}

public function getFillableOptions(): array
{
return [
Expand Down

0 comments on commit 54f76a4

Please sign in to comment.