filagora/filagora-compliance
Compliance add-on for Filagora that helps Laravel shops meet legal requirements with ready-made consent, policy, and GDPR-friendly tools. Lightweight and easy to integrate into existing Filagora storefronts.
This package layers GDPR/privacy tooling onto Filagora: a Filament admin area, a privacy-request lifecycle, cookie consent, policy acceptance, data export/erasure, and retention pruning. It requires filagora/filagora-core ^0.2.1; filagora/filagora-entitlements and filagora/filagora-cms are optional.
Minimal steps:
composer require filagora/filagora-compliance and run php artisan migrate. All eight tables ship in a single migration; the schema snapshot means php artisan filagora:adopt accepts an existing site's tables without a re-migration.php artisan vendor:publish --tag=filagora-compliance-config
Look first at: gates, models, kpis, mail, routes, retention, schedule, anonymize, and the workflow toggles.dsar_request_email (where retention-failure reports and default DSAR mail go), plus cookie_policy_page and privacy_page for your site's CMS pages.->plugin(\Filagora\Compliance\Filament\FilagoraCompliancePlugin::make())
It appears as a "Privacy and compliance" navigation group with Privacy requests, Cookie groups, Cookie policy versions, Privacy policy versions, a read-only Privacy operation audit, and a KPI widget.// config/filagora-compliance.php
'enable_cookie_consent_workflow' => true,
'enforce_gdpr' => true,
'schedule' => ['enabled' => true, 'daily_at' => '02:30'],
First real use case: wire your own data into export/erasure. Register a PrivacyContributor in a service provider, then submit a request through the PrivacyRequests service and work it from the Filament Privacy requests screen — generate the export, download it, open its audit, and check that the requester was told.
Submit from the front end, never by inserting rows. PrivacyRequests::submit() is where every guard lives (workflow enabled, known and offered type, details present, person not erased, no duplicate open request of that type). It refuses with a reason instead of failing, so surface that reason in your form validation rather than catching exceptions:
$result = $privacyRequests->submit(
person: $user,
type: PrivacyRequestType::Export,
details: $request->string('details'),
);
// show $result's refusal reason when it did not go through
Drive the lifecycle through the service. changeStatus(), hold(), release(), reject(), reply(), fulfilExport(), fulfilErasure() all audit to PrivacyAudit and notify the person. The Filament actions call exactly the same methods, so an admin doing it by hand and a queued job doing it programmatically leave identical audit trails. Use changeStatus() (not a raw update()) even for hand-fulfilled requests — that path audits the hand fulfilment.
Contribute export and erasure sections. This is the main integration point for your own models. One contributor does both directions: PersonalDataExport writes one JSON file with a user section plus a section per registered contributor; PersonalDataErasure runs every contributor's erase, then the UserAnonymizer, all in a single transaction. Register in a provider:
public function boot(PrivacyContributorRegistry $contributors): void
{
$contributors->register(new OrdersPrivacyContributor());
}
ConfiguredColumnsAnonymizer (the default) replaces identity columns listed under anonymize.attributes — set that carefully, because erasure is irreversible and happens last inside the transaction. The package registers its own ComplianceDataContributor (exports cookie choice, acceptances, requests; erases cookie choice) so you can see the intended shape.
Cookie consent on the front end. Use CookieConsent: enabled groups with their cookies, the active policy version, a stored choice, normalize(), save() and requiresReconsent(). normalize() applies precedence cookie's own choice → its group's → default, and forces required groups on — call it server-side before persisting, don't trust posted values. save() stores one row per user with a hashed address and user agent, which is what requiresReconsent() later compares against.
Policy acceptance. PolicyAcceptance gives you activeVersion() (latest published, cached by id for 60 seconds), hasAcknowledgedActivePolicy(), stateFor() and accept(). Gate the parts of your app that need it on enforce_gdpr being on and hasAcknowledgedActivePolicy(), and re-check after publishing a new policy version — 60-second caching means a just-published version can lag briefly.
Publish cookie policy versions from your CMS. When filagora/filagora-cms is installed, publishing the page named by cookie_policy_page creates a cookie policy version automatically. Otherwise manage them in the Cookie policy versions resource; they're day-numbered, so avoid hand-crafting versions if you can publish through the CMS.
Listen rather than poll. PrivacyRequestSubmitted, PrivacyRequestStatusChanged and PrivacyReplySent fire for every path. Hook your own notifications, Slack pings or ticket syncs there; leave the built-in mail alone or switch it off with mail.enabled once you have your own. Links in that mail are named routes under routes and are simply omitted when not configured — good for local, bad if you forget to set them in production.
Retention. Register rules with core's RetentionRuleRegistry; php artisan filagora:compliance:prune --dry-run reports what would go, and the scheduled daily run (per schedule.daily_at) executes them. A failing rule is reported and mailed to the DSAR address without stopping the others — so a broken rule is a quiet problem, not a loud one. The package's own PrivacyRequestRetention deletes settled requests by status window under retention.privacy_requests.
Authorisation. Access flows through core's AccessChecker and GateAbilities, so the package works without filagora-entitlements. Abilities are configured under gates; the release describes six abilities (see requests, handle them, erase a person, manage cookie policy and groups, manage privacy policy versions, read the audit) surfaced through three config keys (privacy_requests, policies, audit). Check the actual gate names in the published config before assigning them to roles.
fulfilErasure() call appears to do nothing, check hold first — it refuses rather than failing.submit() refuses a duplicate. If support staff need a resubmission, they must settle the old one; don't work around it by inserting rows, or you lose the audit trail.enforce_gdpr.delete_export_after_days after fulfilment while the request is kept with export_path cleared; the request itself only goes when retention prunes it, potentially years later. Don't assume a null export_path means the request is pruned.--dry-run before you trust the schedule. prune deletes settled requests by status window. Confirm which statuses count as settled in retention.privacy_requests before enabling the daily schedule, and make sure dsar_request_email is a monitored mailbox — that's where failing rules report.filagora-compliance.models entries is supported, but your replacements need to keep the columns and relations the resources and services rely on (the audit relations, the cookie relation manager, export_path). Extend the package models rather than rewriting from scratch.filagora:adopt accepts tables that already match "index for index". If your production tables drifted from the original site's schema, adopt will not paper over it — reconcile first.requiresReconsent() right after a publish.kpis — tune the thresholds there rather than editing the widget, and be aware a deadline-based KPI depends on the dates changeStatus() maintains; never edit status dates directly.PrivacyContributor for your own data, the PrivacyRequests / CookieConsent / PolicyAcceptance services for behaviour, the Privacy* events for side effects, gates and models for per-app wiring. Reach for raw model updates last — everything meaningful in this package is audited, and bypassing the services is how you end up with an audit trail that doesn't match reality.How can I help you explore Laravel packages today?