Skip to content

Commit

Permalink
feat: add backed enum cast
Browse files Browse the repository at this point in the history
  • Loading branch information
edipoReboucas committed Oct 23, 2023
1 parent 898480c commit 235a6e3
Show file tree
Hide file tree
Showing 15 changed files with 367 additions and 21 deletions.
31 changes: 31 additions & 0 deletions docs/docs/casting.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,38 @@ $user->getOriginalDocumentAttributes()['birthdate']; // Returns birthdate as an
// 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');
```

## Casting to Enum with backing value (aka BackedEnum)

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

```php
enum Size: string
{
case Small = 'small';
case Big = 'big';
}

class Box extends \Mongolid\Model\AbstractModel {
protected $casts = [
'box_size' => Size::class,
];
}
```

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

If you need to manipulate its raw value, then you can access it through `getDocumentAttributes()` method.

To write a value on an attribute with enum cast, you should pass the enum instance like `Size::Small`
Internally, Mongolid will manage to set the property as enum backing value.

Check out some usages and examples:

```php
$box = Box::first();
$box->box_size = Size::Small; // Set box_size with enum instance
$box->box_size; // Returns box_size as enum instance
$box->getDocumentAttributes()['box_size']; // Returns box_size backing value
```
12 changes: 10 additions & 2 deletions src/DataMapper/DataMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -372,16 +372,24 @@ protected function parseToDocument($entity)
{
$schemaMapper = $this->getSchemaMapper();
$parsedDocument = $schemaMapper->map($entity);

if (is_object($entity)) {
$setter = $this->getAttributeSetter($entity);

foreach ($parsedDocument as $field => $value) {
$entity->$field = $value;
$setter($field, $value);
}
}

return $parsedDocument;
}

private function getAttributeSetter($entity): callable
{
return $entity instanceof ModelInterface
? fn ($field, $value) => $entity->setDocumentAttribute($field, $value)
: fn ($field, $value) => $entity->$field = $value;
}

/**
* Returns a SchemaMapper with the $schema or $schemaClass instance.
*
Expand Down
12 changes: 10 additions & 2 deletions src/DataMapper/EntityAssembler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

use Mongolid\Container\Container;
use Mongolid\Model\ModelInterface;
use Mongolid\Schema\Schema;
use Mongolid\Model\PolymorphableModelInterface;
use Mongolid\Schema\Schema;

/**
* EntityAssembler have the responsibility of assembling the data coming from
Expand All @@ -32,6 +32,7 @@ public function assemble($document, Schema $schema)
{
$entityClass = $schema->entityClass;
$model = Container::make($entityClass);
$setter = $this->getAttributeSetter($model);

foreach ($document as $field => $value) {
$fieldType = $schema->fields[$field] ?? null;
Expand All @@ -40,14 +41,21 @@ public function assemble($document, Schema $schema)
$value = $this->assembleDocumentsRecursively($value, substr($fieldType, 7));
}

$model->$field = $value;
$setter($field, $value);
}

$entity = $this->morphingTime($model);

return $this->prepareOriginalAttributes($entity);
}

private function getAttributeSetter($model): callable
{
return $model instanceof ModelInterface
? fn ($field, $value) => $model->setDocumentAttribute($field, $value)
: fn ($field, $value) => $model->$field = $value;
}

/**
* Returns the return of polymorph method of the given entity if available.
*
Expand Down
49 changes: 49 additions & 0 deletions src/Model/Casts/BackedEnumCast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Mongolid\Model\Casts;

use BackedEnum;
use Mongolid\Model\Casts\Exceptions\InvalidTypeException;

class BackedEnumCast implements CastInterface
{
/**
* @param class-string<BackedEnum> $backedEnum
*/
public function __construct(
private string $backedEnum
) {
}


/**
* @param int|string|null $value
*/
public function get(mixed $value): ?BackedEnum
{
if (is_null($value)) {
return null;
}

return ($this->backedEnum)::from($value);
}

/**
* @param BackedEnum|string|int|null $value
*/
public function set(mixed $value): string|int|null
{
if (is_null($value)) {
return null;
}

if (!$value instanceof $this->backedEnum) {
throw new InvalidTypeException(
"$this->backedEnum|null",
$value
);
}

return $value->value;
}
}
5 changes: 4 additions & 1 deletion src/Model/Casts/CastResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Mongolid\Model\Casts;

use BackedEnum;
use Mongolid\Model\Casts\DateTime\DateTimeCast;
use Mongolid\Model\Casts\DateTime\ImmutableDateTimeCast;
use Mongolid\Model\Casts\Exceptions\InvalidCastException;
Expand All @@ -27,7 +28,9 @@ public static function resolve(string $castName): CastInterface
self::$cache[$castName] = match($castName) {
self::DATE_TIME => new DateTimeCast(),
self::IMMUTABLE_DATE_TIME => new ImmutableDateTimeCast(),
default => throw new InvalidCastException($castName),
default => is_subclass_of($castName, BackedEnum::class)
? new BackedEnumCast($castName)
: throw new InvalidCastException($castName),
};

return self::$cache[$castName];
Expand Down
4 changes: 1 addition & 3 deletions src/Model/Casts/Exceptions/InvalidCastException.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@
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";
$message = "Invalid cast attribute: $cast";

parent::__construct($message);
}
Expand Down
18 changes: 18 additions & 0 deletions src/Model/Casts/Exceptions/InvalidTypeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Mongolid\Model\Casts\Exceptions;

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

class InvalidTypeException extends InvalidArgumentException
{
public function __construct(string $expectedType, mixed $value)
{
$invalidType = is_object($value)
? $value::class
: gettype($value);

parent::__construct("Value expected type $expectedType, given $invalidType");
}
}
1 change: 1 addition & 0 deletions src/Model/HasAttributesTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public static function fill(
HasAttributesInterface $object = null,
bool $force = false
): HasAttributesInterface {

if (!$object) {
$object = Container::make(static::class);
}
Expand Down
22 changes: 11 additions & 11 deletions src/Model/HasLegacyAttributesTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,6 @@ 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 @@ -135,11 +129,6 @@ 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 Expand Up @@ -214,6 +203,12 @@ public function __get($key)
return $this->{$this->buildMutatorMethod($key, 'get')}();
}

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

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

return $this->getAttribute($key);
}

Expand All @@ -229,6 +224,11 @@ public function __set($key, $value)
$value = $this->{$this->buildMutatorMethod($key, 'set')}($value);
}

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

$this->setAttribute($key, $value);
}

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

namespace Mongolid\Tests\Integration;

use Mongolid\Tests\Stubs\Box;
use Mongolid\Tests\Stubs\Legacy\Box as LegacyBox;
use Mongolid\Tests\Stubs\Size;

/**
* @requires PHP >= 8.1
*/
class BackedEnumCastTest extends IntegrationTestCase
{
public function testShouldCreateAndSaveBoxWithCastedAttributes(): void
{
// Set
$box = new Box();
$box->box_size = Size::Big;

// Actions
$box->save();

// Assertions
$this->assertEquals(Size::Big, $box->box_size);
$this->assertEquals(
'big',
$box->getOriginalDocumentAttributes()['box_size']
);
}

public function testShouldUpdateBoxWithCastedAttributes(): void
{
// Set
$box = new Box();
$box->box_size = Size::Small;

// Actions
$box->update();

// Assertions
$this->assertEquals(Size::Small, $box->box_size);
$this->assertEquals(
'small',
$box->getOriginalDocumentAttributes()['box_size']
);
}

public function testShouldCreateAndSaveLegacyBoxWithCastedAttributes(): void
{
// Set
$legacyBox = new LegacyBox();
$legacyBox->box_size = Size::Big;

// Actions
$legacyBox->save();

// Assertions
$this->assertEquals(Size::Big, $legacyBox->box_size);
$this->assertEquals(
'big',
$legacyBox->getOriginalDocumentAttributes()['box_size']
);
}

public function testShouldUpdateLegacyBoxWithCastedAttributes(): void
{
// Set
$legacyBox = new LegacyBox();
$legacyBox->box_size = Size::Small;

// Actions
$legacyBox->save();

// Assertions
$this->assertEquals(Size::Small, $legacyBox->box_size);
$this->assertEquals(
'small',
$legacyBox->getOriginalDocumentAttributes()['box_size']
);
}
}
17 changes: 17 additions & 0 deletions tests/Stubs/Box.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Mongolid\Tests\Stubs;

use Mongolid\Model\AbstractModel;

class Box extends AbstractModel
{
/**
* @var string
*/
protected $collection = 'boxes';

protected array $casts = [
'box_size' => Size::class,
];
}
18 changes: 18 additions & 0 deletions tests/Stubs/Legacy/Box.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Mongolid\Tests\Stubs\Legacy;

use Mongolid\LegacyRecord;
use Mongolid\Tests\Stubs\Size;

class Box extends LegacyRecord
{
/**
* @var string
*/
protected $collection = 'boxes';

protected array $casts = [
'box_size' => Size::class,
];
}
9 changes: 9 additions & 0 deletions tests/Stubs/Size.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Mongolid\Tests\Stubs;

enum Size: string
{
case Small = 'small';
case Big = 'big';
}
Loading

0 comments on commit 235a6e3

Please sign in to comment.