Repository: BenSampo/laravel-enum (php)

Issue:
Adding hasFlag for elequent query builder
Hi,
i think one important feature for flagged enums would to query DB using query builder and enums.

    Order::where("client_id",Auth::id())
                   ->whereRaw("order_status"." & ?",[OrderStatus::Ready()])
                   ->get();
its what i was done.

but could have a nicer method like:

        ->whereHasFlag("order_status",OrderStatus::Ready())

it would be my pleasure to contribute and make a PR.
but i dont know where to start, to modify query builder?

Pull request description:
feat: provide trait to query flagged enums with Eloquent
- [x] Added or updated tests
- [x] Added or updated the [README.md](../README.md)
- [x] Detailed changes in the [CHANGELOG.md](../CHANGELOG.md) unreleased section

**Related Issue/Intent**
Resolves #155 

**Changes**
Provide a trait to query flagged enums using Eloquent

**Note**
- [x] ~Still going through few details, and experimenting with potential `hasAllFlags` and `hasAnyFlags`~

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index acbd8bd..14ae476 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased](https://github.com/BenSampo/laravel-enum/compare/v3.0.0...master)
 
+### Added
+
+- Provide trait to query flagged enums using Eloquent
+
 ## [3.0.0](https://github.com/BenSampo/laravel-enum/compare/v2.2.0...v3.0.0) - 2020-08-07
 
 ### Added
diff --git a/README.md b/README.md
index 15febd6..0ef93d8 100644
--- a/README.md
+++ b/README.md
@@ -403,6 +403,27 @@ UserPermissions::Admin()->getBitmask(); // 1111;
 UserPermissions::DeleteComments()->getBitmask(); // 1000;
 ```
 
+### Flagged enums in Eloquent queries
+To use flagged enums directly in your Eloquent queries, you may use the `QueriesFlaggedEnums` trait on your model which provides you with the following methods:
+
+#### hasFlag($column, $flag): Builder
+```php
+User::hasFlag('permissions', UserPermissions::DeleteComments())->get();
+```
+#### notHasFlag($column, $flag): Builder
+```php
+User::notHasFlag('permissions', UserPermissions::DeleteComments())->get();
+```
+#### hasAllFlags($column, $flags): Builder
+```php
+User::hasAllFlags('permissions', [UserPermissions::EditComment(), UserPermissions::ReadComment()])->get();
+```
+#### hasAnyFlags($column, $flags): Builder
+```php
+User::hasAnyFlags('permissions', [UserPermissions::DeleteComments(), UserPermissions::EditComments()])->get();
+```
+
+
 ## Attribute Casting
 
 You may cast model attributes to enums using Laravel 7.x's built in custom casting. This will cast the attribute to an enum instance when getting and back to the enum value when setting.
diff --git a/phpunit.xml b/phpunit.xml
index 29a5639..3cd43f6 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -14,4 +14,8 @@
             <directory suffix="Test.php">./tests/</directory>
         </testsuite>
     </testsuites>
+    <php>
+        <env name="DB_CONNECTION" value="sqlite"/>
+        <env name="DB_DATABASE" value=":memory:"/>
+    </php>
 </phpunit>
diff --git a/src/Traits/QueriesFlaggedEnums.php b/src/Traits/QueriesFlaggedEnums.php
new file mode 100644
index 0000000..7d82b27
--- /dev/null
+++ b/src/Traits/QueriesFlaggedEnums.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace BenSampo\Enum\Traits;
+
+trait QueriesFlaggedEnums
+{
+    /**
+     * @param \Illuminate\Database\Eloquent\Builder $query
+     * @param string $column
+     * @param int $flag
+     * @return \Illuminate\Database\Eloquent\Builder
+     */
+    public function scopeHasFlag($query, $column, $flag)
+    {
+        return $query->whereRaw("$column & ? > 0", [$flag]);
+    }
+
+    /**
+     * @param \Illuminate\Database\Eloquent\Builder $query
+     * @param string $column
+     * @param int $flag
+     * @return \Illuminate\Database\Eloquent\Builder
+     */
+    public function scopeNotHasFlag($query, $column, $flag)
+    {
+        return $query->whereRaw("not $column & ? > 0", [$flag]);
+    }
+
+    /**
+     * @param \Illuminate\Database\Eloquent\Builder $query
+     * @param string $column
+     * @param int[] $flags
+     * @return \Illuminate\Database\Eloquent\Builder
+     */
+    public function scopeHasAllFlags($query, $column, $flags)
+    {
+        $mask = array_sum($flags);
+
+        return $query->whereRaw("$column & ? = ?", [$mask, $mask]);
+    }
+
+    /**
+     * @param \Illuminate\Database\Eloquent\Builder $query
+     * @param string $column
+     * @param int[] $flags
+     * @return \Illuminate\Database\Eloquent\Builder
+     */
+    public function scopeHasAnyFlags($query, $column, $flags)
+    {
+        $mask = array_sum($flags);
+
+        return $query->whereRaw("$column & ? > 0", [$mask]);
+    }
+}


Test patch:
diff --git a/tests/Models/WithQueriesFlaggedEnums.php b/tests/Models/WithQueriesFlaggedEnums.php
new file mode 100644
index 0000000..479a48d
--- /dev/null
+++ b/tests/Models/WithQueriesFlaggedEnums.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace BenSampo\Enum\Tests\Models;
+
+use BenSampo\Enum\Traits\QueriesFlaggedEnums;
+use Illuminate\Database\Eloquent\Model;
+
+class WithQueriesFlaggedEnums extends Model
+{
+    use QueriesFlaggedEnums;
+
+    public $table = 'test_models';
+    
+    protected $guarded = [];
+}
diff --git a/tests/QueriesFlaggedEnumsTest.php b/tests/QueriesFlaggedEnumsTest.php
new file mode 100644
index 0000000..32fa66c
--- /dev/null
+++ b/tests/QueriesFlaggedEnumsTest.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace BenSampo\Enum\Tests;
+
+use BenSampo\Enum\Tests\Enums\SuperPowers;
+use BenSampo\Enum\Tests\Models\WithQueriesFlaggedEnums as TestModel;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Foundation\Application;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Orchestra\Testbench\TestCase;
+
+class QueriesFlaggedEnumsTest extends TestCase
+{
+    use RefreshDatabase;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->setUpDatabase($this->app);
+
+        TestModel::create([
+            'superpowers' =>  SuperPowers::flags([SuperPowers::Flight, SuperPowers::Immortality])
+        ]);
+
+        TestModel::create([
+            'superpowers' =>  SuperPowers::flags([SuperPowers::Strength, SuperPowers::Immortality])
+        ]);
+    }
+
+    protected function setUpDatabase(Application $app)
+    {
+        $app['db']->connection()->getSchemaBuilder()->create('test_models', function (Blueprint $table) {
+            $table->increments('id');
+            $table->integer('superpowers');
+            $table->timestamps();
+        });
+    }
+    
+    /** @test */
+    public function it_can_ensure_a_flag_is_present()
+    {
+        $this->assertEquals(2, TestModel::query()->hasFlag('superpowers', SuperPowers::Immortality)->count());
+        $this->assertEquals(1, TestModel::query()->hasFlag('superpowers', SuperPowers::Flight)->count());
+        $this->assertEquals(0, TestModel::query()->hasFlag('superpowers', SuperPowers::Invisibility)->count());
+    }
+
+    /** @test */
+    public function it_can_ensure_a_flag_is_missing()
+    {
+        $this->assertEquals(0, TestModel::query()->notHasFlag('superpowers', SuperPowers::Immortality)->count());
+        $this->assertEquals(1, TestModel::query()->notHasFlag('superpowers', SuperPowers::Flight)->count());
+        $this->assertEquals(2, TestModel::query()->notHasFlag('superpowers', SuperPowers::Invisibility)->count());
+    }
+
+    /** @test */
+    public function it_can_ensure_all_flags_are_present()
+    {
+        $this->assertEquals(0, TestModel::query()->hasAllFlags('superpowers', [SuperPowers::Strength, SuperPowers::Flight])->count());
+        $this->assertEquals(1, TestModel::query()->hasAllFlags('superpowers', [SuperPowers::Immortality, SuperPowers::Flight])->count());
+        $this->assertEquals(2, TestModel::query()->hasAllFlags('superpowers', [SuperPowers::Immortality])->count());
+    }
+
+    /** @test */
+    public function it_can_ensure_any_flag_is_present()
+    {       
+        $this->assertEquals(2, TestModel::query()->hasAnyFlags('superpowers', [SuperPowers::Strength, SuperPowers::Flight])->count());
+        $this->assertEquals(1, TestModel::query()->hasAnyFlags('superpowers', [SuperPowers::Invisibility, SuperPowers::Flight])->count());
+        $this->assertEquals(2, TestModel::query()->hasAnyFlags('superpowers', [SuperPowers::Immortality])->count());
+    }
+}