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

Encryption Bundle Laravel Package

helppc/encryption-bundle

helppc/encryption-bundle is a Laravel/PHP package that bundles reusable encryption utilities, making it easier to add secure encryption and decryption to your application with a simple, framework-friendly integration and less boilerplate. It helps centralize encryption logic and reduce repetitive cr...

View on GitHub
Deep Wiki
Context7

Getting Started

1. Check your Symfony version before anything else

Since 2.1.0 the bundle requires ^7.4 || ^8.0. That is the whole supported matrix: Symfony 7.4 (current LTS) and Symfony 8.x. Symfony 6.x and 7.0–7.3 are not installable — Composer will refuse rather than degrade, which is the good outcome, but it is worth knowing before you start reading the code and wondering why the constraint fails.

composer require helppc/encryption-bundle:^2.1
composer show | grep -i encryption

You should see helppc/encryption-bundle plus upstream spaze/encryption. If the install additionally drags in helppc/encryption (the fork with extra contracts), stop and check composer.json — the bundle is documented as depending only on classes that exist upstream, and building against fork-only interfaces is a forward-compatibility trap. Everything in this assessment assumes the upstream dependency.

You are on the 2.x line. 2.1.0 is a drop-in upgrade from 2.0.0 — nothing under src/ changed and no configuration behaves differently — so if you are adopting fresh, adopt 2.1.x and do not bother reading the 2.0.0 diff.

Symfony Flex will normally register the bundle in config/bundles.php; on a non-Flex app add it manually.

2. Produce key material before touching config

