Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 52 additions & 13 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,76 @@ run-name: test
on:
workflow_dispatch:
push:
branches: master
branches:
- master
- main
pull_request:
branches: master
branches:
- master
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
php-versions:
- 8.5
- 8.4
- 8.3
php-version:
- 8.2
- 8.1
- 8.3
- 8.4
- 8.5

name: PHP ${{ matrix.php-versions }}
name: Test PHP ${{ matrix.php-version }}
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
php-version: ${{ matrix.php-version }}
extensions: json
coverage: none
tools: composer:v2
cache: composer

- name: Install dependencies
run: composer install
run: composer install --prefer-dist --no-interaction --no-progress

- name: Run tests
run: vendor/bin/phpunit
run: vendor/bin/pest --parallel --colors=never

- name: Run static analysis
run: vendor/bin/phpstan
run: vendor/bin/phpstan analyse --no-progress --configuration=phpstan.neon.dist

quality:
runs-on: ubuntu-latest
timeout-minutes: 15
name: Code Quality
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.5
extensions: json
coverage: none
tools: composer:v2
cache: composer

- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-progress

- name: Run Pint
run: vendor/bin/pint --test -v

- name: Run Rector (dry run)
run: vendor/bin/rector process --dry-run --no-progress-bar
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/.idea/
/.phpunit.result.cache
/.phpunit.cache
/cache.properties
/vendor/
composer.lock
116 changes: 111 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,44 @@
# 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-bits flags in a database, you will need to store it as UNSIGNED BIGINT in MySQL or an equivalent in your datastore.
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.

This package also comes with a trait which you can use to implement binary flags directly in your own class.

### Trait naming
For new code, prefer `Reinder83\BinaryFlags\Traits\InteractsWithNumericFlags`.
`Reinder83\BinaryFlags\Traits\BinaryFlags` remains available for backward compatibility.
For enum-based usage, use `Reinder83\BinaryFlags\BinaryEnumFlags` (which uses `Traits\InteractsWithEnumFlags`).


## Installing
To install this package simply run the following command in the root of your project.
```
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.

- 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.

### 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.

Using integer-compatible bits (`BIT_1` through `BIT_63`) prevents these issues and is the supported path for `v3.0.0`.

To prepare for `v3.0.0`, cast incoming values before using the API:

```php
$flags->setMask((int) $maskFromLegacySource);
$flags->addFlag((int) $incomingFlag);
```

See [UPGRADE-v3.md](UPGRADE-v3.md) for migration details.

## Methods
The following methods can be used:

Expand All @@ -25,6 +51,14 @@ This can be passed as first argument in the constructor.
##### getMask(): int
Retrieve the current mask.

When using `BinaryEnumFlags`, `getMask()` returns a `Mask` object instead.
Use `getMaskValue(): int` on enum-based flags if you need the numeric mask.

##### getMaskValue(): int
_Since: v2.1.0_ \
Returns the numeric mask value for storage/interoperability.
This method is only available on enum-backed flags (`BinaryEnumFlags`).

##### setOnModifyCallback(callable $onModify)
Set a callback function which is called when the mask changes.
This can be passed as second argument in the constructor.
Expand All @@ -47,7 +81,7 @@ When you want to match any of the given flags set `$checkAll` to `false`.

##### checkAnyFlag(int $mask): bool
_Since: v1.0.1_ \
For you convenient I've added an alias to checkFlag with `$checkAll` set to `false`.
For your convenience I've added an alias to checkFlag with `$checkAll` set to `false`.

##### count(): int
_Since: v1.2.0_ \
Expand Down Expand Up @@ -77,7 +111,7 @@ You can treat a BinaryFlags object as an iterable, where each iteration will ret

## Example usage

Below some example usage code
Below is some example usage code

##### Create classes
```php
Expand Down Expand Up @@ -137,6 +171,78 @@ var_export($exampleFlags->checkAnyFlag(ExampleFlags::FOO | ExampleFlags::BAZ));

```

##### Enum usage (optional)
```php
use Reinder83\BinaryFlags\BinaryEnumFlags;
use Reinder83\BinaryFlags\Mask;

enum Permission: int
{
case CanView = Bits::BIT_1;
case CanBook = Bits::BIT_2;
case CanCancel = Bits::BIT_3;
}

class PermissionFlags extends BinaryEnumFlags
{
protected static function getFlagEnumClass(): string
{
return Permission::class;
}
}

$flags = new PermissionFlags(Permission::CanView);
$flags->addFlag(Permission::CanBook);
$flags->addFlag(Mask::forEnum(Permission::class, Permission::CanCancel));

var_export($flags->checkFlag(Permission::CanBook));
// true

var_export($flags->getFlagNames());
// 'Can View, Can Book, Can Cancel'
```

##### Migrating from numeric flags to enum flags
```php
// Before (numeric PermissionFlags)
use Reinder83\BinaryFlags\BinaryFlags;

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(); // int

// After (enum PermissionFlags)
use Reinder83\BinaryFlags\BinaryEnumFlags;
use Reinder83\BinaryFlags\Mask;

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);

// Save as integer for storage/interop
$storedMask = $flags->getMaskValue();
```

##### Flag names example
_By default, the flag names are based on the constant names_
```php
Expand Down Expand Up @@ -228,7 +334,7 @@ class Test extends Model
$test = Test::find(1);

// do binary operations on the flags class as described earlier
$test->flags->checkFlag(ExampleFlag::FOO);
$test->flags->checkFlag(ExampleFlags::FOO);
```


Expand Down
66 changes: 66 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Release Notes - v2.1.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.

## 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`.

## 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.

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();
```
37 changes: 37 additions & 0 deletions UPGRADE-v3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Upgrade Guide for v3.0.0

## Summary
`v3.0.0` removes support for `float` values in masks and flags.
`v3.0.0` also removes `Bits::BIT_64`.

## What Changed
- `v2.x`: `int|float` accepted in mask/flag methods.
- `v3.0.0`: only `int` is accepted.
- `v2.x`: `Bits::BIT_64` exists but is not reliable in real bitwise usage.
- `v3.0.0`: `Bits::BIT_64` is removed. Use `BIT_1` through `BIT_63`.

## 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.
3. Ensure database or external sources provide integer-compatible values.

## Example
Before:
```php
$flags->setMask($legacyValue);
$flags->addFlag($legacyFlag);
```

After:
```php
$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`.

## 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.

Staying with integer-compatible bits prevents those runtime issues.
13 changes: 9 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,21 @@
},
"config": {
"platform": {
"php": "8.1"
"php": "8.2"
},
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"require": {
"php": "^8.1",
"php": "^8.2",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"phpstan/phpstan": "^1.10"
"phpstan/phpstan": "^1.10",
"pestphp/pest": "^2.36",
"laravel/pint": "^1",
"rector/rector": "^1"
},
"autoload-dev": {
"psr-4": {
Expand Down
Loading
Loading