New in Symfony 8.2 (in development). A phone number, a birth date, a social security number: some columns should not be readable in plain text in a backup lying around, or in a database copied to a developer's machine. Encrypting them used to take a third-party bundle, or a hand-written Doctrine type. Symfony 8.2 brings the KeyManagement component, and its Doctrine bridge encrypts a column with an attribute.
Before
final class EncryptedPhoneType extends Type { public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string { // a key to load, an IV to draw, openssl_encrypt(), the tag to store… } public function convertToPHPValue($value, AbstractPlatform $platform): ?string { // … and the reverse } }
Then one such type per kind of value (text, date…), to declare in doctrine.yaml.
Since Symfony 8.2
use Symfony\Component\KeyManagement\Bridge\DoctrineOrm\Attribute\Encrypted; #[ORM\Column(nullable: true)] #[Encrypted(key: 'patient.contact')] private ?string $phone = null;
Doctrine writes an encrypted, binary value to the database, then decrypts it when it reads the entity back. The property keeps its type: a string stays a string, and a date stays a DateTimeImmutable, because the column's type converts the value first, and the result is then encrypted. There is no type to declare and no listener to write: when it compiles the container, Symfony finds the marked properties in src/Entity/ and declares the Doctrine types they need.
Encryption is randomized: the same value, encrypted twice, never gives the same bytes. That is what keeps anyone from guessing who shares a number, but it is also why an encrypted column can no longer be searched with a WHERE or sorted with an ORDER BY. So you only encrypt what needs it.
The component uses envelope encryption: each value is encrypted by a data key, itself encrypted by a master key that never leaves the KMS (AWS KMS, Google Cloud KMS, Vault…). In this environment, the KMS is local (OpenSSL), and the data keys are stored in the database (key_management.store). The key parameter then names a scope: each scope has its own data key.
Your task
The clinic keeps its patients in the Patient entity. The /patients preview shows what the application reads and, under inDatabase, what the database really holds: for now, everything is in plain text.
- Encrypt the phone number under the key
patient.contact. - Encrypt the birth date under the key
patient.identity. - Leave the name in plain text: the list is sorted by name, in SQL.
The application must read exactly the same patients as before.