Skip to content

Commit d6d2c31

Browse files
authored
Fixed #33 (#35)
1 parent 0aa29e7 commit d6d2c31

8 files changed

Lines changed: 124 additions & 29 deletions

File tree

README.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,44 @@ $bitmask->set(EXECUTE);
3434
```
3535

3636
Exists [EnumBitMask](/src/EnumBitMask.php), which allows the same using PHP enum:
37+
The `EnumBitMask` allows you to work with bitmasks using PHP enums. It automatically detects if an enum is `int`-backed and uses the case's value. For `int`-backed enums, each case **must represent a single bit value** (e.g., 1, 2, 4, 8, etc.). Otherwise, an `InvalidEnumException` will be thrown.
3738

3839
```php
3940
use BitMask\EnumBitMask;
4041

4142
enum Permissions
4243
{
43-
case READ;
44-
case WRITE;
45-
case EXECUTE;
44+
case READ; // Mapped to 1
45+
case WRITE; // Mapped to 2
46+
case EXECUTE; // Mapped to 4
4647
}
4748

48-
// two arguments: required enum class-string and integer mask (default: 0)
49+
// Automatically maps UnitEnum based on their position
4950
$bitmask = new EnumBitMask(Permissions::class, 0b111);
5051
echo sprintf('mask: %d', $bitmask->get()); // mask: 7
5152
if ($bitmask->has(Permissions::READ)) {}
5253
$bitmask->remove(Permissions::EXECUTE);
5354
$bitmask->set(Permissions::EXECUTE);
5455

