Skip to content

Commit 4bd17b6

Browse files
committed
BIT_MODIFIER
1 parent aad11d0 commit 4bd17b6

File tree

10 files changed

+176
-18
lines changed

10 files changed

+176
-18
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

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

5+
## 2.2.0 - 2023-06-09
6+
7+
- fixed serious bug in [Getters](docs/getters.md) where
8+
getting by integer would not match value first.
9+
- added support for [BIT_MODIFIER](docs/bitmasks.md#modifiers)
10+
511
## 2.1.0 - 2023-05-12
612

713
- Support for [Attributes](docs/attributes.md)

composer.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"require-dev": {
7373
"composer/composer": "^2.5",
7474
"henzeb/enumhancer-ide-helper": "main-dev",
75+
"infection/infection": "^0.27.0",
7576
"mockery/mockery": "^1.5",
7677
"nunomaduro/larastan": "^2.3",
7778
"orchestra/testbench": "^v7.18|^8.0",
@@ -98,7 +99,10 @@
9899
"test-dox": "vendor/bin/phpunit --testdox"
99100
},
100101
"config": {
101-
"sort-packages": true
102+
"sort-packages": true,
103+
"allow-plugins": {
104+
"infection/extension-installer": true
105+
}
102106
},
103107
"extra": {
104108
"laravel": {

docs/bitmasks.md

+31-2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,35 @@ Note: when the flag `BIT_VALUES` is set, each case must have a valid bit set.
4545
For example: `7` would throw a fatal error as this consists of the bits `1` `2`
4646
and `4`.
4747

48+
### Modifiers
49+
50+
Sometimes, you want to use your enum as a modifier where
51+
a combination might result in another case. This is not
52+
possible by default, so you need to enable the
53+
`BIT_MODIFIER` flag
54+
55+
````php
56+
enum Permission: int
57+
{
58+
use Henzeb\Enumhancer\Concerns\Bitmasks;
59+
60+
private const BIT_VALUES = true;
61+
62+
private const BIT_MODIFIER = true;
63+
64+
case Nothing = 0;
65+
case Read = 1;
66+
case Write = 2;
67+
case ReadAndWrite = 3;
68+
}
69+
70+
Permission::mask(
71+
Permission::Read,
72+
Permission::Write
73+
)->cases(); // returns [Permission::ReadWrite]
74+
75+
````
76+
4877
## Bits
4978

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

6897
### mask
6998

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

74103
````php

src/Helpers/Bitmasks/Bitmask.php

+13-6
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,19 @@ public function value(): int
9191

9292
public function cases(): array
9393
{
94-
return array_values(
95-
array_filter(
96-
$this->enumFQCN::cases(),
97-
fn(UnitEnum $case) => $this->bitmask & EnumBitmasks::getBit($case)
98-
)
99-
);
94+
$matchingCases = [];
95+
96+
foreach ($this->enumFQCN::cases() as $case) {
97+
$value = EnumBitmasks::getBit($case);
98+
if ($this->bitmask === $value) {
99+
return [$case];
100+
}
101+
if ($this->bitmask & $value) {
102+
$matchingCases[] = $case;
103+
}
104+
}
105+
106+
return $matchingCases;
100107
}
101108

102109
public function __toString(): string

src/Helpers/Bitmasks/EnumBitmasks.php

+34-9
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ private static function countSetBits(int $bit): int
3030

3131
public static function isBit(mixed $bit): bool
3232
{
33-
return self::isInt($bit) && self::countSetBits($bit) === 1;
33+
return self::isInt($bit) && (self::countSetBits($bit) === 1 || $bit === 0);
3434
}
3535

3636
public static function validateBitmaskCases(string $enum): void
@@ -67,18 +67,42 @@ public static function ignoreIntValues(string $enum): bool
6767
return true;
6868
}
6969

70+
public static function isModifier(BackedEnum|string $enum): bool
71+
{
72+
/**
73+
* @var UnitEnum $enum
74+
*/
75+
76+
EnumCheck::check($enum);
77+
78+
if (self::ignoreIntValues($enum)) {
79+
return false;
80+
}
81+
82+
foreach ((new ReflectionClass($enum))->getConstants() as $constant => $value) {
83+
if (strtolower($constant) === 'bit_modifier' and is_bool($value)) {
84+
return $value;
85+
}
86+
}
87+
return false;
88+
}
89+
7090
private static function validateBitCases(BackedEnum|string $enum): void
7191
{
92+
if (self::isModifier($enum)) {
93+
return;
94+
}
95+
7296
foreach ($enum::cases() as $case) {
73-
self::validateBitCase($case);
97+
if (self::validateBitCase($case)) {
98+
self::triggerInvalidBitCase($case::class, $case);
99+
}
74100
}
75101
}
76102

77-
private static function validateBitCase(BackedEnum $case): void
103+
private static function validateBitCase(BackedEnum $case): bool
78104
{
79-
if (self::isInt($case->value) && !self::isBit($case->value)) {
80-
self::triggerInvalidBitCase($case::class, $case);
81-
}
105+
return self::isInt($case->value) && !self::isBit($case->value);
82106
}
83107

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

90114
if (self::ignoreIntValues($enum::class)
91-
|| !filter_var($value, FILTER_VALIDATE_INT)
115+
|| !is_int($value)
92116
) {
93117
return pow(
94118
2,
95119
(int)array_search($enum, $enum::cases())
96120
);
97121
}
98122

99-
return (int)$value;
123+
return $value;
100124
}
101125

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

243267
protected static function triggerInvalidBitCase(UnitEnum|string $enum, UnitEnum $case): never
244268
{
269+
$enum = is_string($enum) ? $enum : $enum::class;
245270
trigger_error(
246-
sprintf('%s::%s is not a valid bit value', $enum::class ?? $enum, $case->name),
271+
sprintf('%s::%s is not a valid bit value', $enum, $case->name),
247272
E_USER_ERROR
248273
);
249274
}

src/Helpers/EnumGetters.php

+10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Henzeb\Enumhancer\Helpers;
44

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

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

128+
if (!$case && is_a($class, BackedEnum::class, true)) {
129+
foreach ($constants as $constant) {
130+
if ($constant->value == $value) {
131+
$case = $constant;
132+
break;
133+
}
134+
}
135+
}
136+
127137
if ($case) {
128138
return $case;
129139
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace Henzeb\Enumhancer\PHPStan\Constants;
4+
5+
use Henzeb\Enumhancer\Helpers\EnumImplements;
6+
use PHPStan\Reflection\ConstantReflection;
7+
use PHPStan\Rules\Constants\AlwaysUsedClassConstantsExtension;
8+
use function strtolower;
9+
10+
class BitmaskModifierConstantAlwaysUsed implements AlwaysUsedClassConstantsExtension
11+
{
12+
13+
public function isAlwaysUsed(ConstantReflection $constant): bool
14+
{
15+
if ($constant->getName() !== strtolower('bit_modifier')) {
16+
return false;
17+
}
18+
19+
$class = $constant->getDeclaringClass();
20+
21+
if (!$class->hasConstant('BIT_VALUES')) {
22+
return false;
23+
}
24+
25+
if (!$class->getBackedEnumType()?->isInteger()) {
26+
return false;
27+
}
28+
29+
30+
return EnumImplements::bitmasks($class->getName());
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Henzeb\Enumhancer\Tests\Fixtures\BackedEnums\Bitmasks;
4+
5+
use Henzeb\Enumhancer\Concerns\Bitmasks;
6+
7+
enum BitmasksCorrectModifierEnum: int
8+
{
9+
use Bitmasks;
10+
11+
const BIT_VALUES = true;
12+
13+
const BIT_MODIFIER = true;
14+
15+
case Neither = 1;
16+
case Read = 2;
17+
case Write = 4;
18+
case Both = 6;
19+
}

tests/Unit/Concerns/BitmasksTest.php

+24
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Henzeb\Enumhancer\Tests\Unit\Concerns;
44

55
use Henzeb\Enumhancer\Helpers\Bitmasks\EnumBitmasks;
6+
use Henzeb\Enumhancer\Tests\Fixtures\BackedEnums\Bitmasks\BitmasksCorrectModifierEnum;
67
use Henzeb\Enumhancer\Tests\Fixtures\BackedEnums\Bitmasks\BitmasksIncorrectIntEnum;
78
use Henzeb\Enumhancer\Tests\Fixtures\BackedEnums\Bitmasks\BitmasksIntEnum;
89
use Henzeb\Enumhancer\Tests\Fixtures\IntBackedEnum;
@@ -11,6 +12,13 @@
1112

1213
class BitmasksTest extends TestCase
1314
{
15+
protected function setUp(): void
16+
{
17+
set_error_handler(static function (int $errno, string $errstr): never {
18+
throw new TypeError($errstr, $errno);
19+
}, E_USER_ERROR);
20+
}
21+
1422
public function testShouldReturnCorrectBit(): void
1523
{
1624
$this->assertEquals(1, IntBackedEnum::TEST->bit());
@@ -140,4 +148,20 @@ public function testShouldThrowErrorWhenCastingTobits()
140148
$this->expectException(TypeError::class);
141149
EnumBitmasks::getBits(BitmasksIntEnum::class, 64);
142150
}
151+
152+
public function testAllowAsModifier()
153+
{
154+
155+
$this->assertSame(
156+
[
157+
1 => "Neither",
158+
2 => "Read",
159+
4 => "Write",
160+
6 => "Both",
161+
],
162+
BitmasksCorrectModifierEnum::bits()
163+
);
164+
165+
$this->assertEquals(6, BitmasksCorrectModifierEnum::mask(2, 4)->value());
166+
}
143167
}

tests/Unit/Concerns/MacrosTest.php

+2
Original file line numberDiff line numberDiff line change
@@ -182,5 +182,7 @@ protected function test()
182182
protected function tearDown(): void
183183
{
184184
MacrosUnitEnum::flushMacros();
185+
186+
restore_error_handler();
185187
}
186188
}

0 commit comments

Comments
 (0)