erikwang2013/encryptable
Encryptable is a Laravel trait that transparently encrypts and decrypts Eloquent model attributes, keeping sensitive database fields safe with minimal code changes. Just declare encrypted columns and let the package handle the rest.
This release is a maintenance/patch release (v2.0.13) on an already-published 2.x line, so treat it as a safe bump rather than a migration event. There are no breaking changes listed, but two behaviour changes are worth verifying against your existing data (see Gotchas).
Minimal steps to get productive:
composer require erikwang2013/encryptable and confirm your app meets the PHP 8.0+ baseline declared in the README.cipher and previous_keys entries — this is where most day-to-day surprises live.$encryptable / $encrypted array — confirm the exact property name against the README quickstart for your version).Where to look first in the docs:
Encryption::db() helper section — this is the escape hatch for encrypting/decrypting outside of model attributes.previous_keys section — this is your key-rotation mechanism, and it changed in this release.First use case to target: a single column containing PII that you already store (an API token, a national ID, a phone number). Do one column, one model, ship it, then expand — the pattern does not get harder, but the failure modes do.
Trait-per-model, explicit attribute lists. Declare encrypted attributes explicitly rather than trying to "encrypt everything." An allowlist keeps you honest about which columns are searchable, indexable, and which will break where() clauses later. Keep the encrypted attribute names mirrored in $fillable/$casts handling as the README prescribes so mass assignment and serialisation behave predictably.
Key rotation via previous_keys is the main operational pattern. The standard workflow is: append the new key as primary, move the old key into previous_keys, deploy, then run a re-encryption pass (read + save each record) and finally remove the old key. In v2.0.13 previous_keys now accepts a base64: prefix, matching the primary key format — so if your APP_KEY-style keys are base64-encoded, rotation entries no longer need to be stripped to raw bytes manually. Normalise this in a single config source (env var → config array) so Laravel and any non-Laravel sibling service produce byte-identical key material.
Cipher resolution across runtimes. Because rules now read the cipher from the parsed encrypter, the practical pattern is: define the cipher once per runtime's config, and assert at boot that it matches what the other services in the same data plane use. AES-256-CBC vs AES-256-GCM is not a cosmetic difference — ciphertext written by one is not readable by the other. Add a smoke test or health check that decrypts a known fixture row.
Use isEncrypted() for backfills and mixed-data migrations. This release documents isEncrypted() as a lightweight format check rather than a decrypt attempt, which makes it the right tool for:
if (! $model->isEncrypted('column')) { re-save })Treat it as a shape check, though: it tells you the value looks like ciphertext, not that it will decrypt successfully with the current key.
Use Encryption::db() for non-model data. Session payloads, cache entries, queued job arguments you control, config blobs in a settings table — the DB helper keeps those on the same key/cipher as your models, which matters enormously during rotation. Anything encrypted with a different helper becomes an orphan when you rotate.
Multi-runtime integration. For Webman/Hyperf/ThinkPHP services touching the same tables, import the package and share key material via environment. Keep a single canonical source for the cipher and keys — duplicated config across runtimes is the most likely cause of "works in Laravel, decrypt error in the worker."
Ciphertext breaks where(). Encrypted columns are random-IV ciphertext; equality search, LIKE, ORDER BY and range queries all stop working. If you need lookups, store a deterministic blind index (HMAC of the normalised value) alongside the ciphertext and query that. Do not put a unique constraint on the ciphertext column itself unless the underlying value is deterministic — it isn't.
Verify the cipher behaviour change explicitly. fix(rules) means validation now reads the cipher from the actual resolved encrypter rather than a config key. If you had an inconsistent config (e.g. config('app.cipher') differing from what the encrypter was constructed with), the old behaviour may have masked a real mismatch. After upgrading, run a decrypt of a sample row in each environment — staging with a different cipher than production is a classic trap.
previous_keys with base64: is additive, not migrating. Supporting the prefix doesn't re-encrypt anything. Old rows stay encrypted with the old key until you rewrite them. Keep old keys in previous_keys for as long as any row remains un-rewritten, and log which key decrypted which record during the transition if you can.
Check for double-encoding on key material. base64: prefix means the package decodes it; a value that the framework already base64-decoded and you then prefix again will produce a wrong-length key and a confusing cipher error. Assert key length at boot.
Format check ≠ integrity check. isEncrypted() is cheap by design. Don't use it as an authorisation or tamper check. GCM gives you authentication tags on decrypt; CBC does not — if tamper-resistance matters for a column, choose the AEAD cipher and accept the format/perf tradeoff.
Serialisation and logging. Encrypted attributes leak in three places people forget: toArray()/JSON responses, Log::info($model) and queue payloads. Use $hidden or explicit resource classes. Also remember that ->toArray() after decryption returns plaintext in memory — dumping a model in a bug report is a data incident.
Config caching. php artisan config:cache in Laravel and equivalent mechanisms in Webman/Hyperf will freeze the cipher and keys at build time. Rotating keys without clearing/rebuilding config is the single most common "rotation didn't work" cause.
Extension points worth knowing. The package's cipher/key resolution and trait are the two places to hook if you need custom behaviour (e.g. per-tenant keys). Since cipher resolution is now centralised on the parsed encrypter, that's the natural seam — but keep any customisation thin, and re-run your decrypt smoke test after each patch release, since minor releases like this one do touch resolution logic.
Upgrade hygiene for this class of release. v2.0.13 is a docs + fix + CI release; the risk is behavioural drift in validation and key handling, not API breakage. Pin the version, run your decrypt smoke tests against a production-shaped fixture, and only then roll across services — starting with the runtime that writes the most rows, not the one that's easiest to deploy.
How can I help you explore Laravel packages today?