55-
$bitmask->set(Unknown::Case); // throws an exception, only Permissions cases available
56+
// Works with int-backed enums
57+
enum Flags: int
58+
{
59+
case User = 1; // 0b001
60+
case Admin = 8; // 0b1000
61+
case Guest = 32; // 0b100000
62+
}
63+
64+
$flagmask = new EnumBitMask(Flags::class, Flags::User | Flags::Admin);
65+
echo sprintf('mask: %d', $flagmask->get()); // mask: 9
66+
$flagmask->set(Flags::Guest);
67+
68+
// Throws an InvalidEnumException if a value is not a single bit
69+
enum BadFlags: int
70+
{
71+
case Bad = 3; // 0b11 - not a single bit
72+
}
73+
74+
new EnumBitMask(BadFlags::class, 3); // Throws InvalidEnumException
5675
```
5776

5877
`EnumBitMask` have a factory methods:

composer.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
"phpstan/phpstan": "*",
2020
"thecodingmachine/phpstan-strict-rules": "*",
2121
"infection/infection": "*",
22-
"vimeo/psalm": "^5.22",
23-
"psalm/plugin-phpunit": "^0.18.4",
24-
"friendsofphp/php-cs-fixer": "^3.51"
22+
"vimeo/psalm": "*",
23+
"psalm/plugin-phpunit": "*",
24+
"friendsofphp/php-cs-fixer": "*"
2525
},
2626
"authors": [
2727
{

src/BitMask.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public function __toString(): string
2222
return (string) $this->mask;
2323
}
2424

25+
#[\Override]
2526
public function get(): int
2627
{
2728
return $this->mask;

src/EnumBitMask.php

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
namespace BitMask;
66

7+
use BackedEnum;
8+
use BitMask\Exception\InvalidEnumException;
79
use BitMask\Exception\NotSingleBitException;
810
use BitMask\Exception\UnknownEnumException;
911
use BitMask\Util\Bits;
1012
use UnitEnum;
1113

12-
/** @psalm-suppress UnusedClass */
1314
final class EnumBitMask implements BitMaskInterface
1415
{
1516
private BitMask $bitmask;
@@ -19,6 +20,7 @@ final class EnumBitMask implements BitMaskInterface
1920

2021
/**
2122
* @param class-string $enum
23+
* @throws InvalidEnumException
2224
* @throws UnknownEnumException
2325
*/
2426
public function __construct(
@@ -28,29 +30,51 @@ public function __construct(
2830
if (!is_subclass_of($this->enum, UnitEnum::class)) {
2931
throw new UnknownEnumException('EnumBitMask enum must be subclass of UnitEnum');
3032
}
31-
foreach ($this->enum::cases() as $index => $case) {
32-
$this->map[$case->name] = Bits::indexToBit($index);
33+
34+
$reflection = new \ReflectionEnum($this->enum);
35+
/** @var null|\ReflectionNamedType $backingType */
36+
$backingType = $reflection->getBackingType();
37+
$backingTypeName = $backingType?->getName();
38+
if ($reflection->isBacked() && $backingTypeName === 'int') {
39+
/** @var \ReflectionEnumBackedCase $case */
40+
foreach ($reflection->getCases() as $case) {
41+
$value = $case->getBackingValue();
42+
if (!is_int($value)) {
43+
throw new InvalidEnumException('Enum must be an int-backed enum with integer values');
44+
}
45+
46+
if (!Bits::isSingleBit($value)) {
47+
throw new InvalidEnumException(sprintf('Enum case "%s" value is not a single bit', $case->name));
48+
}
49+
50+
$this->map[$case->name] = $value;
51+
}
52+
} else {
53+
foreach ($this->enum::cases() as $index => $case) {
54+
$this->map[$case->name] = Bits::indexToBit($index);
55+
}
3356
}
34-
$this->bitmask = new BitMask($mask, count($this->enum::cases()) - 1);
57+
58+
$this->bitmask = new BitMask($mask, count($this->map) > 0 ? Bits::getMostSignificantBit(max($this->map)) : null);
3559
}
3660

3761
/**
3862
* Create an instance with given flags on
3963
*
4064
* @param class-string $enum
41-
* @throws UnknownEnumException|NotSingleBitException
65+
* @throws UnknownEnumException|NotSingleBitException|InvalidEnumException
4266
*/
4367
public static function create(string $enum, UnitEnum ...$bits): self
4468
{
45-
return (new EnumBitMask($enum))->set(...$bits);
69+
return (new self($enum))->set(...$bits);
4670
}
4771

4872
/**
4973
* Create an instance with all flags on
5074
*
5175
* @psalm-suppress MixedMethodCall, MixedArgument
5276
* @param class-string $enum
53-
* @throws UnknownEnumException|NotSingleBitException
77+
* @throws UnknownEnumException|NotSingleBitException|InvalidEnumException
5478
*/
5579
public static function all(string $enum): self
5680
{
@@ -62,25 +86,26 @@ public static function all(string $enum): self
6286
*
6387
* @psalm-suppress PossiblyUnusedMethod
6488
* @param class-string $enum
65-
* @throws UnknownEnumException|NotSingleBitException
89+
* @throws UnknownEnumException|NotSingleBitException|InvalidEnumException
6690
*/
6791
public static function none(string $enum): self
6892
{
69-
return self::create($enum);
93+
return new self($enum);
7094
}
7195

7296
/**
7397
* Create an instance without given flags on
7498
*
7599
* @psalm-suppress PossiblyUnusedMethod
76100
* @param class-string $enum
77-
* @throws UnknownEnumException|NotSingleBitException
101+
* @throws UnknownEnumException|NotSingleBitException|InvalidEnumException
78102
*/
79103
public static function without(string $enum, UnitEnum ...$bits): self
80104
{
81105
return self::all($enum)->remove(...$bits);
82106
}
83107

108+
#[\Override]
84109
public function get(): int
85110
{
86111
return $this->bitmask->get();
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace BitMask\Exception;
6+
7+
use Exception;
8+
9+
final class InvalidEnumException extends Exception implements BitMaskExceptionInterface {}

tests/EnumBitMaskTest.php

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
use BitMask\EnumBitMask;
88
use BitMask\Exception\OutOfRangeException;
99
use BitMask\Exception\UnknownEnumException;
10-
use BitMask\Tests\fixtures\Enum\BackedInt;
1110
use BitMask\Tests\fixtures\Enum\BackedString;
11+
use BitMask\Tests\fixtures\Enum\EmptyEnum;
1212
use BitMask\Tests\fixtures\Enum\Permissions;
13+
use BitMask\Tests\fixtures\Enum\RandomBackedInt;
1314
use BitMask\Tests\fixtures\Enum\Unknown;
1415
use PHPUnit\Framework\TestCase;
1516

@@ -115,24 +116,15 @@ public function testRemoveTwice(): void
115116
assertSame(0, $enumBitmask->get());
116117
}
117118

118-
public function testBackedEnum(): void
119+
public function testBackedStringEnum(): void
119120
{
120-
// backed string
121121
$backedStringEnumBitmask = new EnumBitMask(BackedString::class, 3);
122122
assertTrue($backedStringEnumBitmask->has(BackedString::Create, BackedString::Read));
123123
assertFalse($backedStringEnumBitmask->has(BackedString::Update, BackedString::Delete));
124124
$backedStringEnumBitmask->remove(BackedString::Create, BackedString::Read);
125125
$backedStringEnumBitmask->set(BackedString::Update, BackedString::Delete);
126126
assertFalse($backedStringEnumBitmask->has(BackedString::Create, BackedString::Read));
127127
assertTrue($backedStringEnumBitmask->has(BackedString::Update, BackedString::Delete));
128-
// backed int
129-
$backedIntEnumBitmask = new EnumBitMask(BackedInt::class, 3);
130-
assertTrue($backedIntEnumBitmask->has(BackedInt::Create, BackedInt::Read));
131-
assertFalse($backedIntEnumBitmask->has(BackedInt::Update, BackedInt::Delete));
132-
$backedIntEnumBitmask->remove(BackedInt::Create, BackedInt::Read);
133-
$backedIntEnumBitmask->set(BackedInt::Update, BackedInt::Delete);
134-
assertFalse($backedIntEnumBitmask->has(BackedInt::Create, BackedInt::Read));
135-
assertTrue($backedIntEnumBitmask->has(BackedInt::Update, BackedInt::Delete));
136128
}
137129

138130
public function testCreateFactory(): void
@@ -174,4 +166,37 @@ public function testWithoutFactory(): void
174166
$enumBitmask = EnumBitMask::without(Permissions::class, Permissions::Delete);
175167
assertSame(7, $enumBitmask->get());
176168
}
169+
170+
public function testIsIntBackedEnumRandom(): void
171+
{
172+
// random backed int
173+
$backedIntEnumBitmask = new EnumBitMask(RandomBackedInt::class, 17);
174+
assertTrue($backedIntEnumBitmask->has(RandomBackedInt::Bit1, RandomBackedInt::Bit3));
175+
assertFalse($backedIntEnumBitmask->has(RandomBackedInt::Bit2));
176+
}
177+
178+
public function testIsIntBackedEnumRandom2(): void
179+
{
180+
// random backed int
181+
$enumBitmask = new EnumBitMask(RandomBackedInt::class);
182+
$enumBitmask->set(RandomBackedInt::Bit3, RandomBackedInt::Bit4);
183+
assertTrue($enumBitmask->has(RandomBackedInt::Bit3, RandomBackedInt::Bit4));
184+
}
185+
186+
public function testWithNormalUnitEnum(): void
187+
{
188+
$bitmask = new EnumBitMask(Permissions::class, 1);
189+
$this->assertInstanceOf(EnumBitMask::class, $bitmask);
190+
$this->assertTrue($bitmask->has(Permissions::Create));
191+
$this->assertFalse($bitmask->has(Permissions::Delete));
192+
$bitmask->set(Permissions::Delete);
193+
$this->assertTrue($bitmask->has(Permissions::Delete));
194+
}
195+
196+
public function testWithEmptyEnum(): void
197+
{
198+
$bitmask = new EnumBitMask(EmptyEnum::class);
199+
$this->assertInstanceOf(EnumBitMask::class, $bitmask);
200+
$this->assertSame(0, $bitmask->get());
201+
}
177202
}

tests/fixtures/Enum/EmptyEnum.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
namespace BitMask\Tests\fixtures\Enum;
4+
5+
enum EmptyEnum {}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace BitMask\Tests\fixtures\Enum;
4+
5+
enum RandomBackedInt: int
6+
{
7+
case Bit1 = 1 << 0;
8+
case Bit2 = 1 << 1;
9+
case Bit4 = 1 << 3;
10+
case Bit3 = 1 << 4;
11+
}

0 commit comments

Comments
 (0)