php-standard-library/hash
Cryptographic and non-cryptographic hashing for PHP with an Algorithm enum, timing-safe comparison, and HMAC support.
Install and verify the surface first. The README is a marketing stub — the real API contract lives in vendor/php-standard-library/hash/src/. Spend five minutes there before writing code.
composer require php-standard-library/hash
ls vendor/php-standard-library/hash/src/
Confirm three things while you're in there: the exact namespace (PhpStandardLibrary\Hash\... per upstream docs — verify), the casing of the Algorithm enum cases (SHA256 vs Sha256 will silently break a copy-pasted snippet), and whether the generator methods return hex or raw binary by default.
First use case: webhook signature verification. This is the highest-value, lowest-risk entry point because Laravel gives you nothing for it out of the box. Today most Laravel codebases do this:
$expected = hash_hmac('sha256', $payload, $secret);
if (! hash_equals($expected, $signature)) {
abort(403);
}
It works, but it's copy-pasted across every integration, each copy a chance to use === instead of hash_equals, or to compare hex against raw binary. Wrapping it once:
use PhpStandardLibrary\Hash\{Algorithm, HMAC, HashComparator};
$expected = HMAC::generate($payload, $secret, Algorithm::SHA256);
if (! HashComparator::equals($expected, $signature)) {
abort(403, 'Invalid signature');
}
Put that in a middleware (VerifyWebhookSignature) and you've standardized every inbound webhook in the app with one class. Don't build a service provider or facade on day one — call the package directly until you have two or three call sites.
Second stop: ETags and upload checksums. Where you'd currently write hash('sha256', file_get_contents($f)) for an S3 integrity check or a CDN ETag, the enum makes the algorithm a config value instead of a magic string.
One helper class, not scattered calls. The package's own win is consistency; don't squander it by calling it from fifteen places. Create app/Support/Hasher.php:
namespace App\Support;
use PhpStandardLibrary\Hash\{Algorithm, HashGenerator};
final class Hasher
{
public static function key(mixed $input): string
{
return HashGenerator::generate(self::canonical($input), Algorithm::SHA256);
}
private static function canonical(mixed $input): string
{
if (is_array($input)) {
ksort($input); // array order is the #1 cache-key bug
}
return json_encode($input, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION);
}
}
Then Cache::remember(Hasher::key(['user' => $id, 'page' => 3]), ...). Every cache key in the app is now the same shape and debuggable.
HMAC for anything you'd otherwise hash with a secret. A common Laravel mistake is pseudonymizing PII in logs with a bare hash:
Log::info('login', ['email' => hash('sha256', $email)]); // reversible via rainbow table
Emails are low-entropy and enumerable. Use HMAC keyed off APP_KEY:
$pseudonym = HMAC::generate($email, config('app.key'), Algorithm::SHA256);
Now it's a stable join key across logs but not reversible by brute force.
Key rotation by design. For API request signing (mobile client ↔ Laravel API), prefix the signature with a key ID so you can rotate without downtime:
// header: X-Signature: v2:<hex>
$expected = HMAC::generate($body, $keys[$version], Algorithm::SHA256);
Load $keys from config, not a constant. Rotating a hardcoded HMAC secret is a deploy-then-pray exercise.
Use the non-cryptographic algorithms where they belong — and only there. For bulk deduplication, cache bucketing, or bloom-style filters, xxh/CRC-class algorithms are dramatically faster and collision risk is acceptable. Keep them out of auth paths; the enum makes the distinction visually obvious in code review in a way that md5( vs sha256( in argument strings does not.
Test against native PHP vectors. Because this package wraps ext-hash, you can assert equivalence — which protects you if you ever swap the package out:
$this->assertSame(
hash('sha256', 'payload'),
HashGenerator::generate('payload', Algorithm::SHA256)
);
That single test also documents the output encoding, which is the thing most likely to bite you.
Bind it in the container only once you have DI needs. If you want to mock hashing in tests (e.g., deterministic ETags), bind a HasherContract interface in AppServiceProvider and back it with the package. Otherwise concrete static calls are fine and simpler.
Never let this replace Illuminate\Support\Facades\Hash. Laravel's facade is password_hash() — bcrypt/argon2 with a per-hash salt and a tunable cost factor. This package is a fast hash. Using it for passwords in a users.password column is a catastrophic regression that will pass every test you write. Enforce the boundary in a team guideline: password hashing → Hash::; everything else → php-standard-library/hash.
Hex vs. raw binary is the classic timing-safe bug. hash_equals() requires both operands to be the same length and encoding. If the package returns raw binary and your incoming header is hex (or vice versa), the comparison returns false — or worse, silently fails in a way that looks like an attack. Normalize explicitly at the boundary and add a test asserting strlen($expected) === strlen($signature).
Algorithm availability is platform-dependent. hash_algos() differs across PHP builds; xxh3/blake3 are not guaranteed. If your Algorithm enum case maps to an unavailable algorithm you'll get an exception at runtime, not boot time. Add a boot-time assertion in a service provider if you configure a default algorithm via env:
abort_unless(in_array(config('hashing.default'), hash_algos(), true), 500, 'Missing hash algorithm');
Don't file_get_contents() a 2GB upload into memory to checksum it. Wrap a stream instead — most hashing APIs expose incremental/streaming context. Otherwise your "cheap integrity check" is a memory exhaustion vector.
Truncating hashes needs care. Short prefixes for readable cache keys or log correlation IDs are fine, but 8 hex chars is 32 bits — collisions at ~65k entries are a birthday-paradox certainty. Keep full-length for anything involving money, auth, or idempotency.
Canonicalization, not the hash, is where cache bugs live. ['a' => 1, 'b' => 2] and ['b' => 2, 'a' => 1] produce different JSON and different hashes. Sort keys. Also watch floats (1.0 vs 1 — hence JSON_PRESERVE_ZERO_FRACTION) and Eloquent models (hash $model->getKey() + updated_at, not $model).
Vendor risk is real here. Zero stars, zero dependents, and release notes that just point at the monorepo mean you are the only person who will notice a regression. Two mitigations: (1) route every call through your own Hasher class so a swap to native hash() is a one-file change, and (2) pin the version in composer.json and let Dependabot open PRs rather than floating ^.
Debug by dumping both sides, always. When a signature check fails, dd(bin2hex($expected), bin2hex($signature), strlen($expected), strlen($signature)) before touching the algorithm choice — 90% of the time it's encoding, a trailing newline from file_get_contents('php://input'), or a proxy re-serializing the JSON body (which changes the bytes and therefore the signature). Log the raw body alongside the failure, not just "invalid signature."
Extension point worth knowing: the Algorithm enum plus a single generator function means a thin decorator — App\Support\Hasher above — is a natural seam for adding a config-driven default algorithm, a length-prefixed signature format, or a metrics counter for hashing calls in hot paths.
How can I help you explore Laravel packages today?