symfony / doctrine-orm-key-management
Symfony Doctrine ORM Key Management Bridge
Package info
github.com/symfony/doctrine-orm-key-management
Type:symfony-key-management-bridge
pkg:composer/symfony/doctrine-orm-key-management
Requires
- php: >=8.4.1
- doctrine/orm: ^3.4
- psr/container: ^1.1|^2.0
- symfony/doctrine-bridge: ^7.4.10|^8.0.10
- symfony/doctrine-dbal-key-management: ^8.2
- symfony/key-management: ^8.2
Requires (Dev)
- symfony/cache: ^7.4|^8.0
- symfony/dependency-injection: ^7.4|^8.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-05 05:59:14 UTC
README
Connects symfony/key-management to the Doctrine ORM: a #[BlindIndexed] attribute
whose listener fills the blind index of a property on every flush, and a schema
listener declaring the table the data key store of
symfony/doctrine-dbal-key-management needs.
This Bridge is experimental. Experimental features are not covered by Symfony's Backward Compatibility Promise.
Blind indexes
An encrypted column cannot be searched, so the application keeps a keyed tag of the value in a sibling column and looks the row up by that. Writing the tag is mechanical and easy to forget, and a row whose tag was not written is a row no search will ever return. The attribute says, on the column holding the tag, where the tag comes from:
use Doctrine\ORM\Mapping as ORM; use Symfony\Component\KeyManagement\Bridge\DoctrineOrm\Attribute\BlindIndexed; #[ORM\Column(type: 'encrypted_string')] private string $email = ''; #[ORM\Column(length: 64)] #[BlindIndexed('email', 'user-email')] private string $emailIndex = '';
Its second argument names the index, which is also what the tags are derived under, so two columns holding an address have an index each.
The query side is unchanged, since it has no entity to read the attribute on:
$repository->findOneBy(['emailIndex' => $index->of($email)]);
The listener is given the indexes of the application keyed by that same name:
use Doctrine\ORM\Events; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\KeyManagement\BlindIndex; use Symfony\Component\KeyManagement\BlindIndex\Projection\Email; use Symfony\Component\KeyManagement\Bridge\DoctrineOrm\EventListener\BlindIndexListener; $eventManager->addEventListener(Events::onFlush, new BlindIndexListener(new ServiceLocator([ 'user-email' => static fn () => new BlindIndex($kms, $wrappedKey, 'user-email', new Email()), ])));
It runs on onFlush rather than on prePersist, which is dispatched when
persist() is called and would have missed a value set afterwards.
Four things it does not do, each of which leaves a tag that does not match its
value: it covers the write path only, it covers the ORM only, so a row inserted
through DBAL or a bulk UPDATE leaves the tag as it was, it only sees a
property it can read as a string, and it cannot be a DBAL type, since a type
converts one property into one column and this writes a second one.
With the FrameworkBundle
Every index service is tagged key_management.blind_index by autoconfiguration,
the listener is wired on onFlush and removed when the application registers no
index, and the data key table joins the schema doctrine:schema:update and the
migrations diff against. Only the name is left to state, in the tag, which hands
it to the $name argument of the index:
services: app.index.user_email: class: Symfony\Component\KeyManagement\BlindIndex arguments: $kms: '@key_management.app' $wrappedKey: !service class: Symfony\Component\KeyManagement\Ciphertext arguments: ['%env(base64:APP_INDEX_KEY)%', 'app-key'] $projection: !service { class: Symfony\Component\KeyManagement\BlindIndex\Projection\Email } tags: - { name: key_management.blind_index, index: 'user-email' }
APP_INDEX_KEY holds the wrapped value key-management:generate-data-key app-key
prints, and the second argument the key_id it prints beside it, which a client
resolving the identifier it was given may spell otherwise.