kduma/eloquent-encryptable
Transparently encrypt sensitive Eloquent attributes at rest. A simple trait encrypts chosen fields on save and decrypts them on read, so your code keeps using plain values while the database stores ciphertext—no schema changes required.
Before installing, check whether Laravel’s native encrypted cast already covers the use case. This package is archived, has very low adoption, and overlaps with a first-party feature; use it only for legacy compatibility or a specific behavior you cannot reproduce with the native cast.
Minimal setup:
composer require kduma/eloquent-encryptable
Pin the exact version in composer.json and consider forking/vendoring it, since archived packages receive no Laravel-version or security updates. Ensure APP_KEY is set and backed up securely; losing it means losing all encrypted data.
Add the package’s encryptable trait to the model and declare the attributes to encrypt. Confirm the exact trait FQCN and property name in the installed README/vendor source, as the package is small and version-specific:
use Kduma\Eloquent\Encryptable\Encryptable;
class Customer extends Model
{
use Encryptable;
protected $encryptable = [
'tax_id',
'phone',
'address',
];
}
Use TEXT/LONGTEXT columns, not VARCHAR(255); ciphertext is much longer than plaintext. For an existing table, plan a backfill rather than expecting existing plaintext rows to decrypt automatically.
First practical use case: encrypt PII or secrets that are only displayed/edited in detail views, never searched or sorted in SQL — for example tax IDs, phone numbers, home addresses, API tokens, or 2FA secrets.
Where to look first: the README, the trait source in vendor/kduma/eloquent-encryptable, the package tests, and the release history. Verify which Laravel versions the pinned release claims to support.
Basic encrypted model attributes. Keep encrypted fields as simple strings, hide them from JSON, and expose them through explicit resources or detail views:
class Customer extends Model
{
use Encryptable;
protected $encryptable = ['tax_id', 'phone'];
protected $hidden = ['tax_id', 'phone'];
}
Searchable fields via a blind index. Encryption is non-deterministic, so where('email', $email) will not work. Store a separate deterministic hash/HMAC column for exact-match lookup:
$customer->email_hash = hash_hmac('sha256', strtolower($email), config('app.key'));
Customer::where('email_hash', $hash)->first();
Use a dedicated HMAC key rather than raw APP_KEY if you need to rotate encryption keys independently.
Backfilling existing plaintext data. Write an artisan command using chunkById, read the plaintext, assign through the model so the trait encrypts it, and save. Do not use DB::table()->update() for the encrypted column.
Key rotation. Plan a dual-read window: decrypt old values with the old key, re-encrypt with the new key, and backfill in batches. This package does not provide key rotation; you must build it.
Combining with accessors and casts. Keep the encrypted attribute as a raw string and layer formatting in accessors/resources. If you need arrays or JSON, verify whether the package serializes values before encryption; otherwise serialize explicitly:
protected $encryptable = ['settings'];
public function setSettingsAttribute($value): void
{
$this->attributes['settings'] = json_encode($value);
}
Testing. Set APP_KEY in phpunit.xml. Assert plaintext through the model and ciphertext through the query builder:
$this->assertSame('123-45-6789', $customer->tax_id);
$this->assertNotSame('123-45-6789', DB::table('customers')->value('tax_id'));
Admin/UI integration. Treat encrypted columns as non-searchable and non-sortable. In Filament/Nova or custom admin screens, show decrypted values only on detail/edit pages, and avoid global search over those columns.
encrypted cast where possible.WHERE, ORDER BY, LIKE, GROUP BY, and unique indexes on encrypted columns are effectively useless. Use blind indexes for exact lookups and accept that fuzzy search requires a separate search store.TEXT; VARCHAR(255) will truncate or fail.APP_KEY is a single point of failure. Rotating it without re-encrypting makes all values undecryptable. Back up the key and test restore procedures.DB::table(), Model::insert(), bulk upserts, and raw SQL skip the trait entirely. Enforce model-level writes or add explicit encryption in those paths.toArray() and API resources may expose decrypted values. Add encrypted attributes to $hidden and use explicit API resources.firstOrCreate/updateOrCreate pitfalls. These query by the plaintext value, which will not match ciphertext. Query by the blind-index hash instead.null, '', and non-string values in your pinned version; behavior may differ from the native cast.Crypt::encryptString vs Crypt::encrypt/serialization). Test decryption compatibility before switching.getRawOriginal()/getAttributes() to see ciphertext versus accessor output. Catch DecryptException during key or data migrations, and log the record identifier — not the ciphertext or plaintext.How can I help you explore Laravel packages today?