├── .github └── workflows │ └── continuous-integration.yml ├── .gitignore ├── .laminas-ci.json ├── LICENSE ├── README.md ├── composer.json ├── infection.json.dist ├── phpcs.xml.dist ├── phpunit.xml.dist ├── psalm.xml ├── src ├── Attributes │ └── MultipleBits.php └── BitmaskFunctionality.php └── tests ├── BitmaskFunctionalityTest.php └── Fixtures └── TestEnum.php /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | # See https://github.com/laminas/laminas-continuous-integration-action 2 | # Generates a job matrix based on current dependencies and supported version 3 | # ranges, then runs all those jobs 4 | name: "Continuous Integration" 5 | 6 | on: 7 | pull_request: 8 | push: 9 | 10 | jobs: 11 | matrix: 12 | name: Generate job matrix 13 | runs-on: ubuntu-latest 14 | outputs: 15 | matrix: ${{ steps.matrix.outputs.matrix }} 16 | steps: 17 | - name: Gather CI configuration 18 | id: matrix 19 | uses: laminas/laminas-ci-matrix-action@1.11.4 20 | 21 | qa: 22 | name: QA Checks 23 | needs: [ matrix ] 24 | runs-on: ${{ matrix.operatingSystem }} 25 | strategy: 26 | fail-fast: false 27 | matrix: ${{ fromJSON(needs.matrix.outputs.matrix) }} 28 | steps: 29 | - name: ${{ matrix.name }} 30 | uses: laminas/laminas-continuous-integration-action@1.14.1 31 | env: 32 | "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }} 33 | "STRYKER_DASHBOARD_API_KEY": ${{ secrets.STRYKER_DASHBOARD_API_KEY }} 34 | with: 35 | job: ${{ matrix.job }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /tests/Fixtures/var 3 | phpunit.xml 4 | composer.lock 5 | /.php_cs.cache 6 | .phpunit.result.cache 7 | -------------------------------------------------------------------------------- /.laminas-ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_php_platform_requirements": { 3 | "8.1": false 4 | }, 5 | "stablePHP": "8.1" 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Aurimas Niekis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Enum BitMask 2 | 3 | [![Latest Stable Version][ico-stable-version]][link-packagist] 4 | [![Software License][ico-license]](LICENSE) 5 | [![Build Status][ico-build]][link-build] 6 | [![Total Downloads][ico-downloads]][link-packagist] 7 | [![Dependents][ico-dependents]][link-dependents] 8 | [![PHP Version Require][ico-php-versions]][link-packagist] 9 | [![Mutation testing badge][ico-mutations]][link-mutations] 10 | [![Type Coverage][ico-type-coverage]][link-type-coverage] 11 | 12 | [![Email][ico-email]][link-email] 13 | 14 | A small library that provides functionality to PHP 8.1 enums to act as BitMask flags. 15 | 16 | ## Why? 17 | 18 | Sometimes you need some flags on the objects, to represent some features, most often its used simple property with the 19 | type of 20 | `bool` but then you start making several properties and then your object size (Serialized/JSON) starts growing quite a 21 | lot. 22 | 23 | The most efficient storage option for flags was always bitmask value. It provides up to 32 unique flags inside a single 24 | `int32` value. 25 | 26 | Another big benefit is the ability to make `AND`, `OR`, `NOT` operations in one call instead of doing many if 27 | expressions to check all those property values. 28 | 29 | ## Install 30 | 31 | Via [Composer][link-composer] 32 | 33 | ```shell 34 | $ composer require framjet/enum-bitmask 35 | ``` 36 | 37 | ## Usage 38 | 39 | The library provides a trait [`BitmaskFunctionality`](src/BitmaskFunctionality.php) which is needed to include inside 40 | int backed enum. 41 | 42 | Here is a tiny example of using `Enum` to provide flags using space-efficient bitmask int value: 43 | 44 | ```php 45 | flags = Flag::set( 70 | Flag::clear($this->flags, Flag::Private, Flag::Protected), 71 | Flag::Public 72 | ); 73 | } 74 | 75 | public function isPublic(): bool 76 | { 77 | return Flag::on($this->flags, Flag::Public); 78 | } 79 | 80 | public function isReadOnly(): bool 81 | { 82 | return Flag::on($this->flags, Flag::ReadOnly); 83 | } 84 | 85 | /** @return list */ 86 | public function getFlags(): array 87 | { 88 | return Flag::parse($this->flags); 89 | } 90 | 91 | public function getFlagsValue(): int 92 | { 93 | return $this->flags; 94 | } 95 | } 96 | 97 | class Container 98 | { 99 | /** @param list $members */ 100 | public function __construct(private array $members = []) 101 | { 102 | } 103 | 104 | public function addMember(Member $member): void 105 | { 106 | $this->members[] = $member; 107 | } 108 | 109 | public function getMembers(Flag ...$flags): array 110 | { 111 | return array_filter($this->members, static fn(Member $m) => Flag::any($m->getFlagsValue(), ...$flags)); 112 | } 113 | } 114 | 115 | $memberPublic = new Member(); 116 | $memberPublic->setPublic(); 117 | 118 | $memberPublic->getFlags(); // [Flag::Public] 119 | 120 | $memberReadOnly = new Member(Flag::build(Flag::ReadOnly)); 121 | 122 | $memberReadOnly->isReadOnly(); // true 123 | $memberReadOnly->isPublic(); // false 124 | 125 | $memberPrivate = new Member(Flag::build(Flag::Private, Flag::ReadOnly)); 126 | 127 | $memberPrivate->isReadOnly(); // true 128 | $memberPrivate->isPublic(); // false 129 | $memberPrivate->getFlags(); // [Flag::Private, Flag::ReadOnly] 130 | 131 | array_map( 132 | static fn(Flag $f) => $f->toString(), 133 | $memberPrivate->getFlags() 134 | ); // ['0b0000_0000_0000_0000_0000_0000_0000_0100', '0b0000_0000_0000_0000_0000_0000_0000_1000'] 135 | 136 | $container = new Container(); 137 | $container->addMember($memberPublic); 138 | $container->addMember($memberReadOnly); 139 | $container->addMember($memberPrivate); 140 | 141 | $container->getMembers(); // [$memberPublic, $memberReadOnly, $memberPrivate] 142 | $container->getMembers(Flag::Public); // [$memberPublic] 143 | $container->getMembers(Flag::ReadOnly); // [$memberReadOnly, $memberPrivate] 144 | $container->getMembers(Flag::ReadOnly, Flag::Public); // [$memberPublic, $memberReadOnly, $memberPrivate] 145 | ``` 146 | 147 | ## License 148 | 149 | Please see [License File](LICENSE) for more information. 150 | 151 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg 152 | 153 | [ico-stable-version]: http://poser.pugx.org/framjet/enum-bitmask/v 154 | 155 | [ico-build]: https://img.shields.io/github/workflow/status/framjet/php-enum-bitmask/Continuous%20Integration 156 | 157 | [ico-quality]: https://img.shields.io/scrutinizer/quality/g/aurimasniekis/php-tdlib-schema 158 | 159 | [ico-downloads]: http://poser.pugx.org/framjet/enum-bitmask/downloads 160 | 161 | [ico-dependents]: http://poser.pugx.org/framjet/enum-bitmask/dependents 162 | 163 | [ico-php-versions]: http://poser.pugx.org/framjet/enum-bitmask/require/php 164 | 165 | [ico-mutations]: https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fframjet%2Fphp-enum-bitmask%2Fmain 166 | 167 | [ico-type-coverage]: https://shepherd.dev/github/framjet/php-enum-bitmask/coverage.svg 168 | 169 | [ico-email]: https://img.shields.io/badge/email-team@framjet.dev-blue.svg 170 | 171 | [link-build]: https://github.com/framjet/php-enum-bitmask/actions 172 | 173 | [link-packagist]: https://packagist.org/packages/framjet/enum-bitmask 174 | 175 | [link-downloads]: https://packagist.org/packages/framjet/enum-bitmask/stats 176 | 177 | [link-dependents]: https://packagist.org/packages/framjet/enum-bitmask/dependents?order_by=downloads 178 | 179 | [link-mutations]: https://dashboard.stryker-mutator.io/reports/github.com/framjet/php-enum-bitmask/main 180 | 181 | [link-type-coverage]: https://shepherd.dev/github/framjet/php-enum-bitmask 182 | 183 | [link-email]: mailto:team@framjet.dev 184 | 185 | [link-composer]: https://getcomposer.org/ 186 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "framjet/enum-bitmask", 3 | "description": "A small library that provides functionality to PHP 8.1 enums to act as BitMask flags", 4 | "type": "library", 5 | "license": "MIT", 6 | "homepage": "https://github.com/framjet/php-enum-bitmask", 7 | "keywords": [ 8 | "bitmask", 9 | "bit", 10 | "mask", 11 | "flags", 12 | "flag", 13 | "enums", 14 | "enum", 15 | "framjet" 16 | ], 17 | "authors": [ 18 | { 19 | "name": "Aurimas Niekis", 20 | "email": "aurimas@niekis.lt", 21 | "homepage": "https://aurimas.niekis.lt/" 22 | } 23 | ], 24 | "require": { 25 | "php": "~8.1.0" 26 | }, 27 | "require-dev": { 28 | "framjet/coding-standard": "^1.0", 29 | "phpunit/phpunit": "^9.5.10", 30 | "psalm/plugin-phpunit": "^0.16.1", 31 | "roave/infection-static-analysis-plugin": "^1.12", 32 | "squizlabs/php_codesniffer": "^3.6.2", 33 | "vimeo/psalm": "^4.15.0" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "FramJet\\Packages\\EnumBitmask\\": "src/" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "FramJet\\Packages\\EnumBitmaskTest\\": "tests/" 43 | } 44 | }, 45 | "scripts": { 46 | "test": "phpunit" 47 | }, 48 | "config": { 49 | "sort-packages": true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "logs": { 8 | "text": "php://stderr", 9 | "github": true, 10 | "badge": { 11 | "branch": "main" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ./src 16 | ./tests 17 | 18 | 19 | ./tests/Fixtures/TestEnum.php 20 | 21 | 22 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | ./tests 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ./src 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Attributes/MultipleBits.php: -------------------------------------------------------------------------------- 1 | */ 15 | trait BitmaskFunctionality 16 | { 17 | /** @return Int32BitMask */ 18 | public static function mask(self ...$bits): int 19 | { 20 | if (count($bits) === 0) { 21 | return 0; 22 | } 23 | 24 | return array_reduce($bits, static fn (?int $sum, self $case) => ($sum ?? 0) | $case->value); 25 | } 26 | 27 | /** @param Int32BitMask $value */ 28 | public static function hasCase(int $value): bool 29 | { 30 | return self::tryFrom($value) !== null; 31 | } 32 | 33 | /** @param Int32BitMask $value */ 34 | public static function valueToString(int $value): string 35 | { 36 | return '0b' . substr(chunk_split(sprintf('%\'032b', $value), 4, '_'), 0, -1); 37 | } 38 | 39 | public function toString(): string 40 | { 41 | return self::valueToString($this->value); 42 | } 43 | 44 | /** @return Int32BitMask */ 45 | public static function build(self ...$bits): int 46 | { 47 | return self::set(0, ...$bits); 48 | } 49 | 50 | /** @param Int32BitMask $value */ 51 | 52 | /** @return Int32BitMask */ 53 | public static function set(int $value, self ...$bits): int 54 | { 55 | return $value | self::mask(...$bits); 56 | } 57 | 58 | /** @param Int32BitMask $value */ 59 | 60 | /** @return Int32BitMask */ 61 | public static function clear(int $value, self ...$bits): int 62 | { 63 | return $value & ~self::mask(...$bits); 64 | } 65 | 66 | /** @param Int32BitMask $value */ 67 | 68 | /** @return Int32BitMask */ 69 | public static function toggle(int $value, self ...$bits): int 70 | { 71 | return $value ^ self::mask(...$bits); 72 | } 73 | 74 | /** @param Int32BitMask $value */ 75 | public static function on(int $value, self $bit): bool 76 | { 77 | return ($value & $bit->value) === $bit->value; 78 | } 79 | 80 | /** @param Int32BitMask $value */ 81 | public static function off(int $value, self $bit): bool 82 | { 83 | return ($value & $bit->value) === 0; 84 | } 85 | 86 | /** @param Int32BitMask $value */ 87 | public static function any(int $value, self ...$bits): bool 88 | { 89 | if (count($bits) === 0) { 90 | return true; 91 | } 92 | 93 | return ($value & self::mask(...$bits)) > 0; 94 | } 95 | 96 | /** @param Int32BitMask $value */ 97 | public static function all(int $value, self ...$bits): bool 98 | { 99 | if (count($bits) === 0) { 100 | return true; 101 | } 102 | 103 | $mask = self::mask(...$bits); 104 | 105 | return ($value & $mask) === $mask; 106 | } 107 | 108 | /** @param Int32BitMask $value */ 109 | public static function none(int $value, self ...$bits): bool 110 | { 111 | if (count($bits) === 0) { 112 | return true; 113 | } 114 | 115 | $mask = self::mask(...$bits); 116 | 117 | return ($value & $mask) === 0; 118 | } 119 | 120 | /** 121 | * @param Int32BitMask $value 122 | * 123 | * @return list 124 | */ 125 | public static function parse(int $value): array 126 | { 127 | return array_filter(self::cases(), static fn (self $case) => self::on($value, $case)); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/BitmaskFunctionalityTest.php: -------------------------------------------------------------------------------- 1 | toString(), 89 | ); 90 | } 91 | 92 | public function testEnumHasCaseFromBitmask(): void 93 | { 94 | self::assertTrue( 95 | TestEnum::hasCase(0b0000_0000_0000_0000_0000_0001_0000_0000) 96 | ); 97 | 98 | self::assertFalse( 99 | TestEnum::hasCase(0b0000_0000_0000_0000_0000_0000_0000_1111) 100 | ); 101 | } 102 | 103 | public function testEnumBitmaskSet(): void 104 | { 105 | $value = TestEnum::set( 106 | 0b0000_0000_0000_0000_0000_0000_0000_1111, 107 | TestEnum::Flag8, 108 | ); 109 | 110 | self::assertSame( 111 | 0b0001_0000_0000_0000_0000_0000_0000_1111, 112 | $value 113 | ); 114 | 115 | $value = TestEnum::set( 116 | 0b0000_0000_0000_0000_0000_0000_0000_1111, 117 | TestEnum::Flag8, 118 | TestEnum::Flag7, 119 | TestEnum::Flag6 120 | ); 121 | 122 | self::assertSame( 123 | 0b0001_0001_0001_0000_0000_0000_0000_1111, 124 | $value 125 | ); 126 | } 127 | 128 | public function testEnumBitmaskClear(): void 129 | { 130 | $value = TestEnum::clear( 131 | 0b0001_0000_0000_0000_0000_0000_0000_1111, 132 | TestEnum::Flag8, 133 | ); 134 | 135 | self::assertSame( 136 | 0b0000_0000_0000_0000_0000_0000_0000_1111, 137 | $value 138 | ); 139 | 140 | $value = TestEnum::clear( 141 | 0b0001_0001_0001_0000_0000_0000_0000_1111, 142 | TestEnum::Flag8, 143 | TestEnum::Flag7, 144 | TestEnum::Flag6 145 | ); 146 | 147 | self::assertSame( 148 | 0b0000_0000_0000_0000_0000_0000_0000_1111, 149 | $value 150 | ); 151 | } 152 | 153 | public function testEnumBitmaskToggle(): void 154 | { 155 | $value = TestEnum::toggle( 156 | 0b0000_0000_0000_0000_0000_0000_0000_0000, 157 | TestEnum::Flag8, 158 | ); 159 | 160 | self::assertSame( 161 | 0b0001_0000_0000_0000_0000_0000_0000_0000, 162 | $value 163 | ); 164 | 165 | $value = TestEnum::toggle( 166 | $value, 167 | TestEnum::Flag8, 168 | ); 169 | 170 | self::assertSame( 171 | 0b0000_0000_0000_0000_0000_0000_0000_0000, 172 | $value 173 | ); 174 | 175 | $value = TestEnum::toggle( 176 | 0b0000_0000_0000_0000_0000_0000_0000_0000, 177 | TestEnum::Flag8, 178 | TestEnum::Flag7, 179 | TestEnum::Flag6 180 | ); 181 | 182 | self::assertSame( 183 | 0b0001_0001_0001_0000_0000_0000_0000_0000, 184 | $value 185 | ); 186 | 187 | $value = TestEnum::toggle( 188 | $value, 189 | TestEnum::Flag7, 190 | TestEnum::Flag6 191 | ); 192 | 193 | self::assertSame( 194 | 0b0001_0000_0000_0000_0000_0000_0000_0000, 195 | $value 196 | ); 197 | } 198 | 199 | public function testEnumBitMaskIsOnOff(): void 200 | { 201 | $value = 0b0001_0000_0000_0000_0000_0000_0000_0000; 202 | 203 | self::assertTrue(TestEnum::on($value, TestEnum::Flag8)); 204 | self::assertFalse(TestEnum::on($value, TestEnum::Flag7)); 205 | 206 | self::assertFalse(TestEnum::off($value, TestEnum::Flag8)); 207 | self::assertTrue(TestEnum::off($value, TestEnum::Flag7)); 208 | } 209 | 210 | public function testEnumBitMaskIsAnyAllNone(): void 211 | { 212 | $value = 0b0000_0000_0000_0000_0000_0000_0011_0011; 213 | 214 | self::assertTrue(TestEnum::any($value, TestEnum::Flag1, TestEnum::Flag3, TestEnum::Flag4)); 215 | self::assertFalse(TestEnum::any($value, TestEnum::Flag3, TestEnum::Flag4)); 216 | self::assertFalse(TestEnum::any($value, TestEnum::Flag5)); 217 | self::assertTrue(TestEnum::any($value)); 218 | 219 | self::assertTrue(TestEnum::all($value, TestEnum::Flag1, TestEnum::Flag2)); 220 | self::assertFalse(TestEnum::all($value, TestEnum::Flag1, TestEnum::Flag2, TestEnum::Flag4)); 221 | self::assertFalse(TestEnum::all($value, TestEnum::Flag3, TestEnum::Flag4)); 222 | self::assertFalse(TestEnum::all($value, TestEnum::Flag5)); 223 | self::assertTrue(TestEnum::all($value)); 224 | 225 | self::assertFalse(TestEnum::none($value, TestEnum::Flag1, TestEnum::Flag2)); 226 | self::assertFalse(TestEnum::none($value, TestEnum::Flag1, TestEnum::Flag2, TestEnum::Flag4)); 227 | self::assertTrue(TestEnum::none($value, TestEnum::Flag3, TestEnum::Flag4)); 228 | self::assertTrue(TestEnum::none($value, TestEnum::Flag5)); 229 | self::assertTrue(TestEnum::none($value)); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /tests/Fixtures/TestEnum.php: -------------------------------------------------------------------------------- 1 |