savannabits/sucipher
Sucipher is a Savannabits Laravel/PHP package that installs via Composer and drops into existing Laravel apps, with publishable configuration and a lightweight, developer-friendly setup. See the repository for installation steps, docs, and usage examples.
Sucipher is a first-release package (v1.0.0) that provides encryption and decryption helpers for Laravel. The first thing to do is read the README on the GitHub repo (savannabits/sucipher) since the release notes only state that encryption/decryption "fully works" — the public API surface (facade vs. helper vs. trait), config keys, and cipher defaults are documented there, not in the changelog.
Minimal setup:
composer require savannabits/sucipher
If the package ships a config file, publish it and review before first use:
php artisan vendor:publish --tag=sucipher-config
Where to look first:
APP_KEY or expects its own key.First practical use case: encrypt a single sensitive value before persisting it and decrypt it on read, e.g. an API token or a PII field:
use Savannabits\Sucipher\Facades\Sucipher;
$encrypted = Sucipher::encrypt($plaintext);
$plaintext = Sucipher::decrypt($encrypted);
Verify round-trip behavior in php artisan tinker before wiring it into models — confirm the output is a string you can store, and that decrypting a value encrypted with a different key fails loudly rather than returning garbage.
Model attribute encryption (write/read hooks). The most common day-to-day pattern is encrypting on save and decrypting on access. If the package doesn't ship a cast or trait, wrap it yourself:
protected function apiToken(): Attribute
{
return Attribute::make(
get: fn ($value) => $value ? Sucipher::decrypt($value) : null,
set: fn ($value) => $value ? Sucipher::encrypt($value) : null,
);
}
If the package does provide a cast, prefer it over hand-rolled accessors — it will handle nulls and already-encrypted values more consistently.
Service-layer boundary instead of model magic. For larger codebases, keep encryption at the service/repository edge so models stay plain. Encrypt just before save(), decrypt only in the presenter or resource layer. This avoids surprising behavior in toArray() and keeps query debugging sane.
Encrypting outbound payloads. A natural fit is encrypting specific payload fields for webhooks or inter-service requests, then decrypting the response server-side. Centralize this in a small CipherTransport wrapper so the cipher choice lives in one place and can be swapped or mocked in tests.
Testing pattern. Mock or bind a deterministic key in your test environment so assertions on encrypted payloads are stable. For feature tests, assert on decrypt(encrypt(x)) === x round-trips rather than hard-coding ciphertext — the algorithm or key source may change between releases.
Integration with queues and logs. Decrypt in the job/middleware, not before dispatch, and never log plaintext. If you use encrypted attributes, be aware Log::info($model) will expose decrypted values through toArray() unless you add the field to $hidden.
Key source ambiguity. The biggest unknown on a v1.0.0 is whether the package derives its key from APP_KEY, a dedicated SUCIPHER_KEY, or the config. Decide this explicitly before storing data — if it silently falls back to APP_KEY, rotating APP_KEY (e.g. during php artisan key:generate) will orphan every ciphertext you've written. Document the key source in your .env comments.
Decrypt failures are not forgiving. Encrypted values are all-or-nothing; a wrong key or truncated value throws rather than degrading. Wrap reads in try/catch (DecryptException or the package's equivalent) when reading legacy or user-supplied data, and decide whether to return null, log, or fail. Do not let a single bad row crash an index page.
Ciphertext is longer than plaintext. Base64 + IV/nonce/tag overhead means a value that fit in VARCHAR(255) may not after encryption. Size columns generously (TEXT for long fields) and test with realistic data before migrating existing rows.
You cannot query encrypted columns. No WHERE token = ?, no LIKE, no index usage. If you need lookup, store a deterministic hash (or a separate blind-index column) alongside the ciphertext — do not try to equality-match encrypted values.
Don't double-encrypt. Accessors/mutators that fire on every save() will re-encrypt already-encrypted values if a model is updated without being refreshed. Guard with an "is this already encrypted?" check or ensure the mutator only runs on changed values. isDirty() in the mutator (or an Attribute with a set hook that only transforms non-null input) helps.
Watch for $casts/serialization leaks. If you also use encrypted casts elsewhere, mixing Laravel's built-in Crypt and Sucipher in one model is a recipe for confusion — pick one per field and annotate it. Values from one cannot be decrypted by the other.
Extension points to inspect. Check for a Cipher contract or driver interface in the package. If one exists, you can swap implementations (e.g. libsodium vs. OpenSSL) via the container without forking. If not, wrap the facade in your own interface so you can migrate later.
Debugging tip. When decryption fails in staging but works in tests, compare config('app.key') and the published sucipher config across environments first — mismatched key derivation (base64 decoding vs. raw bytes) is the most common cause on first install.
Version pinning. This is v1.0.0 with no track record. Pin to ^1.0 in composer.json and re-run your round-trip tests after every minor bump; treat any change in cipher defaults as a data migration, not a routine update.
How can I help you explore Laravel packages today?