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

ywh/doctrine-encryption-bundle

A Symfony bundle that adds transparent field-level encryption to Doctrine entities, encrypting sensitive columns at rest via Doctrine DBAL types and event listeners. Configure keys and ciphers to protect data with minimal changes to existing mappings.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install: composer require ywh/doctrine-encryption-bundle. Confirm the constraint resolves against your Symfony/Doctrine versions — this is a Symfony2-era bundle (no Flex recipe, no symfony/* v4+ constraints in most tags), so on modern stacks you'll typically need composer require ... --ignore-platform-reqs and expect to port service definitions yourself.
  2. Register the bundle in app/AppKernel.php: new Ywh\DoctrineEncryptionBundle\YwhDoctrineEncryptionBundle(). (Check src/ for the exact class name — namespaces differ between early tags.)
  3. Configure the key — first stop is DependencyInjection/Configuration.php in the bundle to see the real option names. Expect something like:
    ywh_doctrine_encryption:
        key: "%encryption_key%"   # 32 bytes, base64 or hex depending on the encryptor
        # algorithm / cipher options, if exposed
    
    Put the key in parameters.yml locally and an env var in prod; never commit it.
  4. Register the DBAL type(s) the extension ships (EncryptedString, EncryptedText, or similar — again, verify in the extension package's src/):
    doctrine:
        dbal:
            types:
                encrypted_string: Ywh\DoctrineEncryption\Types\EncryptedString
    
  5. Annotate a column on an entity:
    /** @ORM\Column(type="encrypted_string") */
    private $ssn;
    
    or the @Encrypted annotation if your version uses the event-subscriber approach instead of a DBAL type.
  6. First use case: a User entity with a ssn, taxId, or apiToken column. Persist, flush, then doctrine:query:sql "SELECT ssn FROM user" — you should see base64 ciphertext in the DB while $user->getSsn() returns plaintext after hydration.

Where to look first, in order: the bundle's Resources/config/services.xml (what it actually wires), DependencyInjection/Configuration.php (real option names), then the extension package's Encryptor class (key format and cipher) and its Type/subscriber classes (when encryption fires).

Implementation Patterns

Pick the right mapping mechanism. Older tags use a Doctrine event subscriber (postLoad/prePersist/preUpdate) keyed off an annotation; newer ones use a DBAL custom type. DBAL types are more reliable because they survive find(), getReference(), partial hydration, and native queries through the connection; subscribers miss bulk DQL and native SQL. Prefer the type-based path if both exist in your checkout.

Blind index for lookups. You cannot WHERE ssn = :x on randomized ciphertext. Add a deterministic companion column:

/** @ORM\Column(type="encrypted_string") */
private $ssn;

/** @ORM\Column(type="string", length=64, unique=true) */
private $ssnIndex;

public function setSsn(string $ssn): void {
    $this->ssn = $ssn;
    $this->ssnIndex = hash_hmac('sha256', $ssn, $this->indexKey);
}

Keep the HMAC key separate from the encryption key and normalize (trim/lowercase) before hashing, or lookups will silently miss.

Deterministic vs randomized. If the package lets you choose, use randomized (IV-per-value) for data at rest and always pair it with a blind index. Deterministic encryption leaks equality across rows — acceptable only when the column is already low-entropy and you accept that tradeoff.

Keep the encryptor out of entities where possible. Inject the bundle's encryptor service into a dedicated EncryptionService or Doctrine listener rather than calling it from getters/setters, so you can swap in a sodium_crypto_secretbox implementation when the bundled one is unmaintained.

Migration pattern for existing plaintext. Never run an in-place UPDATE. Add nullable ssn_enc and ssn_idx columns, backfill with a one-off command iterating in batches (->iterate() on a Query with ->setMaxResults() chunks — avoid loading 100k entities at once), then enforce NOT NULL, swap the property mapping, and drop the old column in a follow-up deploy.

Test environment. Point the key at a fixed test value and use SQLite in-memory. Assert on the raw connection value to prove encryption happened:

$raw = $this->em->getConnection()->fetchOne('SELECT ssn FROM user WHERE id = ?', [$id]);
$this->assertNotSame('123-45-6789', $raw);

That one assertion catches "I forgot to register the DBAL type" immediately.

Read-only projections. If a query returns scalar rows (e.g. SELECT ssn FROM ... via getArrayResult() or a custom repository), the DBAL type applies — but raw Connection::fetchAll() bypasses it. Route every read of a sensitive column through the ORM or explicitly decrypt.

Gotchas and Tips

  • It's effectively abandoned. 2 stars, zero dependents, Symfony2 target. Budget time to fork it. Check composer.json for PHP/Symfony constraints before assuming it installs; on Symfony 5+/PHP 8 you'll be hand-porting services.xml to services.yaml and replacing @service references with proper DI.
  • Column length blowup. AES-GCM with a random IV plus base64 encoding adds roughly 40–50% plus overhead. A VARCHAR(255) holding a 200-char value will truncate or throw. Use TEXT/BLOB, or size columns at ~1.5× the plaintext max.
  • Unique constraints on ciphertext are broken with randomized encryption — two inserts of the same value produce different ciphertext and both succeed. Enforce uniqueness on the blind index column only.
  • Bulk DQL/native updates skip the listener. UPDATE User u SET u.ssn = :x via DQL, or $conn->executeUpdate(), writes plaintext. The DBAL-type approach also bypasses on DQL UPDATE because it goes through the type only for parameter binding — verify explicitly, and prefer flushing entities for sensitive writes.
  • Second-level cache and result cache can store hydrated/decrypted values in Redis/APCu. Either disable second-level cache for encrypted entities or confirm exactly what's cached; treat cache stores as sensitive infrastructure.
  • Serialization leaks. json_encode($user) and Symfony's serializer will emit decrypted plaintext. Add @Groups/#[Ignore] on encrypted properties and audit every API resource that exposes the entity.
  • Debugging: the SQL logger and doctrine:query:sql show ciphertext (that's your proof it works). dump($user) in a controller shows plaintext (that's your proof hydration works). If you see plaintext in the DB, the type isn't registered under the exact name used in @ORM\Column(type=...) — typos here fail silently as string.
  • Key rotation has no built-in support. Plan for it: tag every ciphertext with a key version, keep the old key decryptable, and write a rotating command that walks rows, decrypts with the old key, re-encrypts with the new one, and records the version in a column. Do this before you need it.
  • Key handling in config. Symfony2's parameters.yml ends up in the repo in most legacy projects. Move to %env(...)% or a mounted secret, and make sure the key isn't dumped by config:dump-reference or a stray debug:container --env-vars in CI logs.
  • Watch for PHP 8 deprecations in the encryptor (implicitly nullable params, openssl_* signature changes, mcrypt if the old tag used it). Pin to sodium_crypto_secretbox via a custom encryptor implementation — the bundle's service definition usually makes the encryptor a swappable service, which is the cleanest extension point available here.
  • Extension points worth using: replace the encryptor service with your own implementing the same interface; add a custom DBAL type wrapping the bundled one if you need versioned keys; and decorate the subscriber instead of forking if you need pre/post hooks for auditing.
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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle