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

odandb/doctrine-ciphersweet-encryption-bundle

A Doctrine bundle that adds CipherSweet field-level encryption to your entities, letting you store sensitive data encrypted while keeping it searchable through blind indexes. Attribute-based config makes setup simple and secure.

View on GitHub
Deep Wiki
Context7

Getting Started

Know what this is first. odandb/doctrine-ciphersweet-encryption-bundle is a Symfony bundle, not a Laravel package. It wires spatie/ciphersweet into Doctrine ORM entities via PHP attributes and custom DBAL types. It ships a Bundle class, config/packages/*.yaml config, and a Symfony DI extension — there is no ServiceProvider, no config/ publish tag, no facade.

If your app is Eloquent-based, stop here and use spatie/laravel-ciphersweet instead. Use this package only when:

  • you already run Doctrine ORM inside Laravel (laravel-doctrine/orm or a hand-rolled EntityManager binding), or
  • you maintain a shared library consumed by both a Symfony service and a Laravel service, and you want one encryption contract.

Install and wire (Doctrine-in-Laravel path):

composer require odandb/doctrine-ciphersweet-encryption-bundle

Then, because there is no Laravel auto-discovery, you must:

  1. Instantiate/bind the provider services yourself (or boot a minimal Symfony container via symfony/dependency-injection) — easiest is to register the bundle's extension config by hand and let it build the cipher service.
  2. Supply keys through CIPHERSWEET_KEY (or a KeyProvider) in your config/ and pass it into the container parameters.
  3. Register the package's DBAL types on the connection: 'types' => [...] in config/database.php under your Doctrine connection, or via Type::addType() in a service provider before the EntityManager is resolved. Order matters — types registered after the metadata driver boots will silently produce unencrypted writes.

Where to look first:

  • README's attribute reference — #[Encrypted] on the entity class, #[EncryptedField] on properties, blind-index attributes.
  • The bundle's DependencyInjection/Configuration.php — every config key with its default and expected type.
  • The DBAL type classes — this is what actually touches your data at read/write time.

First use case: a single high-value column you never query by range — e.g. Patient.ssn or Integration.secret. Encrypt it, add a blind index only if you need exact-match lookup. Do not start by encrypting ten columns; start with one and prove the round-trip plus a re-encryption of existing rows in staging.

Implementation Patterns

Attribute-driven entity mapping. Encryption is declared on the model, not in config:

#[Encrypted]
class Patient
{
    #[EncryptedField]
    private string $ssn;

    #[BlindIndex('ssn_idx')]
    private string $ssnIndex;
}

The bundle derives the index from the plaintext on persist and stores the hash in a separate column. You only ever read the hash column for lookups.

Query through the index, never the ciphertext. Encrypted columns are non-deterministic. WHERE ssn = ? returns nothing — always WHERE ssnIndex = ? with the index value computed by the bundle's transformer service. Wrap that in a small repository method so callers can't get it wrong:

public function findBySsn(string $ssn): ?Patient
{
    return $this->em->getRepository(Patient::class)
        ->findOneBy(['ssnIndex' => $this->blindIndexer->compute($ssn, 'ssn_idx')]);
}

DBAL type pattern for legacy tables. If you can't annotate an entity (third-party or generated model), map the column to the package's encrypted DBAL type in config/database.php. This is the seam that lets Symfony-style encryption work under a Laravel Doctrine connection without touching the model.

Migration pattern. Adding encryption to a populated column is a two-phase migration, not one:

  1. Add the new ciphertext column + index column, backfill by reading plaintext through the entity and re-saving (or a chunkById raw pass with the cipher service).
  2. Drop the plaintext column in a later deploy, after every consumer reads the new column.

Never do both in one migration — you lose the rollback path.

Testing. In-memory SQLite is fine, but you must register the DBAL types on the test connection before the schema tool runs, or SchemaTool will emit TEXT where you expect ciphertext and your assertions pass for the wrong reason. Assert on ciphertext inequality (value in DB ≠ plaintext) rather than on a fixed hash.

Extension points worth knowing. The KeyProvider and the blind-index transformer are the two interfaces to implement — the first for KMS/rotation, the second if you need a deterministic index over a normalized value (lowercased email, E.164 phone).

Gotchas and Tips

0.13.0 swapped symfony/property-info for symfony/type-info. symfony/type-info was introduced in Symfony 7.1 and requires PHP 8.2+. Consequences for a Laravel shop:

  • Laravel 11/12 ship Symfony 7.x components, so 0.13.0 is installable alongside them.
  • Laravel 10 (Symfony 6.4) cannot satisfy this. Composer will either fail or pull a standalone symfony/type-info that fights your locked symfony/* versions. Verify with composer why symfony/type-info before upgrading; if you're on Laravel 10, pin 0.12.1.
  • The dependency change is transitive and invisible until a composer update resolves differently in CI than locally. Commit your composer.lock bump as its own PR.

0.13.0 removed ORM deprecations. That means it targets a newer doctrine/orm and drops calls that older ORM versions still expected. Check your doctrine/orm constraint — doctrine/orm 2.x users on older minors should treat 0.13.0 as breaking and pin 0.12.1. This is a 0.x package, so semver gives you nothing here.

No Laravel integration surface. Two recurring failures:

  • Class "Odandb\...\Bundle" not found at boot — someone added the bundle to config/app.php's provider list. It's a Symfony Bundle, not a ServiceProvider. Remove it.
  • Unknown column type "encrypted" — the DBAL type was never registered, or was registered on a different connection than the one the EntityManager uses. Register per-connection, and in phpunit.xml too.

Blind indexes are deterministic — that's the whole tradeoff. Same plaintext → same hash, forever, across all rows. That means:

  • Equality leaks (attacker learns two rows share a value).
  • Low-entropy fields (boolean-ish flags, country codes, status enums) are trivially dictionary-attacked. Never add a blind index to a low-cardinality column.
  • Don't index a prefix of a secret ("last 4 of SSN") unless you accept the leakage.

Key rotation silently breaks indexes. Rotating the encryption key without recomputing every blind-index column leaves you unable to find existing rows. Rotation is a data migration: decrypt, re-encrypt, re-index, in a transaction or a resumable batch job that records position.

Decryption failures are almost never the cipher. Ranked causes:

  1. Wrong key for that environment (staging key in prod .env after a copy-paste).
  2. Missing/additional AAD — if the field was written with a different associated-data context, the MAC fails.
  3. A column that was backfilled while the DBAL type wasn't yet registered, so half your rows are plaintext. Check with SELECT LENGTH(col) — real ciphertext is much longer.

Debugging visibility. Query logs, Telescope, and Laravel's query listener will all render ciphertext blobs and can blow up log storage. Exclude encrypted columns from DB::listen payloads. Conversely, dd() on an entity gives you the decrypted value — that's the leak you actually have to guard against in error trackers and toArray() paths.

0.x versioning is the meta-gotcha. This package is pre-1.0 with a two-release-old API surface history; treat every minor as potentially breaking and read the changelog. Pin exactly ("0.13.0", not "^0.13") and upgrade deliberately.

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.
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
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle