From e0779fb6105b69446cfc079405f29142b296bd32 Mon Sep 17 00:00:00 2001 From: mostafaznv Date: Sat, 9 Aug 2025 10:49:19 +0330 Subject: [PATCH 01/14] feat: add `AsBitmask` cast and bitmask support for eloquent models --- docs/casting.md | 95 +++- src/Laravel/Casts/AsBitmask.php | 86 +++ .../Bitmasks/BitmaskPreferenceEnum.php | 28 + .../Models/CastsBitmaskEnumsModel.php | 23 + tests/Unit/Laravel/Casts/AsBitmaskTest.php | 522 ++++++++++++++++++ 5 files changed, 753 insertions(+), 1 deletion(-) create mode 100644 src/Laravel/Casts/AsBitmask.php create mode 100644 tests/Fixtures/BackedEnums/Bitmasks/BitmaskPreferenceEnum.php create mode 100644 tests/Fixtures/Models/CastsBitmaskEnumsModel.php create mode 100644 tests/Unit/Laravel/Casts/AsBitmaskTest.php diff --git a/docs/casting.md b/docs/casting.md index e423e5f..72d8064 100644 --- a/docs/casting.md +++ b/docs/casting.md @@ -1,7 +1,7 @@ # Eloquent Attribute Casting Laravel supports casting backed enums out of the box, but what if you don't want -to use backed enums? This is where `CastsBasicEnumerations` comes in. +to use backed enums? This is where `CastsBasicEnumerations` and `AsBitmask` comes in. Note: for attribute casting with [State](state.md) see [here](#state). @@ -104,3 +104,96 @@ class YourModel extends Model } ``` + + +### Bitmask +When you need to store multiple enum values in a single database column, you can use [Bitmasks](bitmasks.md) together with the `AsBitmask` cast. This allows you to efficiently store and retrieve sets of enum values as a single integer. + +##### Enum: +First, define your enum and use the `Bitmasks` trait. Each case should have a unique power-of-two value. + +```php +namespace App\Enums; + +use Henzeb\Enumhancer\Concerns\Bitmasks; + +enum Preferences: string +{ + use Bitmasks; + + private const BIT_VALUES = true; + + case LogActivity = 1; + case PushNotification = 2; + case TwoFactorAuth = 4; + case DarkMode = 8; +} +``` + +##### Model: +In your Eloquent model, use the AsBitmask cast for the relevant attribute. This will handle conversion between the integer in the database and your enum values. + +```php +namespace App\Models; + +use Henzeb\Enumhancer\Laravel\Casts\AsBitmask; +use Illuminate\Database\Eloquent\Model; +use Henzeb\Enumhancer\Laravel\Concerns\CastsStatefulEnumerations; +use App\Enums\Preferences; + +class YourModel extends Model +{ + protected function casts(): array + { + return [ + 'preferences' => AsBitmask::class . ':' . Preferences::class, + ]; + } +} +``` + +#### Usage Examples + +##### Setting Values +You can assign enum values to the attribute in several ways: +```php +$model = new YourModel; + +// using the mask helper (stores 5: LogActivity + TwoFactorAuth) +$model->preferences = Preferences::mask( + Preferences::LogActivity, + Preferences::TwoFactorAuth, +); + +// using an array (also stores 5) +$model->preferences = [ + Preferences::LogActivity, + Preferences::TwoFactorAuth, +]; + +// single value (stores 1) +$model->preferences = Preferences::LogActivity; + +// using a comma-separated string (stores 11: DarkMode + LogActivity + PushNotification) +$model->preferences = 'DarkMode,LogActivity,PushNotification'; + +// no preferences (stores 0) +$model->preferences = []; +$model->preferences = ''; +$model->preferences = Preferences::mask(); +``` + +##### Retrieving Values +When you retrieve the model, the `preferences` attribute will be an instance of Bitmask: +```php +$model = YourModel::first(); +$model->preferences; // Bitmask instance with set values + +// check if a specific preference is set +$model->preferences->has(Preferences::LogActivity); // true or false + +// cet the raw integer value +$model->preferences->value(); // e.g. 5 for LogActivity and TwoFactorAuth +``` + +Tip: Using bitmasks is a space-efficient way to store multiple enum values in a single column, and the AsBitmask cast makes working with them in Eloquent models seamless. diff --git a/src/Laravel/Casts/AsBitmask.php b/src/Laravel/Casts/AsBitmask.php new file mode 100644 index 0000000..53a23b2 --- /dev/null +++ b/src/Laravel/Casts/AsBitmask.php @@ -0,0 +1,86 @@ + + */ + protected string $enum; + + + public function __construct(string $enum) + { + if (!enum_exists($enum)) { + throw new InvalidArgumentException("Enum class [$enum] does not exist."); + } + + $this->enum = $enum; + } + + + public function get($model, string $key, mixed $value, array $attributes): Bitmask + { + if ($value instanceof Bitmask) { + return $value; + } + + return $this->enum::fromMask((int)$value); + } + + public function set($model, string $key, mixed $value, array $attributes): int + { + if (is_array($value)) { + return $this->enum::mask(...$value)->value(); + } + + if (is_string($value)) { + $cases = explode(',', $value); + $cases = array_filter( + array_map('trim', $cases), + fn($case) => !empty($case) + ); + + return $this->enum::mask(...$cases)->value(); + } + + if ($value instanceof BackedEnum) { + return $this->enum::mask($value->name)->value(); + } + + if ($value instanceof Bitmask) { + return $value->value(); + } + + + throw new InvalidArgumentException('The value must be an array of enum cases, string, or single enum case.'); + } + + public function serialize($model, string $key, $value, array $attributes): string + { + if ($value instanceof Bitmask) { + $cases = $value->cases(); + $enabled = []; + + foreach ($cases as $case) { + $enabled[] = $case->name; + } + + + return implode(',', $enabled); + } + + + return (string)$value; + } +} diff --git a/tests/Fixtures/BackedEnums/Bitmasks/BitmaskPreferenceEnum.php b/tests/Fixtures/BackedEnums/Bitmasks/BitmaskPreferenceEnum.php new file mode 100644 index 0000000..a2afa03 --- /dev/null +++ b/tests/Fixtures/BackedEnums/Bitmasks/BitmaskPreferenceEnum.php @@ -0,0 +1,28 @@ +value(); + } +} diff --git a/tests/Fixtures/Models/CastsBitmaskEnumsModel.php b/tests/Fixtures/Models/CastsBitmaskEnumsModel.php new file mode 100644 index 0000000..c17df31 --- /dev/null +++ b/tests/Fixtures/Models/CastsBitmaskEnumsModel.php @@ -0,0 +1,23 @@ + AsBitmask::class . ':' . BitmaskPreferenceEnum::class, + ]; + } +} diff --git a/tests/Unit/Laravel/Casts/AsBitmaskTest.php b/tests/Unit/Laravel/Casts/AsBitmaskTest.php new file mode 100644 index 0000000..a059e27 --- /dev/null +++ b/tests/Unit/Laravel/Casts/AsBitmaskTest.php @@ -0,0 +1,522 @@ +set('database.default', 'sqlite'); + config()->set('database.connections.sqlite', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + } + + protected function setUpDatabaseRequirements(Closure $callback): void + { + $this->app['db']->connection() + ->getSchemaBuilder() + ->create('casts_bitmask_enums', function (Blueprint $table) { + $table->id(); + $table->unsignedInteger('preferences') + ->default(BitmaskPreferenceEnum::allOptionsEnabled()) + ->comment('bitmask preferences'); + + $table->timestamps(); + }); + } + + + # init + public function testEnumClassExistsThrowsExceptionForInvalidEnum(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Enum class [InvalidEnum] does not exist.'); + + new AsBitmask('InvalidEnum'); + } + + + # set + public function testSetReturnsCorrectMaskForEnumInput() + { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); + + $result = $cast->set($model, 'preferences', BitmaskPreferenceEnum::AutoUpdates, []); + $this->assertEquals(16, $result); + + $result = $cast->set($model, 'preferences', BitmaskPreferenceEnum::PushNotification, []); + $this->assertEquals(2, $result); + + $result = $cast->set($model, 'preferences', BitmaskPreferenceEnum::DarkMode, []); + $this->assertEquals(8, $result); + + $result = $cast->set($model, 'preferences', BitmaskPreferenceEnum::LogActivity, []); + $this->assertEquals(1, $result); + } + + public function testSetThrowsExceptionForInvalidEnumValueType() + { + $this->expectException(TypeError::class); + + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); + + $cast->set($model, 'preferences', BitmasksIncorrectIntEnum::Read, []); + } + + public function testSetReturnsCorrectMaskForBitmaskInput() + { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); + + + # test 1 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::AutoUpdates + ); + + $result = $cast->set($model, 'preferences', $value, []); + $this->assertEquals(16, $result); + + + # test 2 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::AutoUpdates, + BitmaskPreferenceEnum::PushNotification + ); + + $result = $cast->set($model, 'preferences', $value, []); + $this->assertEquals(18, $result); + + + # test 3 + $value = BitmaskPreferenceEnum::mask(); + $result = $cast->set($model, 'preferences', $value, []); + $this->assertEquals(0, $result); + } + + public function testSetThrowsExceptionForInvalidBitmaskValueType() + { + $this->expectException(TypeError::class); + + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); + + + $value = BitmasksIncorrectIntEnum::mask( + BitmasksIncorrectIntEnum::Read, + BitmasksIncorrectIntEnum::Execute, + ); + + $cast->set($model, 'preferences', $value, []); + } + + public function testSetReturnsCorrectMaskForStringInput() + { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); + + $result = $cast->set($model, 'preferences', 'AutoUpdates', []); + $this->assertEquals(16, $result); + + $result = $cast->set($model, 'preferences', 'PushNotification', []); + $this->assertEquals(2, $result); + + $result = $cast->set($model, 'preferences', 'DarkMode', []); + $this->assertEquals(8, $result); + + $result = $cast->set($model, 'preferences', 'LogActivity', []); + $this->assertEquals(1, $result); + + $result = $cast->set($model, 'preferences', 'LogActivity,DarkMode', []); + $this->assertEquals(9, $result); + + $result = $cast->set($model, 'preferences', 'LogActivity, PushNotification ,DarkMode', []); + $this->assertEquals(11, $result); + + $result = $cast->set($model, 'preferences', '', []); + $this->assertEquals(0, $result); + } + + public function testSetThrowsExceptionForInvalidStringValueType() + { + $this->expectException(TypeError::class); + + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); + + $cast->set($model, 'preferences', 'RANDOM_CASE', []); + } + + public function testSetReturnsCorrectMaskForArrayInput() + { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); + + $result = $cast->set($model, 'preferences', [BitmaskPreferenceEnum::DarkMode, BitmaskPreferenceEnum::LogActivity], []); + $this->assertEquals(9, $result); + + $result = $cast->set($model, 'preferences', [BitmaskPreferenceEnum::TwoFactorAuth], []); + $this->assertEquals(4, $result); + + $result = $cast->set($model, 'preferences', [BitmaskPreferenceEnum::TwoFactorAuth, BitmaskPreferenceEnum::DataExport, BitmaskPreferenceEnum::PushNotification], []); + $this->assertEquals(38, $result); + + $result = $cast->set($model, 'preferences', [], []); + $this->assertEquals(0, $result); + + $result = $cast->set($model, 'preferences', [BitmaskPreferenceEnum::TwoFactorAuth, 'AutoUpdates', BitmaskPreferenceEnum::PushNotification], []); + $this->assertEquals(22, $result); + + $result = $cast->set($model, 'preferences', ['DataExport', 'AutoUpdates'], []); + $this->assertEquals(48, $result); + } + + public function testSetThrowsExceptionForInvalidArrayValueType() + { + $this->expectException(TypeError::class); + + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); + + $cast->set($model, 'preferences', [BitmaskPreferenceEnum::DarkMode, BitmasksIncorrectIntEnum::Read], []); + } + + public function testSetThrowsExceptionForInvalidValueTypes() + { + $this->expectException(InvalidArgumentException::class); + + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); + + $cast->set($model, 'preferences', 3, []); + } + + + # model + public function testReturnsAllItemsEnabled(): void + { + DB::table('casts_bitmask_enums')->insert([ + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $record = CastsBitmaskEnumsModel::query()->first(); + + + $this->assertInstanceOf(Bitmask::class, $record->preferences); + + $this->assertEquals(63, $record->preferences->value()); + $this->assertTrue($record->preferences->has(BitmaskPreferenceEnum::LogActivity)); + $this->assertTrue($record->preferences->has(BitmaskPreferenceEnum::PushNotification)); + $this->assertTrue($record->preferences->has(BitmaskPreferenceEnum::PushNotification)); + $this->assertTrue($record->preferences->has(BitmaskPreferenceEnum::DarkMode)); + $this->assertTrue($record->preferences->has(BitmaskPreferenceEnum::AutoUpdates)); + $this->assertTrue($record->preferences->has(BitmaskPreferenceEnum::DataExport)); + } + + #[DataProvider('modelTestDataset')] + public function testStoresCorrectMaskToDatabase($dataset): void + { + $model = new CastsBitmaskEnumsModel; + $model->preferences = $dataset['preferences']; + $model->save(); + + # test 1 + $this->assertEquals($dataset['value'], $model->preferences->value()); + + foreach ($dataset['has'] as $has) { + $this->assertTrue($model->preferences->has($has)); + } + + $array = $model->toArray(); + $this->assertEquals($dataset['serialized'], $array['preferences']); + + + + # test 2 + $model = CastsBitmaskEnumsModel::find($model->id); + + $this->assertEquals($dataset['value'], $model->preferences->value()); + + foreach ($dataset['has'] as $has) { + $this->assertTrue($model->preferences->has($has)); + } + + $array = $model->toArray(); + $this->assertEquals($dataset['serialized'], $array['preferences']); + } + + public function testBitmaskOperations(): void + { + $model = new CastsBitmaskEnumsModel; + $model->preferences = [BitmaskPreferenceEnum::DarkMode, BitmaskPreferenceEnum::AutoUpdates]; + $model->save(); + + + # test 1 + $this->assertTrue($model->preferences->has(BitmaskPreferenceEnum::DarkMode)); + $this->assertTrue($model->preferences->has(BitmaskPreferenceEnum::AutoUpdates)); + + + # test 2 + $model->preferences = $model->preferences->set(BitmaskPreferenceEnum::LogActivity); + $model->preferences = $model->preferences->unset(BitmaskPreferenceEnum::AutoUpdates); + $model->save(); + + $this->assertTrue($model->preferences->has(BitmaskPreferenceEnum::LogActivity)); + $this->assertTrue($model->preferences->has(BitmaskPreferenceEnum::DarkMode)); + $this->assertFalse($model->preferences->has(BitmaskPreferenceEnum::AutoUpdates)); + + + # test 3 + $model->preferences = $model->preferences->toggle(BitmaskPreferenceEnum::AutoUpdates); + $model->preferences = $model->preferences->toggle(BitmaskPreferenceEnum::LogActivity); + $model->save(); + + $this->assertFalse($model->preferences->has(BitmaskPreferenceEnum::LogActivity)); + $this->assertTrue($model->preferences->has(BitmaskPreferenceEnum::DarkMode)); + $this->assertTrue($model->preferences->has(BitmaskPreferenceEnum::AutoUpdates)); + + + # test 4 + $model = CastsBitmaskEnumsModel::find($model->id); + + $this->assertTrue($model->preferences->has(BitmaskPreferenceEnum::DarkMode)); + $this->assertTrue($model->preferences->has(BitmaskPreferenceEnum::AutoUpdates)); + $this->assertFalse($model->preferences->has(BitmaskPreferenceEnum::LogActivity)); + $this->assertEquals(24, $model->preferences->value()); + } + + + # get + public function testGetReturnsBitmaskForValidValue() + { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); + + + # test 1 + $result = $cast->get($model, 'preferences', 16, []); + $this->assertEquals(16, $result->value()); + $this->assertTrue($result->has(BitmaskPreferenceEnum::AutoUpdates)); + + + # test 2 + $result = $cast->get($model, 'preferences', 17, []); + $this->assertEquals(17, $result->value()); + $this->assertTrue($result->has(BitmaskPreferenceEnum::LogActivity)); + $this->assertTrue($result->has(BitmaskPreferenceEnum::AutoUpdates)); + + + # test 3 + $result = $cast->get($model, 'preferences', '3', []); + $this->assertEquals(3, $result->value()); + $this->assertTrue($result->has(BitmaskPreferenceEnum::LogActivity)); + $this->assertTrue($result->has(BitmaskPreferenceEnum::PushNotification)); + + + # test 4 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, + BitmaskPreferenceEnum::DarkMode, + ); + + $result = $cast->get($model, 'preferences', $value, []); + $this->assertEquals(9, $result->value()); + $this->assertEquals($result, $value); + $this->assertTrue($result->has(BitmaskPreferenceEnum::LogActivity)); + $this->assertTrue($result->has(BitmaskPreferenceEnum::DarkMode)); + } + + + # serialize + public function testSerializeReturnsEmptyStringForNonBitmaskValues() + { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); + + $result = $cast->serialize($model, 'preferences', '', []); + + $this->assertEquals('', $result); + } + + public function testSerializeReturnsCorrectStringForBitmaskValues() + { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); + + + # test 1 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, + BitmaskPreferenceEnum::DarkMode, + ); + + $result = $cast->serialize($model, 'preferences', $value, []); + $this->assertEquals('LogActivity,DarkMode', $result); + + + # test 2 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::AutoUpdates, + ); + + $result = $cast->serialize($model, 'preferences', $value, []); + $this->assertEquals('AutoUpdates', $result); + + + # test 3 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, + BitmaskPreferenceEnum::DarkMode, + BitmaskPreferenceEnum::DataExport, + ); + + $result = $cast->serialize($model, 'preferences', $value, []); + $this->assertEquals('LogActivity,DarkMode,DataExport', $result); + } + + + # datasets + public static function modelTestDataset(): array + { + return [ + [ + # dataset 1 + [ + 'preferences' => BitmaskPreferenceEnum::AutoUpdates, + 'value' => 16, + 'serialized' => 'AutoUpdates', + 'has' => [ + BitmaskPreferenceEnum::AutoUpdates + ] + ], + + # dataset 2 + [ + 'preferences' => 'DarkMode', + 'value' => 8, + 'serialized' => 'DarkMode', + 'has' => [ + BitmaskPreferenceEnum::DarkMode + ] + ], + + # dataset 3 + [ + 'preferences' => 'DataExport,TwoFactorAuth,PushNotification', + 'value' => 38, + 'serialized' => 'DataExport,TwoFactorAuth,PushNotification', + 'has' => [ + BitmaskPreferenceEnum::DataExport, + BitmaskPreferenceEnum::TwoFactorAuth, + BitmaskPreferenceEnum::PushNotification, + ] + ], + + # dataset 4 + [ + 'preferences' => ' DataExport, TwoFactorAuth ,DarkMode ', + 'value' => 44, + 'serialized' => 'DataExport,TwoFactorAuth,DarkMode', + 'has' => [ + BitmaskPreferenceEnum::DataExport, + BitmaskPreferenceEnum::TwoFactorAuth, + BitmaskPreferenceEnum::DarkMode, + ] + ], + + # dataset 5 + [ + 'preferences' => '', + 'value' => 0, + 'serialized' => '', + 'has' => [] + ], + + # dataset 6 + [ + 'preferences' => [ + BitmaskPreferenceEnum::DarkMode, + BitmaskPreferenceEnum::LogActivity, + ], + 'value' => 9, + 'serialized' => 'DarkMode,LogActivity', + 'has' => [ + BitmaskPreferenceEnum::DarkMode, + BitmaskPreferenceEnum::LogActivity, + ] + ], + + # dataset 7 + [ + 'preferences' => BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::TwoFactorAuth, + BitmaskPreferenceEnum::LogActivity, + ), + 'value' => 5, + 'serialized' => 'TwoFactorAuth,LogActivity', + 'has' => [ + BitmaskPreferenceEnum::TwoFactorAuth, + BitmaskPreferenceEnum::LogActivity, + ] + ], + + # dataset 8 + [ + 'preferences' => BitmaskPreferenceEnum::mask(), + 'value' => 0, + 'serialized' => '', + 'has' => [] + ], + + # dataset 8 + [ + 'preferences' => [], + 'value' => 0, + 'serialized' => '', + 'has' => [] + ], + ] + ]; + } +} From ce0c827d4db238bea87edb3c17ca65d4a4695791 Mon Sep 17 00:00:00 2001 From: mostafaznv Date: Sat, 9 Aug 2025 10:55:12 +0330 Subject: [PATCH 02/14] docs: enhance bitmask documentation with laravel casting example --- docs/bitmasks.md | 3 +++ docs/casting.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/bitmasks.md b/docs/bitmasks.md index a022fcd..15be405 100644 --- a/docs/bitmasks.md +++ b/docs/bitmasks.md @@ -336,3 +336,6 @@ Returns the class name of the enum the Bitmask belongs to. Permission::mask()->forEnum(); // returns Permission::class PermissionInt::mask()->forEnum(); // returns PermissionInt::class ```` + +### Laravel Casting +For details on integrating bitmask enums with Eloquent models, see [Laravel Casting](casting.md#bitmask). diff --git a/docs/casting.md b/docs/casting.md index 72d8064..c2367d5 100644 --- a/docs/casting.md +++ b/docs/casting.md @@ -196,4 +196,4 @@ $model->preferences->has(Preferences::LogActivity); // true or false $model->preferences->value(); // e.g. 5 for LogActivity and TwoFactorAuth ``` -Tip: Using bitmasks is a space-efficient way to store multiple enum values in a single column, and the AsBitmask cast makes working with them in Eloquent models seamless. +> Using bitmasks is a space-efficient way to store multiple enum values in a single column, and the AsBitmask cast makes working with them in Eloquent models seamless. From 2a58da2ba49a44f13f94db9248c4509993445857 Mon Sep 17 00:00:00 2001 From: mostafaznv Date: Sat, 9 Aug 2025 11:08:56 +0330 Subject: [PATCH 03/14] refactor: use the `$casts` property instead of the `casts` method, preventing tests from failing on older laravel versions --- tests/Fixtures/Models/CastsBitmaskEnumsModel.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/Fixtures/Models/CastsBitmaskEnumsModel.php b/tests/Fixtures/Models/CastsBitmaskEnumsModel.php index c17df31..77e42d6 100644 --- a/tests/Fixtures/Models/CastsBitmaskEnumsModel.php +++ b/tests/Fixtures/Models/CastsBitmaskEnumsModel.php @@ -14,10 +14,7 @@ class CastsBitmaskEnumsModel extends Model # casts - protected function casts(): array - { - return [ - 'preferences' => AsBitmask::class . ':' . BitmaskPreferenceEnum::class, - ]; - } + protected $casts = [ + 'preferences' => AsBitmask::class . ':' . BitmaskPreferenceEnum::class, + ]; } From 84fb581a24f741e7a70723ffd7fafc372156c309 Mon Sep 17 00:00:00 2001 From: mostafaznv Date: Sat, 9 Aug 2025 12:14:12 +0330 Subject: [PATCH 04/14] ci: update test matrix to include php 8.5 and laravel 10-12 support --- .github/workflows/tests.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3b5f60e..2134008 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,15 +13,23 @@ on: jobs: tests: - name: PHP ${{ matrix.php }} - ${{ matrix.stability }} + name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.dependency-version }} runs-on: ubuntu-latest env: CC_TOKEN: ${{ secrets.CODECLIMATE_TOKEN }} strategy: fail-fast: true matrix: - php: [ '8.1','8.4' ] - stability: [ prefer-lowest, prefer-stable ] + php: ['8.1', '8.2', '8.3', '8.4', '8.5'] + laravel: [10.*, 11.*, 12.*] + dependency-version: [ prefer-stable ] + include: + - laravel: 10.* + testbench: 8.* + - laravel: 11.* + testbench: 9.* + - laravel: 12.* + testbench: 10.* steps: - name: Checkout Code @@ -55,7 +63,9 @@ jobs: with: timeout_minutes: 5 max_attempts: 5 - command: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress --quiet + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update --quiet + composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest - name: Lint PHP files run: find src/ tests/ -name "*.php" -exec php -l {} \; From 4a29ce3ad05787405c15137ef8603c999b5b1a84 Mon Sep 17 00:00:00 2001 From: mostafaznv Date: Sat, 9 Aug 2025 12:17:11 +0330 Subject: [PATCH 05/14] ci: update test matrix to include php 8.5 and laravel 10-12 support --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2134008..b5deb92 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -63,7 +63,7 @@ jobs: with: timeout_minutes: 5 max_attempts: 5 - run: | + command: | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update --quiet composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest From 5fda6698b5d364bd64b699b87715ec1a88504604 Mon Sep 17 00:00:00 2001 From: mostafaznv Date: Sat, 9 Aug 2025 12:26:37 +0330 Subject: [PATCH 06/14] ci: update test matrix to exclude incompatible php and laravel combinations --- .github/workflows/tests.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b5deb92..d79073b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,6 +30,13 @@ jobs: testbench: 9.* - laravel: 12.* testbench: 10.* + exclude: + - laravel: 11.* + php: 8.1 + - laravel: 12.* + php: 8.0 + - laravel: 12.* + php: 8.1 steps: - name: Checkout Code From 8a8b426773185c5ed00a418a79afab749ca34fa2 Mon Sep 17 00:00:00 2001 From: mostafaznv Date: Sat, 9 Aug 2025 12:32:04 +0330 Subject: [PATCH 07/14] ci: remove php 8.5 from test matrix --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d79073b..e521b8d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,8 +20,8 @@ jobs: strategy: fail-fast: true matrix: - php: ['8.1', '8.2', '8.3', '8.4', '8.5'] - laravel: [10.*, 11.*, 12.*] + php: ['8.1', '8.2', '8.3', '8.4'] + laravel: ['10.*', '11.*', '12.*'] dependency-version: [ prefer-stable ] include: - laravel: 10.* From 4f529765c1788a6211250b380ea7357c115f04de Mon Sep 17 00:00:00 2001 From: mostafaznv Date: Sat, 9 Aug 2025 15:07:54 +0330 Subject: [PATCH 08/14] feat: add `InteractsWithBitmask` trait for bitmask query scopes in eloquent --- README.md | 1 + docs/bitmasks.md | 3 + docs/laravel.eloquent.md | 62 +++ src/Laravel/Traits/InteractsWithBitmask.php | 45 +++ .../Models/CastsBitmaskEnumsModel.php | 3 + .../Traits/InteractsWithBitmaskTest.php | 363 ++++++++++++++++++ 6 files changed, 477 insertions(+) create mode 100644 docs/laravel.eloquent.md create mode 100644 src/Laravel/Traits/InteractsWithBitmask.php create mode 100644 tests/Unit/Laravel/Traits/InteractsWithBitmaskTest.php diff --git a/README.md b/README.md index 5d47247..e95b134 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ implemented the methods of `Getters`, `Extractor` and `Reporters`. - [FormRequest](docs/formrequests.md) - [Implicit (basic) enum binding](docs/binding.md) - [Validation](docs/laravel.validation.md) +- [Eloquent](docs/laravel.eloquent.md) ### Laravel's auto-discovery diff --git a/docs/bitmasks.md b/docs/bitmasks.md index 15be405..2972542 100644 --- a/docs/bitmasks.md +++ b/docs/bitmasks.md @@ -339,3 +339,6 @@ PermissionInt::mask()->forEnum(); // returns PermissionInt::class ### Laravel Casting For details on integrating bitmask enums with Eloquent models, see [Laravel Casting](casting.md#bitmask). + +### Laravel Eloquent +For details on using bitmask enums with Eloquent query scopes, see [Laravel Eloquent](laravel.eloquent.md#bitmask-query-scopes). diff --git a/docs/laravel.eloquent.md b/docs/laravel.eloquent.md new file mode 100644 index 0000000..7bdf903 --- /dev/null +++ b/docs/laravel.eloquent.md @@ -0,0 +1,62 @@ +# Laravel Eloquent + + + +## Bitmask Query Scopes +The `InteractsWithBitmask` trait adds **expressive, reusable query scopes** to your Eloquent models for working with **bitmask columns**. +It simplifies filtering records based on bitwise values without manually writing bitwise SQL conditions. + +### Features +- **whereBitmask** – Adds a `WHERE` condition to match records where the given bitmask **contains all bits** from the provided value. +- **orWhereBitmask** – Adds an `OR WHERE` condition for the same logic. +- Works with both **integer values** and **Bitmask enum instances**. + + +### Configuration +Apply the `InteractsWithBitmask` trait to your model, and set up the cast for your bitmask column. +```php +use Henzeb\Enumhancer\Laravel\Casts\AsBitmask; +use Henzeb\Enumhancer\Laravel\Traits\InteractsWithBitmask; +use Henzeb\Enumhancer\Tests\Fixtures\BackedEnums\Bitmasks\BitmaskPreferenceEnum; +use Illuminate\Database\Eloquent\Model; + + +class MyModel extends Model +{ + use InteractsWithBitmask; + + + protected $casts = [ + 'preferences' => AsBitmask::class . ':' . BitmaskPreferenceEnum::class, + ]; +} +``` + +### Usage Examples + +Using an Integer Value +```php +# match where 'preferences' has all bits in 5 set +MyModel::whereBitmask('preferences', 5)->get(); + +# same but using or condition +MyModel::orWhereBitmask('preferences', 5)->get(); +``` + +Using a Bitmask Enum Instance +```php +$value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::AutoUpdates, + BitmaskPreferenceEnum::DarkMode, +); + +# match records where both flags are set +MyModel::whereBitmask('preferences', $value)->get(); + +# or condition +MyModel::orWhereBitmask('preferences', $value)->get(); +``` + + +> [!NOTE] +> If the value is `0`, the query matches **only** records where the column is exactly `zero`, ensuring no bits are set. diff --git a/src/Laravel/Traits/InteractsWithBitmask.php b/src/Laravel/Traits/InteractsWithBitmask.php new file mode 100644 index 0000000..fdc28b5 --- /dev/null +++ b/src/Laravel/Traits/InteractsWithBitmask.php @@ -0,0 +1,45 @@ +value(); + } + + if ($value === 0) { + $query->where($column, 0); + + return; + } + + $query->whereRaw("`$column` & ? = ?", [ + $value, $value + ]); + } + + public function scopeOrWhereBitmask(Builder $query, string $column, Bitmask|int $value): void + { + if ($value instanceof Bitmask) { + $value = $value->value(); + } + + if ($value === 0) { + $query->orWhere($column, 0); + + return; + } + + + $query->orWhereRaw("`$column` & ? = ?", [ + $value, $value + ]); + } +} diff --git a/tests/Fixtures/Models/CastsBitmaskEnumsModel.php b/tests/Fixtures/Models/CastsBitmaskEnumsModel.php index 77e42d6..226d4fa 100644 --- a/tests/Fixtures/Models/CastsBitmaskEnumsModel.php +++ b/tests/Fixtures/Models/CastsBitmaskEnumsModel.php @@ -3,12 +3,15 @@ namespace Henzeb\Enumhancer\Tests\Fixtures\Models; use Henzeb\Enumhancer\Laravel\Casts\AsBitmask; +use Henzeb\Enumhancer\Laravel\Traits\InteractsWithBitmask; use Henzeb\Enumhancer\Tests\Fixtures\BackedEnums\Bitmasks\BitmaskPreferenceEnum; use Illuminate\Database\Eloquent\Model; class CastsBitmaskEnumsModel extends Model { + use InteractsWithBitmask; + protected $table = 'casts_bitmask_enums'; protected $guarded = []; diff --git a/tests/Unit/Laravel/Traits/InteractsWithBitmaskTest.php b/tests/Unit/Laravel/Traits/InteractsWithBitmaskTest.php new file mode 100644 index 0000000..9941aeb --- /dev/null +++ b/tests/Unit/Laravel/Traits/InteractsWithBitmaskTest.php @@ -0,0 +1,363 @@ +set('database.default', 'sqlite'); + config()->set('database.connections.sqlite', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + } + + protected function setUpDatabaseRequirements(Closure $callback): void + { + $this->app['db']->connection() + ->getSchemaBuilder() + ->create('casts_bitmask_enums', function (Blueprint $table) { + $table->id(); + $table->unsignedInteger('preferences') + ->default(BitmaskPreferenceEnum::allOptionsEnabled()) + ->comment('bitmask preferences'); + + $table->timestamps(); + }); + + + $this->record1 = CastsBitmaskEnumsModel::query()->create([ + 'preferences' => [ + BitmaskPreferenceEnum::AutoUpdates, + BitmaskPreferenceEnum::DarkMode, + ] + ]); + + $this->record2 = CastsBitmaskEnumsModel::query()->create([ + 'preferences' => BitmaskPreferenceEnum::DataExport + ]); + + $this->record3 = CastsBitmaskEnumsModel::query()->create([ + 'preferences' => 'TwoFactorAuth,DarkMode,PushNotification' + ]); + + $this->record4 = CastsBitmaskEnumsModel::query()->create([ + 'preferences' => [] + ]); + } + + + # where + public function testWhereBitmaskAppliesCorrectConditionForIntegerValue(): void + { + # test 1 + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', 8)->get(); + $this->assertCount(2, $results); + $this->assertEquals([$this->record1->id, $this->record3->id], $results->pluck('id')->toArray()); + + + # test 2 + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', 32)->get(); + $this->assertCount(1, $results); + $this->assertEquals($this->record2->id, $results->first()->id); + + + # test 3 + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', 24)->get(); + $this->assertCount(1, $results); + $this->assertEquals($this->record1->id, $results->first()->id); + + + # test 4 + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', 1)->get(); + $this->assertCount(0, $results); + + + # test 4 + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', 0)->get(); + $this->assertCount(1, $results); + $this->assertEquals($this->record4->id, $results->first()->id); + } + + public function testWhereBitmaskAppliesCorrectConditionForBitmaskValue(): void + { + # test 1 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::DarkMode + ); + + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', $value)->get(); + $this->assertCount(2, $results); + $this->assertEquals([$this->record1->id, $this->record3->id], $results->pluck('id')->toArray()); + + + # test 2 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::PushNotification + ); + + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', $value)->get(); + $this->assertCount(1, $results); + $this->assertEquals($this->record3->id, $results->first()->id); + + + # test 3 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::PushNotification, + BitmaskPreferenceEnum::DarkMode, + BitmaskPreferenceEnum::TwoFactorAuth, + ); + + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', $value)->get(); + $this->assertCount(1, $results); + $this->assertEquals($this->record3->id, $results->first()->id); + + + # test 4 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::PushNotification, + BitmaskPreferenceEnum::DarkMode, + BitmaskPreferenceEnum::AutoUpdates, + ); + + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', $value)->get(); + $this->assertCount(0, $results); + + + # test 5 + $value = BitmaskPreferenceEnum::mask(); + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', $value)->get(); + $this->assertCount(1, $results); + $this->assertEquals($this->record4->id, $results->first()->id); + } + + + # or-where + public function testOrWhereBitmaskAppliesCorrectConditionForIntegerValue(): void + { + # test 1 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask('preferences', 1) + ->orWhereBitmask('preferences', 8) + ->get(); + + $this->assertCount(2, $results); + $this->assertEquals([$this->record1->id, $this->record3->id], $results->pluck('id')->toArray()); + + + # test 2 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask('preferences', 3) + ->orWhereBitmask('preferences', 32) + ->get(); + + $this->assertCount(1, $results); + $this->assertEquals($this->record2->id, $results->first()->id); + + + # test 3 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask('preferences', 1) + ->orWhereBitmask('preferences', 24) + ->orWhereBitmask('preferences', 32) + ->get(); + + $this->assertCount(2, $results); + $this->assertEquals([$this->record1->id, $this->record2->id], $results->pluck('id')->toArray()); + + + # test 4 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask('preferences', 1) + ->orWhereBitmask('preferences', 3) + ->get(); + + $this->assertCount(0, $results); + + + # test 5 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask('preferences', 1) + ->orWhereBitmask('preferences', 0) + ->get(); + + $this->assertCount(1, $results); + $this->assertEquals($this->record4->id, $results->first()->id); + + + # test 6 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask('preferences', 1) + ->orWhereBitmask('preferences', 24) + ->orWhereBitmask('preferences', 32) + ->orWhereBitmask('preferences', 8) + ->get(); + + $this->assertCount(3, $results); + $this->assertEquals([$this->record1->id, $this->record2->id, $this->record3->id], $results->pluck('id')->toArray()); + + } + + public function testOrWhereBitmaskAppliesCorrectConditionForBitmaskValue(): void + { + # test 1 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity + ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::DarkMode + ) + ) + ->get(); + + $this->assertCount(2, $results); + $this->assertEquals([$this->record1->id, $this->record3->id], $results->pluck('id')->toArray()); + + + # test 2 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, + BitmaskPreferenceEnum::PushNotification, + ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::DataExport + ) + ) + ->get(); + + $this->assertCount(1, $results); + $this->assertEquals($this->record2->id, $results->first()->id); + + + # test 3 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, + ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::AutoUpdates, + BitmaskPreferenceEnum::DarkMode, + ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::DataExport + ) + ) + ->get(); + + $this->assertCount(2, $results); + $this->assertEquals([$this->record1->id, $this->record2->id], $results->pluck('id')->toArray()); + + + # test 4 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, + ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, + BitmaskPreferenceEnum::PushNotification, + ) + ) + ->get(); + + $this->assertCount(0, $results); + + + # test 5 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, + ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask() + ) + ->get(); + + $this->assertCount(1, $results); + $this->assertEquals($this->record4->id, $results->first()->id); + + + # test 6 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, + ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::DarkMode, + BitmaskPreferenceEnum::AutoUpdates, + ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::DataExport + ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::DarkMode + ) + ) + ->get(); + + $this->assertCount(3, $results); + $this->assertEquals([$this->record1->id, $this->record2->id, $this->record3->id], $results->pluck('id')->toArray()); + } +} From 66cc322997796f288da6bbd6f209759a7dc477b8 Mon Sep 17 00:00:00 2001 From: mostafaznv Date: Sun, 10 Aug 2025 02:32:04 +0330 Subject: [PATCH 09/14] test: refactor `AsBitmask` tests to use `Pest` --- tests/Unit/Laravel/Casts/AsBitmaskTest.php | 782 ++++++++++----------- 1 file changed, 383 insertions(+), 399 deletions(-) diff --git a/tests/Unit/Laravel/Casts/AsBitmaskTest.php b/tests/Unit/Laravel/Casts/AsBitmaskTest.php index a059e27..c309638 100644 --- a/tests/Unit/Laravel/Casts/AsBitmaskTest.php +++ b/tests/Unit/Laravel/Casts/AsBitmaskTest.php @@ -2,521 +2,505 @@ namespace Henzeb\Enumhancer\Tests\Unit\Laravel\Casts; -use Closure; use Henzeb\Enumhancer\Helpers\Bitmasks\Bitmask; use Henzeb\Enumhancer\Laravel\Casts\AsBitmask; -use Henzeb\Enumhancer\Laravel\Providers\EnumhancerServiceProvider; use Henzeb\Enumhancer\Tests\Fixtures\BackedEnums\Bitmasks\BitmaskPreferenceEnum; use Henzeb\Enumhancer\Tests\Fixtures\BackedEnums\Bitmasks\BitmasksIncorrectIntEnum; use Henzeb\Enumhancer\Tests\Fixtures\Models\CastsBitmaskEnumsModel; +use Henzeb\Enumhancer\Tests\TestCase; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; use InvalidArgumentException; -use Orchestra\Testbench\TestCase; -use PHPUnit\Framework\Attributes\DataProvider; use TypeError; -class AsBitmaskTest extends TestCase -{ - protected function getPackageProviders($app): array - { - return [ - EnumhancerServiceProvider::class - ]; - } +uses(TestCase::class)->in('Unit'); - protected function getEnvironmentSetUp($app): void - { - config()->set('database.default', 'sqlite'); - config()->set('database.connections.sqlite', [ - 'driver' => 'sqlite', - 'database' => ':memory:', - 'prefix' => '', - ]); - } +beforeEach(function () { + $this->attr = 'preferences'; - protected function setUpDatabaseRequirements(Closure $callback): void - { - $this->app['db']->connection() - ->getSchemaBuilder() - ->create('casts_bitmask_enums', function (Blueprint $table) { - $table->id(); - $table->unsignedInteger('preferences') - ->default(BitmaskPreferenceEnum::allOptionsEnabled()) - ->comment('bitmask preferences'); - - $table->timestamps(); - }); - } + $this->app['db']->connection() + ->getSchemaBuilder() + ->create('casts_bitmask_enums', function (Blueprint $table) { + $table->id(); + $table->unsignedInteger($this->attr) + ->default(BitmaskPreferenceEnum::allOptionsEnabled()) + ->comment('bitmask preferences'); + $table->timestamps(); + }); +}); - # init - public function testEnumClassExistsThrowsExceptionForInvalidEnum(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Enum class [InvalidEnum] does not exist.'); - new AsBitmask('InvalidEnum'); - } +it('throws exception for invalid enums', function () { + new AsBitmask('InvalidEnum'); +})->throws(InvalidArgumentException::class, 'Enum class [InvalidEnum] does not exist.'); - # set - public function testSetReturnsCorrectMaskForEnumInput() - { - $model = new CastsBitmaskEnumsModel; - $enum = BitmaskPreferenceEnum::class; - $cast = new AsBitmask($enum); - $result = $cast->set($model, 'preferences', BitmaskPreferenceEnum::AutoUpdates, []); - $this->assertEquals(16, $result); +# set +test('`set` method returns correct mask for enum input', function () { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); - $result = $cast->set($model, 'preferences', BitmaskPreferenceEnum::PushNotification, []); - $this->assertEquals(2, $result); + expect($cast->set($model, $this->attr, BitmaskPreferenceEnum::AutoUpdates, [])) + ->toBe(16) + ->and($cast->set($model, $this->attr, BitmaskPreferenceEnum::PushNotification, [])) + ->toBe(2) + ->and($cast->set($model, $this->attr, BitmaskPreferenceEnum::DarkMode, [])) + ->toBe(8) + ->and($cast->set($model, $this->attr, BitmaskPreferenceEnum::LogActivity, [])) + ->toBe(1); +}); - $result = $cast->set($model, 'preferences', BitmaskPreferenceEnum::DarkMode, []); - $this->assertEquals(8, $result); +test('`set` method throws exception for invalid enum value type', function () { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); - $result = $cast->set($model, 'preferences', BitmaskPreferenceEnum::LogActivity, []); - $this->assertEquals(1, $result); - } + $cast->set($model, $this->attr, BitmasksIncorrectIntEnum::Read, []); - public function testSetThrowsExceptionForInvalidEnumValueType() - { - $this->expectException(TypeError::class); +})->throws(TypeError::class, 'This method can only be used with an enum'); - $model = new CastsBitmaskEnumsModel; - $enum = BitmaskPreferenceEnum::class; - $cast = new AsBitmask($enum); +test('`set` method returns correct mask for bitmask input', function () { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); - $cast->set($model, 'preferences', BitmasksIncorrectIntEnum::Read, []); - } + # test 1 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::AutoUpdates + ); - public function testSetReturnsCorrectMaskForBitmaskInput() - { - $model = new CastsBitmaskEnumsModel; - $enum = BitmaskPreferenceEnum::class; - $cast = new AsBitmask($enum); + $result = $cast->set($model, $this->attr, $value, []); + expect($result)->toBe(16); - # test 1 - $value = BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::AutoUpdates - ); + # test 2 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::AutoUpdates, + BitmaskPreferenceEnum::PushNotification + ); - $result = $cast->set($model, 'preferences', $value, []); - $this->assertEquals(16, $result); + $result = $cast->set($model, $this->attr, $value, []); + expect($result)->toBe(18); - # test 2 - $value = BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::AutoUpdates, - BitmaskPreferenceEnum::PushNotification - ); + # test 3 + $value = BitmaskPreferenceEnum::mask(); + $result = $cast->set($model, $this->attr, $value, []); + expect($result)->toBe(0); +}); - $result = $cast->set($model, 'preferences', $value, []); - $this->assertEquals(18, $result); +test('`set` method throws exception for invalid bitmask value type', function () { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); + $value = BitmasksIncorrectIntEnum::mask( + BitmasksIncorrectIntEnum::Read, + BitmasksIncorrectIntEnum::Execute, + ); - # test 3 - $value = BitmaskPreferenceEnum::mask(); - $result = $cast->set($model, 'preferences', $value, []); - $this->assertEquals(0, $result); - } + $cast->set($model, $this->attr, $value, []); - public function testSetThrowsExceptionForInvalidBitmaskValueType() - { - $this->expectException(TypeError::class); +})->throws(TypeError::class, 'Henzeb\Enumhancer\Tests\Fixtures\BackedEnums\Bitmasks\BitmasksIncorrectIntEnum::Execute is not a valid bit value'); - $model = new CastsBitmaskEnumsModel; - $enum = BitmaskPreferenceEnum::class; - $cast = new AsBitmask($enum); +test('`set` method returns correct mask for string input', function () { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); - $value = BitmasksIncorrectIntEnum::mask( - BitmasksIncorrectIntEnum::Read, - BitmasksIncorrectIntEnum::Execute, - ); + # test 1 + $result = $cast->set($model, $this->attr, 'AutoUpdates', []); + expect($result)->toBe(16); - $cast->set($model, 'preferences', $value, []); - } - public function testSetReturnsCorrectMaskForStringInput() - { - $model = new CastsBitmaskEnumsModel; - $enum = BitmaskPreferenceEnum::class; - $cast = new AsBitmask($enum); + # test 2 + $result = $cast->set($model, $this->attr, 'PushNotification', []); + expect($result)->toBe(2); - $result = $cast->set($model, 'preferences', 'AutoUpdates', []); - $this->assertEquals(16, $result); - $result = $cast->set($model, 'preferences', 'PushNotification', []); - $this->assertEquals(2, $result); + # test 3 + $result = $cast->set($model, $this->attr, 'DarkMode', []); + expect($result)->toBe(8); - $result = $cast->set($model, 'preferences', 'DarkMode', []); - $this->assertEquals(8, $result); - $result = $cast->set($model, 'preferences', 'LogActivity', []); - $this->assertEquals(1, $result); + # test 4 + $result = $cast->set($model, $this->attr, 'LogActivity', []); + expect($result)->toBe(1); - $result = $cast->set($model, 'preferences', 'LogActivity,DarkMode', []); - $this->assertEquals(9, $result); - $result = $cast->set($model, 'preferences', 'LogActivity, PushNotification ,DarkMode', []); - $this->assertEquals(11, $result); + # test 5 + $result = $cast->set($model, $this->attr, 'LogActivity,DarkMode', []); + expect($result)->toBe(9); - $result = $cast->set($model, 'preferences', '', []); - $this->assertEquals(0, $result); - } - public function testSetThrowsExceptionForInvalidStringValueType() - { - $this->expectException(TypeError::class); + # test 6 + $result = $cast->set($model, $this->attr, 'LogActivity, PushNotification ,DarkMode', []); + expect($result)->toBe(11); - $model = new CastsBitmaskEnumsModel; - $enum = BitmaskPreferenceEnum::class; - $cast = new AsBitmask($enum); - $cast->set($model, 'preferences', 'RANDOM_CASE', []); - } + # test 7 + $result = $cast->set($model, $this->attr, '', []); + expect($result)->toBe(0); +}); - public function testSetReturnsCorrectMaskForArrayInput() - { - $model = new CastsBitmaskEnumsModel; - $enum = BitmaskPreferenceEnum::class; - $cast = new AsBitmask($enum); +test('`set` method throws exception for invalid string value type', function () { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); - $result = $cast->set($model, 'preferences', [BitmaskPreferenceEnum::DarkMode, BitmaskPreferenceEnum::LogActivity], []); - $this->assertEquals(9, $result); + $cast->set($model, $this->attr, 'RANDOM_CASE', []); - $result = $cast->set($model, 'preferences', [BitmaskPreferenceEnum::TwoFactorAuth], []); - $this->assertEquals(4, $result); +})->throws(TypeError::class, 'This method can only be used with an enum'); - $result = $cast->set($model, 'preferences', [BitmaskPreferenceEnum::TwoFactorAuth, BitmaskPreferenceEnum::DataExport, BitmaskPreferenceEnum::PushNotification], []); - $this->assertEquals(38, $result); +test('`set` method returns correct mask for array input', function () { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); - $result = $cast->set($model, 'preferences', [], []); - $this->assertEquals(0, $result); - $result = $cast->set($model, 'preferences', [BitmaskPreferenceEnum::TwoFactorAuth, 'AutoUpdates', BitmaskPreferenceEnum::PushNotification], []); - $this->assertEquals(22, $result); + # test 1 + $result = $cast->set($model, $this->attr, [BitmaskPreferenceEnum::DarkMode, BitmaskPreferenceEnum::LogActivity], []); + expect($result)->toBe(9); - $result = $cast->set($model, 'preferences', ['DataExport', 'AutoUpdates'], []); - $this->assertEquals(48, $result); - } - public function testSetThrowsExceptionForInvalidArrayValueType() - { - $this->expectException(TypeError::class); + # test 2 + $result = $cast->set($model, $this->attr, [BitmaskPreferenceEnum::TwoFactorAuth], []); + expect($result)->toBe(4); - $model = new CastsBitmaskEnumsModel; - $enum = BitmaskPreferenceEnum::class; - $cast = new AsBitmask($enum); - $cast->set($model, 'preferences', [BitmaskPreferenceEnum::DarkMode, BitmasksIncorrectIntEnum::Read], []); - } + # test 3 + $result = $cast->set($model, $this->attr, [BitmaskPreferenceEnum::TwoFactorAuth, BitmaskPreferenceEnum::DataExport, BitmaskPreferenceEnum::PushNotification], []); + expect($result)->toBe(38); - public function testSetThrowsExceptionForInvalidValueTypes() - { - $this->expectException(InvalidArgumentException::class); - $model = new CastsBitmaskEnumsModel; - $enum = BitmaskPreferenceEnum::class; - $cast = new AsBitmask($enum); + # test 4 + $result = $cast->set($model, $this->attr, [], []); + expect($result)->toBe(0); - $cast->set($model, 'preferences', 3, []); - } + # test 5 + $result = $cast->set($model, $this->attr, [BitmaskPreferenceEnum::TwoFactorAuth, 'AutoUpdates', BitmaskPreferenceEnum::PushNotification], []); + expect($result)->toBe(22); - # model - public function testReturnsAllItemsEnabled(): void - { - DB::table('casts_bitmask_enums')->insert([ - 'created_at' => now(), - 'updated_at' => now(), - ]); - $record = CastsBitmaskEnumsModel::query()->first(); + # test 6 + $result = $cast->set($model, $this->attr, ['DataExport', 'AutoUpdates'], []); + expect($result)->toBe(48); +}); +test('`set` method throws exception for invalid array value type', function () { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); - $this->assertInstanceOf(Bitmask::class, $record->preferences); + $cast->set($model, $this->attr, [BitmaskPreferenceEnum::DarkMode, BitmasksIncorrectIntEnum::Read], []); - $this->assertEquals(63, $record->preferences->value()); - $this->assertTrue($record->preferences->has(BitmaskPreferenceEnum::LogActivity)); - $this->assertTrue($record->preferences->has(BitmaskPreferenceEnum::PushNotification)); - $this->assertTrue($record->preferences->has(BitmaskPreferenceEnum::PushNotification)); - $this->assertTrue($record->preferences->has(BitmaskPreferenceEnum::DarkMode)); - $this->assertTrue($record->preferences->has(BitmaskPreferenceEnum::AutoUpdates)); - $this->assertTrue($record->preferences->has(BitmaskPreferenceEnum::DataExport)); - } +})->throws(TypeError::class); - #[DataProvider('modelTestDataset')] - public function testStoresCorrectMaskToDatabase($dataset): void - { - $model = new CastsBitmaskEnumsModel; - $model->preferences = $dataset['preferences']; - $model->save(); +test('`set` method throws exception for invalid value types', function () { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); - # test 1 - $this->assertEquals($dataset['value'], $model->preferences->value()); + $cast->set($model, $this->attr, 3, []); - foreach ($dataset['has'] as $has) { - $this->assertTrue($model->preferences->has($has)); - } +})->throws(InvalidArgumentException::class); - $array = $model->toArray(); - $this->assertEquals($dataset['serialized'], $array['preferences']); +# get +test('`get` method returns bitmask for valid value', function () { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); - # test 2 - $model = CastsBitmaskEnumsModel::find($model->id); + # test 1 + $result = $cast->get($model, $this->attr, 16, []); + expect($result->value()) + ->toBe(16) + ->and($result->has(BitmaskPreferenceEnum::AutoUpdates)) + ->toBeTrue(); - $this->assertEquals($dataset['value'], $model->preferences->value()); - foreach ($dataset['has'] as $has) { - $this->assertTrue($model->preferences->has($has)); - } + # test 2 + $result = $cast->get($model, $this->attr, 17, []); + expect($result->value()) + ->toBe(17) + ->and($result->has(BitmaskPreferenceEnum::LogActivity)) + ->toBeTrue() + ->and($result->has(BitmaskPreferenceEnum::AutoUpdates)) + ->toBeTrue(); - $array = $model->toArray(); - $this->assertEquals($dataset['serialized'], $array['preferences']); - } + # test 3 + $result = $cast->get($model, $this->attr, '3', []); + expect($result->value()) + ->toBe(3) + ->and($result->has(BitmaskPreferenceEnum::LogActivity)) + ->toBeTrue() + ->and($result->has(BitmaskPreferenceEnum::PushNotification)) + ->toBeTrue(); - public function testBitmaskOperations(): void - { - $model = new CastsBitmaskEnumsModel; - $model->preferences = [BitmaskPreferenceEnum::DarkMode, BitmaskPreferenceEnum::AutoUpdates]; - $model->save(); + # test 4 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, + BitmaskPreferenceEnum::DarkMode, + ); - # test 1 - $this->assertTrue($model->preferences->has(BitmaskPreferenceEnum::DarkMode)); - $this->assertTrue($model->preferences->has(BitmaskPreferenceEnum::AutoUpdates)); + $result = $cast->get($model, $this->attr, $value, []); + expect($result->value()) + ->toBe(9) + ->and($result) + ->toBe($value) + ->and($result->has(BitmaskPreferenceEnum::LogActivity)) + ->toBeTrue() + ->and($result->has(BitmaskPreferenceEnum::DarkMode)) + ->toBeTrue(); +}); - # test 2 - $model->preferences = $model->preferences->set(BitmaskPreferenceEnum::LogActivity); - $model->preferences = $model->preferences->unset(BitmaskPreferenceEnum::AutoUpdates); - $model->save(); +# model +test('returns all items enabled', function () { + DB::table('casts_bitmask_enums')->insert([ + 'created_at' => now(), + 'updated_at' => now(), + ]); - $this->assertTrue($model->preferences->has(BitmaskPreferenceEnum::LogActivity)); - $this->assertTrue($model->preferences->has(BitmaskPreferenceEnum::DarkMode)); - $this->assertFalse($model->preferences->has(BitmaskPreferenceEnum::AutoUpdates)); + $record = CastsBitmaskEnumsModel::query()->first(); + expect($record->preferences) + ->toBeInstanceOf(Bitmask::class) + ->and($record->preferences->value()) + ->toBe(63) + ->and($record->preferences->has(BitmaskPreferenceEnum::LogActivity)) + ->toBeTrue() + ->and($record->preferences->has(BitmaskPreferenceEnum::PushNotification)) + ->toBeTrue() + ->and($record->preferences->has(BitmaskPreferenceEnum::PushNotification)) + ->toBeTrue() + ->and($record->preferences->has(BitmaskPreferenceEnum::DarkMode)) + ->toBeTrue() + ->and($record->preferences->has(BitmaskPreferenceEnum::AutoUpdates)) + ->toBeTrue() + ->and($record->preferences->has(BitmaskPreferenceEnum::DataExport)) + ->toBeTrue(); +}); + +test('stores correct mask to database', function (mixed $preferences, int $value, string $serialized, array $has) { + $model = new CastsBitmaskEnumsModel; + $model->preferences = $preferences; + $model->save(); + + + # test 1 + expect($model->preferences->value())->toBe($value); + + + foreach ($has as $item) { + expect($model->preferences->has($item))->toBeTrue(); + } - # test 3 - $model->preferences = $model->preferences->toggle(BitmaskPreferenceEnum::AutoUpdates); - $model->preferences = $model->preferences->toggle(BitmaskPreferenceEnum::LogActivity); - $model->save(); + $array = $model->toArray(); + expect($array['preferences'])->toBe($serialized); - $this->assertFalse($model->preferences->has(BitmaskPreferenceEnum::LogActivity)); - $this->assertTrue($model->preferences->has(BitmaskPreferenceEnum::DarkMode)); - $this->assertTrue($model->preferences->has(BitmaskPreferenceEnum::AutoUpdates)); + # test 2 + $model = CastsBitmaskEnumsModel::find($model->id); - # test 4 - $model = CastsBitmaskEnumsModel::find($model->id); + expect($model->preferences->value())->toBe($value); - $this->assertTrue($model->preferences->has(BitmaskPreferenceEnum::DarkMode)); - $this->assertTrue($model->preferences->has(BitmaskPreferenceEnum::AutoUpdates)); - $this->assertFalse($model->preferences->has(BitmaskPreferenceEnum::LogActivity)); - $this->assertEquals(24, $model->preferences->value()); + foreach ($has as $item) { + expect($model->preferences->has($item))->toBeTrue(); } + $array = $model->toArray(); + expect($array['preferences'])->toBe($serialized); + +})->with([ + [ + 'preferences' => BitmaskPreferenceEnum::AutoUpdates, + 'value' => 16, + 'serialized' => 'AutoUpdates', + 'has' => [ + BitmaskPreferenceEnum::AutoUpdates + ] + ], + [ + 'preferences' => 'DarkMode', + 'value' => 8, + 'serialized' => 'DarkMode', + 'has' => [ + BitmaskPreferenceEnum::DarkMode + ] + ], + [ + 'preferences' => 'DataExport,TwoFactorAuth,PushNotification', + 'value' => 38, + 'serialized' => 'PushNotification,TwoFactorAuth,DataExport', + 'has' => [ + BitmaskPreferenceEnum::DataExport, + BitmaskPreferenceEnum::TwoFactorAuth, + BitmaskPreferenceEnum::PushNotification, + ] + ], + [ + 'preferences' => ' DataExport, TwoFactorAuth ,DarkMode ', + 'value' => 44, + 'serialized' => 'TwoFactorAuth,DarkMode,DataExport', + 'has' => [ + BitmaskPreferenceEnum::DataExport, + BitmaskPreferenceEnum::TwoFactorAuth, + BitmaskPreferenceEnum::DarkMode, + ] + ], + [ + 'preferences' => '', + 'value' => 0, + 'serialized' => '', + 'has' => [] + ], + [ + 'preferences' => [ + BitmaskPreferenceEnum::DarkMode, + BitmaskPreferenceEnum::LogActivity, + ], + 'value' => 9, + 'serialized' => 'LogActivity,DarkMode', + 'has' => [ + BitmaskPreferenceEnum::DarkMode, + BitmaskPreferenceEnum::LogActivity, + ] + ], + [ + 'preferences' => BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::TwoFactorAuth, + BitmaskPreferenceEnum::LogActivity, + ), + 'value' => 5, + 'serialized' => 'LogActivity,TwoFactorAuth', + 'has' => [ + BitmaskPreferenceEnum::TwoFactorAuth, + BitmaskPreferenceEnum::LogActivity, + ] + ], + [ + 'preferences' => BitmaskPreferenceEnum::mask(), + 'value' => 0, + 'serialized' => '', + 'has' => [] + ], + [ + 'preferences' => [], + 'value' => 0, + 'serialized' => '', + 'has' => [] + ], +]); + +test('bitmask operations', function () { + $model = new CastsBitmaskEnumsModel; + $model->preferences = [BitmaskPreferenceEnum::DarkMode, BitmaskPreferenceEnum::AutoUpdates]; + $model->save(); - # get - public function testGetReturnsBitmaskForValidValue() - { - $model = new CastsBitmaskEnumsModel; - $enum = BitmaskPreferenceEnum::class; - $cast = new AsBitmask($enum); + # test 1 + expect($model->preferences->has(BitmaskPreferenceEnum::DarkMode)) + ->toBeTrue() + ->and($model->preferences->has(BitmaskPreferenceEnum::AutoUpdates)) + ->toBeTrue(); - # test 1 - $result = $cast->get($model, 'preferences', 16, []); - $this->assertEquals(16, $result->value()); - $this->assertTrue($result->has(BitmaskPreferenceEnum::AutoUpdates)); + # test 2 + $model->preferences = $model->preferences->set(BitmaskPreferenceEnum::LogActivity); + $model->preferences = $model->preferences->unset(BitmaskPreferenceEnum::AutoUpdates); + $model->save(); - # test 2 - $result = $cast->get($model, 'preferences', 17, []); - $this->assertEquals(17, $result->value()); - $this->assertTrue($result->has(BitmaskPreferenceEnum::LogActivity)); - $this->assertTrue($result->has(BitmaskPreferenceEnum::AutoUpdates)); + expect($model->preferences->has(BitmaskPreferenceEnum::LogActivity)) + ->toBeTrue() + ->and($model->preferences->has(BitmaskPreferenceEnum::DarkMode)) + ->toBeTrue() + ->and($model->preferences->has(BitmaskPreferenceEnum::AutoUpdates)) + ->toBeFalse(); - # test 3 - $result = $cast->get($model, 'preferences', '3', []); - $this->assertEquals(3, $result->value()); - $this->assertTrue($result->has(BitmaskPreferenceEnum::LogActivity)); - $this->assertTrue($result->has(BitmaskPreferenceEnum::PushNotification)); + # test 3 + $model->preferences = $model->preferences->toggle(BitmaskPreferenceEnum::AutoUpdates); + $model->preferences = $model->preferences->toggle(BitmaskPreferenceEnum::LogActivity); + $model->save(); - # test 4 - $value = BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::LogActivity, - BitmaskPreferenceEnum::DarkMode, - ); + expect($model->preferences->has(BitmaskPreferenceEnum::LogActivity)) + ->toBeFalse() + ->and($model->preferences->has(BitmaskPreferenceEnum::DarkMode)) + ->toBeTrue() + ->and($model->preferences->has(BitmaskPreferenceEnum::AutoUpdates)) + ->toBeTrue(); - $result = $cast->get($model, 'preferences', $value, []); - $this->assertEquals(9, $result->value()); - $this->assertEquals($result, $value); - $this->assertTrue($result->has(BitmaskPreferenceEnum::LogActivity)); - $this->assertTrue($result->has(BitmaskPreferenceEnum::DarkMode)); - } - # serialize - public function testSerializeReturnsEmptyStringForNonBitmaskValues() - { - $model = new CastsBitmaskEnumsModel; - $enum = BitmaskPreferenceEnum::class; - $cast = new AsBitmask($enum); + # test 4 + $model = CastsBitmaskEnumsModel::find($model->id); - $result = $cast->serialize($model, 'preferences', '', []); + expect($model->preferences->has(BitmaskPreferenceEnum::DarkMode)) + ->toBeTrue() + ->and($model->preferences->has(BitmaskPreferenceEnum::AutoUpdates)) + ->toBeTrue() + ->and($model->preferences->has(BitmaskPreferenceEnum::LogActivity)) + ->toBeFalse() + ->and($model->preferences->value()) + ->toBe(24); +}); - $this->assertEquals('', $result); - } - public function testSerializeReturnsCorrectStringForBitmaskValues() - { - $model = new CastsBitmaskEnumsModel; - $enum = BitmaskPreferenceEnum::class; - $cast = new AsBitmask($enum); +# serialize +test('`serialize` method returns empty string for non bitmask values', function () { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); - # test 1 - $value = BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::LogActivity, - BitmaskPreferenceEnum::DarkMode, - ); + $result = $cast->serialize($model, $this->attr, '', []); + expect($result)->toBe(''); +}); - $result = $cast->serialize($model, 'preferences', $value, []); - $this->assertEquals('LogActivity,DarkMode', $result); +test('`serialize` method returns correct string for bitmask values', function () { + $model = new CastsBitmaskEnumsModel; + $enum = BitmaskPreferenceEnum::class; + $cast = new AsBitmask($enum); - # test 2 - $value = BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::AutoUpdates, - ); - $result = $cast->serialize($model, 'preferences', $value, []); - $this->assertEquals('AutoUpdates', $result); + # test 1 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, + BitmaskPreferenceEnum::DarkMode, + ); + $result = $cast->serialize($model, $this->attr, $value, []); + expect($result)->toBe('LogActivity,DarkMode'); - # test 3 - $value = BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::LogActivity, - BitmaskPreferenceEnum::DarkMode, - BitmaskPreferenceEnum::DataExport, - ); - $result = $cast->serialize($model, 'preferences', $value, []); - $this->assertEquals('LogActivity,DarkMode,DataExport', $result); - } + # test 2 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::AutoUpdates, + ); + $result = $cast->serialize($model, $this->attr, $value, []); + expect($result)->toBe('AutoUpdates'); - # datasets - public static function modelTestDataset(): array - { - return [ - [ - # dataset 1 - [ - 'preferences' => BitmaskPreferenceEnum::AutoUpdates, - 'value' => 16, - 'serialized' => 'AutoUpdates', - 'has' => [ - BitmaskPreferenceEnum::AutoUpdates - ] - ], - - # dataset 2 - [ - 'preferences' => 'DarkMode', - 'value' => 8, - 'serialized' => 'DarkMode', - 'has' => [ - BitmaskPreferenceEnum::DarkMode - ] - ], - - # dataset 3 - [ - 'preferences' => 'DataExport,TwoFactorAuth,PushNotification', - 'value' => 38, - 'serialized' => 'DataExport,TwoFactorAuth,PushNotification', - 'has' => [ - BitmaskPreferenceEnum::DataExport, - BitmaskPreferenceEnum::TwoFactorAuth, - BitmaskPreferenceEnum::PushNotification, - ] - ], - - # dataset 4 - [ - 'preferences' => ' DataExport, TwoFactorAuth ,DarkMode ', - 'value' => 44, - 'serialized' => 'DataExport,TwoFactorAuth,DarkMode', - 'has' => [ - BitmaskPreferenceEnum::DataExport, - BitmaskPreferenceEnum::TwoFactorAuth, - BitmaskPreferenceEnum::DarkMode, - ] - ], - - # dataset 5 - [ - 'preferences' => '', - 'value' => 0, - 'serialized' => '', - 'has' => [] - ], - - # dataset 6 - [ - 'preferences' => [ - BitmaskPreferenceEnum::DarkMode, - BitmaskPreferenceEnum::LogActivity, - ], - 'value' => 9, - 'serialized' => 'DarkMode,LogActivity', - 'has' => [ - BitmaskPreferenceEnum::DarkMode, - BitmaskPreferenceEnum::LogActivity, - ] - ], - - # dataset 7 - [ - 'preferences' => BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::TwoFactorAuth, - BitmaskPreferenceEnum::LogActivity, - ), - 'value' => 5, - 'serialized' => 'TwoFactorAuth,LogActivity', - 'has' => [ - BitmaskPreferenceEnum::TwoFactorAuth, - BitmaskPreferenceEnum::LogActivity, - ] - ], - - # dataset 8 - [ - 'preferences' => BitmaskPreferenceEnum::mask(), - 'value' => 0, - 'serialized' => '', - 'has' => [] - ], - - # dataset 8 - [ - 'preferences' => [], - 'value' => 0, - 'serialized' => '', - 'has' => [] - ], - ] - ]; - } -} + + # test 3 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, + BitmaskPreferenceEnum::DataExport, + BitmaskPreferenceEnum::DarkMode, + ); + + $result = $cast->serialize($model, 'preferences', $value, []); + expect($result)->toBe('LogActivity,DarkMode,DataExport'); +}); From a96cd267d3f0d279234ab141bf5043126236c245 Mon Sep 17 00:00:00 2001 From: mostafaznv Date: Sun, 10 Aug 2025 02:32:44 +0330 Subject: [PATCH 10/14] refactor: remove unnecessary TestCase usage in multiple test files --- tests/Pest.php | 6 +++-- tests/TestCase.php | 26 ++++++++++++++----- tests/Unit/Helpers/EnumBladeTest.php | 3 --- tests/Unit/Helpers/EnumReporterTest.php | 5 +--- .../Concerns/CastsBasicEnumerationsTest.php | 7 +++-- .../Middleware/SubstituteEnumsTest.php | 1 - .../Laravel/Mixins/FormRequestMixinTest.php | 2 -- .../EnumhancerServiceProviderTest.php | 2 -- .../Reporters/LaravelLogReporterTest.php | 2 -- 9 files changed, 28 insertions(+), 26 deletions(-) diff --git a/tests/Pest.php b/tests/Pest.php index f726141..23dc95a 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -11,10 +11,12 @@ | */ -use PHPUnit\Framework\TestCase; +use Henzeb\Enumhancer\Tests\TestCase; +use Illuminate\Foundation\Testing\Concerns\InteractsWithViews; ini_set('memory_limit', '512M'); -uses(TestCase::class)->in('Unit'); + +uses(TestCase::class, InteractsWithViews::class)->in('Unit'); /* |-------------------------------------------------------------------------- diff --git a/tests/TestCase.php b/tests/TestCase.php index 2d317c3..14ab956 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -8,8 +8,10 @@ use Henzeb\Enumhancer\Tests\Fixtures\EnhancedIntBackedEnum; use Henzeb\Enumhancer\Tests\Fixtures\SimpleEnum; use Henzeb\Enumhancer\Tests\Fixtures\UnitEnums\Defaults\DefaultsEnum; +use Orchestra\Testbench\TestCase as OrchestraTestCase; -class TestCase extends \Orchestra\Testbench\TestCase + +class TestCase extends OrchestraTestCase { protected function setUp(): void { @@ -17,12 +19,24 @@ protected function setUp(): void parent::setUp(); } - protected function getPackageProviders($app) + protected function getPackageProviders($app): array + { + return [ + EnumhancerServiceProvider::class, + ]; + } + + protected function defineEnvironment($app): void { - return [EnumhancerServiceProvider::class]; + config()->set('database.default', 'sqlite'); + config()->set('database.connections.sqlite', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); } - protected function defineRoutes($router) + protected function defineRoutes($router): void { // For SubstituteEnumsTest $router->middleware('api')->get('/simpleapi/{status}', @@ -32,7 +46,7 @@ function (SimpleEnum $status) { ); } - protected function defineWebRoutes($router) + protected function defineWebRoutes($router): void { // For SubstituteEnumsTest $router->get('/noparams', @@ -69,4 +83,4 @@ function (DefaultsEnum $status) { } ); } -} \ No newline at end of file +} diff --git a/tests/Unit/Helpers/EnumBladeTest.php b/tests/Unit/Helpers/EnumBladeTest.php index f47bedc..65d6034 100644 --- a/tests/Unit/Helpers/EnumBladeTest.php +++ b/tests/Unit/Helpers/EnumBladeTest.php @@ -5,13 +5,10 @@ use Henzeb\Enumhancer\Tests\Fixtures\EnhancedBackedEnum; use Henzeb\Enumhancer\Tests\Fixtures\EnhancedUnitEnum; use Henzeb\Enumhancer\Tests\Fixtures\IntBackedEnum; -use Illuminate\Foundation\Testing\Concerns\InteractsWithViews; -use Orchestra\Testbench\TestCase; use function Henzeb\Enumhancer\Functions\backing; use function Henzeb\Enumhancer\Functions\name; use function Henzeb\Enumhancer\Functions\value; -uses(TestCase::class, InteractsWithViews::class); test('should render value', function ($enum, $keepValueCase = true) { $method = $keepValueCase ? 'register' : 'registerLowercase'; diff --git a/tests/Unit/Helpers/EnumReporterTest.php b/tests/Unit/Helpers/EnumReporterTest.php index 4757434..dd77d09 100644 --- a/tests/Unit/Helpers/EnumReporterTest.php +++ b/tests/Unit/Helpers/EnumReporterTest.php @@ -8,11 +8,8 @@ use Henzeb\Enumhancer\Tests\Fixtures\EnhancedBackedEnum; use Henzeb\Enumhancer\Tests\Fixtures\EnhancedUnitEnum; use Illuminate\Support\Facades\Log; -use Orchestra\Testbench\TestCase; use Psr\Log\LoggerInterface; -uses(TestCase::class); - beforeEach(function () { $this->app->getProviders(EnumhancerServiceProvider::class); }); @@ -128,4 +125,4 @@ public function report(string $enum, ?string $key, ?\BackedEnum $context): void test('make or report array should error with non enum', function () { EnumReporter::getOrReportArray(stdClass::class, [], null, new LaravelLogReporter()); -})->throws(TypeError::class); \ No newline at end of file +})->throws(TypeError::class); diff --git a/tests/Unit/Laravel/Concerns/CastsBasicEnumerationsTest.php b/tests/Unit/Laravel/Concerns/CastsBasicEnumerationsTest.php index b965a92..b4a9a27 100644 --- a/tests/Unit/Laravel/Concerns/CastsBasicEnumerationsTest.php +++ b/tests/Unit/Laravel/Concerns/CastsBasicEnumerationsTest.php @@ -7,8 +7,7 @@ use Henzeb\Enumhancer\Tests\Fixtures\Models\CastsBasicEnumsNoPropertyModel; use Henzeb\Enumhancer\Tests\Fixtures\StringBackedGetEnum; use Henzeb\Enumhancer\Tests\Fixtures\SubsetUnitEnum; -use Henzeb\Enumhancer\Tests\TestCase; -uses(TestCase::class); + test('should cast correctly from string', function (\UnitEnum $enum, string $key, bool $keepCase = true) { $model = $keepCase ? new CastsBasicEnumsModel() : new CastsBasicEnumsLowerCaseModel(); @@ -100,12 +99,12 @@ test('should return non-enum value in getStorableEnumValue', function () { $model = new CastsBasicEnumsModel(); - + // Use reflection to test the protected method $reflection = new ReflectionClass($model); $method = $reflection->getMethod('getStorableEnumValue'); $method->setAccessible(true); - + // Test with a non-UnitEnum value - this should hit line 79 $result = $method->invoke($model, 'some_string', 'some_string'); expect($result)->toBe('some_string'); diff --git a/tests/Unit/Laravel/Middleware/SubstituteEnumsTest.php b/tests/Unit/Laravel/Middleware/SubstituteEnumsTest.php index 4de65ea..6f6e176 100644 --- a/tests/Unit/Laravel/Middleware/SubstituteEnumsTest.php +++ b/tests/Unit/Laravel/Middleware/SubstituteEnumsTest.php @@ -3,7 +3,6 @@ use Henzeb\Enumhancer\Tests\TestCase; use Illuminate\Support\Facades\Config; -uses(TestCase::class); beforeEach(function () { Config::set('app.key', 'base64:+vvg9yApP0djYSZlVTA0y4QnzdC7icL1U5qExdW4gts='); diff --git a/tests/Unit/Laravel/Mixins/FormRequestMixinTest.php b/tests/Unit/Laravel/Mixins/FormRequestMixinTest.php index 7bafac6..9574c3c 100644 --- a/tests/Unit/Laravel/Mixins/FormRequestMixinTest.php +++ b/tests/Unit/Laravel/Mixins/FormRequestMixinTest.php @@ -3,10 +3,8 @@ use Henzeb\Enumhancer\Contracts\Mapper; use Henzeb\Enumhancer\Tests\Fixtures\SimpleEnum; use Henzeb\Enumhancer\Tests\Fixtures\UnitEnums\Defaults\DefaultsEnum; -use Henzeb\Enumhancer\Tests\TestCase; use Illuminate\Foundation\Http\FormRequest; -uses(TestCase::class); test('as enum', function () { $request = new FormRequest( diff --git a/tests/Unit/Laravel/Provider/EnumhancerServiceProviderTest.php b/tests/Unit/Laravel/Provider/EnumhancerServiceProviderTest.php index 10b8e0c..7aec391 100644 --- a/tests/Unit/Laravel/Provider/EnumhancerServiceProviderTest.php +++ b/tests/Unit/Laravel/Provider/EnumhancerServiceProviderTest.php @@ -3,9 +3,7 @@ use Henzeb\Enumhancer\Enums\LogLevel; use Henzeb\Enumhancer\Helpers\EnumReporter; use Henzeb\Enumhancer\Laravel\Reporters\LaravelLogReporter; -use Henzeb\Enumhancer\Tests\TestCase; -uses(TestCase::class); test('has set laravel reporter', function () { $reporter = EnumReporter::get(); diff --git a/tests/Unit/Laravel/Reporters/LaravelLogReporterTest.php b/tests/Unit/Laravel/Reporters/LaravelLogReporterTest.php index 35516f6..254ac07 100644 --- a/tests/Unit/Laravel/Reporters/LaravelLogReporterTest.php +++ b/tests/Unit/Laravel/Reporters/LaravelLogReporterTest.php @@ -3,12 +3,10 @@ use Henzeb\Enumhancer\Enums\LogLevel; use Henzeb\Enumhancer\Laravel\Reporters\LaravelLogReporter; use Henzeb\Enumhancer\Tests\Fixtures\EnhancedBackedEnum; -use Henzeb\Enumhancer\Tests\TestCase; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Log; use Psr\Log\LoggerInterface; -uses(TestCase::class); beforeEach(function () { Config::set('logging.default', 'stack'); From f1b605b6df7c10436209b5ebe32924db8170720e Mon Sep 17 00:00:00 2001 From: mostafaznv Date: Sun, 10 Aug 2025 09:37:53 +0330 Subject: [PATCH 11/14] test: update `serialize` test to use dynamic attribute variable --- tests/Unit/Laravel/Casts/AsBitmaskTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/Laravel/Casts/AsBitmaskTest.php b/tests/Unit/Laravel/Casts/AsBitmaskTest.php index c309638..f19372c 100644 --- a/tests/Unit/Laravel/Casts/AsBitmaskTest.php +++ b/tests/Unit/Laravel/Casts/AsBitmaskTest.php @@ -501,6 +501,6 @@ BitmaskPreferenceEnum::DarkMode, ); - $result = $cast->serialize($model, 'preferences', $value, []); + $result = $cast->serialize($model, $this->attr, $value, []); expect($result)->toBe('LogActivity,DarkMode,DataExport'); }); From 08fd1bf02094c4c950e1cf072398deded8487a9b Mon Sep 17 00:00:00 2001 From: mostafaznv Date: Sun, 10 Aug 2025 09:55:52 +0330 Subject: [PATCH 12/14] test: refactor `InteractsWithBitmask` tests to use `Pest` --- tests/Pest.php | 3 +- .../Traits/InteractsWithBitmaskTest.php | 654 +++++++++--------- 2 files changed, 348 insertions(+), 309 deletions(-) diff --git a/tests/Pest.php b/tests/Pest.php index 23dc95a..0698118 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -13,10 +13,11 @@ use Henzeb\Enumhancer\Tests\TestCase; use Illuminate\Foundation\Testing\Concerns\InteractsWithViews; +use Illuminate\Foundation\Testing\RefreshDatabase; ini_set('memory_limit', '512M'); -uses(TestCase::class, InteractsWithViews::class)->in('Unit'); +uses(TestCase::class, InteractsWithViews::class, RefreshDatabase::class)->in('Unit'); /* |-------------------------------------------------------------------------- diff --git a/tests/Unit/Laravel/Traits/InteractsWithBitmaskTest.php b/tests/Unit/Laravel/Traits/InteractsWithBitmaskTest.php index 9941aeb..2fe1eb5 100644 --- a/tests/Unit/Laravel/Traits/InteractsWithBitmaskTest.php +++ b/tests/Unit/Laravel/Traits/InteractsWithBitmaskTest.php @@ -2,362 +2,400 @@ namespace Henzeb\Enumhancer\Tests\Unit\Laravel\Traits; -use Closure; -use Henzeb\Enumhancer\Laravel\Providers\EnumhancerServiceProvider; use Henzeb\Enumhancer\Tests\Fixtures\BackedEnums\Bitmasks\BitmaskPreferenceEnum; use Henzeb\Enumhancer\Tests\Fixtures\Models\CastsBitmaskEnumsModel; use Illuminate\Database\Schema\Blueprint; -use Orchestra\Testbench\TestCase; - - -class InteractsWithBitmaskTest extends TestCase -{ - private CastsBitmaskEnumsModel $record1; - private CastsBitmaskEnumsModel $record2; - private CastsBitmaskEnumsModel $record3; - private CastsBitmaskEnumsModel $record4; - - protected function getPackageProviders($app): array - { - return [ - EnumhancerServiceProvider::class - ]; - } - - protected function getEnvironmentSetUp($app): void - { - config()->set('database.default', 'sqlite'); - config()->set('database.connections.sqlite', [ - 'driver' => 'sqlite', - 'database' => ':memory:', - 'prefix' => '', - ]); - } - - protected function setUpDatabaseRequirements(Closure $callback): void - { - $this->app['db']->connection() - ->getSchemaBuilder() - ->create('casts_bitmask_enums', function (Blueprint $table) { - $table->id(); - $table->unsignedInteger('preferences') - ->default(BitmaskPreferenceEnum::allOptionsEnabled()) - ->comment('bitmask preferences'); - - $table->timestamps(); - }); - - - $this->record1 = CastsBitmaskEnumsModel::query()->create([ - 'preferences' => [ - BitmaskPreferenceEnum::AutoUpdates, - BitmaskPreferenceEnum::DarkMode, - ] - ]); - - $this->record2 = CastsBitmaskEnumsModel::query()->create([ - 'preferences' => BitmaskPreferenceEnum::DataExport - ]); - - $this->record3 = CastsBitmaskEnumsModel::query()->create([ - 'preferences' => 'TwoFactorAuth,DarkMode,PushNotification' - ]); - - $this->record4 = CastsBitmaskEnumsModel::query()->create([ - 'preferences' => [] - ]); - } - - - # where - public function testWhereBitmaskAppliesCorrectConditionForIntegerValue(): void - { - # test 1 - $results = CastsBitmaskEnumsModel::whereBitmask('preferences', 8)->get(); - $this->assertCount(2, $results); - $this->assertEquals([$this->record1->id, $this->record3->id], $results->pluck('id')->toArray()); - - - # test 2 - $results = CastsBitmaskEnumsModel::whereBitmask('preferences', 32)->get(); - $this->assertCount(1, $results); - $this->assertEquals($this->record2->id, $results->first()->id); - - - # test 3 - $results = CastsBitmaskEnumsModel::whereBitmask('preferences', 24)->get(); - $this->assertCount(1, $results); - $this->assertEquals($this->record1->id, $results->first()->id); - - - # test 4 - $results = CastsBitmaskEnumsModel::whereBitmask('preferences', 1)->get(); - $this->assertCount(0, $results); - - - # test 4 - $results = CastsBitmaskEnumsModel::whereBitmask('preferences', 0)->get(); - $this->assertCount(1, $results); - $this->assertEquals($this->record4->id, $results->first()->id); - } - - public function testWhereBitmaskAppliesCorrectConditionForBitmaskValue(): void - { - # test 1 - $value = BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::DarkMode - ); - - $results = CastsBitmaskEnumsModel::whereBitmask('preferences', $value)->get(); - $this->assertCount(2, $results); - $this->assertEquals([$this->record1->id, $this->record3->id], $results->pluck('id')->toArray()); - - - # test 2 - $value = BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::PushNotification - ); - $results = CastsBitmaskEnumsModel::whereBitmask('preferences', $value)->get(); - $this->assertCount(1, $results); - $this->assertEquals($this->record3->id, $results->first()->id); +beforeEach(function () { + $this->attr = 'preferences'; - # test 3 - $value = BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::PushNotification, - BitmaskPreferenceEnum::DarkMode, - BitmaskPreferenceEnum::TwoFactorAuth, - ); + $this->app['db']->connection() + ->getSchemaBuilder() + ->create('casts_bitmask_enums', function (Blueprint $table) { + $table->id(); + $table->unsignedInteger($this->attr) + ->default(BitmaskPreferenceEnum::allOptionsEnabled()) + ->comment('bitmask preferences'); - $results = CastsBitmaskEnumsModel::whereBitmask('preferences', $value)->get(); - $this->assertCount(1, $results); - $this->assertEquals($this->record3->id, $results->first()->id); + $table->timestamps(); + }); - # test 4 - $value = BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::PushNotification, - BitmaskPreferenceEnum::DarkMode, + $this->record1 = CastsBitmaskEnumsModel::query()->create([ + $this->attr => [ BitmaskPreferenceEnum::AutoUpdates, - ); - - $results = CastsBitmaskEnumsModel::whereBitmask('preferences', $value)->get(); - $this->assertCount(0, $results); - - - # test 5 - $value = BitmaskPreferenceEnum::mask(); - $results = CastsBitmaskEnumsModel::whereBitmask('preferences', $value)->get(); - $this->assertCount(1, $results); - $this->assertEquals($this->record4->id, $results->first()->id); - } - - - # or-where - public function testOrWhereBitmaskAppliesCorrectConditionForIntegerValue(): void - { - # test 1 - $results = CastsBitmaskEnumsModel::query() - ->whereBitmask('preferences', 1) - ->orWhereBitmask('preferences', 8) - ->get(); - - $this->assertCount(2, $results); - $this->assertEquals([$this->record1->id, $this->record3->id], $results->pluck('id')->toArray()); + BitmaskPreferenceEnum::DarkMode, + ] + ]); + + $this->record2 = CastsBitmaskEnumsModel::query()->create([ + $this->attr => BitmaskPreferenceEnum::DataExport + ]); + + $this->record3 = CastsBitmaskEnumsModel::query()->create([ + $this->attr => 'TwoFactorAuth,DarkMode,PushNotification' + ]); + + $this->record4 = CastsBitmaskEnumsModel::query()->create([ + $this->attr => [] + ]); +}); + + +# where +test('whereBitmask applies correct condition for integer value', function () { + # test 1 + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', 8)->get(); + $this->assertEquals([$this->record1->id, $this->record3->id], $results->pluck('id')->toArray()); + expect($results) + ->toHaveCount(2) + ->and($results->pluck('id')->toArray()) + ->toBe([ + $this->record1->id, $this->record3->id + ]); - # test 2 - $results = CastsBitmaskEnumsModel::query() - ->whereBitmask('preferences', 3) - ->orWhereBitmask('preferences', 32) - ->get(); + # test 2 + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', 32)->get(); + $this->assertCount(1, $results); + $this->assertEquals($this->record2->id, $results->first()->id); + expect($results) + ->toHaveCount(1) + ->and($results->first()->id) + ->toBe($this->record2->id); + + + # test 3 + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', 24)->get(); + $this->assertCount(1, $results); + $this->assertEquals($this->record1->id, $results->first()->id); + expect($results) + ->toHaveCount(1) + ->and($results->first()->id) + ->toBe($this->record1->id); + + + # test 4 + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', 1)->get(); + expect($results)->toHaveCount(0); + + + # test 5 + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', 0)->get(); + $this->assertCount(1, $results); + $this->assertEquals($this->record4->id, $results->first()->id); + expect($results) + ->toHaveCount(1) + ->and($results->first()->id) + ->toBe($this->record4->id); +}); + +test('whereBitmask applies correct condition for bitmask value', function () { + # test 1 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::DarkMode + ); + + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', $value)->get(); + $this->assertCount(2, $results); + $this->assertEquals([$this->record1->id, $this->record3->id], $results->pluck('id')->toArray()); + + expect($results) + ->toHaveCount(2) + ->and($results->pluck('id')->toArray()) + ->toBe([ + $this->record1->id, $this->record3->id + ]); - $this->assertCount(1, $results); - $this->assertEquals($this->record2->id, $results->first()->id); + # test 2 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::PushNotification + ); + + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', $value)->get(); + $this->assertCount(1, $results); + $this->assertEquals($this->record3->id, $results->first()->id); + expect($results) + ->toHaveCount(1) + ->and($results->first()->id) + ->toBe($this->record3->id); + + + # test 3 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::PushNotification, + BitmaskPreferenceEnum::DarkMode, + BitmaskPreferenceEnum::TwoFactorAuth, + ); + + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', $value)->get(); + $this->assertCount(1, $results); + $this->assertEquals($this->record3->id, $results->first()->id); + expect($results) + ->toHaveCount(1) + ->and($results->first()->id) + ->toBe($this->record3->id); + + # test 4 + $value = BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::PushNotification, + BitmaskPreferenceEnum::DarkMode, + BitmaskPreferenceEnum::AutoUpdates, + ); + + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', $value)->get(); + expect($results)->toHaveCount(0); + + + # test 5 + $value = BitmaskPreferenceEnum::mask(); + $results = CastsBitmaskEnumsModel::whereBitmask('preferences', $value)->get(); + expect($results) + ->toHaveCount(1) + ->and($results->first()->id) + ->toBe($this->record4->id); +}); + + +# or-where +test('orWhereBitmask applies correct condition for integer value', function () { + # test 1 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask('preferences', 1) + ->orWhereBitmask('preferences', 8) + ->get(); + + expect($results) + ->toHaveCount(2) + ->and($results->pluck('id')->toArray()) + ->toBe([ + $this->record1->id, $this->record3->id + ]); - # test 3 - $results = CastsBitmaskEnumsModel::query() - ->whereBitmask('preferences', 1) - ->orWhereBitmask('preferences', 24) - ->orWhereBitmask('preferences', 32) - ->get(); - $this->assertCount(2, $results); - $this->assertEquals([$this->record1->id, $this->record2->id], $results->pluck('id')->toArray()); + # test 2 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask('preferences', 3) + ->orWhereBitmask('preferences', 32) + ->get(); + + expect($results) + ->toHaveCount(1) + ->and($results->first()->id) + ->toBe($this->record2->id); + + + # test 3 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask('preferences', 1) + ->orWhereBitmask('preferences', 24) + ->orWhereBitmask('preferences', 32) + ->get(); + + $this->assertCount(2, $results); + $this->assertEquals([$this->record1->id, $this->record2->id], $results->pluck('id')->toArray()); + expect($results) + ->toHaveCount(2) + ->and($results->pluck('id')->toArray()) + ->toBe([ + $this->record1->id, $this->record2->id + ]); - # test 4 - $results = CastsBitmaskEnumsModel::query() - ->whereBitmask('preferences', 1) - ->orWhereBitmask('preferences', 3) - ->get(); + # test 4 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask('preferences', 1) + ->orWhereBitmask('preferences', 3) + ->get(); - $this->assertCount(0, $results); + expect($results)->toHaveCount(0); - # test 5 - $results = CastsBitmaskEnumsModel::query() - ->whereBitmask('preferences', 1) - ->orWhereBitmask('preferences', 0) - ->get(); + # test 5 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask('preferences', 1) + ->orWhereBitmask('preferences', 0) + ->get(); - $this->assertCount(1, $results); - $this->assertEquals($this->record4->id, $results->first()->id); + $this->assertCount(1, $results); + $this->assertEquals($this->record4->id, $results->first()->id); + expect($results) + ->toHaveCount(1) + ->and($results->first()->id) + ->toBe($this->record4->id); - # test 6 - $results = CastsBitmaskEnumsModel::query() - ->whereBitmask('preferences', 1) - ->orWhereBitmask('preferences', 24) - ->orWhereBitmask('preferences', 32) - ->orWhereBitmask('preferences', 8) - ->get(); + # test 6 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask('preferences', 1) + ->orWhereBitmask('preferences', 24) + ->orWhereBitmask('preferences', 32) + ->orWhereBitmask('preferences', 8) + ->get(); - $this->assertCount(3, $results); - $this->assertEquals([$this->record1->id, $this->record2->id, $this->record3->id], $results->pluck('id')->toArray()); + expect($results) + ->toHaveCount(3) + ->and($results->pluck('id')->toArray()) + ->toBe([ + $this->record1->id, $this->record2->id, $this->record3->id + ]); - } +}); - public function testOrWhereBitmaskAppliesCorrectConditionForBitmaskValue(): void - { - # test 1 - $results = CastsBitmaskEnumsModel::query() - ->whereBitmask( - column: 'preferences', - value: BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::LogActivity - ) +test('orWhereBitmask applies correct condition for bitmask value', function () { + # test 1 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity ) - ->orWhereBitmask( - column: 'preferences', - value: BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::DarkMode - ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::DarkMode ) - ->get(); - - $this->assertCount(2, $results); - $this->assertEquals([$this->record1->id, $this->record3->id], $results->pluck('id')->toArray()); + ) + ->get(); + + expect($results) + ->toHaveCount(2) + ->and($results->pluck('id')->toArray()) + ->toBe([ + $this->record1->id, $this->record3->id + ]); - # test 2 - $results = CastsBitmaskEnumsModel::query() - ->whereBitmask( - column: 'preferences', - value: BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::LogActivity, - BitmaskPreferenceEnum::PushNotification, - ) + # test 2 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, + BitmaskPreferenceEnum::PushNotification, ) - ->orWhereBitmask( - column: 'preferences', - value: BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::DataExport - ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::DataExport ) - ->get(); + ) + ->get(); - $this->assertCount(1, $results); - $this->assertEquals($this->record2->id, $results->first()->id); + expect($results) + ->toHaveCount(1) + ->and($results->first()->id) + ->toBe($this->record2->id); - # test 3 - $results = CastsBitmaskEnumsModel::query() - ->whereBitmask( - column: 'preferences', - value: BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::LogActivity, - ) + # test 3 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, ) - ->orWhereBitmask( - column: 'preferences', - value: BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::AutoUpdates, - BitmaskPreferenceEnum::DarkMode, - ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::AutoUpdates, + BitmaskPreferenceEnum::DarkMode, ) - ->orWhereBitmask( - column: 'preferences', - value: BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::DataExport - ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::DataExport ) - ->get(); - - $this->assertCount(2, $results); - $this->assertEquals([$this->record1->id, $this->record2->id], $results->pluck('id')->toArray()); + ) + ->get(); + + expect($results) + ->toHaveCount(2) + ->and($results->pluck('id')->toArray()) + ->toBe([ + $this->record1->id, $this->record2->id + ]); - # test 4 - $results = CastsBitmaskEnumsModel::query() - ->whereBitmask( - column: 'preferences', - value: BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::LogActivity, - ) + # test 4 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, ) - ->orWhereBitmask( - column: 'preferences', - value: BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::LogActivity, - BitmaskPreferenceEnum::PushNotification, - ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, + BitmaskPreferenceEnum::PushNotification, ) - ->get(); + ) + ->get(); - $this->assertCount(0, $results); + expect($results)->toHaveCount(0); - # test 5 - $results = CastsBitmaskEnumsModel::query() - ->whereBitmask( - column: 'preferences', - value: BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::LogActivity, - ) - ) - ->orWhereBitmask( - column: 'preferences', - value: BitmaskPreferenceEnum::mask() + # test 5 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, ) - ->get(); - - $this->assertCount(1, $results); - $this->assertEquals($this->record4->id, $results->first()->id); - - - # test 6 - $results = CastsBitmaskEnumsModel::query() - ->whereBitmask( - column: 'preferences', - value: BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::LogActivity, - ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask() + ) + ->get(); + + expect($results) + ->toHaveCount(1) + ->and($results->first()->id) + ->toBe($this->record4->id); + + + # test 6 + $results = CastsBitmaskEnumsModel::query() + ->whereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::LogActivity, ) - ->orWhereBitmask( - column: 'preferences', - value: BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::DarkMode, - BitmaskPreferenceEnum::AutoUpdates, - ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::DarkMode, + BitmaskPreferenceEnum::AutoUpdates, ) - ->orWhereBitmask( - column: 'preferences', - value: BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::DataExport - ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::DataExport ) - ->orWhereBitmask( - column: 'preferences', - value: BitmaskPreferenceEnum::mask( - BitmaskPreferenceEnum::DarkMode - ) + ) + ->orWhereBitmask( + column: 'preferences', + value: BitmaskPreferenceEnum::mask( + BitmaskPreferenceEnum::DarkMode ) - ->get(); - - $this->assertCount(3, $results); - $this->assertEquals([$this->record1->id, $this->record2->id, $this->record3->id], $results->pluck('id')->toArray()); - } -} + ) + ->get(); + + expect($results) + ->toHaveCount(3) + ->and($results->pluck('id')->toArray()) + ->toBe([ + $this->record1->id, $this->record2->id, $this->record3->id + ]); +}); From 55e9882d12d9fc62503acc8fb6bc764cfe977ac3 Mon Sep 17 00:00:00 2001 From: mostafaznv Date: Sun, 10 Aug 2025 12:49:41 +0330 Subject: [PATCH 13/14] ci: simplify test matrix, add stability levels, and streamline dependency installation --- .github/workflows/tests.yml | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e521b8d..7f3b44c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,30 +13,15 @@ on: jobs: tests: - name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.dependency-version }} + name: PHP ${{ matrix.php }} - ${{ matrix.stability }} runs-on: ubuntu-latest env: CC_TOKEN: ${{ secrets.CODECLIMATE_TOKEN }} strategy: fail-fast: true matrix: - php: ['8.1', '8.2', '8.3', '8.4'] - laravel: ['10.*', '11.*', '12.*'] - dependency-version: [ prefer-stable ] - include: - - laravel: 10.* - testbench: 8.* - - laravel: 11.* - testbench: 9.* - - laravel: 12.* - testbench: 10.* - exclude: - - laravel: 11.* - php: 8.1 - - laravel: 12.* - php: 8.0 - - laravel: 12.* - php: 8.1 + php: [ '8.1','8.4' ] + stability: [ prefer-lowest, prefer-stable ] steps: - name: Checkout Code @@ -65,14 +50,22 @@ jobs: restore-keys: ${{ runner.os }}-composer-${{ matrix.stability }}-${{ matrix.php }} if: ${{ !env.ACT }} - - name: Install PHP Dependencies + - name: Install PHP Dependencies (prefer-lowest) + if: ${{ matrix.stability == 'prefer-lowest' }} uses: nick-invision/retry@v2 with: timeout_minutes: 5 max_attempts: 5 - command: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update --quiet - composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest + command: composer update --prefer-lowest --prefer-dist --no-interaction --no-progress --quiet --with "laravel/framework:10.14.0" + + - name: Install PHP Dependencies (prefer-stable) + if: ${{ matrix.stability == 'prefer-stable' }} + uses: nick-invision/retry@v2 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --prefer-dist --no-interaction --no-progress --quiet + - name: Lint PHP files run: find src/ tests/ -name "*.php" -exec php -l {} \; From 31cf1e0b156fc7d86cb707230a5a3c6a8b99f6e3 Mon Sep 17 00:00:00 2001 From: mostafaznv Date: Sun, 10 Aug 2025 13:09:11 +0330 Subject: [PATCH 14/14] ci: simplify test matrix, add stability levels, and streamline dependency installation --- .github/workflows/tests.yml | 14 ++------------ composer.json | 2 +- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7f3b44c..3b5f60e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -50,22 +50,12 @@ jobs: restore-keys: ${{ runner.os }}-composer-${{ matrix.stability }}-${{ matrix.php }} if: ${{ !env.ACT }} - - name: Install PHP Dependencies (prefer-lowest) - if: ${{ matrix.stability == 'prefer-lowest' }} + - name: Install PHP Dependencies uses: nick-invision/retry@v2 with: timeout_minutes: 5 max_attempts: 5 - command: composer update --prefer-lowest --prefer-dist --no-interaction --no-progress --quiet --with "laravel/framework:10.14.0" - - - name: Install PHP Dependencies (prefer-stable) - if: ${{ matrix.stability == 'prefer-stable' }} - uses: nick-invision/retry@v2 - with: - timeout_minutes: 5 - max_attempts: 5 - command: composer update --prefer-dist --no-interaction --no-progress --quiet - + command: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress --quiet - name: Lint PHP files run: find src/ tests/ -name "*.php" -exec php -l {} \; diff --git a/composer.json b/composer.json index ef5d127..b52651d 100644 --- a/composer.json +++ b/composer.json @@ -73,7 +73,7 @@ "composer/composer": "2.8.9", "henzeb/enumhancer-ide-helper": "main-dev", "mockery/mockery": "^1.5", - "orchestra/testbench": "^8|^9|^10", + "orchestra/testbench": "^8.6.0|^9|^10", "pestphp/pest": "^2.0|^3.0", "phpstan/phpstan": "^2.0" },