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
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
tests:
runs-on: ubuntu-latest

strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4]

name: PHP ${{ matrix.php }}

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mbstring
coverage: none

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

- name: Check code style
run: vendor/bin/pint --test

- name: Run Rector
run: vendor/bin/rector --dry-run

- name: Run PHPStan
run: vendor/bin/phpstan

- name: Run tests
run: vendor/bin/pest --colors=always
24 changes: 22 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,32 @@ All notable changes to `bitmask` will be documented in this file.

## [Unreleased]

## [1.0.0] - 2025-01-11
> Major refactor: Unified API with enum support

### Changed
- Consolidated all bitmask variants into single immutable `Bitmask` class
- Factory methods: `make()`, `tiny()`, `small()`, `medium()` for different sizes
- Direct enum support in all methods (BackedEnum and UnitEnum)
- PHP 8.2+ required

### Added
- `Size` enum with computed `maxValue()`
- `maskValue()` helper for enum-to-int conversion
- UnitEnum support (position-inferred values)

### Removed
- `BaseBitmask`, `TinyBitmask`, `SmallBitmask`, `MediumBitmask` classes
- `MaxValue` enum (replaced by `Size::maxValue()`)
- `PowerOfTwo` value object (replaced by `isPowerOfTwo()` helper)

## [0.0.1] - 2024-05-03
> Initial release

### Added
- `Bitmask` class and its variants `TinyBitmask`, `SmallBitmask` and `MediumBitmask`
- Initial bitmask implementation
- Playground examples

[unreleased]: https://github.com/dotgksh/bitmask/compare/v0.0.1...HEAD
[unreleased]: https://github.com/dotgksh/bitmask/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/dotgksh/bitmask/compare/v0.0.1...v1.0.0
[0.0.1]: https://github.com/dotgksh/bitmask/releases/tag/v0.0.1
75 changes: 75 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

A PHP bitmask value object library (`gksh/bitmask`) that provides type-safe bitwise operations with direct enum support. Single immutable `Bitmask` class with factory methods for different sizes (8, 16, 24, 32-bit).

## Commands

```bash
composer install # Install dependencies
composer test # Run all checks (refactor, lint, types, unit tests)
composer test:unit # Run Pest unit tests only
composer test:types # Run PHPStan static analysis
composer test:lint # Check code style with Pint
composer test:refacto # Check Rector refactoring rules
composer lint # Fix code style with Pint
composer refacto # Apply Rector refactoring

# Run a single test file
./vendor/bin/pest tests/BitmaskSpec.php

# Run tests matching a pattern
./vendor/bin/pest --filter="sets flag"
```

## Architecture

### Core Class

`Bitmask` - Immutable (`final readonly`) value object with factory methods:
- `Bitmask::make($value, $size)` - 32-bit default
- `Bitmask::tiny($value)` - 8-bit (max 255)
- `Bitmask::small($value)` - 16-bit (max 65,535)
- `Bitmask::medium($value)` - 24-bit (max 16,777,215)

Operations: `set()`, `unset()`, `toggle()`, `has()`, `value()`, `size()`, `equals()`

### Key Components

- `Enums/Size` - Defines bit-widths (UInt8, UInt16, UInt24, UInt32) with computed `maxValue()`
- `Contracts/ValueObject` - Interface requiring `value()` and `equals()` methods
- `Support/helpers.php` - Contains `isPowerOfTwo()` and `maskValue()` helper functions

### Design Pattern

All flags must be powers of two (1, 2, 4, 8, 16...). The `isPowerOfTwo()` helper enforces this constraint. Bitmasks are immutable - operations return new instances.

Accepts `int`, `BackedEnum`, or `UnitEnum` in all methods:
- `BackedEnum`: Uses the enum's integer value directly
- `UnitEnum`: Infers value from position (1 << position)

## Testing

Uses Pest PHP with data providers. Tests use `Flag` (BackedEnum) and `UnitFlag` (UnitEnum) defined in `tests/`. Common patterns:

```php
// Parameterized tests with enum cases
it('sets flag', function (Flag $flag) {
// ...
})->with(Flag::cases());

// Testing boundary conditions
it('throws if out of bounds', function (Size $size, int $value) {
// ...
})->with([...])->throws(InvalidArgumentException::class);
```

## Code Quality

- PHPStan at max level with strict types
- Pint for PSR-12 code style
- Rector for automated refactoring
- PHP 8.2+ required
54 changes: 38 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ A simple way to use bitmask and bitwise operations in PHP.
composer require gksh/bitmask
```

## 🧪 Usage
## Usage
Streamline flag handling by encoding boolean options into simple integers through bitmasking.

> Please see [ide.php](./playground/ide.php) for full example and [playground](./playground) for more.

```php
use Gksh\Bitmask\Bitmask;

enum Panel: int
{
case Project = 1;
Expand All @@ -30,26 +32,18 @@ enum Panel: int
case Extensions = 8;
}