The bundle ships a GenerateEncryptionKeyCommand (key generation is the only crypto-adjacent code the bundle owns — it delegates to libsodium's random_bytes() / sodium_crypto_box_keypair() because upstream exposes no public keygen API). Find its exact name rather than guessing:

bin/console list encryption
bin/console help <command-name>

Run it into a file outside the repo (or into your secrets store) and keep the raw output away from VCS. Note that libsodium keypairs are binary; if the command emits binary, base64 it for env-var transport and check how the config expects keys to be encoded.

3. Read three things, in this order

Where Why
src/DependencyInjection/Configuration.php The actual config contract — group names, key lists, encoding. This file is the documentation.
src/Resources/config/ Service IDs, aliases, and how per-group services are wired. Tells you what you can autowire.
tests/ The only trustworthy usage examples. Copy the test fixture config as a starting point.

4. First use case: encrypt one class of secret

Start with a single, high-value column — outbound API tokens, webhook signing secrets, or one PII field. Declare one group for it, wire it, and assert a round trip in a test before you roll it out to real data. Do not start by encrypting an entire entity graph; start with one value and one call site, because the interesting problems (column width, query patterns, rotation) only appear once you have data under encryption.

# config/packages/encryption.yaml  (shape is illustrative — confirm keys in Configuration.php)
encryption:
    groups:
        app.tokens:
            keys:
                - '%env(base64:APP_ENCRYPTION_KEY)%'

Then:

bin/console lint:container
bin/console cache:clear

If the group name is wrong or the key is missing, this is where you find out — not at runtime.

Implementation Patterns

Treat group names as public API. The bundle's headline property is that a misconfigured group fails at container compile time, which means group names are baked into your service wiring. Put them behind constants or a small enum in your app so a rename is a single diff and every stale reference fails a bin/console lint:container run in CI rather than a production decrypt.

final class EncryptionGroup
{
    public const TOKENS = 'app.tokens';
    public const PII    = 'app.pii';
}

One group per blast radius, not per entity. Group keys are shared across everything in the group. Split by who needs to decrypt and how often keys should rotate: app.tokens (rotated often, narrow audience) should not share a key with app.pii (rotated rarely, wide audience). Rotating a shared group forces re-encryption across unrelated domains.

Inject the narrow contract, not a container or a god-service. The bundle's interfaces are deliberately narrow — take advantage of that by type-hinting the per-group encryption service in the constructor of exactly the class that needs it. If autowiring the right group requires a binding, put the binding next to the service rather than in the global services.yaml:

services:
    App\Billing\WebhookSecretStore:
        arguments:
            $encryption: '@<bundle service for app.tokens>'

That keeps the group identity visible at the call site in review, and a typo'd service reference is a compile error.

Encrypt at the boundary, store a value object. Do not scatter encrypt() calls through entity setters. Wrap ciphertext in a small value object (EncryptedString) that owns base64/encoding concerns and refuses to be var_dumped or logged. Decrypt in exactly one place — the repository/DTO hydration path — so "who can decrypt this field" is answerable by grep.

Doctrine integration is yours to write. The bundle gives you a typed facade plus config; it does not promise a Doctrine type. Two workable options:

  • Custom DBAL type — cleanest for one or two columns, but the ciphertext is non-deterministic so the column must be unqueryable (see Gotchas).
  • postLoad/prePersist listener — better when encryption is conditional (only if a feature flag is on), which is exactly how you migrate an existing plaintext column without a big-bang cutover.

Either way, widen the column first.

Key rotation: add, never remove. The pattern the bundle is built for is: generate a new key → append it to the group's key list → deploy → re-encrypt → only then drop the old key. The deploy in the middle is safe because decryption still has the old key; the re-encryption pass is what lets you eventually delete it. Write the re-encryption as an idempotent console command, batched, with a --dry-run, and run it while the old key is still present. Track which key version encrypted each row (a separate small key_version column beats guessing) so "how much is left to migrate" is a query, not a hope.

Test with a fixed key, and test the failure path. Define a deterministic test key in config/packages/test/encryption.yaml (never a real one). Then assert both directions:

self::assertSame($plain, $encryption->decrypt($encryption->encrypt($plain)));
self::assertNotSame($plain, $encryption->encrypt($plain)); // non-deterministic

Also assert that a ciphertext from group A does not decrypt under group B — that is the misconfiguration class the bundle cannot catch for you.

CI gate. Because misconfiguration is a compile-time failure, add bin/console lint:container and a cache:clear against a production-like env to CI. This is cheap and catches config drift that unit tests miss. If you maintain several apps on different majors (or you are the one maintaining a bundle that depends on this one), mirror upstream's approach: pin Symfony per job (composer require symfony/framework-bundle:^7.4 in one job, ^8.0 in the next) rather than trusting the constraints to float somewhere sensible — a "7.4" job that silently resolves half the stack to 8.x proves nothing.

Gotchas and Tips

The compile-time failure is the feature — do not defeat it. Any temptation to lazy-resolve the group from the container at runtime ($container->get('encryption.' . $group)) throws away the bundle's main advantage. If you need dynamic group selection, keep the set of groups finite and select an already-injected service from an enum-keyed map.

The bundle's extension reads kernel parameters at compile time. The 2.1.0 test fix is the useful clue here: building the extension's ContainerConfigurator reads kernel.environment on 7.4 and 8.0 (optional only from 8.1) and kernel.build_dir on 7.4. Anywhere you compile a container outside a real kernel — a hand-rolled ContainerBuilder in an integration test, a custom build script, tooling that dumps the container for static analysis — must set both parameters explicitly, or you get a confusing failure that has nothing to do with encryption. Real apps via bin/console and Kernel::boot() are unaffected; this only bites bespoke harnesses.

Keys in env vars: encoding will bite you. libsodium keys are binary. Env vars carry strings. Mixing raw and base64 keys produces the worst failure mode — decryption that almost works, or a length error that surfaces deep in a request. Pick one encoding, encode it in the config expression (%env(base64:...)%), and write it down. Prefer Symfony's secrets vault or a mounted file over a long-lived env var if your deploy target supports it.

Never rotate by deleting. Removing an old key makes every row encrypted with it unrecoverable, permanently, with no error until someone reads that row. Rotation is always add-then-re-encrypt-then-prune, in that order, across at least two deploys.

Ciphertext is non-deterministic — plan your schema and queries around it. You cannot WHERE token = :ciphertext, cannot LIKE, cannot GROUP BY, cannot build a unique index on the encrypted value. If you need lookup, store a separate HMAC/blind index of the plaintext alongside the ciphertext. Also widen the column: base64-encoded ciphertext plus a nonce routinely quadruples the byte length of the plaintext. VARCHAR(255) holding a 32-byte token will truncate silently on MySQL in non-strict modes.

Bind ciphertext to its context. Encryption alone does not stop someone with DB write access from copying a valid ciphertext from row 7 into row 12 — both decrypt fine. If upstream supports associated data/context, include a stable record identifier (group + table + primary key) so a swapped ciphertext fails to decrypt. If it does not, at minimum store the group name and a record discriminator next to the ciphertext and validate on read.

Do not catch fork-only exception types. helppc/encryption ships a marker exception interface that upstream lacks. Catching a broad \Throwable around decryption hides real bugs; catching a class that vanishes on an upstream bump is worse. Wrap decryption in one adapter of your own and catch upstream exceptions there.

Logging and serialization leaks. Ciphertext in logs is mostly harmless; plaintext in var_dump, dd(), Messenger failure transport, or json_encode of a DTO is the real risk. Mark plaintext properties #[\SensitiveParameter], and give your value object a __debugInfo() / __serialize() that returns '***'.

Package maturity — better than it looks, still pin it. The 1-star / zero-dependents caution stands, but the 2.1.0 release notes are a genuine quality signal: PHPUnit, PHPStan level 10, PHPCS, Rector and composer validate --strict all green, run across Symfony 7.4, 8.0 and 8.1 on two PHP versions. That is stricter than most of the packages you already depend on. Two caveats follow from it: (a) pin exactly and upgrade deliberately — composer require helppc/encryption-bundle:2.1.*, commit composer.lock, read the changelog between bumps, because for a package whose value proposition is compile-time safety a signature change in the config tree is precisely the breaking change you will not notice until deploy; (b) green CI proves the pinned stacks, so a "works on 8.2" assumption is yours to verify, not theirs. Budget an afternoon to read the DependencyInjection/ directory on first adoption; it is small, and it is the whole contract.

Debugging toolkit. bin/console debug:config encryption shows the resolved tree after env interpolation — the fastest way to confirm your key actually arrived; bin/console debug:container --tag= and --show-arguments reveal which group service got bound to which constructor. When a decrypt fails in staging but not locally, the usual culprits in order: wrong key version for that row, base64 vs raw mismatch, trailing newline in a file-sourced key.

Extension points. The bundle is intentionally a thin wrapper. If you need behaviour it does not have (Doctrine types, rotation automation, blind indexes), write it in your application against the narrow injected interface rather than forking the bundle — the surface you would be extending is small enough that a fork buys you little and costs you upstream security fixes. Note that 2.1.0 changed nothing outside tests/, so there is no new extension point to chase in this release; the interface you saw in 2.0.0 is the interface you get.

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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
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