dwgebler/encrypted-fields-bundle
Symfony bundle that encrypts and decrypts Doctrine entity fields via a simple attribute, using a master key to generate secure per-row encryption keys stored in a separate table. Supports key rotation, UUID and composite primary keys.
Correct the premise before anything else: this is a Symfony bundle, not a Laravel package. dwgebler/encrypted-fields-bundle is configured through Symfony Flex, Symfony YAML, and Symfony console commands, and it hooks Doctrine ORM lifecycle events. There is no service provider, no Artisan command, and no Eloquent integration. Against a Laravel/Eloquent codebase the realistic fit is zero; against a Symfony + Doctrine codebase it is a credible fit. Every downstream estimate in this assessment assumes we resolve that question first, because the integration cost differs by roughly an order of magnitude.
Architecture fit (Symfony + Doctrine). Good. Encryption is declarative via #[EncryptedField] on entity properties, with options for array-element targeting (elements: ['baz']), master-key override (useMasterKey: true), and per-property custom key namespaces (key: '...'). Encryption runs on persist/update, decryption on hydrate, so business logic is untouched. The key hierarchy is the strongest design decision: a single hex master key wraps a unique per-row data key stored in a dedicated encryption_key table. That makes master-key rotation a re-wrap of small key rows rather than a full dataset re-encryption — materially better than the common "one key for everything" shortcut. AES-256-GCM default gives authenticated encryption (confidentiality plus tamper detection), which is the correct cipher choice. Rotation is supported via gebler:encryption:rotate-key with --generate-new-key, --database-key, and --database-key-file.
The constraint that usually kills this in product terms: encrypted fields stop being queryable. GCM with random nonces produces non-deterministic ciphertext, so WHERE, ORDER BY, LIKE, GROUP BY, uniqueness constraints, joins, and reporting/export on those columns stop working at the database layer. Every feature that filters or sorts on a candidate field must be re-architected (blind index, deterministic secondary column, or a separate search store). In my experience this is frequently 5–10x the effort of the encryption work itself, and it is the single scoping decision a TPM must force before committing engineering time.
Other technical risks.
UPDATE/DELETE, raw SQL, DBAL-level writes, and some partial-hydration paths can bypass them and silently write plaintext into a column the team believes is encrypted. That is the worst possible failure mode for a compliance control.preUpdate that shipped in 1.x — the bug class is exactly the one that makes this category of package dangerous. Also flag a metadata anomaly: the listed last release date (2026-05-28) is future-dated relative to evaluation; verify tag provenance before trusting the artifact.VARCHAR(255) columns need widening and typed columns (int, date, bool, enum) need a representation change. Silent truncation under non-strict MySQL is unrecoverable.Key questions to resolve before approval.
laravel-doctrine/orm?.env file), and who holds rotation authority?Stack fit. Three scenarios, three answers. (a) Symfony 6/7 + Doctrine ORM: native fit — Flex recipe, YAML config, make:migration for the encryption_key table, zero application code changes beyond attributes. (b) Laravel + Eloquent: no viable path; do not pursue. (c) Laravel with laravel-doctrine/orm already in place: possible, but the bundle becomes a hand-wired guest in a non-Symfony container — we translate the YAML into a service provider registration ourselves, hand-write the encryption_key migration, and wrap the rotate command in an Artisan command. Budget this as real engineering, not a composer require.
Migration path.
composer.json constraints directly and validate against our PHP and Doctrine versions. Confirm 2.0 (required for UUID/composite primary keys, and for the preUpdate data-loss fix) is actually what we install.encryption_key table migration, source the master key from a secrets manager rather than a checked-in file, encrypt exactly one low-risk unqueried field.TEXT or sufficiently wide VARCHAR and verify sql_mode is strict. Do this before the first encrypted write, not after.Compatibility notes. PHP 8.1+ is implied by attribute syntax; OpenSSL extension is mandatory. UUID and composite primary keys are supported only from 2.0 — relevant if we use ramsey/uuid or composite keys. Laravel version is largely decoupled because integration is via Doctrine, but the Laravel↔Doctrine bridge versions become part of our compatibility matrix. The DB engine is abstracted by Doctrine, but validate transaction and isolation behavior on our specific engine for the key table.
Sequencing.
gebler:encryption:rotate-key --generate-new-key against a restored copy of a production-like backup; confirm all rows decrypt, confirm idempotency and interrupt behavior.encryption_key table access, publish the rotation runbook, set a rotation schedule.Maintenance. Treat upstream as effectively frozen. Vendor the package (a Composer fork or a VCS repositories entry we control) and assign a named owner. Budget 2–4 engineer-days per year for security patching plus Symfony/Doctrine major-version churn — Doctrine's event system and Symfony's container/config APIs are the two surfaces most likely to break. The codebase is thin enough that fork-and-patch is realistic if the maintainer disappears, which is the main reason this is defensible at all despite the adoption numbers.
Support. No vendor, no community to speak of, no observed security policy or CVE process. Escalation means reading the source. That is acceptable only if we have a PHP engineer comfortable with Doctrine event listeners on staff; otherwise the dependency is unowned risk. Author an internal runbook covering: manual decryption of a single row, key rotation, rotation rollback, and restore-from-backup using --database-key-file.
Scaling. Two cost centers to benchmark. Read path: per-row key resolution implies an extra lookup against the encryption_key table; hydrating 10k entities could mean 10k key-table queries — check whether the bundle batches, and if not, plan a wrapped-key cache (Redis/APCu keyed by row id), weighing the security tradeoff of caching key material. Write path: OpenSSL encrypt-per-field-per-row is CPU-bound and lands inside the request cycle; measure p95 write latency, not average. Storage: model ~1.3–1.4x raw byte expansion, up to ~2.7x with base64, before backfilling large tables; batch the backfill to avoid lock contention.
Failure modes, ranked by severity.
--database-key restore.WHERE email = ? silently returns zero rows and a feature appears to work but breaks quietly. Mitigation: contract tests that assert queries against encrypted columns fail loudly.sql_mode.Ramp-up. ~1 engineer-day for a Symfony-native developer to read the source and wire a pilot. ~3–5 engineer-days for a Laravel + Doctrine shop (service-provider translation, hand-written migration, Artisan wrapper for rotation). Add 1–2 days for security review and runbook authoring. This is not a fit-and-forget dependency — it requires a named owner and a scheduled rotation ritual, and it should be adopted through a pilot with a vendored fallback, not as a direct dependency on a 7-star single-maintainer repository.
How can I help you explore Laravel packages today?