Skip to content

Commit

Permalink
BIT_MODIFIER
Browse files Browse the repository at this point in the history
  • Loading branch information
henzeb committed Jun 9, 2023
1 parent aad11d0 commit 4bd17b6
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 18 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to `Enumhancer` will be documented in this file

## 2.2.0 - 2023-06-09

- fixed serious bug in [Getters](docs/getters.md) where
getting by integer would not match value first.
- added support for [BIT_MODIFIER](docs/bitmasks.md#modifiers)

## 2.1.0 - 2023-05-12

- Support for [Attributes](docs/attributes.md)
Expand Down
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"require-dev": {
"composer/composer": "^2.5",
"henzeb/enumhancer-ide-helper": "main-dev",
"infection/infection": "^0.27.0",
"mockery/mockery": "^1.5",
"nunomaduro/larastan": "^2.3",
"orchestra/testbench": "^v7.18|^8.0",
Expand All @@ -98,7 +99,10 @@
"test-dox": "vendor/bin/phpunit --testdox"
},
"config": {
"sort-packages": true
"sort-packages": true,
"allow-plugins": {
"infection/extension-installer": true
}
},
"extra": {
"laravel": {
Expand Down
33 changes: 31 additions & 2 deletions docs/bitmasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,35 @@ Note: when the flag `BIT_VALUES` is set, each case must have a valid bit set.
For example: `7` would throw a fatal error as this consists of the bits `1` `2`
and `4`.

### Modifiers

Sometimes, you want to use your enum as a modifier where
a combination might result in another case. This is not
possible by default, so you need to enable the
`BIT_MODIFIER` flag

````php
enum Permission: int
{
use Henzeb\Enumhancer\Concerns\Bitmasks;

private const BIT_VALUES = true;

private const BIT_MODIFIER = true;

case Nothing = 0;
case Read = 1;
case Write = 2;
case ReadAndWrite = 3;
}

Permission::mask(
Permission::Read,
Permission::Write
)->cases(); // returns [Permission::ReadWrite]

````

## Bits

When you want to use the bits in a dropdown, you can easily use `bits`.
Expand All @@ -67,8 +96,8 @@ if [Mappers](mappers.md) are being used, any value will be mapped.

### mask

To get a mask, simply call the static method on your enum. just like with
[Comparison](comparison.md), you can add as many enum or values that represent enums
To get a mask, simply call the static method on your enum. just like with
[Comparison](comparison.md), you can add as many enum or values that represent enums
as you need.

````php
Expand Down
19 changes: 13 additions & 6 deletions src/Helpers/Bitmasks/Bitmask.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,19 @@ public function value(): int

public function cases(): array
{
return array_values(
array_filter(
$this->enumFQCN::cases(),
fn(UnitEnum $case) => $this->bitmask & EnumBitmasks::getBit($case)
)
);
$matchingCases = [];

foreach ($this->enumFQCN::cases() as $case) {
$value = EnumBitmasks::getBit($case);
if ($this->bitmask === $value) {
return [$case];
}
if ($this->bitmask & $value) {
$matchingCases[] = $case;
}
}

return $matchingCases;
}

public function __toString(): string
Expand Down
43 changes: 34 additions & 9 deletions src/Helpers/Bitmasks/EnumBitmasks.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ private static function countSetBits(int $bit): int

public static function isBit(mixed $bit): bool
{
return self::isInt($bit) && self::countSetBits($bit) === 1;
return self::isInt($bit) && (self::countSetBits($bit) === 1 || $bit === 0);
}

public static function validateBitmaskCases(string $enum): void
Expand Down Expand Up @@ -67,18 +67,42 @@ public static function ignoreIntValues(string $enum): bool
return true;
}

public static function isModifier(BackedEnum|string $enum): bool
{
/**
* @var UnitEnum $enum
*/

EnumCheck::check($enum);

if (self::ignoreIntValues($enum)) {
return false;
}

foreach ((new ReflectionClass($enum))->getConstants() as $constant => $value) {
if (strtolower($constant) === 'bit_modifier' and is_bool($value)) {
return $value;
}
}
return false;
}

private static function validateBitCases(BackedEnum|string $enum): void
{
if (self::isModifier($enum)) {
return;
}

foreach ($enum::cases() as $case) {
self::validateBitCase($case);
if (self::validateBitCase($case)) {
self::triggerInvalidBitCase($case::class, $case);
}
}
}

private static function validateBitCase(BackedEnum $case): void
private static function validateBitCase(BackedEnum $case): bool
{
if (self::isInt($case->value) && !self::isBit($case->value)) {
self::triggerInvalidBitCase($case::class, $case);
}
return self::isInt($case->value) && !self::isBit($case->value);
}

public static function getBit(UnitEnum $enum): int
Expand All @@ -88,15 +112,15 @@ public static function getBit(UnitEnum $enum): int
$value = EnumValue::value($enum);

if (self::ignoreIntValues($enum::class)
|| !filter_var($value, FILTER_VALIDATE_INT)
|| !is_int($value)
) {
return pow(
2,
(int)array_search($enum, $enum::cases())
);
}

return (int)$value;
return $value;
}

public static function getMask(string $class, UnitEnum|string|int ...$enums): Bitmask
Expand Down Expand Up @@ -242,8 +266,9 @@ public static function throwMismatch(string $expected, string $given): never

protected static function triggerInvalidBitCase(UnitEnum|string $enum, UnitEnum $case): never
{
$enum = is_string($enum) ? $enum : $enum::class;
trigger_error(
sprintf('%s::%s is not a valid bit value', $enum::class ?? $enum, $case->name),
sprintf('%s::%s is not a valid bit value', $enum, $case->name),
E_USER_ERROR
);
}
Expand Down
10 changes: 10 additions & 0 deletions src/Helpers/EnumGetters.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Henzeb\Enumhancer\Helpers;

use BackedEnum;
use Henzeb\Enumhancer\Concerns\Mappers;
use ReflectionClass;
use UnitEnum;
Expand Down Expand Up @@ -124,6 +125,15 @@ private static function match(UnitEnum|string $class, int|string $value): ?UnitE

$case = self::findCase($constants, $value);

if (!$case && is_a($class, BackedEnum::class, true)) {
foreach ($constants as $constant) {
if ($constant->value == $value) {
$case = $constant;
break;
}
}
}

if ($case) {
return $case;
}
Expand Down
32 changes: 32 additions & 0 deletions src/PHPStan/Constants/BitmaskModifierConstantAlwaysUsed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Henzeb\Enumhancer\PHPStan\Constants;

use Henzeb\Enumhancer\Helpers\EnumImplements;
use PHPStan\Reflection\ConstantReflection;
use PHPStan\Rules\Constants\AlwaysUsedClassConstantsExtension;
use function strtolower;

class BitmaskModifierConstantAlwaysUsed implements AlwaysUsedClassConstantsExtension
{

public function isAlwaysUsed(ConstantReflection $constant): bool
{
if ($constant->getName() !== strtolower('bit_modifier')) {
return false;
}

$class = $constant->getDeclaringClass();

if (!$class->hasConstant('BIT_VALUES')) {
return false;
}

if (!$class->getBackedEnumType()?->isInteger()) {
return false;
}


return EnumImplements::bitmasks($class->getName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Henzeb\Enumhancer\Tests\Fixtures\BackedEnums\Bitmasks;

use Henzeb\Enumhancer\Concerns\Bitmasks;

enum BitmasksCorrectModifierEnum: int
{
use Bitmasks;

const BIT_VALUES = true;

const BIT_MODIFIER = true;

case Neither = 1;
case Read = 2;
case Write = 4;
case Both = 6;
}
24 changes: 24 additions & 0 deletions tests/Unit/Concerns/BitmasksTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Henzeb\Enumhancer\Tests\Unit\Concerns;

use Henzeb\Enumhancer\Helpers\Bitmasks\EnumBitmasks;
use Henzeb\Enumhancer\Tests\Fixtures\BackedEnums\Bitmasks\BitmasksCorrectModifierEnum;
use Henzeb\Enumhancer\Tests\Fixtures\BackedEnums\Bitmasks\BitmasksIncorrectIntEnum;
use Henzeb\Enumhancer\Tests\Fixtures\BackedEnums\Bitmasks\BitmasksIntEnum;
use Henzeb\Enumhancer\Tests\Fixtures\IntBackedEnum;
Expand All @@ -11,6 +12,13 @@

class BitmasksTest extends TestCase
{
protected function setUp(): void
{
set_error_handler(static function (int $errno, string $errstr): never {
throw new TypeError($errstr, $errno);
}, E_USER_ERROR);
}

public function testShouldReturnCorrectBit(): void
{
$this->assertEquals(1, IntBackedEnum::TEST->bit());
Expand Down Expand Up @@ -140,4 +148,20 @@ public function testShouldThrowErrorWhenCastingTobits()
$this->expectException(TypeError::class);
EnumBitmasks::getBits(BitmasksIntEnum::class, 64);
}

public function testAllowAsModifier()
{

$this->assertSame(
[
1 => "Neither",
2 => "Read",
4 => "Write",
6 => "Both",
],
BitmasksCorrectModifierEnum::bits()
);

$this->assertEquals(6, BitmasksCorrectModifierEnum::mask(2, 4)->value());
}
}
2 changes: 2 additions & 0 deletions tests/Unit/Concerns/MacrosTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,5 +182,7 @@ protected function test()
protected function tearDown(): void
{
MacrosUnitEnum::flushMacros();

restore_error_handler();
}
}

0 comments on commit 4bd17b6

Please sign in to comment.