Bitwise operators and when to use them
Concept
Bitwise operators manipulate values at the binary level, operating on each bit of an integer independently. PHP provides six bitwise operators: AND (&), OR (|), XOR (^), NOT (~), left shift (<<), and right shift (>>). While they look similar to logical operators, they operate on individual bits rather than boolean truthiness, and they return integers rather than booleans.
The most common legitimate use of bitwise operators in PHP is the bitmask flag pattern. Instead of an array of booleans or multiple boolean columns in a database, you represent a set of boolean options as individual bits in a single integer. Each flag is a power of two (1, 2, 4, 8, 16, …), so each occupies exactly one bit position. You set flags with |, check flags with &, unset flags with & ~, and toggle flags with ^. PHP's own error_reporting() uses this pattern: E_ALL & ~E_NOTICE means "all errors except notices".
Bitwise NOT (~) flips all bits. On a signed 32-bit integer, ~0 is -1 and ~5 is -6. This is because PHP integers are signed and use two's complement representation. On 64-bit PHP (the default), integers are 64-bit signed. Left shift (<<) multiplies by a power of 2: $x << 3 is $x * 8. Right shift (>>) divides by a power of 2: $x >> 2 is $x / 4 (floor division for negative numbers in arithmetic shift).
In production PHP, you encounter bitwise operators in: permission systems (file permissions, user role bitmasks), flag fields in database columns (a single INT column storing multiple boolean flags), low-level protocol parsing (network protocol headers, EXIF data), cryptographic operations (sodium_crypto_* internals), and OPcache/JIT flag configurations. The PREG_OFFSET_CAPTURE | PREG_SET_ORDER pattern in preg_match_all is a classic PHP API example.
Compared to JavaScript, PHP's bitwise operators operate on 64-bit integers (JavaScript only has 32-bit), which matters for large bitmasks. Compared to C, PHP does not have unsigned integers, so right shift is arithmetic (sign-extending) rather than logical on negative numbers.
| Operator | Name | Example | Result |
|---|---|---|---|
& | AND | 0b1100 & 0b1010 | 0b1000 (8) |
| | OR | 0b1100 | 0b1010 | 0b1110 (14) |
^ | XOR | 0b1100 ^ 0b1010 | 0b0110 (6) |
~ | NOT | ~0b0101 | -6 (two's complement) |
<< | Left shift | 1 << 3 | 8 |
>> | Right shift | 8 >> 2 | 2 |
Code Example
<?php
declare(strict_types=1);
// Bitmask flag pattern — user permissions
const PERM_READ = 1; // 0001
const PERM_WRITE = 2; // 0010
const PERM_EXECUTE = 4; // 0100
const PERM_ADMIN = 8; // 1000
// Grant permissions
$userPerms = PERM_READ | PERM_WRITE; // 0011 = 3
// Check a permission
function hasPermission(int $userPerms, int $flag): bool
{
return ($userPerms & $flag) !== 0;
}
var_dump(hasPermission($userPerms, PERM_READ)); // true
var_dump(hasPermission($userPerms, PERM_EXECUTE)); // false
// Add a permission
$userPerms |= PERM_EXECUTE; // now 0111 = 7
// Remove a permission
$userPerms &= ~PERM_WRITE; // 0101 = 5 — clears WRITE bit
// Toggle a permission
$userPerms ^= PERM_ADMIN; // 1101 = 13 — flips ADMIN bit
// PHP's own error reporting uses this
error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
// PCRE flags combined with |
preg_match_all('/(\d+)/', '12 34 56', $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
// Storing bitmask in a database column (SQLite/MySQL INT)
// SELECT * FROM users WHERE (permissions & 4) != 0 — has EXECUTE
// Fast power-of-2 check using bitwise AND
function isPowerOfTwo(int $n): bool
{
return $n > 0 && ($n & ($n - 1)) === 0;
}
var_dump(isPowerOfTwo(16)); // true
var_dump(isPowerOfTwo(12)); // false
// Left/right shift as multiplication/division
$value = 1;
echo $value << 4; // 16 (1 * 2^4)
echo 64 >> 3; // 8 (64 / 2^3)
// Bitwise NOT — two's complement
echo ~0; // -1
echo ~5; // -6 (-(5+1))
// XOR trick: swap without a temp variable
$a = 10;
$b = 25;
$a ^= $b;
$b ^= $a;
$a ^= $b;
// $a = 25, $b = 10
// Extracting individual bit fields (e.g. parsing a network byte)
$byte = 0b10110100; // hypothetical status byte
$bits5to3 = ($byte >> 3) & 0b111; // extract bits 5-3
echo decbin($bits5to3); // '110'
// Binary, octal, hex literals for clarity
$flags = 0b00001111; // binary literal (PHP 5.4+)
$mode = 0755; // octal (file permissions)
$color = 0xFF6600; // hexInterview Q&A
Q: When is a bitmask integer preferable to a boolean column per feature in a database schema, and when is it a bad idea?
A bitmask column stores multiple boolean flags in a single INT column, which reduces schema width and can simplify certain bulk queries. It is a reasonable choice when: the set of flags is fixed and small (under 30), the flags are always read and written together as a unit, and you rarely need to query by individual flags. The downsides are significant for production systems: standard SQL indexing does not work on bitmask columns (a B-tree index cannot accelerate WHERE (flags & 4) != 0 without a functional index), adding new flags requires careful migration and documentation, and it is opaque to database admins and BI tools. For any flag that needs to be independently queried, indexed, or is likely to grow, separate boolean columns or a many-to-many permissions table (role/permission models) are far superior. In Laravel applications, the spatie/laravel-permission package uses a proper join table rather than bitmasks precisely for this reason.
Q: What does the ~ (bitwise NOT) operator actually do in PHP, and why does ~0 equal -1?
The tilde operator flips every bit in the binary representation of the integer. PHP integers are 64-bit signed two's complement numbers. The integer 0 is 0000…0000 (64 zeros). Flipping all bits gives 1111…1111 (64 ones), which in two's complement represents -1. More generally, ~n === -(n + 1) for any integer n. This identity is useful for inverting bitmasks: ~PERM_WRITE is an integer with every bit set except the WRITE bit, so perms & ~PERM_WRITE clears exactly the WRITE flag while leaving all others untouched. Developers sometimes confuse ~ (bitwise NOT) with ! (logical NOT) — ~true is -2 (because true casts to int 1, ~1 = -2), not false.
Q: How do PHP's bitwise shift operators behave differently from JavaScript's, and does the difference matter in practice?
PHP's << and >> operate on 64-bit signed integers. JavaScript's bitwise operators coerce operands to 32-bit signed integers first (>> is arithmetic sign-extending shift) and it also has a zero-filling right shift >>>. The practical difference: in PHP, a bitmask can use up to 63 bits (bit 63 is the sign bit); in JavaScript only up to 31. If you need more than 31 independent flags and need to share bitmask logic between PHP and JavaScript, JavaScript's >>> operator does not exist in PHP, and you may need BigInt on the JS side. In typical web application permission systems with under 20 flags, the difference is irrelevant. It becomes relevant in protocol parsing or cryptographic code where you shift values into the upper 32 bits of a 64-bit word — PHP handles this natively, JavaScript requires BigInt arithmetic.