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 Encryption Bundle Laravel Package

matt9mg/doctrine-encryption-bundle

Symfony bundle for transparent field-level encryption in Doctrine entities, letting you store sensitive data encrypted in the database while keeping normal entity usage through configurable encryptors and mappings.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a thin Symfony bundle that registers Doctrine DBAL custom types which transparently encrypt/decrypt column values on persist and hydration. Treat it as a type-level shim, not a full encryption framework — the entire value proposition is "annotate a column, get ciphertext at rest."

Install and wire up:

composer require matt9mg/doctrine-encryption-bundle
  • Symfony 5.4+ Flex auto-registers bundles; on older setups add it to config/bundles.php.
  • Publish/author the config at config/packages/doctrine_encryption.yaml and supply a key. Generate one first (bin2hex(random_bytes(32)) or sodium_crypto_secretbox_keygen), store it in .env / secrets, never in the repo.
  • Register the type with Doctrine if the bundle doesn't do it via a compiler pass: doctrine.dbal.types with the FQCN, or a Type::addType() call in a kernel boot hook.

First use case — one column on one entity:

#[ORM\Column(type: 'encrypted_string', length: 1024)]
private string $iban;

The PHP-side contract is unchanged: $entity->getIban() returns plaintext, the iban column holds ciphertext. That's the whole integration. Verify the exact type alias (encrypted_string, encrypted, encrypt — varies) in the bundle's Doctrine/DBAL/Types directory before writing migrations.

Where to look first, in order:

  1. DependencyInjection/Configuration.php — the only source of truth for config keys (cipher, key, previous keys, key provider service).
  2. Doctrine/DBAL/Types/* — the type names and the actual cipher implementation. Read the ~50 lines; you need to know whether it's defuse/php-encryption, libsodium, or OpenSSL, because that dictates key format and rotation strategy.
  3. README.md for the entity annotation syntax.

Before you migrate: the DB column type doesn't change (ciphertext is still a string), so doctrine:migrations:diff will be empty. That means any existing plaintext rows will fail to decrypt on first read. Plan a backfill command, or ship the column as new.

Implementation Patterns

Transparent encryption is a read/write boundary, nothing more. Everything above the DBAL type layer — repositories, DQL, the Symfony serializer, Messenger payloads, dump() — sees plaintext. Everything at or below raw SQL sees ciphertext. Design around that line.

Searchable fields: pair the encrypted column with a blind index.

#[ORM\Column(type: 'encrypted_string', length: 1024)]
private string $email;

#[ORM\Column(length: 64, unique: true)]
private string $emailHash;   // hash_hmac('sha256', strtolower($email), $indexKey)

Set both in the constructor/setter so they can never diverge, and query on emailHash. This is the only pattern that gives you WHERE email = :x and a real unique constraint. Do not attempt LIKE against an encrypted column.

Keep the entity's public API plaintext; isolate crypto in the type. Don't sprinkle encrypt()/decrypt() calls in services — that's the failure mode this bundle exists to avoid. If you find yourself decrypting in a controller, the type isn't applied to that column or you're going through a raw query.

Bulk operations bypass the type. NativeQuery, Connection::executeStatement, and DQL UPDATE ... SET field = :val with a literal all skip type conversion. If you must bulk-update an encrypted field, load entities and flush(), or replicate the cipher in the migration. Same for getNativeQuery result sets — you'll be handed ciphertext and silently mistype it as plaintext.

Fixtures and tests: keep a fixed test key in .env.test, write fixtures in plaintext, and add one functional assertion that reads the raw column and asserts it does not equal the plaintext. That single test catches accidental type de-registration and key misconfiguration, which are the two realistic production breakages.

Key rotation: if the config supports a previous_keys / key-ring list, you can decrypt with old keys and re-encrypt on write gradually. If it doesn't, rotation requires a maintenance command that iterates rows, decrypts with the old key, re-persists with the new one — write it early, not during an incident.

Testing/verification helper: a small console command app:encryption:verify that decrypts a sample of rows and reports failures is worth the twenty minutes. It's also your health check after a deploy that touched secrets.

Gotchas and Tips

Column length will bite you. Ciphertext is plaintext + IV + tag, then base64 (~1.37×) depending on implementation. A 255-char plaintext field needs TEXT or varchar(1024). Silent truncation in MySQL non-strict mode produces undecryptable garbage that only surfaces on read.

A missing or wrong key breaks every query touching the column, including findAll() and any join that hydrates the entity. A single bad key turns into an application-wide 500, not a localized error. Consider wrapping hydration in a try/catch at the type level or a fallback that logs and returns a sentinel — but be aware that's a deliberate weakening of guarantees.

Second-level cache and profiler data store plaintext. The Doctrine result cache keeps hydrated (plaintext) entities. If you're encrypting for GDPR, the encryption at rest is undermined by a Redis cache holding the same values. Disable L2 cache for these entities or accept the tradeoff explicitly.

Serialization and logging are out of scope. Symfony Serializer, JMS, Logger calls that dump entities, and Doctrine's SQL logger (which shows bound parameters — encrypted — but also the hydrated entity in some profilers) all handle plaintext. Field encryption solves disk-at-rest and backups; it does not solve log leakage.

Unique indexes on encrypted columns are meaningless unless the cipher is deterministic, and deterministic encryption leaks equality — which is exactly what a blind index gives you with less risk. Prefer the hash column.

DBAL 3 compatibility is the likely break point. DBAL 3 replaced Type::getName() with getTypes() registration; small packages with one star often lag. Check composer.json constraints against your DBAL major version before adopting, and run the test suite against your actual stack. The same applies to PHP 8 attributes vs. legacy annotations.

Adoption risk is real: 1 star, 0 dependents. Read the type classes end-to-end — the surface is small enough that you can. If it's unmaintained or the DBAL version is incompatible, the fallback is a ~40-line custom type using sodium_crypto_secretbox plus a KeyProvider service; you lose nothing but the autoloading. Don't build a migration path into the bundle's API — keep your entities depending on a type alias you control, so swapping the implementation is a config change rather than an entity rewrite.

Extension points, in order of usefulness: (1) override the type alias in doctrine.dbal.types to point at your own subclass; (2) a postLoad/prePersist listener for audit logging; (3) a custom key provider service so keys come from Vault/KMS rather than a YAML scalar. If the bundle doesn't expose (3) as a config option, that alone may justify writing your own type — KMS-backed keys are usually a hard requirement for the GDPR use case this package advertises.

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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
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
cgv/localized-resource