phil / privacy-bundle
Privacy law in code for Symfony: personal fields declared by attribute, logs redacted, export and erasure, delayed account deletion, scheduled retention purges with the register of processing, cookie consent refused by default, third-party frames held until consent, requests by verified link, an inc
Package info
github.com/parler-haut-interagir-librement/privacy-bundle
Type:symfony-bundle
pkg:composer/phil/privacy-bundle
Requires
- php: >=8.5
- ext-mbstring: *
- ext-sodium: *
- monolog/monolog: ^3.0
- phil/email-address: ^0.1@dev
- phil/url: ^0.1@dev
- psr/clock: ^1.0
- psr/log: ^3.0
- symfony/clock: ^8.2
- symfony/config: ^8.2
- symfony/dependency-injection: ^8.2
- symfony/event-dispatcher-contracts: ^3.6
- symfony/filesystem: ^8.2
- symfony/http-foundation: ^8.2
- symfony/http-kernel: ^8.2
- symfony/lock: ^8.2
- symfony/messenger: ^8.2
- symfony/routing: ^8.2
- symfony/security-core: ^8.2
- symfony/security-csrf: ^8.2
- symfony/security-http: ^8.2
- symfony/workflow: ^8.2
- symfony/yaml: ^8.2
- twig/twig: ^3.21
Requires (Dev)
- ext-pdo: *
- ext-zip: *
- doctrine/dbal: ^4.5
- doctrine/orm: ^3.7
- doctrine/persistence: ^4.1
- phpunit/phpunit: ^13.4
- psr/cache: ^3.0
- symfony/asset-mapper: ^8.2
- symfony/browser-kit: ^8.2
- symfony/cache: ^8.2
- symfony/console: ^8.2
- symfony/css-selector: ^8.2
- symfony/dom-crawler: ^8.1
- symfony/event-dispatcher: ^8.2
- symfony/framework-bundle: ^8.2
- symfony/mailer: ^8.2
- symfony/mime: ^8.2
- symfony/monolog-bundle: ^4.1
- symfony/rate-limiter: ^8.2
- symfony/scheduler: ^8.2
- symfony/security-bundle: ^8.2
- symfony/stimulus-bundle: ^3.7
- symfony/translation: ^8.2
- symfony/twig-bundle: ^8.2
Suggests
- ext-zlib: To compress the archives of an export, which are stored without it
- doctrine/dbal: For phil:privacy:anonymize, and the tables of the account deletions, the consent proofs, the requests without an account and the incidents
- doctrine/orm: To draw the anonymization plan from #[Anonymize] and #[PersonalData] on Doctrine entities
- doctrine/persistence: To draw the register of processing from every Doctrine entity
- phpunit/phpunit: For AssertsDeclaredCookies, which fails a functional test on a response that sets an undeclared cookie
- symfony/asset-mapper: To serve the Stimulus controllers of the consent and of the frames held until it without a build step
- symfony/console: For phil:privacy:export, phil:privacy:erase, phil:privacy:register, phil:privacy:anonymize, phil:privacy:retention, phil:privacy:deletions, phil:privacy:incident and phil:privacy:incidents
- symfony/mailer: To send the links of the requests made without an account
- symfony/rate-limiter: Required by the requests made without an account, guest_requests.enabled
- symfony/scheduler: For the schedule phil_privacy, which runs the account deletions and the retention schedule
- symfony/stimulus-bundle: To register the Stimulus controllers of the consent and of the frames held until it
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-15 03:40:58 UTC
README
Declare personal fields, redact logs, honour the rights of members and of people without an account, apply retention periods, keep the registers, ask consent to cookies, and anonymize database copies from code.
phil/privacy-bundle puts privacy law into a Symfony application that holds personal information. A #[PersonalData]
attribute declares a personal field, and an application test can check that every one has a retention rule. A
Monolog processor redacts denied keys, query strings, URL credentials, token path segments, command line options and
exception messages before a line is written. Subject access and erasure answers are assembled from one contract per
module, wired by autoconfiguration and run from the console. The retention schedule declares the register of
processing — purpose, recipients, processors and location of each store —, which the command prints with the
declared fields and fails on in CI while a gap remains; it also becomes purge orders, with a run refused when a job
line has no purger. A small register keeps every confidentiality incident and names the notifications still owed. A
copy of the database is anonymized column by column — emails, names, telephones in fictional ranges, addresses,
IBANs, shifted dates, hashes, placeholder text — from #[Anonymize], the declared categories and the configuration,
with locale packs for eight locales and a guard that refuses to touch a production database.
Out of the box, a member downloads their data, as a ZIP archive with a readable README and one JSON file per module, and deletes their account from their preferences. A deletion is a workflow: a request, a delay during which it can be cancelled, an export, then the erasure of every module, one execution per person, confirmed by Symfony's sudo mode. Once turned on, a person without an account asks for the same through a link sent to their address, behind rate limits and with the same answer whether the address is known or not. Retention periods are applied by a command or a scheduled message. Consent to cookies is refused by default, as easy to refuse as to accept, can be withdrawn at any time, respects the Global Privacy Control, and leaves a dated proof of the version of the text. Every cookie is declared in a YAML inventory from which the banner and the cookie policy are drawn, and a functional test fails on a cookie it does not declare. A video or a map of another site waits for its consent behind a preview the site serves itself. Once turned on, each member chooses on the same page whether the emails sent to them may count their opens or follow their clicks, each purpose refused until they tick it.
It exists because privacy laws such as Québec's Law 25 give a person the right to see and erase what a business holds about them, and expect the business to know what it keeps and for how long. Keeping that knowledge in the code keeps it true as the code changes, and a developer who needs real data gets a copy in which no person can be found. It is for the developers who build those applications, and for the person in charge of privacy who answers the requests and keeps the register.
The bundle does not decide your denylist, category vocabulary or retention schedule. It does not know what a module
holds: each module exports and erases its own records through a port, and each store is purged by its own adapter. It
does not verify the identity of someone who writes in by other means than the site, nor back the database up. For a
tamper-evident record of who did what, pair it with phil/audit-bundle; to back the database up and restore a copy to
anonymize, with phil/backup-bundle.
It is a Symfony bundle on Monolog, Workflow, Lock, Messenger, Yaml, the Security components, Twig and
phil/email-address and phil/url, with the Console, Scheduler, Rate Limiter and Mailer components, Doctrine
Persistence, DBAL and ORM as options. It is a skeleton of ports and adapters: it carries what the design decides, and
ports such as PersonalDataExporter, PersonalDataEraser, RetentionPurger, Anonymizer and AnonymizationTarget
for what the application decides.
Caution
A member's export and erasure are on by default on every site. Turning one off takes a legal right away from every member and needs the full sentence of Turning a right off; every build and console command then warns about it.
Status
| Area | State |
|---|---|
#[PersonalData], DeclaredPersonalFields, PersonalField |
Implemented and tested |
DataSubject, DataCategory, ModuleName, ErasureReport |
Implemented and tested |
Denylist, UrlRedactor, CommandLineRedactor, ContextRedactor |
Implemented and tested |
RetentionPeriod, RetentionRule, RetentionSchedule, PurgeOrder and the enums |
Implemented and tested |
RedactingProcessor |
Implemented, tested, wired from configuration and tagged monolog.processor |
SubjectExport, SubjectErasure |
Implemented, tested, wired over every tagged module |
phil:privacy:export, phil:privacy:erase |
Implemented and tested |
ProcessingRegister, ProcessingActivity, phil:privacy:register |
Implemented and tested: the register of processing from the schedule and the declarations, Markdown or JSON, --strict |
RetentionRun, RetentionJob, phil:privacy:retention, the schedule phil_privacy |
Implemented, tested and wired from a schedule |
AccountDeletions, the workflow, phil:privacy:deletions |
Implemented, tested and wired |
| The consent to cookies: banner, page, Twig functions, Stimulus controller, proofs | Implemented, tested and wired |
The cookie inventory, the cookie policy, AssertsDeclaredCookies |
Implemented, tested and wired |
privacy_embed(): third-party frames held until consent |
Implemented, tested and wired |
| A member's export and erasure from their preferences, as a ZIP archive | Implemented, tested and on by default |
| The requests of people without an account, by a verified link | Implemented, tested and wired; off until enabled |
The register of confidentiality incidents, phil:privacy:incident, phil:privacy:incidents |
Implemented, tested and wired |
Each member's consent to tracked emails, EmailTrackingConsents |
Implemented, tested and wired; off until enabled |
| The Stimulus controllers through AssetMapper and StimulusBundle | Implemented and tested |
PersonalDataExporter, PersonalDataEraser, RetentionScheduleSource, RetentionPurger |
Ports |
#[Anonymize], #[AsAnonymizer], the thirteen anonymizers, the eight locale packs |
Implemented and tested |
AnonymizationPlanReader, AnonymizationRun, ProductionGuard, ConnectionIdentity |
Implemented, tested and wired |
phil:privacy:anonymize |
Implemented and tested on SQLite; MySQL, MariaDB and PostgreSQL through Doctrine DBAL |
PhilPrivacyBundle |
Everything above, with its configuration |
Documentation
- Installation — requirements, installing with Composer, registering the bundle alongside MonologBundle.
- Configuration — every key of
phil_privacy, its default and effect, the services it wires, and each build-time error. - Declaring personal data — the
#[PersonalData]attribute, reading declarations through parents, and the inventory test that fails on uncovered fields. - Encrypting personal data at rest — which columns to encrypt with
#[Encrypted]of Symfony's KeyManagement component, blind indexes with#[BlindIndexed]for searching, and rotation. - Log redaction — what the processor redacts and why, the denylist comparison and patterns, URL and command line rules, throwables, and processor order in Monolog.
- Subject export and erasure —
DataSubject, the per-module contracts,SubjectExport,SubjectErasurewith failure isolation, how the bundle wires them, the register of processing, and the ZIP archive of an access answer. - Retention schedule and purges — periods and UTC cutoffs with month clamping, schedule
lines and their refused contradictions,
RetentionRun, and running the job. - Deleting an account — the workflow, the delay and the cancellation, what guarantees one execution per person, running the sweep, and where the export goes.
- Consent to cookies — declaring categories, the Twig functions, the rules, the cookie and the proof, working without JavaScript, and overriding the templates.
- The cookie inventory — the YAML file, the banner, the page and the policy drawn from it, and the test that fails on an undeclared cookie.
- Third-party frames —
privacy_embed(), the preview served by the site, the players that write fewer trackers, and the Stimulus controller. - Requests without an account — the verified link, the steps, the guards against abuse, and who a verified address is.
- The register of incidents — recording, completing and printing it, the notifications a serious risk asks for, and how long it is kept.
- Tracked emails — each member's consent to the counting of opens and the following of clicks, what a sender asks, the version of the text, and its export and erasure.
- Legal framework — ePrivacy and the CNIL, Québec's Law 25, the GDPR and the proposed Digital Omnibus, and what the default configuration does about each.
- Export and erasure from the preferences — the routes, the current member, the sudo mode, and what it takes to turn a right off.
- Anonymization — the plan from attributes, categories and configuration, every anonymizer and its options, the locale packs, the command, the production guard, and a copy to share.
- Extension points — the ports method by method, custom anonymizers and locale packs, the invariants an adapter keeps, example adapters, and the service ids you can rely on.
- Testing — how the bundle is tested, running its tests in a monorepo or alone, and how to test your adapters and configuration.
- API reference — every public type by layer, with its role and state.
Quick start
composer require phil/privacy-bundle:^0.1@dev
# config/packages/phil_privacy.yaml phil_privacy: log_redaction: denylist: ['*password*', '*token*', '*secret*', '*authorization*', '*cookie*', query_string] token_path_prefixes: ['/p/'] short_options: { p: password }
final class Member { #[PersonalData('email', retention: 'R-002')] private string $email; } DeclaredPersonalFields::uncoveredIn(Member::class, $schedule); // [] once every field has a rule
#[PersonalData('email', retention: 'R-002')] #[Anonymize('email', options: ['domain' => 'example.org'])] private string $email;
bin/console phil:privacy:anonymize --dry-run # the plan, and what it leaves in clear bin/console phil:privacy:anonymize --confirm # never in production
A malformed configuration entry fails the container build, not the first log line.
Requirements
PHP 8.5 or later with ext-sodium and ext-intl, monolog/monolog 3, Symfony 8.2 and Twig 3. The requests without an
account need symfony/rate-limiter, and symfony/mailer for the bundle's sender. MonologBundle is needed for the
processor to reach your loggers through its tag, a firewall for a member to reach their preferences, and a database for
the account deletions unless a store is named. See Installation.
Contributing
Contributions are welcome on the PHIL monorepo: read the contribution guide and the code of conduct. Report a vulnerability privately, as the security policy explains, never in a public issue.
License
MIT, © Philippe Gamache, doing business as Le Gnome Archiviste; a Parler Haut Interagir Librement project. See LICENSE.