Download the PHP package zuko/laravel-bit-masks without Composer
On this page you can find all versions of the php package zuko/laravel-bit-masks. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Informations about the package laravel-bit-masks
<img src="https://avatars0.githubusercontent.com/u/6666271?v=3&s=96" alt="Z-Logo" title="Halu Universe" align="right" />
zuko/laravel-bit-masks
:performing_arts: Laravel Bitmasks :performing_arts:
Also have a Tiếng Việt version of this README.
Bitmask toolkit for Laravel — flag class generator, Eloquent integration, fluent query scopes and bit-level helpers.
Store dozens of boolean flags in a single integer column (e.g. "which networks is this email listed in?" across 500M+ rows), and work with them through a clean, typed API instead of hand-rolled bitwise SQL.
- ⚙️ Generator —
php artisan make:bitmaskscaffolds int-backed flag enums (or constants classes) with power-of-two values. - 🧩 Eloquent trait — declare your mask columns once; casting, helpers and query scopes are wired automatically.
- 🔍 Fluent scopes —
whereMaskHas,whereMaskHasAny,whereMaskMissing,whereMaskEquals(+orWhere*variants). - 🛠 Value object — immutable
BitMaskwithhas / add / remove / toggle / intersect / diff / names .... - 📦 Collections — the same filters, in-memory, on Eloquent collections or plain collections.
- 🧵 Wide masks — a single logical mask spanning several BIGINT columns, for more than 63 flags (e.g. 126 across two columns) without hand-juggling
networks_1/networks_2. - 🗃 Junction (pivot) masks — the same flag API backed by a
(owner, flag_id)table instead of columns, for large or dynamic flag sets — with EXISTS-based scopes and a reverse-lookup index.
Requirements
- PHP 8.2+
- Laravel 11, 12 or 13
Installation
The service provider is auto-discovered.
Quick start
1. Generate a flag enum:
2. Add the column (migration):
3. Declare it on the model:
4. Done — the whole API is live:
The BitMask value object
Immutable — every mutation returns a new instance. Anywhere a flag is accepted, you may pass an int, an int-backed enum case, another BitMask, an (nested) iterable of those — or, when a flag enum is bound, a flag name as a string (see Flag names as strings).
| Method | Description |
|---|---|
value(): int |
Raw integer value |
isEmpty(): bool |
No bit set |
has(...$flags): bool |
All given flags present |
hasAny(...$flags): bool |
At least one flag present |
hasNone(...$flags): bool |
None of the flags present |
equals($flags): bool |
Exact match |
add(...$flags): self |
Set flags (OR) |
remove(...$flags): self |
Unset flags (AND NOT) |
toggle(...$flags): self |
Flip flags (XOR) |
clear(): self |
Empty mask |
intersect($flags): self |
Bits in both (AND) |
union($flags): self |
Bits in either (OR) |
diff($flags): self |
Bits here but not there |
bits(): array |
Set bit positions, e.g. [1, 3] |
values(): array |
Power-of-two components, e.g. [2, 8] |
flags(): array |
Enum cases (when bound) or values |
names(): array |
Enum case names (requires bound enum) |
count(): int |
Number of set bits (Countable) |
toBits(): string |
Binary string, e.g. "1010" |
enum() / withEnum($class) |
Read / bind the flag enum |
BitMask::resolve(mixed, ?enum): int is the underlying normalizer — use it whenever you need a plain integer. Passing the optional enum class also resolves flag names.
Serialization: json_encode($mask) and (string) $mask both yield the integer value.
Flag enums — BitMaskFlags
Generated enums ship with this trait; any int-backed enum can adopt it:
Flag names as strings
Whenever a flag enum is bound (or passed explicitly), plain string names are
accepted anywhere flags are — value objects, model helpers, query scopes and
collection macros. Matching is forgiving: case-insensitive, separators ignored —
'gmail', 'Gmail', 'YAHOO_MAIL' and 'yahoo mail' all resolve.
For raw data work (imports, queues, APIs) the direct string-to-int bridges:
The resolver behind all of this is Zuko\BitMasks\Support\FlagName
(resolve / tryResolve / value / tryValue), usable standalone and aware of
both flag enums and --type=constants classes. Numeric strings ('5') keep
resolving as plain integer values, never as names.
Eloquent integration — HasBitMasks
Declare mask columns via $bitMasks (plain names, or column => FlagEnum::class). The trait then:
- Casts each column to a
BitMask(viaAsBitMask), unless you declared your own cast. -
Adds instance helpers (mutations are in-memory; chain
->save()to persist): - Adds query scopes (portable across MySQL / PostgreSQL / SQLite):
| Scope | SQL | Matches rows… |
|---|---|---|
whereMaskHas($col, $flags) |
(col & m) = m |
with all flags |
whereMaskHasAny($col, $flags) |
(col & m) != 0 |
with any flag |
whereMaskMissing($col, $flags) |
(col & m) = 0 |
with none of the flags |
whereMaskEquals($col, $flags) |
col = m |
exact mask |
Each has an orWhere* twin, and accepts an optional $boolean argument for manual grouping:
You can also assign masks naturally — the cast resolves anything flag-ish:
Setting attributes without the trait
The cast is usable standalone:
Collections
The scopes have in-memory twins, registered as Collection macros — they work on Eloquent collections of trait-using models and on plain collections of arrays/objects:
Generator reference
Names are normalized into identifiers (yahoo mail → YahooMail / YAHOO_MAIL); duplicates and >63-bit overflows are rejected. --start lets you append new flags to an existing sequence without renumbering (generate a second class, or regenerate with the full list).
--namespace, --path and --type fall back to the published config (see
Configuration) before the built-in defaults.
Modular structure (nwidart/laravel-modules)
When your application uses nwidart/laravel-modules, there are two ways to generate inside a module:
The module:make-bitmask command follows the nwidart convention: module is an optional positional argument that falls back to module:use's stored module.
The generator reads the module's own composer.json PSR-4 autoload to determine the correct namespace and source directory — so modules with custom namespaces (e.g. MyLink\Cerm\Core\ mapping to app/, or Vendor\CRM\Post\ mapping to src/) work correctly. Falls back to modules.namespace + modules.paths.app_folder from the nwidart config when no composer.json exists. Explicit --namespace or --path still override the module-derived values.
--type=constants produces a plain class for codebases that prefer constants:
Schema helper
Wide masks — more than 63 flags
One BIGINT holds 63 usable flags. When you need more (this package was built for "which of 100+ networks is this email listed in?"), a wide mask presents a single logical mask backed by several BIGINT columns — 63 flags each — so two columns give you 126 flags, three give 189, and so on.
The distinction is in how flags are addressed. A single-column flag enum uses the
bit value (1 << n); a wide-mask flag enum uses the global index (0,
1, … 125), because a mask past bit 62 can't fit in one PHP integer. Index n
routes to column n / 63, bit n % 63.
1. Flag enum backed by global index:
2. Columns — the wideBitMask blueprint macro creates networks_1, networks_2:
3. Declare it on the model with an array value (vs. a bare enum for a single column):
4. Same API — the value is now a WideBitMask:
The scopes emit per-column bitwise predicates automatically (AND across columns
for has / missing / equals, OR for hasAny), all wrapped in a single
grouped clause so they compose with orWhere* and your other conditions.
WideBitMask mirrors BitMask (has / hasAny / hasNone / equals / add / remove / toggle / clear / bits / flags / names / count); ->columns() returns the raw
per-column integers, and bits() returns global indices.
Junction (pivot) masks — large or dynamic flag sets
When flags are too many even for a wide mask, or you want each membership as its
own row (easy bulk load, per-flag reverse lookups, flag_ids that aren't
contiguous bit positions), store them in a thin junction table —
(owner_key, flag_id), one row per set flag — instead of mask columns. The
flag_id is the int-backed enum case's value (any non-negative integer, not a
bit position), so the flag count is effectively unbounded.
It's the same declaration and the same API — only the storage differs.
1. Flag enum (values are arbitrary flag ids):
2. Junction table — the flagPivot macro adds the flag_id column, the
composite primary key, and a reverse index (flag_id, owner) for
"which owners carry flag X?":
3. Declare it on the model with a pivot key. foreignPivotKey, flagKey
and ownerKey are optional — they default to the model's foreign key, flag_id,
and the model's primary key:
4. Same API — the value is now a FlagSet:
Notes:
- Mutations flush on
->save()(same contract as the column strategies) — the pivot rows are reconciled to the buffered set. A flush needs the owner key, so save the model at least once. Deleting the model purges its junction rows. - Scopes emit correlated
EXISTS/NOT EXISTSsubqueries:whereMaskHasis anEXISTSper flag,whereMaskHasAnyanEXISTS … flag_id IN (…),whereMaskMissingitsNOT EXISTS, andwhereMaskEqualsan exact match. All four (and theirorWhere*twins) compose with your other conditions. FlagSetmirrorsBitMask(has / hasAny / hasNone / equals / add / remove / toggle / clear / flags / names / count);->ids()returns the raw flag ids.
Choosing a storage strategy
| Flags | Strategy | Declaration |
|---|---|---|
| ≤ 63 | single column | 'networks' => Network::class |
| 64 – a few hundred | wide mask | 'networks' => ['columns' => N, 'enum' => …] |
| many / dynamic / bulk-loaded | junction table | 'networks' => ['pivot' => 'table', 'enum' => …] |
Bitmask columns keep every flag in the row (one-row point lookups, no joins); junction tables trade that for unbounded, individually-indexable flags.
Limits & querying at scale
- One column holds 63 usable flags (bit 63 is the sign bit on signed BIGINT engines). The generator enforces this; need more? Reach for a wide mask (several BIGINT columns behind one logical name), or — beyond a few hundred dynamic flags — a
(flag_id, row_id)junction table with a composite index usually queries better. -
whereMaskHas-style predicates can't use a plain B-tree index; on huge tables (hundreds of millions of rows) PostgreSQL partial indexes keep hot-flag queries fast: - Point lookups (
WHERE email = ?) are unaffected — the mask rides along in the row.
Configuration
config/bit-masks.php holds the generator defaults; each make:bitmask option
still overrides its config value per invocation:
When --module is used, the module's root namespace comes from its own
composer.json PSR-4 autoload (not the global config). The sub-namespace
(BitMasks) is derived from these values by stripping the first segment
(e.g. App\BitMasks → BitMasks), and appended to the module's root.
So changing namespace to App\Enums\Flags means modules will generate
into {ModuleNamespace}\Enums\Flags.
Testing
License
MIT © Zuko
All versions of laravel-bit-masks with dependencies
illuminate/console Version ^11.0|^12.0|^13.0
illuminate/contracts Version ^11.0|^12.0|^13.0
illuminate/database Version ^11.0|^12.0|^13.0
illuminate/support Version ^11.0|^12.0|^13.0