diff --git a/CHANGELOG.md b/CHANGELOG.md index ec28a7a..3411c29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to `Enumhancer` will be documented in this file +## 1.6.0 - 2022-06-04 +- added Eloquent Casting support for basic enumerations. + ## 1.5.0 - 2022-05-31 - added `Extractor` to extract enums from a string mentioned by value - some documentation repairs @@ -23,11 +26,11 @@ All notable changes to `Enumhancer` will be documented in this file ## 1.2.0 - 2022-02-26 -- added Value (for use with unit enums) +- added Value (for use with basic enums) ## 1.1.0 - 2022-02-25 -- added From. Useful for situations where you need them with non-backed enums +- added From. Useful for situations where you need them with basic enums ## 1.0.2 - 2022-02-16 diff --git a/README.md b/README.md index 7dcd181..99ac294 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,14 @@ This package currently supports the following: - Properties - Reporting (Logging) - Subset -- Value (for unit enums) +- Value (for basic enums) + +Laravel specific features: +- Casting (Laravel Specific) + +Note: While functionality that also exists in Spatie's PHP Enum is made backwards +compatible to allow for an easy migration to PHP native enums, currently this is +not the case for the PHPUnit assertions or Faker Provider. While functionality that also exists in Spatie's PHP Enum is made backwards compatible to allow for an easy migration to PHP native enums, currently this is @@ -57,7 +64,7 @@ for that functionality. Note: all traits can be used next to each other, except for `Mappers`, which has implemented the methods of `Makers`, `Extractor` and `Reporters`. -### Functionality +### Features - [Constructor](docs/constructor.md) - [Comparison](docs/comparison.md) - [Extractor](docs/extractor.md) @@ -70,7 +77,11 @@ implemented the methods of `Makers`, `Extractor` and `Reporters`. - [Subset](docs/subset.md) - [Value](docs/value.md) -### Laravel +### Laravel specific Features + +- [Casting](docs/casting.md) + +### Laravel's auto-discovery When you are installing this package into a laravel project, Enumhancer will automatically set the global `Reporter` for the `makeOrReport` methods, so that it will use Laravel's `Log` facade. diff --git a/docs/casting.md b/docs/casting.md new file mode 100644 index 0000000..cdbd798 --- /dev/null +++ b/docs/casting.md @@ -0,0 +1,28 @@ +# Eloquent Attribute Casting + +Laravel supports casting backed enums out of the box, but what if you don't want to +use backed enums? This is where `CastsBasicEnumerations` comes in. + +## Usage + +```php +enum yourEnum { + case MY_ENUM; +} + +use Illuminate\Database\Eloquent\Model; +use Henzeb\Enumhancer\Laravel\Concerns\CastsBasicEnumerations; + +class YourModel extends Model +{ + use CastsBasicEnumerations; + + $casts = [ + 'column' => YourEnum::class + ]; +} + +``` +### Lowercase values +By default, it will use of the basic enumeration as the value. If you want the +lowercase variant, you can add the `$keepEnumCase` property and set it to false. diff --git a/docs/labels.md b/docs/labels.md index 40b09d9..a5f0a26 100644 --- a/docs/labels.md +++ b/docs/labels.md @@ -2,7 +2,7 @@ Just like [Spatie's PHP Enum](https://github.com/spatie/enum), you can add labels to your enums. This is largely backwards compatible with their package, except that it -also works for UnitEnum's in which case it returns the name if not specified. +also works for basic enums in which case it returns the name if not specified. ## Usage diff --git a/docs/subset.md b/docs/subset.md index 3bf6d00..366db66 100644 --- a/docs/subset.md +++ b/docs/subset.md @@ -64,8 +64,7 @@ YourEnum::of( ``` ### values -This method returns an array of values of the specified subset. -This uses the [Value](value.md) trait, when enum is a `UnitEnum`. +This method returns an array of values of the specified subset. ```php YourEnum::of( diff --git a/docs/value.md b/docs/value.md index 1ca887b..44069a5 100644 --- a/docs/value.md +++ b/docs/value.md @@ -1,7 +1,8 @@ # Value -When you have a UnitEnum you have no value. You have to use the `name` value. But what if you -want to have the lowercase version of that name? That's where `Value` comes in. +When you have a basic enum, you have no value. You have to use the `name` value. But +what if you want to have the lowercase version of that name? That's where `Value` +comes in. ## Usage diff --git a/src/Concerns/Value.php b/src/Concerns/Value.php index 4c6649a..ade5e66 100644 --- a/src/Concerns/Value.php +++ b/src/Concerns/Value.php @@ -2,10 +2,12 @@ namespace Henzeb\Enumhancer\Concerns; +use Henzeb\Enumhancer\Helpers\EnumValue; + trait Value { final public function value(): string|int { - return $this->value ?? strtolower($this->name); + return EnumValue::value($this); } } diff --git a/src/Helpers/EnumValue.php b/src/Helpers/EnumValue.php new file mode 100644 index 0000000..21012c3 --- /dev/null +++ b/src/Helpers/EnumValue.php @@ -0,0 +1,14 @@ +value ?? ($keepCase ? $enum->name : strtolower($enum->name)); + } +} diff --git a/src/Laravel/Concerns/CastsBasicEnumerations.php b/src/Laravel/Concerns/CastsBasicEnumerations.php new file mode 100644 index 0000000..18b5adf --- /dev/null +++ b/src/Laravel/Concerns/CastsBasicEnumerations.php @@ -0,0 +1,86 @@ +getCasts()[$key]; + + /** + * @TODO: remove when fixed: https://github.com/laravel/framework/issues/42658 + */ + if ($this->shouldUseWorkaround($castType)) { + return $this->enumToArrayWorkaround($value); + } + + if ($value instanceof $castType) { + return $value; + } + + return EnumMakers::make($castType, $value); + } + + protected function setEnumCastableAttribute($key, $value) + { + $enumClass = $this->getCasts()[$key]; + + $keepEnumCase = property_exists($this, 'keepEnumCase') ? $this->keepEnumCase : true; + + if (!isset($value)) { + $this->attributes[$key] = null; + } elseif ($value instanceof $enumClass) { + $this->attributes[$key] = EnumValue::value($value, $keepEnumCase); + } else { + + if ($value instanceof UnitEnum) { + $value = EnumValue::value($value); + } + + $this->attributes[$key] = EnumValue::value( + EnumMakers::make($enumClass, $value), + $keepEnumCase + ); + } + } + + /** + * @TODO: remove when fixed: https://github.com/laravel/framework/issues/42658 + * @param $value + * @return object + */ + private function enumToArrayWorkaround(string|int $value): object + { + return new class($value) { + public function __construct(public readonly string|int $value) + { + } + }; + } + + /** + * @TODO: remove when fixed: https://github.com/laravel/framework/issues/42658 + * @param string $enumClass + * @return bool + */ + private function shouldUseWorkaround(string $enumClass): bool + { + return (!is_subclass_of($enumClass, BackedEnum::class, true)) + && 'toArray' === (debug_backtrace(2)[5]['function'] ?? null); + } +} diff --git a/tests/Fixtures/CastsBasicEnumsLowerCaseModel.php b/tests/Fixtures/CastsBasicEnumsLowerCaseModel.php new file mode 100644 index 0000000..018709d --- /dev/null +++ b/tests/Fixtures/CastsBasicEnumsLowerCaseModel.php @@ -0,0 +1,19 @@ + SubsetUnitEnum::class, + 'intBackedEnum' => IntBackedEnum::class, + 'stringBackedEnum' => StringBackedMakersEnum::class + ]; +} diff --git a/tests/Fixtures/CastsBasicEnumsModel.php b/tests/Fixtures/CastsBasicEnumsModel.php new file mode 100644 index 0000000..f1ddc0c --- /dev/null +++ b/tests/Fixtures/CastsBasicEnumsModel.php @@ -0,0 +1,17 @@ + SubsetUnitEnum::class, + 'intBackedEnum' => IntBackedEnum::class, + 'stringBackedEnum' => StringBackedMakersEnum::class + ]; +} diff --git a/tests/Unit/Laravel/Concerns/CastsBasicEnumsTest.php b/tests/Unit/Laravel/Concerns/CastsBasicEnumsTest.php new file mode 100644 index 0000000..a4d76c4 --- /dev/null +++ b/tests/Unit/Laravel/Concerns/CastsBasicEnumsTest.php @@ -0,0 +1,107 @@ +setRawAttributes([ + $key => EnumValue::value($enum, $keepCase) + ]); + + $this->assertEquals( + $enum, + $model->$key, + ); + } + + /** + * @return void + * + * @dataProvider providesEnums + */ + public function testShouldCastCorrectlyToString(UnitEnum $enum, string $key, bool $keepCase = true) + { + $model = $keepCase ? new CastsBasicEnumsModel() : new CastsBasicEnumsLowerCaseModel(); + $model->$key = $enum; + + $this->assertEquals( + EnumValue::value($enum, $keepCase), + $model->toArray()[$key], + ); + } + + public function testShouldHandleNull() { + $model = new CastsBasicEnumsModel(); + $model->unitEnum = null; + + $this->assertEquals(null, $model->unitEnum); + } + + public function testShouldHandleObjectInAttribute() { + $model = new CastsBasicEnumsModel(); + $model->setRawAttributes(['unitEnum'=>SubsetUnitEnum::ENUM]); + + $this->assertEquals(SubsetUnitEnum::ENUM, $model->unitEnum); + } + + public function testShouldHandleStringValue() { + $model = new CastsBasicEnumsModel(); + $model->unitEnum = 'enum'; + + $this->assertEquals('ENUM', $model->getAttributes()['unitEnum']); + + $this->assertEquals(SubsetUnitEnum::ENUM, $model->unitEnum); + } + + public function testShouldHandleStringValueLowerCase() { + $model = new CastsBasicEnumsLowerCaseModel(); + $model->unitEnum = 'ENUM'; + + $this->assertEquals('enum', $model->getAttributes()['unitEnum']); + } + + public function testShouldFailIfStringIsNotValid() { + $this->expectException(ValueError::class); + $model = new CastsBasicEnumsModel(); + $model->unitEnum = 'NotAnEnum'; + } + + public function testShouldFailIfEnumIsNotValid() { + $this->expectException(ValueError::class); + + $model = new CastsBasicEnumsModel(); + $model->unitEnum = IntBackedEnum::TEST; + } +}