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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
6 changes: 6 additions & 0 deletions docs/bitmasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,9 @@ 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).

### Laravel Eloquent
For details on using bitmask enums with Eloquent query scopes, see [Laravel Eloquent](laravel.eloquent.md#bitmask-query-scopes).
95 changes: 94 additions & 1 deletion docs/casting.md
Original file line number Diff line number Diff line change
@@ -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).

Expand Down Expand Up @@ -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
```

> 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.
62 changes: 62 additions & 0 deletions docs/laravel.eloquent.md
Original file line number Diff line number Diff line change
@@ -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.
86 changes: 86 additions & 0 deletions src/Laravel/Casts/AsBitmask.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace Henzeb\Enumhancer\Laravel\Casts;

use BackedEnum;
use Henzeb\Enumhancer\Concerns\Bitmasks;
use Henzeb\Enumhancer\Helpers\Bitmasks\Bitmask;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use InvalidArgumentException;


class AsBitmask implements CastsAttributes
{
public bool $withoutObjectCaching = true;

/**
* @var class-string<Bitmasks>
*/
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;
}
}
45 changes: 45 additions & 0 deletions src/Laravel/Traits/InteractsWithBitmask.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Henzeb\Enumhancer\Laravel\Traits;

use Henzeb\Enumhancer\Helpers\Bitmasks\Bitmask;
use Illuminate\Database\Eloquent\Builder;


trait InteractsWithBitmask
{
public function scopeWhereBitmask(Builder $query, string $column, Bitmask|int $value): void
{
if ($value instanceof Bitmask) {
$value = $value->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
]);
}
}
28 changes: 28 additions & 0 deletions tests/Fixtures/BackedEnums/Bitmasks/BitmaskPreferenceEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Henzeb\Enumhancer\Tests\Fixtures\BackedEnums\Bitmasks;

use Henzeb\Enumhancer\Concerns\Bitmasks;


enum BitmaskPreferenceEnum: int
{
use Bitmasks;

private const BIT_VALUES = true;

case LogActivity = 1;
case PushNotification = 2;
case TwoFactorAuth = 4;
case DarkMode = 8;
case AutoUpdates = 16;
case DataExport = 32;


public static function allOptionsEnabled(): int
{
$all = self::cases();

return self::mask(...$all)->value();
}
}
23 changes: 23 additions & 0 deletions tests/Fixtures/Models/CastsBitmaskEnumsModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

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 = [];


# casts
protected $casts = [
'preferences' => AsBitmask::class . ':' . BitmaskPreferenceEnum::class,
];
}
7 changes: 5 additions & 2 deletions tests/Pest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
|
*/

use PHPUnit\Framework\TestCase;
use Henzeb\Enumhancer\Tests\TestCase;
use Illuminate\Foundation\Testing\Concerns\InteractsWithViews;
use Illuminate\Foundation\Testing\RefreshDatabase;

ini_set('memory_limit', '512M');
uses(TestCase::class)->in('Unit');

uses(TestCase::class, InteractsWithViews::class, RefreshDatabase::class)->in('Unit');

/*
|--------------------------------------------------------------------------
Expand Down
Loading