Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Consent Contracts Laravel Package

setono/consent-contracts

Framework-agnostic PHP contracts for consent management. setono/consent-contracts defines the interfaces your app and third-party consent providers implement, keeping cookie and GDPR consent logic decoupled, testable, and easy to swap between implementations.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require setono/consent-contracts

This is a contracts-only package: it ships interfaces and (probably) an enum for consent types, with no ServiceProvider, no config file, no publishable assets, and no Laravel auto-discovery. That is the whole point — it exists so that libraries and apps can talk about consent without depending on a concrete CMP (Cookiebot, CookieYes, OneTrust, Usercentrics, or your own banner).

Where to look first:

  1. The src/ directory — read every interface before writing a line of code. Expect roughly: a ConsentInterface (a named consent a subject has/hasn't given), a ConsentCheckerInterface (the hasConsent(...) question your app actually asks), a storage/persistence contract, and a ConsentType/category enum describing the classes of consent.
  2. The README.md — for the intended vocabulary and the "mainstream CMP" category mapping introduced in v1.1.0.
  3. The changelog diff (v1.0.0…v1.1.0) — see the Gotchas section; "wording" changes in a contracts package are worth verifying by hand.

Your first use case should be the narrow one: stop asking your app "is this user opted in to marketing?" in twenty different places. Bind ConsentCheckerInterface in your AppServiceProvider to a small implementation backed by whatever you already have (a cookie, a user_consents table, or your CMP's JS API mirrored server-side). Everything else — banners, scripts, tracking — can then depend on the interface rather than on a vendor SDK.

// app/Providers/AppServiceProvider.php
public function register(): void
{
    $this->app->scoped(ConsentCheckerInterface::class, CookieConsentChecker::class);
}

Implementation Patterns

Bind, don't extend. The package gives you the shape of the conversation; you supply the answer. A typical CookieConsentChecker reads a decoded cookie bag and answers from a pre-normalised array, falling back to DB rows for logged-in users so that consent survives a cleared cookie jar.

Single mapping table for categories. The v1.1.0 wording change is a reminder that every CMP names its categories slightly differently. Keep exactly one place in your app where external category names map to the package's taxonomy, and test it:

// config/consent.php
return [
    'map' => [
        'necessary'    => ConsentType::Necessary,
        'preferences'  => ConsentType::Preferences,
        'statistics'   => ConsentType::Statistics,
        'marketing'    => ConsentType::Marketing,
    ],
];

Route-level enforcement via middleware. Don't scatter if (! $checker->hasConsent(...)) through controllers. A RequiresConsent middleware on a route group is declarative and greppable:

Route::middleware('consent:marketing')->group(function () {
    Route::get('/pricing', PricingController::class);
});

Blade directive for the view layer. A @consent('statistics') … @endconsent directive keeps third-party script tags out of the markup when consent is absent — useful for the server-rendered half of your analytics snippet, and it makes audits trivial.

Context/subject handling. Anonymous visitors and logged-in users usually have different consent lifecycles (cookie-scoped vs. account-scoped). Resolve the subject once per request in a scoped() binding, and have the checker consult it — this avoids the classic bug where a logged-in user's stored consents are ignored because the cookie was never set.

Write an audit trail, not just a boolean. Even if the checker only answers yes/no today, persist (subject, consent, granted, source, ip/ua hash, occurred_at) when consent changes. GDPR defensibility is the reason people adopt this package, and retrofitting history is painful.

Fake it in tests. Because it's an interface, $this->app->bind(ConsentCheckerInterface::class, fn () => new AlwaysGrantedChecker()) gives you a one-liner test double. Assert both branches: content present when granted, and a 403/absent marker when not.

Decorator for caching. If your checker hits the database per call, wrap it: CachedConsentChecker implements ConsentCheckerInterface holding a per-request memo array. Decoration keeps the contract clean and is the intended extension point — no subclassing of package code.

Gotchas and Tips

  • A "wording" change in a contracts package is not automatically harmless. v1.1.0 rewords things to align with mainstream CMP platforms. If that reword touched enum case names or backing values (e.g. Statistics → Analytics), it is a backward-compatibility break shipped under a minor version — anything persisted as a string will no longer round-trip through ::from(). Diff src/ between tags before upgrading, and if you have stored consent rows, write a migration that maps old values to new ones.
  • Target [ConsentCheckerInterface] is not instantiable is the error you'll hit if you forget the container binding. There's no service provider to do it for you — that's by design, but it surprises people coming from batteries-included packages.
  • Never store or compare raw strings against the enum. Use ConsentType::tryFrom($raw) and handle null explicitly; CMP payloads routinely contain categories you don't model, and a thrown ValueError inside a Blade directive is an ugly 500 on the checkout page.
  • Cookies lie about the schema. CMP cookie payloads change shape between vendor versions and plan tiers. Validate and normalise at the boundary (one parser class, thoroughly tested with real captured cookie values), and fail closed — unknown category means no consent.
  • Consent must be re-resolved per request, not cached in a singleton. Use scoped() or singleton with an explicit flush in middleware; a long-lived queue worker holding a stale checker is a real GDPR problem.
  • Server-side ≠ client-side. The package models the contract; it can't stop a <script> that's already in the page. If a tag fires from JS before your server-rendered check runs, you have a compliance gap the interface can't close — gate scripts at the source.
  • Extension points are the interfaces, nothing else. There's no event dispatcher, no config to merge, no base class. If you need behaviour the package doesn't define, add your own interface in your app namespace that extends theirs; don't patch the vendor directory.
  • Pin the version ("setono/consent-contracts": "^1.1") and read the diff on every bump. For a package this small, the upgrade cost is minutes — but only if you actually look, because a semver-minor here can still change the values you've persisted.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package