Skip to content

Commit

Permalink
Merge pull request #220 from leroy-merlin-br/feat/add-datetime-casts
Browse files Browse the repository at this point in the history
feat: add datetime casts
  • Loading branch information
gabrielgomes94 authored Oct 23, 2023
2 parents 5335128 + 9f0ce95 commit b6ddd93
Show file tree
Hide file tree
Showing 16 changed files with 479 additions and 0 deletions.
44 changes: 44 additions & 0 deletions docs/docs/casting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
sidebar_position: 6
---

# 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',
'birthdate' => 'immutable_datetime',
];
}
```

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 `getDocumentAttributes()` 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');


```

10 changes: 10 additions & 0 deletions src/Model/Casts/CastInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Mongolid\Model\Casts;

interface CastInterface
{
public function get(mixed $value): mixed;

public function set(mixed $value): mixed;
}
35 changes: 35 additions & 0 deletions src/Model/Casts/CastResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Mongolid\Model\Casts;

use Mongolid\Model\Casts\DateTime\DateTimeCast;
use Mongolid\Model\Casts\DateTime\ImmutableDateTimeCast;
use Mongolid\Model\Casts\Exceptions\InvalidCastException;

class CastResolver
{
private const DATE_TIME = 'datetime';
private const IMMUTABLE_DATE_TIME = 'immutable_datetime';

private static array $cache = [];

public static array $validCasts = [
self::DATE_TIME,
self::IMMUTABLE_DATE_TIME,
];

public static function resolve(string $castName): CastInterface
{
if ($cast = self::$cache[$castName] ?? null) {
return $cast;
}

self::$cache[$castName] = match($castName) {
self::DATE_TIME => new DateTimeCast(),
self::IMMUTABLE_DATE_TIME => new ImmutableDateTimeCast(),
default => throw new InvalidCastException($castName),
};

return self::$cache[$castName];
}
}
32 changes: 32 additions & 0 deletions src/Model/Casts/DateTime/BaseDateTimeCast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Mongolid\Model\Casts\DateTime;

use DateTimeInterface;
use MongoDB\BSON\UTCDateTime;
use MongoDB\BSON\UTCDateTimeInterface;
use Mongolid\Model\Casts\CastInterface;

abstract class BaseDateTimeCast implements CastInterface
{
/**
* @param UTCDateTime|null $value
*/
abstract public function get(mixed $value): ?DateTimeInterface;

/**
* @param DateTimeInterface|UTCDateTimeInterface|null $value
*/
public function set(mixed $value): UTCDateTime|null
{
if (is_null($value)) {
return null;
}

if ($value instanceof UTCDateTimeInterface) {
return $value;
}

return new UTCDateTime($value);
}
}
22 changes: 22 additions & 0 deletions src/Model/Casts/DateTime/DateTimeCast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Mongolid\Model\Casts\DateTime;

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

class DateTimeCast extends BaseDateTimeCast
{
/**
* @param UTCDateTime|null $value
*/
public function get(mixed $value): ?DateTime
{
if (is_null($value)) {
return null;
}

return LocalDateTime::get($value);
}
}
24 changes: 24 additions & 0 deletions src/Model/Casts/DateTime/ImmutableDateTimeCast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Mongolid\Model\Casts\DateTime;

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

class ImmutableDateTimeCast extends BaseDateTimeCast
{
/**
* @param UTCDateTime|null $value
*/
public function get(mixed $value): ?DateTimeImmutable
{
if (is_null($value)) {
return null;
}

return DateTimeImmutable::createFromMutable(
LocalDateTime::get($value)
);
}
}
17 changes: 17 additions & 0 deletions src/Model/Casts/Exceptions/InvalidCastException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Mongolid\Model\Casts\Exceptions;

use InvalidArgumentException;
use Mongolid\Model\Casts\CastResolver;

class InvalidCastException extends InvalidArgumentException
{
public function __construct(string $cast)
{
$available = implode(',', CastResolver::$validCasts);
$message = "Invalid cast attribute: $cast. Use a valid one like $available";

parent::__construct($message);
}
}
18 changes: 18 additions & 0 deletions src/Model/HasAttributesTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use Exception;
use Illuminate\Support\Str;
use Mongolid\Container\Container;
use Mongolid\Model\Casts\CastResolver;
use stdClass;

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

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

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

if ($casterName = $this->casts[$key] ?? null) {
$caster = CastResolver::resolve($casterName);
$value = $caster->get($this->attributes[$key] ?? null);

return $value;
}

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

if ($casterName = $this->casts[$key] ?? null) {
$caster = CastResolver::resolve($casterName);
$value = $caster->set($value);
}

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

Expand Down
18 changes: 18 additions & 0 deletions src/Model/HasLegacyAttributesTrait.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?php
namespace Mongolid\Model;

use Mongolid\Model\Casts\CastResolver;

/**
* This trait adds attribute getter, setters and also a useful
* `fill` method that can be used with $fillable and $guarded
Expand Down Expand Up @@ -53,6 +55,11 @@ trait HasLegacyAttributesTrait
*/
public $mutable = false;

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

/**
* Get an attribute from the model.
*
Expand All @@ -64,6 +71,12 @@ public function getAttribute(string $key)
{
$inAttributes = array_key_exists($key, $this->attributes);

if ($casterName = $this->casts[$key] ?? null) {
$caster = CastResolver::resolve($casterName);

return $caster->get($this->attributes[$key] ?? null);
}

if ($inAttributes) {
return $this->attributes[$key];
} elseif ('attributes' == $key) {
Expand Down Expand Up @@ -122,6 +135,11 @@ public function cleanAttribute(string $key)
*/
public function setAttribute(string $key, $value)
{
if ($casterName = $this->casts[$key] ?? null) {
$caster = CastResolver::resolve($casterName);
$value = $caster->set($value);
}

$this->attributes[$key] = $value;
}

Expand Down
69 changes: 69 additions & 0 deletions tests/Integration/DateTimeCastTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace Integration;

use DateTime;
use MongoDB\BSON\UTCDateTime;
use Mongolid\Tests\Integration\IntegrationTestCase;
use Mongolid\Tests\Stubs\ExpirablePrice;
use Mongolid\Tests\Stubs\Legacy\LegacyRecordUser;
use Mongolid\Util\LocalDateTime;

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',
LocalDateTime::get($price->getOriginalDocumentAttributes()['expires_at'])
->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'));
}

public function testShouldSaveAndReadLegacyRecordWithCastedAttibutes(): void
{
// Set
$entity = new class extends LegacyRecordUser {
protected array $casts = [
'expires_at' => 'datetime',
];
};
$entity->expires_at = DateTime::createFromFormat('d/m/Y', '02/10/2025');

// Actions
$entity->save();

// Assertions
$this->assertInstanceOf(DateTime::class, $entity->expires_at);
$this->assertInstanceOf(UTCDateTime::class, $entity->getOriginalDocumentAttributes()['expires_at']);
}
}
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 array $casts = [
'expires_at' => 'datetime',
];
}
Loading

0 comments on commit b6ddd93

Please sign in to comment.