class Panels extends TinyBitmask
class Ide
{
public function isVisible(Panel $panel): bool
{
return $this->has($panel->value);
}
public Bitmask $panels;

public function togglePanel(Panel $panel): Panels
public function __construct()
{
return $this->toggle($panel->value);
$this->panels = Bitmask::tiny(); // 8-bit
}
}

class Ide
{
public Panels $panels;

public function togglePanel(Panel $panel): self
{
$this->panels->togglePanel($panel);
$this->panels = $this->panels->toggle($panel);

return $this;
}
Expand All @@ -59,8 +53,36 @@ $ide = (new Ide())
->togglePanel(Panel::Project)
->togglePanel(Panel::Terminal);

$ide->panels->isVisible(Panel::Terminal); // true
$ide->panels->isVisible(Panel::Extensions); // false
$ide->panels->has(Panel::Terminal); // true
$ide->panels->has(Panel::Extensions); // false
```

### Features

- **Immutable**: Operations return new instances, original unchanged
- **Enum support**: Pass `BackedEnum` directly — no `->value` extraction needed
- **Size variants**: `tiny()` (8-bit), `small()` (16-bit), `medium()` (24-bit), `make()` (32-bit default)

### Factory Methods

```php
Bitmask::make() // 32-bit (default)
Bitmask::tiny() // 8-bit, for TINYINT columns
Bitmask::small() // 16-bit, for SMALLINT columns
Bitmask::medium() // 24-bit, for MEDIUMINT columns
```

### Operations

```php
$mask = Bitmask::tiny()
->set(Flag::A) // Set a flag
->unset(Flag::B) // Unset a flag
->toggle(Flag::C); // Toggle a flag

$mask->has(Flag::A); // Check if flag is set
$mask->value(); // Get integer value
$mask->size(); // Get Size enum
```

## Testing
Expand Down
12 changes: 6 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
}
],
"require": {
"php": "^8.2.0"
"php": "^8.2"
},
"require-dev": {
"laravel/pint": "^1.13.7",
"pestphp/pest": "^2.28.1",
"phpstan/phpstan": "^1.10.50",
"rector/rector": "^0.19.8",
"symfony/var-dumper": "^6.4.0|^7.0.0"
"laravel/pint": "^1.18",
"pestphp/pest": "^3.0|^4.0",
"phpstan/phpstan": "^2.1",
"rector/rector": "^2.0",
"symfony/var-dumper": "^7.2"
},
"autoload": {
"psr-4": {
Expand Down
42 changes: 11 additions & 31 deletions playground/aaronfrancis.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

require __DIR__.'/../vendor/autoload.php';

use Gksh\Bitmask\TinyBitmask;
use Gksh\Bitmask\Bitmask;

enum FindMethod: int
{
Expand Down Expand Up @@ -36,35 +36,15 @@ public function methodName(): string
}
}

class FindAttempts extends TinyBitmask
{
public ?int $pid = null;

public function recordAttempt(FindMethod $method): FindAttempts
{
return $this->set($method->value);
}

public function resetAttempt(FindMethod $method): FindAttempts
{
return $this->unset($method->value);
}

public function hasAttempted(FindMethod $method): bool
{
return $this->has($method->value);
}
}

class Property // extends Model
{
public ?int $pid = null;

public FindAttempts $attempts;
public Bitmask $attempts;

public function __construct(?FindAttempts $attempts = null)
public function __construct(?Bitmask $attempts = null)
{
$this->attempts = $attempts ?? new FindAttempts();
$this->attempts = $attempts ?? Bitmask::tiny();
}

public function save(): void
Expand All @@ -73,7 +53,7 @@ public function save(): void
dump([
'property' => $this,
'attempted' => array_map(fn (FindMethod $method) => [
$method->name => $this->attempts->hasAttempted($method),
$method->name => $this->attempts->has($method),
], FindMethod::cases()),
]);
}
Expand All @@ -87,7 +67,7 @@ public function handle(): void
// Loop through the methods.
foreach (FindMethod::cases() as $method) {
// Skip ones we've already tried.
if ($property->attempts->hasAttempted($method)) {
if ($property->attempts->has($method)) {
continue;
}

Expand All @@ -98,7 +78,7 @@ public function handle(): void
// are currently disabled for any reason. We don't
// record an attempt, as we'll try those again.
if ($result !== false) {
$property->attempts->recordAttempt($method);
$property->attempts = $property->attempts->set($method);
}

// Stop processing once we find the PID.
Expand Down Expand Up @@ -128,11 +108,11 @@ private function queryProperties(): array
{
// Imagine this is querying the DB.
return [
new Property(FindAttempts::make()),
new Property(FindAttempts::make(FindMethod::ADDRESS->value)),
new Property(FindAttempts::make(FindMethod::PARCEL->value | FindMethod::STREET->value)),
new Property(Bitmask::tiny()),
new Property(Bitmask::tiny(FindMethod::ADDRESS)),
new Property(Bitmask::tiny(FindMethod::PARCEL->value | FindMethod::STREET->value)),
];
}
}

(new FindIds())->handle();
(new FindIds)->handle();
Loading
Loading