maize-tech/laravel-legal-consent
Laravel package for managing legal consents and compliance. Track user acceptance of terms, privacy policies, and other legal documents, handle versioned agreements, and maintain an auditable consent history in your application.
Requirements first. As of 3.3.0 the package targets PHP 8.2–8.5 and Laravel 11, 12 and 13. The release is primarily a compatibility + tooling bump (Pest/Pint/Larastan, Laravel 13 support), so if you are still on Laravel 9/10 or PHP 8.1 you must stay pinned to 3.2.x — this is the single most important thing to check before running composer update.
composer require maize-tech/laravel-legal-consent
php artisan vendor:publish --provider="Maize\LegalConsent\LegalConsentServiceProvider"
php artisan migrate
Publish the config and migrations explicitly (tags are usually legal-consent-config / legal-consent-migrations — confirm with php artisan vendor:publish and pick by number if the tag names have drifted). Read the published config/legal-consent.php before migrating: it controls the table names, the route registration (prefix + middleware), and the models used for consent records and consent types.
Then wire the trait into your authenticatable model:
use Maize\LegalConsent\Concerns\HasLegalConsents; // verify exact namespace in src/
class User extends Authenticatable
{
use HasLegalConsents;
}
Where to look first: config/legal-consent.php → the package migrations (to see exactly what columns are persisted: consent type, document/version, timestamps, IP/user agent) → the trait in src/ (that is the whole public surface you will actually call) → the registered routes in routes/.
First use case: a registration or checkout form where the user must accept Terms & Privacy Policy. Create one consent type per legal document (terms, privacy, marketing, cookie policy), render the checkboxes, and on submit persist the acceptance through the trait. That gives you a queryable audit trail of who accepted which document version, when — which is the actual value of the package versus a boolean column on users.
Consent types are reference data, not enums. Seed them (LegalConsentType / equivalent model) in a dedicated seeder and treat them as slowly-changing rows with slugs you reference in code. Use the slug in your frontend and validation rules (Rule::exists(...)) rather than hard-coded IDs, and give the seeder updateOrCreate semantics so re-running it in every environment is safe.
Versioning drives re-consent. The package's model is document-version aware. When you publish new Terms, insert a new version/row rather than mutating the old one. Your "does this user need to re-accept?" check then becomes: latest version of each required type vs. the user's latest accepted record for that type. Put that behind a single method/Accessor on your User (or a small ConsentStatus service) so views, middleware and API responses all agree.
Form submission / frontend integration. Two viable shapes:
auth:sanctum (and throttle) rather than being public. If you need custom payloads (e.g. extra metadata, tenant ID), publish/override the controller instead of fighting the default validation.Enforcement pattern. Don't scatter if (! $user->hasAccepted(...)) checks. Use a small middleware (EnsureLegalConsentsAccepted) applied to the route groups that legally require it (checkout, data export, marketing opt-in), redirecting to a re-consent page. Keep marketing consent opt-in and independently revocable — never bundle it into the terms acceptance flow.
Audit / events. Listen for the package's acceptance events to write to your own append-only audit log, fire analytics, or dispatch queued jobs (welcome mail after terms accepted). If the package does not emit an event you need, wrap the trait call in your own action class — one place to add the event, easy to test.
Testing (Pest, matching the package's own migration). With RefreshDatabase, seed the consent types in a shared beforeEach, use factories for accepted/not-accepted states, and assert on the database (assertDatabaseHas on the consents table) rather than on the trait's return value alone. Add one feature test per protected route: no consent → redirect, current consent → 200, stale version → re-consent.
Contribution/workflow note for 3.3.0. The repo now runs Pest + Pint + Larastan. If you fork or send a PR, run composer update in the package, vendor/bin/pint before committing, and expect Larastan to reject untyped arrays — mirror the typing style of the migrated tests.
composer why-not maize-tech/laravel-legal-consent 3.3.0 before upgrading; if another dependency pins Laravel 10, you stay on 3.2.0. There are no new migrations and no config-shape changes in this release, so the upgrade itself needs no data backfill.consensable/model type. If you introduce Relation::enforceMorphMap() after consents exist, previously stored FQCNs stop resolving and historical consents silently disappear from relations. Introduce the morph map before going to production, or backfill the column in one migration.php artisan route:clear && php artisan route:cache. Same for config: php artisan config:clear after editing legal-consent.php — a stale cached config is the #1 cause of "the package routes 404 in staging but work locally".app.timezone at UTC and format at the edges; if you display "accepted on" in a user's timezone, store UTC and convert — never store local time.foreign_key cascade may not be enabled), and decide your retention window explicitly rather than letting the table grow forever.softDeletes or an admin impersonation feature, verify which model key the consent is recorded against — consent captured during impersonation is a compliance problem. Block consent-writing routes for impersonated sessions.accepted flag with a timestamp), not delete the row. Deleting destroys the audit trail; the package's relational model assumes append-style history.How can I help you explore Laravel packages today?