diff --git a/README.md b/README.md index df46d5e..ec54114 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ # BinaryFlags With this class you can easily add flags to your projects. -The number of flags you can use is limited to the architecture of your system, e.g.: 32 flags on a 32-bit system or 64 flags on 64-bit system. -To store 64-bit flags in a database, you will need to store it as UNSIGNED BIGINT in MySQL or an equivalent in your datastore. +The number of usable flags is limited by PHP's signed integer size: 31 flags on a 32-bit system or 63 flags on a 64-bit system. +If you store 64-bit integer masks in a database, use a type that can hold signed 64-bit values, such as `BIGINT` in MySQL or the equivalent in your datastore. This package also comes with a trait which you can use to implement binary flags directly in your own class. @@ -20,19 +20,19 @@ To install this package simply run the following command in the root of your pro composer require reinder83/binary-flags ``` -## Deprecation Notice (Upcoming v3.0.0 Breaking Change) -Starting in `v2.1.0`, passing `float` values as masks or flags is deprecated. +## v3.0.0 Breaking Changes +As of `v3.0.0`, masks and flags are `int`-only. -- Current `v2.x` behavior: floats are still accepted for backward compatibility, but trigger a deprecation warning. -- `v3.0.0` behavior: masks and flags will be `int`-only. -- `v3.0.0` behavior: `Bits::BIT_64` will be removed. +- Passing `float` values to numeric mask/flag methods from `strict_types=1` call sites now throws a `TypeError`. +- For non-strict callers, PHP scalar coercion can still convert `float` to `int` before the method is entered, so validate or cast external values before calling the API. +- `Bits::BIT_64` has been removed. ### BIT_64 Notice -`Bits::BIT_64` is being removed because PHP numbers for bitwise flags are signed. The 64th bit is the sign bit, so it cannot be used reliably as a normal flag. +`Bits::BIT_64` was removed because PHP numbers for bitwise flags are signed. The 64th bit is the sign bit, so it cannot be used reliably as a normal flag. -Using integer-compatible bits (`BIT_1` through `BIT_63`) prevents these issues and is the supported path for `v3.0.0`. +Use `BIT_1` through `BIT_63` for portable numeric flags. -To prepare for `v3.0.0`, cast incoming values before using the API: +If you still receive mask values from loose legacy sources, cast them before using the API: ```php $flags->setMask((int) $maskFromLegacySource); diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e5243fb..b0f3f32 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,66 +1,16 @@ -# Release Notes - v2.1.0 +# Release Notes - v3.0.0 -## Added -- New enum-backed API: - - `BinaryEnumFlags` - - `Traits\InteractsWithEnumFlags` - - `Flag` enum and `Mask` value object -- Enum-backed flags now return a `Mask` object from `getMask()`. -- New `getMaskValue(): int` method for enum-backed flags to persist/interoperate with integer masks. -- Deprecation warnings for passing `float` values as masks/flags. -- README migration notice for the upcoming `v3.0.0` integer-only API. -- `UPGRADE-v3.md` with migration instructions. -- New primary numeric trait: `Traits\InteractsWithNumericFlags`. -- `Traits\BinaryFlags` is now deprecated and kept for backward compatibility. +## Changed +- Numeric `BinaryFlags` APIs now accept `int` values only. +- Passing `float` values to numeric mask/flag methods now raises `TypeError` for `strict_types=1` callers. +- Non-strict callers should still cast or validate external values before calling the API because PHP scalar coercion can convert `float` to `int` at the call boundary. +- The numeric iterator and JSON serialization contracts are now documented as `int`-based. -## Deprecated -- Passing `float` to BinaryFlags mask/flag methods is deprecated in `v2.1.0`. -- Float support will be removed in `v3.0.0`. -- `Bits::BIT_64` will be removed in `v3.0.0`. +## Removed +- `Bits::BIT_64`. +- The v2.x float-normalization/deprecation path in `Traits\InteractsWithNumericFlags`. ## BIT_64 Notice -`Bits::BIT_64` is being removed because PHP numbers for bitwise flags are signed. The 64th bit is the sign bit, so it cannot be used reliably as a normal flag. +`Bits::BIT_64` was removed because PHP numbers for bitwise flags are signed. The 64th bit is the sign bit, so it cannot be used reliably as a normal flag. Using integer-compatible bits avoids these issues. - -## Migration Recommendation -Cast external/legacy mask and flag values to `int` before calling BinaryFlags methods. - -```php -$flags->setMask((int) $mask); -$flags->addFlag((int) $flag); -``` - -## Enum Migration Example -```php -// Before: numeric PermissionFlags -class PermissionFlags extends BinaryFlags -{ - public const CAN_VIEW = Bits::BIT_1; - public const CAN_BOOK = Bits::BIT_2; -} - -$flags = new PermissionFlags($storedMask); -$flags->addFlag(PermissionFlags::CAN_VIEW | PermissionFlags::CAN_BOOK); -$storedMask = $flags->getMask(); - -// After: enum-backed PermissionFlags -enum Permission: int -{ - case CanView = Bits::BIT_1; - case CanBook = Bits::BIT_2; -} - -class PermissionFlags extends BinaryEnumFlags -{ - protected static function getFlagEnumClass(): string - { - return Permission::class; - } -} - -$flags = new PermissionFlags(Mask::fromInt($storedMask, Permission::class)); -$flags->addFlag(Permission::CanView); -$flags->addFlag(Permission::CanBook); -$storedMask = $flags->getMaskValue(); -``` diff --git a/UPGRADE-v3.md b/UPGRADE-v3.md index ce8281a..d0893ef 100644 --- a/UPGRADE-v3.md +++ b/UPGRADE-v3.md @@ -12,7 +12,7 @@ ## How to Migrate 1. Find every call that passes mask/flag values into BinaryFlags methods. -2. Ensure values are cast to `int` before passing them. +2. Ensure values are cast or validated as `int` before passing them. 3. Ensure database or external sources provide integer-compatible values. ## Example @@ -28,10 +28,11 @@ $flags->setMask((int) $legacyValue); $flags->addFlag((int) $legacyFlag); ``` -## v2.1+ Deprecation Signal -Starting in `v2.1.0`, float inputs trigger deprecation warnings to help detect call sites before moving to `v3.0.0`. +## Runtime Impact +Code paths that still pass `float` values into numeric flag APIs now fail with `TypeError` when called from `strict_types=1` code. +For non-strict callers, PHP scalar coercion can still convert `float` to `int` at the call boundary, so external values should be validated or cast before calling the API. ## Why BIT_64 Is Being Removed -`BIT_64` is being removed because PHP numbers for bitwise flags are signed. The 64th bit is the sign bit, so it cannot be used reliably as a normal flag. +`BIT_64` was removed because PHP numbers for bitwise flags are signed. The 64th bit is the sign bit, so it cannot be used reliably as a normal flag. Staying with integer-compatible bits prevents those runtime issues. diff --git a/composer.json b/composer.json index d09ac60..9b68d29 100644 --- a/composer.json +++ b/composer.json @@ -39,10 +39,10 @@ "ext-json": "*" }, "require-dev": { - "phpstan/phpstan": "^1.10", - "pestphp/pest": "^2.36", + "phpstan/phpstan": "^2.1", + "pestphp/pest": "^3.8", "laravel/pint": "^1", - "rector/rector": "^1" + "rector/rector": "^2.4" }, "autoload-dev": { "psr-4": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 1ff9017..fafe8bf 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -4,3 +4,4 @@ parameters: - src ignoreErrors: - message: '#Dead catch - ReflectionException is never thrown in the try block#' + - message: '#Trait Reinder83\\BinaryFlags\\Traits\\BinaryFlags is used zero times and is not analysed#' diff --git a/src/BinaryEnumFlags.php b/src/BinaryEnumFlags.php index 3739022..9ead6a8 100644 --- a/src/BinaryEnumFlags.php +++ b/src/BinaryEnumFlags.php @@ -1,5 +1,7 @@ + * @implements Iterator */ abstract class BinaryFlags implements Countable, Iterator, JsonSerializable { @@ -23,7 +25,7 @@ abstract class BinaryFlags implements Countable, Iterator, JsonSerializable /** * Initiate class */ - public function __construct(int|float $mask = 0, ?Closure $onModify = null) + public function __construct(int $mask = 0, ?Closure $onModify = null) { $this->setMask($mask); @@ -64,11 +66,11 @@ public function next(): void /** * Return the key of the current element * - * @return int|float the flag + * @return int the flag * * @since 1.2.0 */ - public function key(): int|float + public function key(): int { return $this->currentPos; } @@ -132,7 +134,7 @@ public function count(): int /** * Specify data which should be serialized to JSON * - * @return array{mask: int|float} data which can be serialized by json_encode, + * @return array{mask: int} data which can be serialized by json_encode, * * @since 1.2.0 */ diff --git a/src/Bits.php b/src/Bits.php index 79e85e6..998106b 100644 --- a/src/Bits.php +++ b/src/Bits.php @@ -1,5 +1,7 @@ */ - public function getFlagNames(int|float|null $mask = null, bool $asArray = false): string|array + public function getFlagNames(?int $mask = null, bool $asArray = false): string|array { - $mask = $mask === null - ? (int) $this->mask - : $this->normalizeMaskOrFlag($mask, 'mask', __METHOD__); + $mask ??= $this->mask; $names = []; foreach (static::getAllFlags() as $flag => $desc) { - if (is_numeric($flag) && ($mask & $flag)) { + if (($mask & $flag) !== 0) { $names[$flag] = $desc; } } @@ -129,10 +110,10 @@ protected function onModify(): void * * @return $this */ - public function setMask(int|float $mask): static + public function setMask(int $mask): static { $before = $this->mask; - $this->mask = $this->normalizeMaskOrFlag($mask, 'mask', __METHOD__); + $this->mask = $mask; if ($before !== $this->mask) { $this->onModify(); @@ -144,7 +125,7 @@ public function setMask(int|float $mask): static /** * This method will return the current mask */ - public function getMask(): int|float + public function getMask(): int { return $this->mask; } @@ -154,10 +135,10 @@ public function getMask(): int|float * * @return $this */ - public function addFlag(int|float $flag): static + public function addFlag(int $flag): static { $before = $this->mask; - $this->mask |= $this->normalizeMaskOrFlag($flag, 'flag', __METHOD__); + $this->mask |= $flag; if ($before !== $this->mask) { $this->onModify(); @@ -171,11 +152,10 @@ public function addFlag(int|float $flag): static * * @return $this */ - public function removeFlag(int|float $flag): static + public function removeFlag(int $flag): static { $before = $this->mask; - $normalizedFlag = $this->normalizeMaskOrFlag($flag, 'flag', __METHOD__); - $this->mask &= ~$normalizedFlag; + $this->mask &= ~$flag; if ($before !== $this->mask) { $this->onModify(); @@ -189,21 +169,18 @@ public function removeFlag(int|float $flag): static * By default it will check all bits in the given flag * When you want to match any of the given flags set $checkAll to false */ - public function checkFlag(int|float $flag, bool $checkAll = true): bool + public function checkFlag(int $flag, bool $checkAll = true): bool { - $normalizedFlag = $this->normalizeMaskOrFlag($flag, 'flag', __METHOD__); - $result = $this->mask & $normalizedFlag; + $result = $this->mask & $flag; - return $checkAll ? $result === $normalizedFlag : $result > 0; + return $checkAll ? $result === $flag : $result > 0; } /** * Check if any given flag(s) are set in the current mask */ - public function checkAnyFlag(int|float $mask): bool + public function checkAnyFlag(int $mask): bool { - $normalizedMask = $this->normalizeMaskOrFlag($mask, 'mask', __METHOD__); - - return $this->checkFlag($normalizedMask, false); + return $this->checkFlag($mask, false); } } diff --git a/tests/EnumErrorPathsTest.php b/tests/EnumErrorPathsTest.php index 099c771..8f8fd38 100644 --- a/tests/EnumErrorPathsTest.php +++ b/tests/EnumErrorPathsTest.php @@ -1,5 +1,7 @@ test = new ExampleFlags(ExampleFlags::FOO | ExampleFlags::BAR); -}); - -test('float mask and flag values are deprecated in v2 and still work', function (): void { - $messages = []; - set_error_handler(function (int $severity, string $message) use (&$messages): bool { - if ($severity === E_USER_DEPRECATED) { - $messages[] = $message; - - return true; - } - - return false; - }); - - try { - new ExampleFlags((float) ExampleFlags::FOO); - $this->test->setMask((float) ExampleFlags::FOO); - $this->test->addFlag((float) ExampleFlags::BAR); - $this->test->removeFlag((float) ExampleFlags::FOO); - $this->test->checkFlag((float) ExampleFlags::BAR); - $this->test->checkAnyFlag((float) ExampleFlags::BAR); - $this->test->getFlagNames((float) ExampleFlags::BAR); - } finally { - restore_error_handler(); - } - - expect($messages)->toHaveCount(7) - ->and($this->test->checkFlag(ExampleFlags::BAR))->toBeTrue() - ->and($this->test->checkFlag(ExampleFlags::FOO))->toBeFalse(); -}); diff --git a/tests/NumericTypeValidationTest.php b/tests/NumericTypeValidationTest.php new file mode 100644 index 0000000..5931181 --- /dev/null +++ b/tests/NumericTypeValidationTest.php @@ -0,0 +1,36 @@ +test = new ExampleFlags(ExampleFlags::FOO | ExampleFlags::BAR); +}); + +test('float mask and flag values are rejected in v3', function (): void { + expect(fn() => new ExampleFlags((float) ExampleFlags::FOO)) + ->toThrow(TypeError::class); + + expect(fn() => $this->test->setMask((float) ExampleFlags::FOO)) + ->toThrow(TypeError::class); + + expect(fn() => $this->test->addFlag((float) ExampleFlags::BAR)) + ->toThrow(TypeError::class); + + expect(fn() => $this->test->removeFlag((float) ExampleFlags::FOO)) + ->toThrow(TypeError::class); + + expect(fn() => $this->test->checkFlag((float) ExampleFlags::BAR)) + ->toThrow(TypeError::class); + + expect(fn() => $this->test->checkAnyFlag((float) ExampleFlags::BAR)) + ->toThrow(TypeError::class); + + expect(fn() => $this->test->getFlagNames((float) ExampleFlags::BAR)) + ->toThrow(TypeError::class); +}); + +test('bit 64 is no longer defined in v3', function (): void { + expect(defined('Reinder83\\BinaryFlags\\Bits::BIT_64'))->toBeFalse(); +}); diff --git a/tests/PermissionEnumTest.php b/tests/PermissionEnumTest.php index 0a8c9e2..3a3bdbc 100644 --- a/tests/PermissionEnumTest.php +++ b/tests/PermissionEnumTest.php @@ -1,5 +1,7 @@ and($flags->getMask()->toInt())->toEqual(Permission::CanView->value | Permission::CanBook->value | Permission::CanCancel->value); }); -test('enum flags do not trigger float deprecation warnings', function (): void { +test('enum flags do not emit deprecation warnings', function (): void { $messages = []; set_error_handler(function (int $severity, string $message) use (&$messages): bool { if ($severity === E_USER_DEPRECATED) { diff --git a/tests/Pest.php b/tests/Pest.php index 18f2653..7a796c0 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,5 +1,7 @@ in(__DIR__); diff --git a/tests/Stubs/BadStringFlag.php b/tests/Stubs/BadStringFlag.php index 329beaa..74241bb 100644 --- a/tests/Stubs/BadStringFlag.php +++ b/tests/Stubs/BadStringFlag.php @@ -1,5 +1,7 @@