diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 34dc032..bca7b21 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -15,23 +15,34 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest] - php-versions: ['7.4', '8.0', '8.1'] - name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} + php-version: ['7.4', '8.0', '8.1', '8.2'] + name: PHP ${{ matrix.php-version }} steps: - name: Checkout uses: actions/checkout@v2 - name: Install PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php-versions }} + php-version: ${{ matrix.php-version }} coverage: xdebug tools: composer - name: Install package run: | composer install - - name: Checks + - name: Phpstan rules for PHP ${{ matrix.php-version }} + if: ${{ matrix.php-version == '7.4' || matrix.php-version == '8.0' }} + run: cp phpstan_below_81.neon phpstan.neon + - name: Main checks run: | - composer ci:pack + composer phpcs + composer phpstan + composer phpunit + - name: Infection and coverage + if: ${{ matrix.php-version == '8.1' || matrix.php-version == '8.2' }} + run: | + composer infection + composer coverage + composer phpunit - name: Upload codecoverage run: | - bash <(curl -s https://codecov.io/bash) \ No newline at end of file + bash <(curl -s https://codecov.io/bash) diff --git a/README.md b/README.md index a348512..f57b0ba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [![PHP build](https://github.com/yaroslavche/BitMask/actions/workflows/php.yml/badge.svg)](https://github.com/yaroslavche/BitMask/actions/workflows/php.yml) [![codecov](https://codecov.io/gh/yaroslavche/bitmask/branch/main/graph/badge.svg)](https://codecov.io/gh/yaroslavche/bitmask) [![Infection MSI](https://badge.stryker-mutator.io/github.com/yaroslavche/BitMask/main)](https://infection.github.io) +[![PHP](http://poser.pugx.org/yaroslavche/bitmask/require/php)](https://packagist.org/packages/yaroslavche/bitmask) # BitMask PHP library for working with bitmask values @@ -87,6 +88,33 @@ $bitmask->executable = false; $bitmask->unknownKey = true; // BitMask\Exception\UnknownKeyException ``` +With PHP ^8.1 `EnumBitMask` can be used: + +```php +enum Permissions { + case Read; + case Write; + case Execute; +} + +use BitMask\EnumBitMask; + +// First argument is required and expects FQCN of the enum +// Second argument is a variadic UnitEnum, and acts like setter of the bits +$bitmask = new EnumBitMask(Permissions::class, Permissions::Read, Permissions::Execute); +// previous statement is equals to following lines: +// $bitmask = new EnumBitMask(Permissions::class); +// $bitmask->set(Permissions::Read, Permissions::Execute); // set, isSet and unset also have variadic args + +$bitmask->isSet(Permissions::Read); // true +$bitmask->isSet(Permissions::Write); // false +$bitmask->isSet(Permissions::Execute); // true +$bitmask->set(Permissions::Write); +$bitmask->isSet(Permissions::Write, Permissions::Read); // true +$bitmask->unset(Permissions::Write); +$bitmask->isSet(Permissions::Write); // false +``` + ## Installing Install package via [composer](https://getcomposer.org/) diff --git a/composer.json b/composer.json index ee15ac7..37754ae 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "php8" ], "require": { - "php": "^7.4|^8.0|^8.1" + "php": "^7.4|^8.0" }, "require-dev": { "phpunit/phpunit": "*", diff --git a/phpstan.neon b/phpstan.neon index 4789c45..a0e6887 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,2 +1,2 @@ -includes: - - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon +parameters: + level: 9 diff --git a/phpstan_below_81.neon b/phpstan_below_81.neon new file mode 100644 index 0000000..263ae93 --- /dev/null +++ b/phpstan_below_81.neon @@ -0,0 +1,6 @@ +parameters: + level: 9 + excludePaths: + analyseAndScan: + - src/EnumBitMask.php + - tests/EnumBitMaskTest.php diff --git a/src/EnumBitMask.php b/src/EnumBitMask.php new file mode 100644 index 0000000..2a2a6f7 --- /dev/null +++ b/src/EnumBitMask.php @@ -0,0 +1,79 @@ += 80100 || throw new UnsupportedPhpVersionException('Requires PHP 8.1 interface UnitEnum'); +class EnumBitMask +{ + private int $bitmask = 0; + /** @var UnitEnum[] $keys */ + private array $keys = []; + + /** + * @param class-string $maskEnum + * @throws UnknownEnumException + */ + public function __construct( + private readonly string $maskEnum, + UnitEnum ...$bits, + ) { + if (!is_subclass_of($this->maskEnum, UnitEnum::class)) { + throw new UnknownEnumException('BitMask enum must be instance of UnitEnum'); + } + $this->keys = $this->maskEnum::cases(); + $this->set(...$bits); + } + + public function get(): int + { + return $this->bitmask; + } + + /** @throws UnknownEnumException */ + public function set(UnitEnum ...$bits): void + { + foreach ($bits as $bit) { + if (!$this->isSet($bit)) { + $this->bitmask += 1 << intval(array_search($bit, $this->keys)); + } + } + } + + /** @throws UnknownEnumException */ + public function unset(UnitEnum ...$bits): void + { + foreach ($bits as $bit) { + if ($this->isSet($bit)) { + $this->bitmask -= 1 << intval(array_search($bit, $this->keys)); + } + } + } + + /** @throws UnknownEnumException */ + public function isSet(UnitEnum ...$bits): bool + { + foreach ($bits as $bit) { + $this->checkEnumCase($bit); + $mask = 1 << intval(array_search($bit, $this->keys)); + if (($this->bitmask & $mask) !== $mask) { + return false; + } + } + return true; + } + + /** @throws UnknownEnumException */ + private function checkEnumCase(UnitEnum $case): void + { + $case instanceof $this->maskEnum || + throw new UnknownEnumException(sprintf('Expected %s enum case, %s provided', $this->maskEnum, $case::class)); + } +} diff --git a/src/Exception/UnknownEnumException.php b/src/Exception/UnknownEnumException.php new file mode 100644 index 0000000..0f040c2 --- /dev/null +++ b/src/Exception/UnknownEnumException.php @@ -0,0 +1,11 @@ +markTestSkipped('PHP ^8.1 only'); + } + } + + public function testNotAnEnum(): void + { + $this->expectException(UnknownEnumException::class); + new EnumBitMask(self::class); + } + + public function testUnknownEnum(): void + { + $this->expectException(UnknownEnumException::class); + new EnumBitMask(Permissions::class, Unknown::Case); + } + + public function testGet(): void + { + $enumBitmask = new EnumBitMask(Permissions::class, Permissions::Create, Permissions::Read); + assertSame(3, $enumBitmask->get()); + $this->expectException(UnknownEnumException::class); + $enumBitmask->isSet(Unknown::Case); + } + + public function testIsSet(): void + { + $enumBitmask = new EnumBitMask(Permissions::class, Permissions::Create, Permissions::Read); + assertTrue($enumBitmask->isSet(Permissions::Create)); + assertTrue($enumBitmask->isSet(Permissions::Read)); + assertFalse($enumBitmask->isSet(Permissions::Update)); + assertFalse($enumBitmask->isSet(Permissions::Delete)); + $this->expectException(UnknownEnumException::class); + $enumBitmask->isSet(Unknown::Case); + } + + public function testSetUnset(): void + { + $enumBitmask = new EnumBitMask(Permissions::class, Permissions::Create, Permissions::Read); + $enumBitmask->unset(Permissions::Create, Permissions::Read); + assertFalse($enumBitmask->isSet(Permissions::Create)); + assertFalse($enumBitmask->isSet(Permissions::Read)); + assertFalse($enumBitmask->isSet(Permissions::Read, Permissions::Update)); + assertSame(0, $enumBitmask->get()); + $enumBitmask->set(Permissions::Update, Permissions::Delete); + assertTrue($enumBitmask->isSet(Permissions::Update)); + assertTrue($enumBitmask->isSet(Permissions::Delete)); + assertTrue($enumBitmask->isSet(Permissions::Update, Permissions::Delete)); + assertSame(12, $enumBitmask->get()); + $this->expectException(UnknownEnumException::class); + $enumBitmask->unset(Unknown::Case); + } +} diff --git a/tests/fixtures/Enum/Permissions.php b/tests/fixtures/Enum/Permissions.php new file mode 100644 index 0000000..db84b00 --- /dev/null +++ b/tests/fixtures/Enum/Permissions.php @@ -0,0 +1,11 @@ +