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

Doctrine Encrypt Bundle Laravel Package

nowo-tech/doctrine-encrypt-bundle

A Symfony bundle for transparent field-level encryption in Doctrine entities. Protect sensitive database columns with encrypted DBAL types, PHP attributes, and configurable key management—ideal for GDPR compliance and secure application data.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install and register. composer require nowo-tech/doctrine-encrypt-bundle. There is no Flex recipe, so add the bundle manually to config/bundles.php (NowoTech\DoctrineEncryptBundle\DoctrineEncryptBundle::class => ['all' => true]) and create config/packages/doctrine_encrypt.yaml. Confirm the exact root key and option names against src/DependencyInjection/Configuration.php — for this family of bundles it is typically doctrine_encrypt: with secret/key and encryptor (one of halite, defuse, mysql_aes).
  2. Store keys outside the repo. Point the key at an env var resolved from Symfony Secrets or your vault (e.g. %env(DOCTRINE_ENCRYPT_KEY)%). Never commit a key; a leaked key plus a leaked dump equals plaintext.
  3. Mark the field. Add the bundle's encrypted-field attribute (annotation-style @Encrypted / #[Encrypted]) to the entity property you want protected. Keep the property typed as string and change the Doctrine column to TEXT/LONGTEXT before the first write — ciphertext is Base64-wrapped and far longer than the plaintext.
  4. Verify the round trip end-to-end. Persist an entity, flush(), clear(), reload, and assert the property is plaintext. Then bypass the ORM (SELECT via dbal or mysql CLI) and assert the stored value is opaque. That second check is the one people skip and regret.
  5. First use case. Pick a narrowly-scoped PII column on an existing table — national_id, iban, date_of_birth, medical notes — not your whole model. Encrypt one field, ship it, then expand. Where to look first in the code: the encryptor implementations under src/, the interface they implement (your extension point), and the Doctrine event subscriber that does the work on postLoad/prePersist/preUpdate.

Implementation Patterns

Attribute-per-field, no accessor changes. Encryption is transparent at hydration time, so getters, setters, form types, and serializers keep working with plaintext. Keep entity properties as plain PHP strings; do not add custom Doctrine types or convertToDatabaseValue logic unless you have a specific reason.

Pick the encryptor against your query budget, not your taste. Halite is authenticated, randomized, and the sane default for data you never filter on. Defuse is the compatibility path for databases already holding Defuse ciphertext. mysql_aes pushes crypto into MySQL/MariaDB (AES_ENCRYPT/AES_DECRYPT) and is the only one of the three that lets you compare ciphertext in SQL — at the cost of vendor lock-in and the key travelling in queries.

Searchable fields = blind index, always. Add a second, unencrypted column holding hash_hmac('sha256', $normalizedValue, $indexKey) and query on that. Index it, put unique constraints on it, and never WHERE the encrypted column. This is the single pattern that decides whether the bundle is pleasant or painful on day three.

Key rotation as an explicit runbook.

  1. Generate a new key, add it as the active key while keeping the old one in the decrypt-only list.
  2. Run the bundle's re-encryption console command (or write one that iterates in batches with EntityManager::clear() every N rows — large tables will exhaust memory otherwise).
  3. Verify a sample by decrypting with the old key removed in staging.
  4. Retire the old key only after your oldest backup has aged out.

Multi-key config is the safety net. Keep historical keys in the config for at least as long as your oldest restorable backup. A restore plus a dropped key is unrecoverable data loss.

Incremental migration on live tables. Add secret_v2 as a nullable encrypted column, backfill asynchronously, dual-read (prefer v2, fall back to v1), then drop v1. Avoid rewriting a populated column in place during a deploy.

Custom encryptor as the extension point. Implement the bundle's encryptor interface, register it as a service, and reference the service id in encryptor:. Useful when you need KMS/HSM-backed keys rather than a raw string in config.

Test fixtures. Set a fixed test key in .env.test, seed fixtures with plaintext (the subscriber handles the rest), and never assert on exact ciphertext — randomized encryptors produce different bytes every run. Assert decrypt-equality instead.

Forms and API payloads. Nothing changes for the happy path, but add explicit tests for an encrypted field inside a CollectionType / nested DTO, since partial-hydration bugs surface there first.

Gotchas and Tips

  • Column length truncation is the classic failure. VARCHAR(255) silently truncates long ciphertext on MySQL (or throws in strict mode). Use TEXT/LONGTEXT from day one; migration later means a full table rewrite.
  • Partial hydration returns ciphertext. SELECT PARTIAL, array hydration, scalar queries, and NEW Dto(...) DQL all skip postLoad. Only full object hydration decrypts. This is the number one "why is my API returning garbage" bug.
  • Native SQL and DBAL bypass the subscriber entirely. Any createNativeQuery or raw connection read gives you Base64. Route those through the encryptor service manually or don't use them on encrypted columns.
  • No sorting, LIKE, joins, aggregates, or unique constraints on encrypted columns. Design the schema around the blind index before writing repository code.
  • Changing the encryptor or key without re-encrypting is a one-way trip. Old rows become undecryptable. Always keep prior keys configured and re-encrypt as a separate, verified step.
  • Deterministic modes leak pattern information. mysql_aes (and any deterministic setup) reveals equal values across rows. Only acceptable for low-cardinality, low-sensitivity lookups with a hashed blind index; otherwise use a randomized encryptor.
  • Performance cost is per-field, per-row, on hydration. Encrypting large TEXT blobs on wide result sets is measurable. Benchmark a 1k-row list before encrypting a field the UI renders in bulk, and consider keeping bulk-displayed content out of scope.
  • Clear the cache after touching keys. bin/console cache:clear, then bin/console debug:config doctrine_encrypt to confirm the live key set — a stale config cache pointing at the wrong key looks exactly like corrupted data.
  • Watch out for empty string versus null. Typically null stays null while '' encrypts; check your entity semantics and be consistent, especially if a NOT NULL column is involved.
  • Keep ciphertext out of logs and dumps. Doctrine SQL loggers, dump() in tests, and error-tracking payloads all capture raw parameters — and after decryption in memory, plaintext PII can leak into exception traces.
  • Portability trap with mysql_aes. It requires MySQL/MariaDB and a session key variable; it will not survive a move to PostgreSQL. Choose the encryptor with your hosting roadmap in mind.
  • Backups require key history. A restore from six months ago needs the key active six months ago. Maintain key versioning in your vault, not just in the config file.
  • Small ecosystem — pin and test. With a handful of stars and no dependents, treat upstream as a dependency you own: pin a version, keep one round-trip integration test of your own (persist → clear → reload), and re-run it on every upgrade. That single test catches most regressions in this class of bundle.
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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect