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

Field Encrypt Bundle Laravel Package

insitaction/field-encrypt-bundle

Insitaction FieldEncryptBundle provides transparent field-level encryption for Doctrine entities in Symfony, letting you mark sensitive fields for automatic encryption and decryption with configurable keys and minimal code changes.

View on GitHub
Deep Wiki
Context7

Getting Started

What v3.0.5 actually changes

One line: Allow symfony 7. It is a constraint widening, not a behaviour change — no new attributes, no config key changes, no deprecations, no crypto changes. Two practical consequences:

  • Require ^3.0.5 (or pin 3.0.5) rather than ^3.0. On Laravel 11 / laravel-doctrine stacks that pull Symfony 7 components, 3.0.4 and below can hit a Composer conflict that only the newest patch resolves.
  • Everything the bundle does is identical to 3.0.4, so you do not need to re-validate encrypted payloads after upgrading. You should still re-run your test suite, because the resolved Symfony component versions underneath you changed.

Where to look first

Before writing any code, spend ten minutes in the vendor directory — the bundle is small enough to read end-to-end:

composer require insitaction/field-encrypt-bundle:^3.0.5
ls vendor/insitaction/field-encrypt-bundle/src

Open, in this order:

  1. composer.json — confirm the Symfony constraint the release widened and whether symfony/framework-bundle (or doctrine/doctrine-bundle) is a hard requirement. This determines whether it drops into a plain Laravel app via laravel-doctrine/orm or needs a bridge. That answer did not change in 3.0.5, but the widened constraint is what makes it installable next to Symfony 7 components.
  2. src/DependencyInjection/Configuration.php + Extension.php — the authoritative list of config keys (cipher, key source, key name). The README may lag behind the release.
  3. The Doctrine hook (EventSubscriber/, EventListener/, or a DBAL/Types/ class) — this tells you whether the bundle encrypts on flush/postLoad (transparent, entity-level) or via a custom column type. The two have very different implications for queries and migrations.
  4. The attribute/annotation class (Attribute/ or Annotation/) — copy its exact FQCN and accepted options rather than trusting docs.
  5. src/Command/ — if a re-encrypt/rotate console command ships with it, that's the single most valuable file for day-to-day operations.

Minimal setup

# config/packages/field_encrypt.yaml  (name may differ - check Extension::getAlias)
field_encrypt:
    key: '%env(FIELD_ENCRYPT_KEY)%'
// .env.local - never commit this, never reuse across environments
FIELD_ENCRYPT_KEY=base64:...
// config/bundles.php
Insitaction\FieldEncryptBundle\FieldEncryptBundle::class => ['all' => true],

First use case

Pick one genuinely sensitive, non-searchable column — User::$ssn, Customer::$iban, Invoice::$bankAccount — and annotate it:

#[Encrypted]           // exact name/namespace: verify against src/Attribute/
private ?string $iban = null;

Then, because ciphertext is larger than plaintext and often binary, widen the column before flushing anything:

#[ORM\Column(type: 'text', nullable: true)]
#[Encrypted]
private ?string $iban = null;

Verify success at the storage layer, not in PHP — reading the entity back through Doctrine will always show plaintext and prove nothing:

$row = $connection->fetchAssociative('SELECT iban FROM customer WHERE id = ?', [$id]);
// assert $row['iban'] !== $plainIban

If that assertion fails, the listener isn't registered, the attribute FQCN is wrong, or the field is being written by something that bypasses the ORM (raw SQL, DBAL, ->executeStatement()).

Implementation Patterns

Transparent entity-level encryption (the expected default)

The bundle's value proposition is that encryption is invisible: postLoad decrypts into the property, onFlush/prePersist/preUpdate encrypts before the value reaches the DBAL. Day-to-day this means:

  • Domain code stays clean — no EncryptionService injected into every service, no manual encrypt()/decrypt() calls, no ValueObject wrapper.
  • Hydration from any source that goes through the ORM (repositories, findBy, DQL, association loading) yields plaintext for free.
  • Anything that doesn't go through the ORM (raw SQL, DBAL fetchAllAssociative, a second app sharing the database, an analytics replica) yields ciphertext. This is usually what you want, but it will surprise a reporting job sooner or later.

Blind indexes for lookup — the pattern you'll actually need

Encrypted columns cannot be compared. If you need WHERE email = :email or a uniqueness constraint, add a sibling hash column and keep it in sync in the entity itself:

#[ORM\Column(length: 64, unique: true)]
private string $emailHash;

public function setEmail(string $email): self
{
    $this->email = $email;
    $this->emailHash = hash_hmac('sha256', mb_strtolower($email), $this->blindIndexKey);
    return $this;
}

Normalize before hashing and never change the normalization rule afterwards without a data migration — otherwise existing users become unfindable. Keep the blind-index key separate from the encryption key so you can rotate them independently.

Batch imports and fixtures

Encryption runs per-entity on flush. For a 100k-row import:

$em->beginTransaction();
foreach ($rows as $i => $row) {
    $em->persist($this->hydrate($row));
    if ($i % 200 === 0) { $em->flush(); $em->clear(); }
}
$em->flush();
$em->commit();

clear() matters — a bloated identity map plus per-entity crypto is a memory cliff. For fixtures, prefer a fixed test key in .env.test so encrypted dump/restore across test runs stays deterministic; DoctrineFixturesBundle + random keys will churn your snapshot diffs.

Backfilling existing plaintext

The bundle does not retro-encrypt rows that already exist when you deploy. Plan a three-step migration:

  1. Add/widen the column, deploy code that reads plaintext-or-ciphertext.
  2. Run a console command that loads each entity, touches the field ($e->setIban($e->getIban())), and flushes — the listener encrypts on update.
  3. Flip the config/deploy so the column is ciphertext-only.

Steps 1 and 2 must tolerate a mixed table; a decrypt() that throws on non-ciphertext input will abort the backfill halfway.

Testing

Assert both layers: entity-level plaintext and database-level ciphertext, using a KernelTestCase against a real database (sqlite works if the type is text; skip it if the bundle uses a binary/blob DBAL type).

self::assertSame($iban, $em->find(Customer::class, $id)->getIban());
self::assertNotSame($iban, $connection->fetchOne('SELECT iban FROM customer WHERE id = ?', [$id]));

Add a "wrong key" test that boots the kernel with a different FIELD_ENCRYPT_KEY and asserts the failure is a caught, logged exception rather than a silent null.

When upgrading to 3.0.5, run this suite on both 3.0.4 and 3.0.5 once if your app resolves Symfony 7 components — the bundle code is unchanged, but the surrounding framework services (EventDispatcher, DoctrineBundle) are not.

Gotchas and Tips

Adoption risk is still the headline, but it is no longer "abandoned". v3.0.5 is the first release since 2023-12-04, and it is a one-line constraint bump — which is either a reassuring sign the maintainer is alive, or a sign of minimum-viable maintenance, depending on your appetite. Zero dependents stands. Pin an exact version in composer.json, read the whole src/ before adopting, and keep the encryption surface behind a thin seam of your own (a single EncryptionInterface or the attribute + one small service) so replacing it with sodium_crypto_secretbox in-house is a day's work, not a quarter's. Do not let the attribute FQCN leak into 200 entities — or at least accept that a fork-and-replace is the exit plan, and budget for it. Also treat the widened Symfony constraint as untested-by-many until your own suite passes: a composer update that silently pulls Symfony 7.1 alongside a 3.0.4-era doctrine/doctrine-bundle is the classic way this bites.

Key loss = data loss. There is no recovery path. The key must live in a secret manager, be included in DR runbooks, and be backed up separately from the database dump — a dump plus key in the same S3 bucket is not encryption. Rotate deliberately: there is almost certainly no built-in rotation, so write a command that decrypts with the old key and re-encrypts with the new one, run it against a restored copy first, and keep the old key readable until the command has been verified.

Column sizing and indexes. Ciphertext adds IV + tag + base64 overhead — budget roughly 1.4× plaintext length plus a constant. VARCHAR(255) on a 200-char plaintext value will silently truncate (or hard-fail, depending on MySQL mode). More importantly: you cannot index, UNIQUE, LIKE, ORDER BY, or range-query an encrypted column. If your schema tool tries to revert the column type, pin it with columnDefinition or a custom DBAL type rather than fighting the diff.

Partial hydration bypasses decryption. ->select('c.id') in a QueryBuilder, a PARTIAL DQL, or a native query mapped to the entity will hydrate the property with raw ciphertext and never fire postLoad decryption. Symptom: base64 garbage leaking into an API response. Guard with a getIban() accessor that detects non-decryptable input in dev, or forbid partial selects on entities containing encrypted fields via a code-review rule.

Double encryption is the most common production bug. If any code path writes to the property and flushes in a loop, or if a preUpdate fires twice, you'll re-encrypt already-ciphertext data. Check $entity->getIban() for a recognizable prefix before treating it as plaintext during any bulk operation, and never call a bundle-level encrypt() manually on a value the listener will also encrypt.

Plaintext escapes through logs, queues, and serializers. The whole point of the bundle is that the property is plaintext in memory. That means dump($entity), Monolog's $context with an object, json_encode in an API response, serialize() into a Messenger envelope, and Symfony Profiler's Doctrine panel (which shows the parameters passed to the DBAL — check whether the bundle encrypts before or after logging) all leak. Add #[Ignore] on serialization and a __debugInfo() that redacts, and audit Messenger payloads explicitly.

Debugging decryption failures. openssl_decrypt/sodium failures typically surface as false or a thrown exception inside postLoad, which turns into an unhelpful hydration error far from the cause. To diagnose, fetch the raw value with DBAL and test it against the configured cipher and key in bin/console doctrine:query:sql — or a tiny throwaway command that boots the kernel and tries decrypt() on one known row. Nine times out of ten the cause is a changed key, a rotated .env, a value written by raw SQL, or a column truncated mid-ciphertext by a too-short VARCHAR.

Extension points worth knowing. The cipher/key resolution and the Doctrine listener are almost always autowired services in this kind of bundle — check Resources/config/services.yaml (or config/services.php) for the service IDs. Swapping the key provider for Vault/AWS Secrets Manager, or subclassing the listener to add audit logging on decrypt, is usually a one-line service override. If the listener isn't registered with an explicit priority, verify it runs before other onFlush subscribers that might rewrite the same field. None of these IDs moved in 3.0.5, so existing overrides keep working across the upgrade.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console