Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Encryptable Laravel Package

maize-tech/laravel-encryptable

Laravel Encryptable adds transparent encryption for Eloquent model attributes, keeping sensitive data secure at rest while preserving a familiar query workflow. A simple trait and cast setup lets you encrypt, decrypt, and search encrypted values without restructuring your models or database.

View on GitHub
Deep Wiki
Context7

Getting Started

Version note: 4.1.0 is a compatibility release only — it adds Laravel 13 and PHP 8.5 to the support matrix (plus a Dependabot bump). No API changes, no deprecations, no migration steps. Treat it as a safe composer update and a signal the package is actively maintained. If you were holding off adopting it because your app targets the newest Laravel/PHP, that blocker is gone.

  1. Install and publish the config:
composer require maize-tech/laravel-encryptable
php artisan vendor:publish --tag="encryptable-config"
  1. Generate/set a dedicated key in .env (do not blindly reuse APP_KEY — see Gotchas), then php artisan config:cache.

  2. Add the trait to one model and declare the attributes to encrypt:

use Maize\Encryptable\Encryptable;

class Customer extends Model
{
    use Encryptable;

    protected array $encryptable = ['tax_id', 'phone', 'address'];
}
  1. Make the columns big enough before you write anything. Ciphertext is base64 of payload + IV + MAC, so budget ~1.4× the plaintext plus overhead. TEXT/LONGTEXT; never VARCHAR(32) for a phone number.

First use case: PII on an existing customers or users table. Write one row in php artisan tinker, reload it, then inspect the raw column (DB::table('customers')->value('phone')) to confirm you see ciphertext, not plaintext. That single check validates key wiring, config caching, and column width in one shot.

Implementation Patterns

Opt-in per model, not globally. Put the trait only on models that hold regulated or secret data (PII, tax IDs, 2FA secrets, API tokens, bank details). Encrypting everything destroys index/query ergonomics for no benefit.

Keep encryption independent of APP_KEY. Configure the package key from its own env var and store it in your secrets manager. If encrypted data and APP_KEY share a lifecycle, one routine key rotation silently bricks historical rows. Document that the key belongs in the same backup set as the database dump — a dump without its key is unrecoverable.

Blind indexes for lookups. Since ciphertext is non-deterministic, where('email', $x) cannot work. Add a sibling column holding a deterministic HMAC of the value:

$model->email_index = hash_hmac('sha256', $email, config('encryptable.key'));

Look up on the index, resolve the row, then decrypt to confirm. Same trick for uniqueness validation and "find by phone" support flows.

Backfill pattern for existing plaintext. Never flip $encryptable on a populated table in one deploy. Instead: (a) add the wide column, (b) ship a queued command that reads rows with DB::table()->orderBy('id')->chunkById() and writes ciphertext via a temporary model without the trait, (c) deploy the trait, (d) drop the plaintext column in a later release. Keep steps (c) and (d) in separate deploys so rollback stays possible.

Explicit casts before encryption. Apply datetime/integer/enum casts before encryption (or avoid encrypting non-strings entirely). Encrypting an already-cast Carbon and later relying on another cast leads to double-conversion surprises; keep encrypted attributes as plain strings.

Serialization is already safe. toArray()/toJson() return decrypted values, so API resources and Inertia props work unchanged. Conversely getAttributes() and any DB::table() access return ciphertext — good for logs, bad for debugging.

Testing. Factories and seeders work transparently (assignment encrypts). For assertDatabaseHas, either assert decrypted values through the model or pre-encrypt the expectation; asserting the raw plaintext string against the table will fail and frustrate whoever writes the next test. Add one test that asserts raw DB content differs from plaintext — it catches accidental trait removal during refactors.

Key rotation runbook. Decrypt with the old key, re-encrypt with the new one, in a single transaction per batch, with the old key still present in config during the window. Never delete the old key until you've verified sample rows.

Gotchas and Tips

  • No searching, sorting, or aggregation. LIKE, ORDER BY, MIN/MAX, and range whereBetween are meaningless on ciphertext. Sort on a non-sensitive column or a derived plaintext field.
  • whereIn, updateOrCreate, and firstOrCreate silently break — they query with plaintext against ciphertext and either miss or create duplicates. Route these through blind indexes.
  • unique: / exists: validation rules will not see existing records. Validate against the index column instead.
  • Double encryption happens when you write ciphertext back through the trait (e.g. $model->fill($rowFromRawQuery)), or when you copy a value from getRawOriginal() instead of the decrypted attribute. Use $model->getRawOriginal('phone') only when you explicitly want the ciphertext.
  • Column width is the #1 production failure. A 255-char VARCHAR overflows silently on MySQL non-strict mode or throws in strict mode. Always TEXT.
  • APP_KEY rotation destroys unreadable rows if the package key falls back to the app key. Pin the dedicated key explicitly and assert it's non-null at boot.
  • config:cache + env changes: if your deploy pipeline caches config before injecting the key, every encrypted write uses the wrong/null key. Put the key in the environment before the cache step, and fail fast if it's missing.
  • Logs and error trackers leak: exception payloads containing model attributes may serialize decrypted values. Audit your Log::info($model) calls and query logs.
  • Backups are environment-bound. A production dump restored into staging without the production key yields gibberish — often discovered mid-incident. Keep the key alongside the dump in the restore runbook.
  • Model events fire with decrypted values, so observers/hooks that diff getOriginal() vs current work as expected; but observers writing to audit tables will store plaintext unless you redact.
  • Extension point: if you need per-attribute control (some columns encrypted, some not, or a different key per tenant), reach for the package's cast/config rather than stacking the trait on a model that also has complex $casts — conflicting cast declarations are the usual source of "it decrypts in tinker but not in the job" bugs.
  • Debugging checklist when a value comes back wrong: confirm the model actually uses the trait; confirm the attribute is in $encryptable; confirm the config cache is fresh; compare getRawOriginal() against what's in the DB. That sequence resolves nearly every report.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource