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 Extension Laravel Package

ywh/doctrine-encryption-extension

ywh/doctrine-encryption-extension adds transparent field-level encryption to Doctrine entities, with configurable DBAL types and event subscribers for securely storing sensitive data in PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a small, low-traffic package (no stars, no dependents), so treat the source as the documentation of record. Budget 20 minutes to read the two or three files that matter before wiring it in — the failure modes of field-level encryption are mostly about when the crypto runs, not about the cipher itself.

  1. Install and vendor it.

    composer require ywh/doctrine-encryption-extension
    ls vendor/ywh/doctrine-encryption-extension/src
    

    Open the README.md and src/ first. You are looking for exactly two things:

    • the integration hook (a DBAL type registered via Doctrine\DBAL\Types\Type, or a Doctrine event subscriber on postLoad/prePersist/preUpdate), and
    • the configuration key it expects (a cipher key/secret, and possibly a cipher name).
  2. Decide the encryption contract before touching entities. Which is it?

    • DBAL type — #[ORM\Column(type: 'encrypted_string')]. Transparent at the persistence layer; works for any entity using that column type.
    • Event subscriber — fields are annotated/attributed and encrypted on flush, decrypted on load. Cheaper to retrofit but bypassed by DQL scalar hydration, array hydration, and native SQL.

    Everything downstream (querying, migrations, testing) depends on this answer.

  3. Provide the key from the environment, never from code. In Symfony:

    # config/services.yaml
    parameters:
        ywh_encryption.key: '%env(base64:APP_ENCRYPTION_KEY)%'
    

    Generate once, store in your secret manager: php -r 'echo base64_encode(random_bytes(32)), PHP_EOL;'. Losing this key means losing the data; there is no recovery path.

  4. First use case — one field, one round-trip test. Pick a low-blast-radius PII column you are not yet querying (e.g. customer.phone). Change the column type, write a functional test that persists, clears the EntityManager, reloads, and asserts equality — and a second assertion that raw SQL returns ciphertext:

    $raw = $connection->fetchOne('SELECT phone FROM customer WHERE id = ?', [$id]);
    self::assertNotSame('+33612345678', $raw);
    

    That one assertion is what tells you whether the extension is actually in the hydration path. Ship that test before you touch a second column.

Implementation Patterns

Registering the type (DBAL-type flavour). DBAL types must exist before the connection is opened, so register them in configuration, not in a service constructor:

# config/packages/doctrine.yaml
doctrine:
    dbal:
        types:
            encrypted_string: YWH\DoctrineEncryptionExtension\DBAL\Types\EncryptedStringType

If the package predates dbal.types support, you need a compiler pass that calls Type::addType() in preBoot — doing it from a controller or a listener is a classic "Unknown column type" bug.

Encrypting structured data. Encrypting a JSON blob is usually better than encrypting five columns: one ciphertext, one key operation, no partial-hydration surprises.

#[ORM\Column(type: 'encrypted_text')]
private ?string $medicalRecordJson = null;   // json_encode() before set, json_decode() on read

Blind indexes for lookups. With a random IV, the same plaintext encrypts to different ciphertext each time — so WHERE email = :email and unique constraints stop working. Keep a deterministic companion column:

#[ORM\Column(length: 64, unique: true)]
private string $emailBlindIndex;   // hash_hmac('sha256', mb_strtolower($email), $hmacKey)

public function setEmail(string $email): void
{
    $this->email = $email;                                   // encrypted column
    $this->emailBlindIndex = $this->blindIndex($email);      // searchable column
}

Use a different key for HMAC than for encryption, and remember the blind index leaks equality (frequency analysis) — it is not a substitute for encryption, it is a lookup aid.

Embeddables and value objects. If the extension is a DBAL type, it composes cleanly: put the encrypted type on the embeddable's fields and the VO's fromString/toString stays crypto-agnostic. If it is a subscriber, embedding usually does not work — the subscriber walks entity properties, not embeddable internals. Verify with a test either way.

Backfilling existing plaintext. Never let doctrine:schema:update or a naive ALTER TABLE rewrite a populated column without a plan. Sequence: add the new encrypted column → dual-write in code → background job re-encrypts row by row in batches → flip reads → drop the old column in a later deploy. Keep the migration down() honest (it can only restore plaintext if you kept a backup).

Testing pattern. Test against the real driver (SQLite in-memory is fine for the round-trip; it is not fine for asserting column lengths). Wrap crypto assertions in a small helper that bypasses the ORM:

$raw = $em->getConnection()->fetchOne('SELECT secret FROM vault WHERE id = ?', [$id]);

If you rely on getArrayResult() or ->select('partial v.{id, secret}') anywhere, add a test for that path specifically — array hydration is the usual place field-level crypto silently does nothing.

Key rotation extension point. Design for a keyring on day one even if the package only supports one key: prefix the ciphertext with a short key id (v2: or a one-byte header) at the application level or by decorating the type. Rotation then becomes re-encrypt-on-write plus a background sweep, instead of an emergency.

Gotchas and Tips

  • Length explosion. Ciphertext is base64 (if the package base64s) plus IV plus auth tag. A VARCHAR(255) holding a 200-char string will truncate or throw. Size as ceil(len * 1.4) + 64, or use TEXT/BLOB. Verify against the actual driver — MySQL in strict mode errors, SQLite silently accepts.
  • LIKE, ORDER BY, and range queries are dead. Encrypted columns only support exact match via a blind index. Any LIKE '%foo%' or ORDER BY on an encrypted column must move to a dedicated search index; catch this in code review, because it fails as "returns nothing", not as an exception.
  • Hydration paths that skip the type. Native SQL, ResultSetMapping, DQL NEW, and partial hydration can hand you ciphertext. The Symfony profiler is the fastest way to spot it: the value in the "Request" panel will look like base64 noise.
  • Ciphertext in your logs. Doctrine's SQL logger, Symfony's profiler, Monolog's exception context, and var_dump($entity) all leak plaintext or ciphertext. Disable doctrine.dbal.logging in prod, add #[SensitiveParameter] to setters taking secrets, and consider __debugInfo() on entities holding decrypted PII.
  • Ordering of type registration. The single most common install error is Unknown column type "encrypted_string" — always caused by Type::addType() running after the connection is booted, or by the type being registered only in test config. If it works in dev and fails in cache:clear-warmed prod, this is why.
  • No integrity binding to the row. Most packages of this vintage encrypt the value without additional authenticated data, which means an attacker with DB write access can swap ciphertext between rows (a salary from another employee). If that threat is in your model, decorate the type to bind table + column + primary key as AAD using sodium_crypto_aead_xchacha20poly1305_ietf_*, and accept that the value must be re-encrypted if the PK changes.
  • findBy() on an encrypted field silently returns empty. No exception, no warning. Add a static-analysis rule or a grep check in CI for findBy([<encrypted field> — it is the highest-value 10-line guardrail you can add.
  • Performance. Decryption runs per hydrated row; a 5 000-row report that used to be a single query now costs 5 000 AEAD opens. Measure before and after on realistic volumes, and prefer encrypting only the columns that genuinely need it rather than "all string columns".
  • Abandonment risk. Zero stars, zero dependents means no security review beyond your own — and crypto code that nobody maintains is a liability. Read the cipher construction (openssl_encrypt? libsodium? IV generation?) before production use; if it uses openssl_encrypt with a non-random IV or ECB, wrap or replace it rather than adopting it. The cleanest mitigation is to isolate it behind your own EncryptorInterface so swapping to paragonie/halite or libsodium later is a one-class change.
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.
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
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo